diff --git a/README.md b/README.md
index 66655d9..fae594d 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ Features
* kerning
* line spacing
* clickable links
+* superscript and subscript
+* html lists of all types
Usage
-----
@@ -47,6 +49,8 @@ Usage
custom font with kerning
alignment
indentation
+ superscript and subscript
+
Minimum Requirements
--------------------
diff --git a/RTLabelProject/Classes/DemoTableViewController.m b/RTLabelProject/Classes/DemoTableViewController.m
index ea1ad2d..53e2e8a 100644
--- a/RTLabelProject/Classes/DemoTableViewController.m
+++ b/RTLabelProject/Classes/DemoTableViewController.m
@@ -91,6 +91,14 @@ - (id)initWithStyle:(UITableViewStyle)style
NSMutableDictionary *row20 = [NSMutableDictionary dictionary];
[row20 setObject:@"Indented bla bla bla bla bla bla bla bla bla bla bla bla bla
" forKey:@"text"];
[self.dataArray addObject:row20];
+
+ NSMutableDictionary *row9 = [NSMutableDictionary dictionary];
+ [row9 setObject:@"Superscriptsup and subscriptsub" forKey:@"text"];
+ [self.dataArray addObject:row9];
+
+ NSMutableDictionary *row10 = [NSMutableDictionary dictionary];
+ row10[@"text"] = @"- one
- two
";
+ [self.dataArray insertObject:row10 atIndex:0];
}
return self;
}
diff --git a/RTLabelProject/Classes/RTLabel.m b/RTLabelProject/Classes/RTLabel.m
index 4cf8624..8d243f4 100755
--- a/RTLabelProject/Classes/RTLabel.m
+++ b/RTLabelProject/Classes/RTLabel.m
@@ -127,6 +127,61 @@ - (void)applyParagraphStyleToText:(CFMutableAttributedStringRef)text attributes:
@implementation RTLabel
+
+static NSString *LettersForIndex(NSInteger index) {
+ NSString *result = @"";
+ index--;
+ do {
+ if(result.length)
+ index--;
+ int rest = index % ('Z' - 'A' + 1);
+ index /= 'Z' - 'A' + 1;
+ result = [[NSString stringWithFormat:@"%c", rest + 'A'] stringByAppendingString:result];
+ } while (index > 0);
+ return result;
+}
+
+static NSString *RomanForIndex(int index) {
+ static NSString *huns[] = {@"", @"C", @"CC", @"CCC", @"CD", @"D", @"DC", @"DCC", @"DCCC", @"CM"};
+ static NSString *tens[] = {@"", @"X", @"XX", @"XXX", @"XL", @"L", @"LX", @"LXX", @"LXXX", @"XC"};
+ static NSString *ones[] = {@"", @"I", @"II", @"III", @"IV", @"V", @"VI", @"VII", @"VIII", @"IX"};
+
+ NSMutableString *result = [NSMutableString new];
+ while (index >= 1000) {
+ [result appendString:@"M"];
+ index -= 1000;
+ }
+
+ [result appendString:huns[index / 100]];
+ index %= 100;
+ [result appendString:tens[index / 10]];
+ index %= 10;
+ [result appendString:ones[index]];
+ return result;
+}
+
+static NSString *ListPointString(NSString *type, NSInteger index) {
+ NSString *point = @"";
+ if([type isEqualToString:@"1"])
+ point = [NSString stringWithFormat:@"%d. ", index];
+ else if([type isEqualToString:@"A"])
+ point = [LettersForIndex(index) stringByAppendingString:@". "];
+ else if([type isEqualToString:@"a"])
+ point = [[LettersForIndex(index) lowercaseString] stringByAppendingString:@". "];
+ else if([type isEqualToString:@"I"])
+ point = [NSString stringWithFormat:@"%@. ", RomanForIndex(index)];
+ else if([type isEqualToString:@"i"])
+ point = [NSString stringWithFormat:@"%@. ", [RomanForIndex(index) lowercaseString]];
+ else if([type isEqualToString:@"circle"])
+ point = @"\u25CB ";
+ else if([type isEqualToString:@"disc"])
+ point = @"\u25CF ";
+ else if([type isEqualToString:@"square"])
+ point = @"\u25A0 ";
+
+ return point;
+}
+
- (id)initWithFrame:(CGRect)_frame
{
self = [super initWithFrame:_frame];
@@ -275,11 +330,7 @@ - (void)render
[self applySingleUnderlineText:attrString atPosition:component.position withLength:[component.text length]];
}
}
-
- NSString *value = [component.attributes objectForKey:@"href"];
- value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
- [component.attributes setObject:value forKey:@"href"];
-
+
[links addObject:component];
}
else if ([component.tagLabel caseInsensitiveCompare:@"u"] == NSOrderedSame || [component.tagLabel caseInsensitiveCompare:@"uu"] == NSOrderedSame)
@@ -312,6 +363,18 @@ - (void)render
{
[self applyCenterStyleToText:attrString attributes:component.attributes atPosition:component.position withLength:[component.text length]];
}
+ else if ([component.tagLabel caseInsensitiveCompare:@"sup"] == NSOrderedSame)
+ {
+ [self applySuperscriptStyle:1 toText:attrString atPosition:component.position withLength:[component.text length]];
+ }
+ else if ([component.tagLabel caseInsensitiveCompare:@"sub"] == NSOrderedSame)
+ {
+ [self applySuperscriptStyle:-1 toText:attrString atPosition:component.position withLength:[component.text length]];
+ }
+ else if ([component.tagLabel caseInsensitiveCompare:@"li"] == NSOrderedSame)
+ {
+ [self applyLiAttributes:component.attributes toText:attrString atPosition:component.position withLength:component.text.length];
+ }
}
// Create the framesetter with the attributed string.
@@ -567,7 +630,6 @@ - (void)applyFontAttributes:(NSDictionary*)attributes toText:(CFMutableAttribute
for (NSString *key in attributes)
{
NSString *value = [attributes objectForKey:key];
- value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
if ([key caseInsensitiveCompare:@"color"] == NSOrderedSame)
{
@@ -610,13 +672,11 @@ - (void)applyFontAttributes:(NSDictionary*)attributes toText:(CFMutableAttribute
if ([attributes objectForKey:@"face"] && [attributes objectForKey:@"size"])
{
NSString *fontName = [attributes objectForKey:@"face"];
- fontName = [fontName stringByReplacingOccurrencesOfString:@"'" withString:@""];
font = [UIFont fontWithName:fontName size:[[attributes objectForKey:@"size"] intValue]];
}
else if ([attributes objectForKey:@"face"] && ![attributes objectForKey:@"size"])
{
NSString *fontName = [attributes objectForKey:@"face"];
- fontName = [fontName stringByReplacingOccurrencesOfString:@"'" withString:@""];
font = [UIFont fontWithName:fontName size:self.font.pointSize];
}
else if (![attributes objectForKey:@"face"] && [attributes objectForKey:@"size"])
@@ -644,6 +704,29 @@ - (void)applyBoldStyleToText:(CFMutableAttributedStringRef)text atPosition:(int)
CFRelease(boldFontRef);
}
+- (void)applyLiAttributes:(NSDictionary*)attributes toText:(CFMutableAttributedStringRef)text atPosition:(int)position withLength:(int)length
+{
+ CFMutableDictionaryRef styleDict = ( CFDictionaryCreateMutable( (0), 0, (0), (0) ) );
+ CGFloat fistLineIndent = 15.0f * [attributes[@"indent"] intValue];
+ CGFloat headIndent = 15.0f + fistLineIndent;
+
+ CTParagraphStyleSetting theSettings[] =
+ {
+
+ { kCTParagraphStyleSpecifierFirstLineHeadIndent, sizeof(CGFloat), &fistLineIndent },
+ { kCTParagraphStyleSpecifierHeadIndent, sizeof(CGFloat), &headIndent },
+ { kCTParagraphStyleSpecifierMinimumLineSpacing, sizeof(CGFloat), &_lineSpacing }, // leading
+ { kCTParagraphStyleSpecifierMaximumLineSpacing, sizeof(CGFloat), &_lineSpacing }, // leading
+ };
+
+ CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, sizeof(theSettings) / sizeof(CTParagraphStyleSetting));
+ CFDictionaryAddValue( styleDict, kCTParagraphStyleAttributeName, theParagraphRef );
+
+ CFAttributedStringSetAttributes( text, CFRangeMake(position, length), styleDict, 0 );
+ CFRelease(theParagraphRef);
+ CFRelease(styleDict);
+}
+
- (void)applyBoldItalicStyleToText:(CFMutableAttributedStringRef)text atPosition:(int)position withLength:(int)length
{
CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, position, kCTFontAttributeName, NULL);
@@ -688,8 +771,6 @@ - (void)applyColor:(NSString*)value toText:(CFMutableAttributedStringRef)text at
- (void)applyUnderlineColor:(NSString*)value toText:(CFMutableAttributedStringRef)text atPosition:(int)position withLength:(int)length
{
-
- value = [value stringByReplacingOccurrencesOfString:@"'" withString:@""];
if ([value rangeOfString:@"#"].location==0) {
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
value = [value stringByReplacingOccurrencesOfString:@"#" withString:@"0x"];
@@ -714,6 +795,33 @@ - (void)applyUnderlineColor:(NSString*)value toText:(CFMutableAttributedStringRe
}
+
+- (void)applySuperscriptStyle:(int)value toText:(CFMutableAttributedStringRef)text atPosition:(int)position withLength:(int)length
+{
+ // Get current font
+ CFTypeRef actualFontRef = CFAttributedStringGetAttribute(text, position, kCTFontAttributeName, NULL);
+ if(!actualFontRef)
+ actualFontRef = (__bridge CTFontRef)[UIFont systemFontOfSize:[UIFont systemFontSize]];
+
+ // Make font smaller
+ CFNumberRef sizeRef = CTFontCopyAttribute(actualFontRef, kCTFontSizeAttribute);
+ float size = 0;
+ CFNumberGetValue(sizeRef, kCFNumberFloat32Type, &size);
+ CTFontRef customFont = CTFontCreateCopyWithAttributes(actualFontRef, size * 0.7f, 0, 0);
+ CFRelease(sizeRef);
+ CFAttributedStringSetAttribute(text, CFRangeMake(position, length), kCTFontAttributeName, customFont);
+
+ // Move base line
+ CFMutableDictionaryRef styleDict = CFDictionaryCreateMutable( 0, 0, NULL, &kCFTypeDictionaryValueCallBacks);
+ CFDictionaryAddValue(styleDict, kCTBaselineReferenceFont, customFont);
+ CFDictionaryAddValue(styleDict, kCTBaselineClassIdeographicLow, (__bridge CFNumberRef)@(value * size/3.5));
+ CFAttributedStringSetAttribute(text, CFRangeMake(position, length), kCTBaselineClassAttributeName, kCTBaselineClassIdeographicLow);
+ CFAttributedStringSetAttribute(text, CFRangeMake(position, length), kCTBaselineReferenceInfoAttributeName, styleDict);
+
+ CFRelease(customFont);
+ CFRelease(styleDict);
+}
+
#pragma mark -
#pragma mark button
@@ -765,14 +873,14 @@ - (void)setHighlighted:(BOOL)highlighted
- (void)setHighlightedText:(NSString *)text
{
- _highlightedText = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"];
+ _highlightedText = text;
RTLabelExtractedComponent *component = [RTLabel extractTextStyleFromText:_highlightedText paragraphReplacement:self.paragraphReplacement];
[self setHighlightedTextComponents:component.textComponents];
}
- (void)setText:(NSString *)text
{
- _text = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"];
+ _text = text;
RTLabelExtractedComponent *component = [RTLabel extractTextStyleFromText:_text paragraphReplacement:self.paragraphReplacement];
[self setTextComponents:component.textComponents];
[self setPlainText:component.plainText];
@@ -781,7 +889,7 @@ - (void)setText:(NSString *)text
- (void)setText:(NSString *)text extractedTextComponent:(RTLabelExtractedComponent*)extractedComponent
{
- _text = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"];
+ _text = text;
[self setTextComponents:extractedComponent.textComponents];
[self setPlainText:extractedComponent.plainText];
[self setNeedsDisplay];
@@ -789,7 +897,7 @@ - (void)setText:(NSString *)text extractedTextComponent:(RTLabelExtractedCompone
- (void)setHighlightedText:(NSString *)text extractedTextComponent:(RTLabelExtractedComponent*)extractedComponent
{
- _highlightedText = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"];
+ _highlightedText = text;
[self setHighlightedTextComponents:extractedComponent.textComponents];
}
@@ -859,60 +967,74 @@ - (NSArray *)components
+ (RTLabelExtractedComponent*)extractTextStyleFromText:(NSString*)data paragraphReplacement:(NSString*)paragraphReplacement
{
- NSScanner *scanner = nil;
- NSString *text = nil;
- NSString *tag = nil;
-
NSMutableArray *components = [NSMutableArray array];
-
- int last_position = 0;
- scanner = [NSScanner scannerWithString:data];
+ NSMutableString *plainText = [NSMutableString new];
+ int listIndent = 0;
+ int listPointCounter[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+ NSString *listPointType[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+ BOOL listPoint = NO;
+
+ NSRegularExpression *white_trimmer = [[NSRegularExpression alloc] initWithPattern:@"\\s+" options:NSRegularExpressionCaseInsensitive error:nil];
+ NSScanner *scanner = [NSScanner scannerWithString:data];
+ scanner.charactersToBeSkipped = nil;
+
while (![scanner isAtEnd])
{
- [scanner scanUpToString:@"<" intoString:NULL];
- [scanner scanUpToString:@">" intoString:&text];
-
- NSString *delimiter = [NSString stringWithFormat:@"%@>", text];
- int position = [data rangeOfString:delimiter].location;
- if (position!=NSNotFound)
- {
- if ([delimiter rangeOfString:@""];
- }
-
- if ([text rangeOfString:@""].location==0)
+ // Scan plain text
+ NSString *text = nil;
+ [scanner scanUpToString:@"<" intoString:&text];
+
+ if(text) {
+ // Replace html entities
+ text = [text stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
+ text = [text stringByReplacingOccurrencesOfString:@">" withString:@">"];
+ text = [text stringByReplacingOccurrencesOfString:@"&" withString:@"&"];
+ text = [text stringByReplacingOccurrencesOfString:@"'" withString:@"'"];
+ text = [text stringByReplacingOccurrencesOfString:@""" withString:@"\""];
+ text = [white_trimmer stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, text.length) withTemplate:@" "];
+
+ if(listPoint && listIndent) {
+ listPoint = NO;
+
+ NSString *point = ListPointString(listPointType[listIndent], listPointCounter[listIndent]);
+ text = [NSString stringWithFormat:@"%@%@", point, text];
+ }
+
+ [plainText appendString:text];
+ }
+
+ // Scan html tag
+ [scanner scanUpToString:@">" intoString:&text];
+ [scanner scanString:@">" intoString:nil]; // Skip closing '>'
+ NSString *tag = [text stringByAppendingString:@">"];
+
+ if ([tag rangeOfString:@""].location==0)
{
- // end of tag
- tag = [text substringFromIndex:2];
- if (position!=NSNotFound)
- {
- for (int i=[components count]-1; i>=0; i--)
- {
- RTLabelComponent *component = [components objectAtIndex:i];
- if (component.text==nil && [component.tagLabel isEqualToString:tag])
- {
- NSString *text2 = [data substringWithRange:NSMakeRange(component.position, position-component.position)];
- component.text = text2;
- break;
- }
+ // End of tag
+ NSString *tag_name = [tag substringWithRange:NSMakeRange(2, tag.length - 3)];
+
+ for (int i=[components count]-1; i>=0; i--)
+ {
+ RTLabelComponent *component = [components objectAtIndex:i];
+ if (component.text==nil && [component.tagLabel isEqualToString:tag_name])
+ {
+ NSString *text2 = [plainText substringWithRange:NSMakeRange(component.position, plainText.length - component.position)];
+ component.text = text2;
+ break;
}
}
+
+ if ([tag_name caseInsensitiveCompare:@"ul"] == NSOrderedSame || [tag_name caseInsensitiveCompare:@"ol"] == NSOrderedSame) {
+ listPointCounter[listIndent] = 0;
+ listIndent--;
+ }
}
- else
+ else if([tag rangeOfString:@"<"].location == 0)
{
- // start of tag
- NSArray *textComponents = [[text substringFromIndex:1] componentsSeparatedByString:@" "];
- tag = [textComponents objectAtIndex:0];
- //NSLog(@"start of tag: %@", tag);
+ // Start of tag
+ NSArray *textComponents = [[tag substringWithRange:NSMakeRange(1, tag.length - 2)] componentsSeparatedByString:@" "];
+ NSString *tag_name = [textComponents objectAtIndex:0];
+
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
for (NSUInteger i=1; i<[textComponents count]; i++)
{
@@ -925,6 +1047,8 @@ + (RTLabelExtractedComponent*)extractTextStyleFromText:(NSString*)data paragraph
NSString *value = [[pair subarrayWithRange:NSMakeRange(1, [pair count] - 1)] componentsJoinedByString:@"="];
value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
value = [value stringByReplacingOccurrencesOfString:@"\"" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
+ value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange(0, 1)];
+ value = [value stringByReplacingOccurrencesOfString:@"'" withString:@"" options:NSLiteralSearch range:NSMakeRange([value length]-1, 1)];
[attributes setObject:value forKey:key];
} else if ([pair count]==1) {
@@ -932,14 +1056,37 @@ + (RTLabelExtractedComponent*)extractTextStyleFromText:(NSString*)data paragraph
}
}
}
- RTLabelComponent *component = [RTLabelComponent componentWithString:nil tag:tag attributes:attributes];
- component.position = position;
+
+ if ([tag_name caseInsensitiveCompare:@"p"] == NSOrderedSame)
+ [plainText appendString:paragraphReplacement];
+ else if ([tag_name caseInsensitiveCompare:@"ul"] == NSOrderedSame || [tag_name caseInsensitiveCompare:@"ol"] == NSOrderedSame) {
+ // Start of html list
+ listIndent++;
+ listPointType[listIndent] = [tag_name caseInsensitiveCompare:@"ol"] == NSOrderedSame ? @"1" : @"circle"; // Default types
+ if(attributes[@"type"])
+ listPointType[listIndent] = attributes[@"type"];
+ }
+ else if([tag_name caseInsensitiveCompare:@"li"] == NSOrderedSame) {
+ // New list point
+ attributes[@"indent"] = @(listIndent);
+ listPoint = YES;
+ listPointCounter[listIndent]++;
+
+ if(plainText.length && [tag_name caseInsensitiveCompare:@"li"] == NSOrderedSame)
+ [plainText appendString:@"\n"];
+ }
+ else if ([tag_name caseInsensitiveCompare:@"br"] == NSOrderedSame) {
+ [plainText appendString:@"\n"];
+ continue;
+ }
+
+ RTLabelComponent *component = [RTLabelComponent componentWithString:nil tag:tag_name attributes:attributes];
+ component.position = plainText.length;
[components addObject:component];
}
- last_position = position;
}
- return [RTLabelExtractedComponent rtLabelExtractComponentsWithTextComponent:components plainText:data];
+ return [RTLabelExtractedComponent rtLabelExtractComponentsWithTextComponent:components plainText:plainText];
}
@@ -949,7 +1096,7 @@ - (void)parse:(NSString *)data valid_tags:(NSArray *)valid_tags
NSScanner *scanner = nil;
NSString *text = nil;
NSString *tag = nil;
-
+
NSMutableArray *components = [NSMutableArray array];
//set up the scanner
@@ -1067,7 +1214,7 @@ - (NSString*)visibleText
- (void)setText:(NSString *)text extractedTextStyle:(NSDictionary*)extractTextStyle
{
- _text = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"];
+ _text = text;
[self setTextComponents:[extractTextStyle objectForKey:@"textComponents"]];
[self setPlainText:[extractTextStyle objectForKey:@"plainText"]];
[self setNeedsDisplay];