// // RTLabel.m // RTLabelProject // /** * Copyright (c) 2010 Muh Hon Cheng * Created by honcheng on 1/6/11. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject * to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT * WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR * PURPOSE AND NONINFRINGEMENT. IN NO EVENT * SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR * IN CONNECTION WITH THE SOFTWARE OR * THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * @author Muh Hon Cheng * @copyright 2011 Muh Hon Cheng * @version * */ #import "RTLabel.h" @interface RTLabelButton : UIButton @property (nonatomic, assign) long componentIndex; @property (nonatomic) NSURL *url; @end @implementation RTLabelButton @end @implementation RTLabelComponent - (id)initWithString:(NSString*)aText tag:(NSString*)aTagLabel attributes:(NSMutableDictionary*)theAttributes { self = [super init]; if (self) { _text = aText; _tagLabel = aTagLabel; _attributes = theAttributes; } return self; } + (id)componentWithString:(NSString*)aText tag:(NSString*)aTagLabel attributes:(NSMutableDictionary*)theAttributes { return [[self alloc] initWithString:aText tag:aTagLabel attributes:theAttributes]; } - (id)initWithTag:(NSString*)aTagLabel position:(int)aPosition attributes:(NSMutableDictionary*)theAttributes { self = [super init]; if (self) { _tagLabel = aTagLabel; _position = aPosition; _attributes = theAttributes; } return self; } +(id)componentWithTag:(NSString*)aTagLabel position:(int)aPosition attributes:(NSMutableDictionary*)theAttributes { return [[self alloc] initWithTag:aTagLabel position:aPosition attributes:theAttributes]; } - (NSString*)description { NSMutableString *desc = [NSMutableString string]; [desc appendFormat:@"text: %@", self.text]; [desc appendFormat:@", position: %ld", (long)self.position]; if (self.tagLabel) [desc appendFormat:@", tag: %@", self.tagLabel]; if (self.attributes) [desc appendFormat:@", attributes: %@", self.attributes]; return desc; } @end @implementation RTLabelExtractedComponent + (RTLabelExtractedComponent*)rtLabelExtractComponentsWithTextComponent:(NSMutableArray*)textComponents plainText:(NSString*)plainText { RTLabelExtractedComponent *component = [[RTLabelExtractedComponent alloc] init]; [component setTextComponents:textComponents]; [component setPlainText:plainText]; return component; } @end @interface RTLabel() - (CGFloat)frameHeight:(CTFrameRef)frame; - (NSArray *)components; - (void)parse:(NSString *)data valid_tags:(NSArray *)valid_tags; - (NSArray*) colorForHex:(NSString *)hexColor; - (void)render; #pragma mark - #pragma mark styling - (void)applyItalicStyleToText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyBoldStyleToText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyBoldItalicStyleToText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyColor:(NSString*)value toText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applySingleUnderlineText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyDoubleUnderlineText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyUnderlineColor:(NSString*)value toText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyFontAttributes:(NSDictionary*)attributes toText:(CFMutableAttributedStringRef)text atPosition:(long)position withLength:(long)length; - (void)applyParagraphStyleToText:(CFMutableAttributedStringRef)text attributes:(NSMutableDictionary*)attributes atPosition:(long)position withLength:(long)length; @end @implementation RTLabel - (id)initWithFrame:(CGRect)_frame { self = [super initWithFrame:_frame]; if (self) { [self initialize]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self initialize]; } return self; } - (void)initialize { [self setBackgroundColor:[UIColor clearColor]]; _font = [UIFont systemFontOfSize:15]; _textColor = [UIColor blackColor]; _text = @""; _textAlignment = RTTextAlignmentLeft; _lineBreakMode = RTTextLineBreakModeWordWrapping; _lineSpacing = 3; _currentSelectedButtonComponentIndex = -1; _paragraphReplacement = @"\n"; [self setMultipleTouchEnabled:YES]; } - (void)setTextAlignment:(RTTextAlignment)textAlignment { _textAlignment = textAlignment; [self setNeedsDisplay]; } - (void)setLineBreakMode:(RTTextLineBreakMode)lineBreakMode { _lineBreakMode = lineBreakMode; [self setNeedsDisplay]; } - (void)drawRect:(CGRect)rect { [self render]; } - (void)render { if (self.currentSelectedButtonComponentIndex==-1) { for (id view in [self subviews]) { if ([view isKindOfClass:[UIView class]]) { [view removeFromSuperview]; } } } if (!self.plainText) return; CGContextRef context = UIGraphicsGetCurrentContext(); if (context != NULL) { // Drawing code. CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGAffineTransform flipVertical = CGAffineTransformMake(1,0,0,-1,0,self.frame.size.height); CGContextConcatCTM(context, flipVertical); } // Initialize an attributed string. CFStringRef string = (__bridge CFStringRef)self.plainText; CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0); CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), string); CFMutableDictionaryRef styleDict1 = ( CFDictionaryCreateMutable( (0), 0, (0), (0) ) ); // Create a color and add it as an attribute to the string. CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGColorSpaceRelease(rgbColorSpace); CFDictionaryAddValue( styleDict1, kCTForegroundColorAttributeName, [self.textColor CGColor] ); CFAttributedStringSetAttributes( attrString, CFRangeMake( 0, CFAttributedStringGetLength(attrString) ), styleDict1, 0 ); CFMutableDictionaryRef styleDict = ( CFDictionaryCreateMutable( (0), 0, (0), (0) ) ); [self applyParagraphStyleToText:attrString attributes:nil atPosition:0 withLength:CFAttributedStringGetLength(attrString)]; CTFontRef thisFont = CTFontCreateWithName ((__bridge CFStringRef)[self.font fontName], [self.font pointSize], NULL); CFAttributedStringSetAttribute(attrString, CFRangeMake(0, CFAttributedStringGetLength(attrString)), kCTFontAttributeName, thisFont); NSMutableArray *links = [NSMutableArray array]; NSMutableArray *textComponents = nil; if (self.highlighted) textComponents = self.highlightedTextComponents; else textComponents = self.textComponents; for (RTLabelComponent *component in textComponents) { long index = [textComponents indexOfObject:component]; component.componentIndex = index; if ([component.tagLabel caseInsensitiveCompare:@"i"] == NSOrderedSame) { // make font italic [self applyItalicStyleToText:attrString atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"b"] == NSOrderedSame) { // make font bold [self applyBoldStyleToText:attrString atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"bi"] == NSOrderedSame) { [self applyBoldItalicStyleToText:attrString atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"a"] == NSOrderedSame) { if (self.currentSelectedButtonComponentIndex==index) { if (self.selectedLinkAttributes) { [self applyFontAttributes:self.selectedLinkAttributes toText:attrString atPosition:component.position withLength:[component.text length]]; } else { [self applyBoldStyleToText:attrString atPosition:component.position withLength:[component.text length]]; [self applyColor:@"#FF0000" toText:attrString atPosition:component.position withLength:[component.text length]]; } } else { if (self.linkAttributes) { [self applyFontAttributes:self.linkAttributes toText:attrString atPosition:component.position withLength:[component.text length]]; } else { [self applyBoldStyleToText:attrString atPosition:component.position withLength:[component.text length]]; [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) { // underline if ([component.tagLabel caseInsensitiveCompare:@"u"] == NSOrderedSame) { [self applySingleUnderlineText:attrString atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"uu"] == NSOrderedSame) { [self applyDoubleUnderlineText:attrString atPosition:component.position withLength:[component.text length]]; } if ([component.attributes objectForKey:@"color"]) { NSString *value = [component.attributes objectForKey:@"color"]; [self applyUnderlineColor:value toText:attrString atPosition:component.position withLength:[component.text length]]; } } else if ([component.tagLabel caseInsensitiveCompare:@"font"] == NSOrderedSame) { [self applyFontAttributes:component.attributes toText:attrString atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"p"] == NSOrderedSame) { [self applyParagraphStyleToText:attrString attributes:component.attributes atPosition:component.position withLength:[component.text length]]; } else if ([component.tagLabel caseInsensitiveCompare:@"center"] == NSOrderedSame) { [self applyCenterStyleToText:attrString attributes:component.attributes atPosition:component.position withLength:[component.text length]]; } } // Create the framesetter with the attributed string. CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString); CFRelease(attrString); // Initialize a rectangular path. CGMutablePathRef path = CGPathCreateMutable(); CGRect bounds = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height); CGPathAddRect(path, NULL, bounds); // Create the frame and draw it into the graphics context //CTFrameRef CTFrameRef frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0), path, NULL); CFRange range; CGSize constraint = CGSizeMake(self.frame.size.width, CGFLOAT_MAX); self.optimumSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, [self.plainText length]), nil, constraint, &range); if (self.currentSelectedButtonComponentIndex==-1) { // only check for linkable items the first time, not when it's being redrawn on button pressed for (RTLabelComponent *linkableComponents in links) { float height = 0.0; CFArrayRef frameLines = CTFrameGetLines(frame); for (CFIndex i=0; i(u_int16_t)(lineRange.location)) || (linkableComponents.position>=lineRange.location && linkableComponents.position" withString:@"\n"]; RTLabelExtractedComponent *component = [RTLabel extractTextStyleFromText:_highlightedText paragraphReplacement:self.paragraphReplacement]; [self setHighlightedTextComponents:component.textComponents]; } - (void)setText:(NSString *)text { _text = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"]; RTLabelExtractedComponent *component = [RTLabel extractTextStyleFromText:_text paragraphReplacement:self.paragraphReplacement]; [self setTextComponents:component.textComponents]; [self setPlainText:component.plainText]; [self setNeedsDisplay]; } - (void)setText:(NSString *)text extractedTextComponent:(RTLabelExtractedComponent*)extractedComponent { _text = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"]; [self setTextComponents:extractedComponent.textComponents]; [self setPlainText:extractedComponent.plainText]; [self setNeedsDisplay]; } - (void)setHighlightedText:(NSString *)text extractedTextComponent:(RTLabelExtractedComponent*)extractedComponent { _highlightedText = [text stringByReplacingOccurrencesOfString:@"
" withString:@"\n"]; [self setHighlightedTextComponents:extractedComponent.textComponents]; } // http://forums.macrumors.com/showthread.php?t=925312 // not accurate - (CGFloat)frameHeight:(CTFrameRef)theFrame { CFArrayRef lines = CTFrameGetLines(theFrame); CGFloat height = 0.0; CGFloat ascent, descent, leading; for (CFIndex index = 0; index < CFArrayGetCount(lines); index++) { CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, index); CTLineGetTypographicBounds(line, &ascent, &descent, &leading); height += (ascent + fabs(descent) + leading); } return ceilf(height); } - (void)dealloc { self.delegate = nil; } - (NSArray *)components { NSScanner *scanner = [NSScanner scannerWithString:self.text]; [scanner setCharactersToBeSkipped:nil]; NSMutableArray *components = [NSMutableArray array]; while (![scanner isAtEnd]) { NSString *currentComponent; BOOL foundComponent = [scanner scanUpToString:@"http" intoString:¤tComponent]; if (foundComponent) { [components addObject:currentComponent]; NSString *string; BOOL foundURLComponent = [scanner scanUpToString:@" " intoString:&string]; if (foundURLComponent) { // if last character of URL is punctuation, its probably not part of the URL NSCharacterSet *punctuationSet = [NSCharacterSet punctuationCharacterSet]; NSInteger lastCharacterIndex = string.length - 1; if ([punctuationSet characterIsMember:[string characterAtIndex:lastCharacterIndex]]) { // remove the punctuation from the URL string and move the scanner back string = [string substringToIndex:lastCharacterIndex]; [scanner setScanLocation:scanner.scanLocation - 1]; } [components addObject:string]; } } else { // first string is a link NSString *string; BOOL foundURLComponent = [scanner scanUpToString:@" " intoString:&string]; if (foundURLComponent) { [components addObject:string]; } } } return [components copy]; } + (RTLabelExtractedComponent*)extractTextStyleFromText:(NSString*)data paragraphReplacement:(NSString*)paragraphReplacement { NSScanner *scanner = nil; NSString *text = nil; NSString *tag = nil; NSMutableArray *components = [NSMutableArray array]; long last_position = 0; scanner = [NSScanner scannerWithString:data]; while (![scanner isAtEnd]) { [scanner scanUpToString:@"<" intoString:NULL]; [scanner scanUpToString:@">" intoString:&text]; NSString *delimiter = [NSString stringWithFormat:@"%@>", text]; long position = (long)[data rangeOfString:delimiter].location; if (position!=NSNotFound) { if ([delimiter rangeOfString:@""]; if ([text rangeOfString:@"=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; } } } } else { // start of tag NSArray *textComponents = [[text substringFromIndex:1] componentsSeparatedByString:@" "]; tag = [textComponents objectAtIndex:0]; //TPLOG(@"start of tag: %@", tag); NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; for (NSUInteger i=1; i<[textComponents count]; i++) { NSArray *pair = [[textComponents objectAtIndex:i] componentsSeparatedByString:@"="]; if ([pair count] > 0) { NSString *key = [[pair objectAtIndex:0] lowercaseString]; if ([pair count]>=2) { // Trim " charactere 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)]; [attributes setObject:value forKey:key]; } else if ([pair count]==1) { [attributes setObject:key forKey:key]; } } } RTLabelComponent *component = [RTLabelComponent componentWithString:nil tag:tag attributes:attributes]; component.position = position; [components addObject:component]; } last_position = position; } return [RTLabelExtractedComponent rtLabelExtractComponentsWithTextComponent:components plainText:data]; } - (void)parse:(NSString *)data valid_tags:(NSArray *)valid_tags { //use to strip the HTML tags from the data NSScanner *scanner = nil; NSString *text = nil; NSString *tag = nil; NSMutableArray *components = [NSMutableArray array]; //set up the scanner scanner = [NSScanner scannerWithString:data]; NSMutableDictionary *lastAttributes = nil; long last_position = 0; while([scanner isAtEnd] == NO) { //find start of tag [scanner scanUpToString:@"<" intoString:NULL]; //find end of tag [scanner scanUpToString:@">" intoString:&text]; NSMutableDictionary *attributes = nil; //get the name of the tag if([text rangeOfString:@"", text]; long position = (long)[data rangeOfString:delimiter].location; BOOL isEnd = [delimiter rangeOfString:@"" withString:@"\n"]; [self setTextComponents:[extractTextStyle objectForKey:@"textComponents"]]; [self setPlainText:[extractTextStyle objectForKey:@"plainText"]]; [self setNeedsDisplay]; } + (NSDictionary*)preExtractTextStyle:(NSString*)data { NSString* paragraphReplacement = @"\n"; RTLabelExtractedComponent *component = [RTLabel extractTextStyleFromText:data paragraphReplacement:paragraphReplacement]; return [NSDictionary dictionaryWithObjectsAndKeys:component.textComponents, @"textComponents", component.plainText, @"plainText", nil]; } @end