热更新demo

CoreTextView.mm 26KB

    // // CoreTextView.m // // Created by Gilad Novik on 2013-01-10. // Copyright (c) 2013 Gilad Novik. // // Distributed under the permissive zlib License // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source distribution. // #import <objc/runtime.h> #import <libxml/HTMLparser.h> #import <CoreText/CoreText.h> #import "CoreTextView.h" #if! __has_feature(objc_arc) #error This file requires ARC. Please set it explicitly using the '-fobjc-arc' flag #endif @implementation CoreTextView { id m_frameSetter; NSMutableDictionary* m_links; } @synthesize attributedString=m_attributedString,contentInset=m_contentInset,delegate=m_delegate,debugBorders=m_debugBorders; -(id)initWithFrame:(CGRect)frame { if ((self=[super initWithFrame:frame])!=nil) { self.contentMode=UIViewContentModeRedraw; m_links=[[NSMutableDictionary alloc] initWithCapacity:2]; [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doLink:)]]; } return self; } -(void)awakeFromNib { [super awakeFromNib]; self.contentMode=UIViewContentModeRedraw; m_links=[[NSMutableDictionary alloc] initWithCapacity:2]; [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doLink:)]]; } - (void)drawRect:(CGRect)rect { [m_links removeAllObjects]; if (m_frameSetter==NULL) return; CGContextRef context=UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGContextSetTextMatrix(context,CGAffineTransformIdentity); CGContextConcatCTM(context, CGAffineTransformScale(CGAffineTransformMakeTranslation(0.0f,self.bounds.size.height),1.0f,-1.0f)); CGPathRef path=CGPathCreateWithRect(UIEdgeInsetsInsetRect(self.bounds,UIEdgeInsetsMake(m_contentInset.bottom, m_contentInset.left, m_contentInset.top, m_contentInset.right)),NULL); id frame=CFBridgingRelease(CTFramesetterCreateFrame((__bridge CTFramesetterRef)(m_frameSetter), CFRangeMake(0, 0), path, NULL)); CTFrameDraw((__bridge CTFrameRef)(frame), context); NSArray* lines=(__bridge NSArray*)CTFrameGetLines((__bridge CTFrameRef)(frame)); CGPoint* origins=(CGPoint*)alloca(sizeof(CGPoint)*lines.count); CTFrameGetLineOrigins((__bridge CTFrameRef)(frame), CFRangeMake(0, 0), origins); [lines enumerateObjectsUsingBlock:^(id line, NSUInteger lineIndex, BOOL* stop) { [(__bridge NSArray*)CTLineGetGlyphRuns((__bridge CTLineRef)(line)) enumerateObjectsUsingBlock:^(id run, NSUInteger index, BOOL* stop) { NSDictionary* attributes=(__bridge NSDictionary*)CTRunGetAttributes((__bridge CTRunRef)run); CGRect bounds; CGFloat ascent,descent; bounds.size.width=CTRunGetTypographicBounds((__bridge CTRunRef)run, CFRangeMake(0,0), &ascent, &descent, NULL); bounds=CGRectMake(origins[lineIndex].x+m_contentInset.left+CTLineGetOffsetForStringIndex((__bridge CTLineRef)(line), CTRunGetStringRange((__bridge CTRunRef)run).location, NULL), origins[lineIndex].y+m_contentInset.bottom-descent, bounds.size.width, ascent+descent); id refCon=(__bridge id)(CTRunDelegateGetRefCon((__bridge CTRunDelegateRef)([(__bridge NSDictionary*)CTRunGetAttributes((__bridge CTRunRef)run) valueForKey:(id)kCTRunDelegateAttributeName]))); if (m_debugBorders) { CGContextSetFillColorWithColor(context,[UIColor colorWithRed:1 green:0 blue:0 alpha:0.3].CGColor); CGContextFillRect(context, bounds); } if ([refCon conformsToProtocol:@protocol(HTMLRenderer)] && [refCon respondsToSelector:@selector(renderInContext:rect:)]) { CGContextSaveGState(context); [refCon renderInContext:context rect:bounds]; CGContextRestoreGState(context); } else if ([refCon isKindOfClass:[NSNumber class]]) // hr { bounds.size.width=UIEdgeInsetsInsetRect(self.bounds, m_contentInset).size.width; bounds.size.height=[refCon floatValue]+descent; bounds.origin.x=m_contentInset.left; bounds.origin.y-=CTFontGetSize((__bridge CTFontRef)[attributes valueForKey:(id)kCTFontAttributeName])/2.0f-ascent; CGContextSetFillColorWithColor(context,(__bridge CGColorRef)[attributes valueForKey:(id)kCTForegroundColorAttributeName]); CGContextFillRect(context, bounds); } else if (attributes[@"image"]) { CGContextDrawImage(context, bounds, [attributes[@"image"] CGImage]); } if (attributes[@"href"]) { [m_links setObject:attributes[@"href"] forKey:[NSValue valueWithCGRect:CGRectMake(bounds.origin.x, self.bounds.size.height-(bounds.origin.y+bounds.size.height)-descent, bounds.size.width, bounds.size.height)]]; } if (m_debugBorders) { CGContextSetFillColorWithColor(context,[UIColor blackColor].CGColor); CGContextStrokeRect(context, bounds); } }]; }]; CGPathRelease(path); CGContextRestoreGState(context); } -(void)setAttributedString:(NSAttributedString*)attributedString { if (m_attributedString==attributedString) return; m_frameSetter=(m_attributedString=attributedString)!=nil ? CFBridgingRelease(CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString)) : nil; [self setNeedsDisplay]; } -(CGSize)sizeThatFits:(CGSize)size { size=m_frameSetter ? CTFramesetterSuggestFrameSizeWithConstraints((__bridge CTFramesetterRef)(m_frameSetter), CFRangeMake(0, 0), NULL, CGSizeMake(size.width, CGFLOAT_MAX), NULL) : CGSizeZero; size.width+=m_contentInset.left+m_contentInset.right; size.height+=m_contentInset.top+m_contentInset.bottom; return CGSizeMake(ceilf(size.width), ceilf(size.height)); } -(void)doLink:(UIGestureRecognizer*)gesture { if (gesture.state!=UIGestureRecognizerStateRecognized) return; CGPoint point=[gesture locationInView:self]; [m_links enumerateKeysAndObjectsUsingBlock:^(NSValue* key, NSURL* url, BOOL* stop) { if (CGRectContainsPoint(key.CGRectValue, point)) { if ([m_delegate respondsToSelector:@selector(coreTextView:openURL:)] && [m_delegate coreTextView:self openURL:url]) return; [[UIApplication sharedApplication] openURL:url]; *stop=YES; } }]; } @end template<typename T> struct RunDelegateT : CTRunDelegateCallbacks { RunDelegateT() { version=kCTRunDelegateVersion1; getAscent=T::ascent; getDescent=T::descent; getWidth=T::width; dealloc=_dealloc; } CTRunDelegateRef create(id refCon) { return CTRunDelegateCreate(this,(void*)CFBridgingRetain(refCon)); } static CGFloat descent(void* ref) { return 0.0; } static CGFloat width(void* ref) { return 0.0f; } static void _dealloc(void* refCon) { CFBridgingRelease(refCon); } }; @implementation HTMLParser { htmlSAXHandler m_handler; NSMutableAttributedString* m_attributedString; NSMutableArray* m_style; CTParagraphStyleSetting m_paragraph[kCTParagraphStyleSpecifierCount]; } @synthesize rendererHandler=m_rendererHandler; -(id)init { if ((self=[super init])!=nil) { xmlSAX2InitHtmlDefaultSAXHandler(&m_handler); struct callbacks { static void startElement(HTMLParser* parser, const xmlChar* name,const xmlChar** atts) { [parser->m_style addObject:[NSMutableDictionary dictionaryWithDictionary:parser->m_style.lastObject]]; [parser->m_style.lastObject removeObjectsForKeys:@[@"width",@"height"]]; // this shoulb be specific to each tag if (xmlStrcasecmp(name,BAD_CAST"u")==0) { [parser->m_style.lastObject setValue:@(kCTUnderlineStyleSingle) forKey:(id)kCTUnderlineStyleAttributeName]; } else if (xmlStrcasecmp(name,BAD_CAST"s")==0) { [parser->m_style.lastObject setValue:@(3.0f) forKey:(id)kCTStrokeWidthAttributeName]; } else if (xmlStrcasecmp(name,BAD_CAST"a")==0) { [parser->m_style.lastObject setValue:(id)[UIColor blueColor].CGColor forKey:(id)kCTForegroundColorAttributeName]; [parser->m_style.lastObject setValue:@(kCTUnderlineStyleSingle) forKey:(id)kCTUnderlineStyleAttributeName]; } if (atts) { for (const xmlChar* key;(key=*atts++);) { const xmlChar* value=*atts++; if (xmlStrcasecmp(key,BAD_CAST"color")==0) { uint8_t r,g,b; // CGFloat a=1.0f; //修改64位warnning // if (sscanf((const char*)value, "#%2hhx%2hhx%2hhx",&r,&g,&b)==3 || sscanf((const char*)value, "rgb(%hhu,%hhu,%hhu)",&r,&g,&b)==3 || sscanf((const char*)value, "rgba(%hhu,%hhu,%hhu,%lf)",&r,&g,&b,&a)==4) float a=1.0f; if (sscanf((const char*)value, "#%2hhx%2hhx%2hhx",&r,&g,&b)==3 || sscanf((const char*)value, "rgb(%hhu,%hhu,%hhu)",&r,&g,&b)==3 || sscanf((const char*)value, "rgba(%hhu,%hhu,%hhu,%f)",&r,&g,&b,&a)==4) { UIColor* color=[UIColor colorWithRed:((CGFloat)r)/255.0f green:((CGFloat)g)/255.0f blue:((CGFloat)b)/255.0f alpha:a]; if (xmlStrcasecmp(name,BAD_CAST"s")==0) { [parser->m_style.lastObject setValue:(id)color.CGColor forKey:(id)kCTStrokeColorAttributeName]; } else { [parser->m_style.lastObject setValue:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName]; } } } else if (xmlStrcasecmp(key,BAD_CAST"size")==0) { [parser->m_style.lastObject setValue:@(strtod((const char*)value, NULL)) forKey:@"size"]; } else if (xmlStrcasecmp(key,BAD_CAST"traits")==0 || xmlStrcasecmp(key,BAD_CAST"image")==0) // reserved - don't allow to override { continue; } else if (xmlStrcasecmp(key,BAD_CAST"style")==0 && xmlStrcasecmp(name,BAD_CAST"u")==0) { if (xmlStrcasecmp(value,BAD_CAST"none")==0) { [parser->m_style.lastObject setValue:@(kCTUnderlineStyleNone) forKey:(id)kCTUnderlineStyleAttributeName]; } else if (xmlStrcasecmp(value,BAD_CAST"thick")==0) { [parser->m_style.lastObject setValue:@(kCTUnderlineStyleThick) forKey:(id)kCTUnderlineStyleAttributeName]; } else if (xmlStrcasecmp(value,BAD_CAST"double")==0) { [parser->m_style.lastObject setValue:@(kCTUnderlineStyleDouble) forKey:(id)kCTUnderlineStyleAttributeName]; } } else if (xmlStrcasecmp(key,BAD_CAST"width")==0 && xmlStrcasecmp(name,BAD_CAST"s")==0) { [parser->m_style.lastObject setValue:@(strtod((const char*)value, NULL)) forKey:(id)kCTStrokeWidthAttributeName]; } else if (xmlStrcasecmp(key,BAD_CAST"src")==0 && xmlStrcasecmp(name,BAD_CAST"a")==0) { [parser->m_style.lastObject setValue:[NSURL URLWithString:[NSString stringWithUTF8String:(const char*)value]] forKey:@"src"]; } else if (xmlStrcasecmp(key,BAD_CAST"src")==0 && xmlStrcasecmp(name,BAD_CAST"img")==0) { UIImage* image=nil; if (xmlStrstr(value,BAD_CAST"://")!=NULL) { if (xmlStrncasecmp(value,BAD_CAST"file://",7)==0) { image=[UIImage imageWithContentsOfFile:[[NSURL URLWithString:[NSString stringWithUTF8String:(const char*)value]] path]]; } else { [parser->m_style.lastObject setValue:[NSURL URLWithString:[NSString stringWithUTF8String:(const char*)value]] forKey:@"src"]; } } else if (xmlStrncasecmp(value,BAD_CAST"base64:",7)==0) { value+=7; static unsigned char base64[256] = { 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 62, 65, 65, 65, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 65, 65, 65, 65, 65, 65, 65, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 65, 65, 65, 65, 65, 65, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, }; NSMutableData* data=[NSMutableData dataWithLength:((xmlStrlen(value)+3)/4)*3]; uint8_t* output=(uint8_t*)data.mutableBytes; while (*value) { uint8_t accumulate[4]; size_t i=0; while (*value && i<4) { accumulate[i++]=base64[*value++]; } if(i >= 2) *output++ = (accumulate[0] << 2) | (accumulate[1] >> 4); if(i >= 3) *output++ = (accumulate[1] << 4) | (accumulate[2] >> 2); if(i >= 4) *output++ = (accumulate[2] << 6) | accumulate[3]; } [data setLength:output-(const uint8_t*)data.bytes]; image=[UIImage imageWithData:data]; } else { image=[UIImage imageNamed:[NSString stringWithUTF8String:(const char*)value]]; } [parser->m_style.lastObject setValue:image forKey:@"image"]; } else if (xmlStrcasecmp(key,BAD_CAST"href")==0) { if (xmlStrcasecmp(name,BAD_CAST"a")!=0) continue; NSString* href=[NSString stringWithUTF8String:(const char*)value]; NSURL* url=[NSURL URLWithString:href]; if (url.scheme==nil) { if (href.length && [[NSCharacterSet characterSetWithCharactersInString:@"+0123456789"] characterIsMember:[href characterAtIndex:0]]) url=[NSURL URLWithString:[@"tel://" stringByAppendingString:href]]; else url=[NSURL URLWithString:[@"http://" stringByAppendingString:href]]; } [parser->m_style.lastObject setValue:url forKey:@"href"]; } else if (xmlStrcasecmp(key,BAD_CAST"width")==0 || xmlStrcasecmp(key,BAD_CAST"width")==0 || xmlStrcasecmp(key,BAD_CAST"descent")==0) { [parser->m_style.lastObject setValue:@(strtof((const char*)value, NULL)) forKey:[NSString stringWithUTF8String:(const char*)key]]; } else { [parser->m_style.lastObject setValue:[NSString stringWithUTF8String:(const char*)value] forKey:[NSString stringWithUTF8String:(const char*)key]]; } } } if (xmlStrcasecmp(name,BAD_CAST"b")==0) { [parser->m_style.lastObject setValue:@([parser->m_style.lastObject[@"traits"] unsignedIntegerValue]|kCTFontBoldTrait) forKey:@"traits"]; } else if (xmlStrcasecmp(name,BAD_CAST"i")==0) { [parser->m_style.lastObject setValue:@([parser->m_style.lastObject[@"traits"] unsignedIntegerValue]|kCTFontItalicTrait) forKey:@"traits"]; } size_t paragraph=0; for (NSString* key in [parser->m_style.lastObject allKeys]) { NSString* value=[parser->m_style.lastObject valueForKey:key]; if ([key isEqualToString:@"align"]) { parser->m_paragraph[paragraph].spec=kCTParagraphStyleSpecifierAlignment; parser->m_paragraph[paragraph].value=alloca(parser->m_paragraph[paragraph].valueSize=sizeof(CTTextAlignment)); if ([value isEqualToString:@"left"]) *((CTTextAlignment*)parser->m_paragraph[paragraph].value)=kCTTextAlignmentLeft; else if ([value isEqualToString:@"right"]) *((CTTextAlignment*)parser->m_paragraph[paragraph].value)=kCTTextAlignmentRight; else if ([value isEqualToString:@"center"]) *((CTTextAlignment*)parser->m_paragraph[paragraph].value)=kCTTextAlignmentCenter; else if ([value isEqualToString:@"justified"]) *((CTTextAlignment*)parser->m_paragraph[paragraph].value)=kCTTextAlignmentJustified; else *((CTTextAlignment*)parser->m_paragraph[paragraph].value)=kCTTextAlignmentNatural; ++paragraph; } else if ([key isEqualToString:@"direction"]) { parser->m_paragraph[paragraph].spec=kCTParagraphStyleSpecifierBaseWritingDirection; parser->m_paragraph[paragraph].value=alloca(parser->m_paragraph[paragraph].valueSize=sizeof(CTWritingDirection)); if ([value isEqualToString:@"ltr"]) *((CTWritingDirection*)parser->m_paragraph[paragraph].value)=kCTWritingDirectionLeftToRight; else if ([value isEqualToString:@"rtl"]) *((CTWritingDirection*)parser->m_paragraph[paragraph].value)=kCTWritingDirectionRightToLeft; else *((CTWritingDirection*)parser->m_paragraph[paragraph].value)=kCTWritingDirectionNatural; ++paragraph; } else if ([key isEqualToString:@"wrap"]) { parser->m_paragraph[paragraph].spec=kCTParagraphStyleSpecifierLineBreakMode; parser->m_paragraph[paragraph].value=alloca(parser->m_paragraph[paragraph].valueSize=sizeof(CTLineBreakMode)); if ([value isEqualToString:@"break-word"]) *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByCharWrapping; else if ([value isEqualToString:@"clip"]) *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByClipping; else if ([value isEqualToString:@"ellipsis-head"]) *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByTruncatingHead; else if ([value isEqualToString:@"ellipsis-tail"] || [value isEqualToString:@"ellipsis"]) *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByTruncatingTail; else if ([value isEqualToString:@"ellipsis-middle"]) *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByTruncatingMiddle; else *((CTLineBreakMode*)parser->m_paragraph[paragraph].value)=kCTLineBreakByWordWrapping; ++paragraph; } else if ([key isEqualToString:@"font"]) { CTFontSymbolicTraits traits=[parser->m_style.lastObject[@"traits"] unsignedIntValue]; CGFloat size=[parser->m_style.lastObject[@"size"] floatValue]; NSString* name=[value stringByAppendingFormat:@"%c%c%g",(traits & kCTFontTraitBold) ? 'B' : '-',(traits & kCTFontTraitItalic) ? 'I' : '-',size]; static NSCache* cache=nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache=[[NSCache alloc] init]; }); id font=[cache objectForKey:name]; if (font==nil && (font=CFBridgingRelease(CTFontCreateWithFontDescriptor((__bridge CTFontDescriptorRef)(CFBridgingRelease(CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)(@{(id)kCTFontFamilyNameAttribute:value,(id)kCTFontTraitsAttribute:@{(id)kCTFontSymbolicTrait:@(traits)}})))),size,NULL)))==nil) { continue; } [parser->m_style.lastObject setValue:font forKey:(id)kCTFontAttributeName]; } } if (paragraph) { [parser->m_style.lastObject setValue:CFBridgingRelease(CTParagraphStyleCreate(parser->m_paragraph,paragraph)) forKey:(id)kCTParagraphStyleAttributeName]; } if (xmlStrcasecmp(name,BAD_CAST"br")==0) { [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"\r\n" attributes:parser->m_style.lastObject]]; } else if (xmlStrcasecmp(name,BAD_CAST"hr")==0) { struct hr_delegate : RunDelegateT<hr_delegate> { static CGFloat ascent(void* ref) { return [(__bridge NSNumber*)ref floatValue]; } }; NSString* height=[parser->m_style.lastObject valueForKey:@"height"]; NSMutableDictionary* attributes=[NSMutableDictionary dictionaryWithDictionary:parser->m_style.lastObject]; [attributes setValue:CFBridgingRelease(hr_delegate().create([NSNumber numberWithFloat:height ? height.floatValue : 1.0f])) forKey:(id)kCTRunDelegateAttributeName]; [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"\r" attributes:attributes]]; } else if (xmlStrcasecmp(name,BAD_CAST"img")==0) { struct image_delegate : RunDelegateT<image_delegate> { static CGFloat descent(void* ref) { NSMutableDictionary* style=(__bridge NSMutableDictionary*)ref; NSNumber* descent=style[@"descent"]; if (descent==nil) { descent=@(0.0f); CTFontRef font=(__bridge CTFontRef)style[(id)kCTFontAttributeName]; if (font) { if ([style[@"valign"] isEqualToString:@"middle"]) { descent=@((height(ref)-CTFontGetSize(font))/2.0f); } } [style setValue:descent forKey:@"descent"]; } return descent.floatValue; } static CGFloat ascent(void* ref) { return height(ref)-descent(ref); } static CGFloat width(void* ref) { NSMutableDictionary* style=(__bridge NSMutableDictionary*)ref; NSNumber* width=style[@"width"]; if (width==nil) { UIImage *image = style[@"image"]; width=@([image size].width); [style setValue:width forKey:@"width"]; } return width.floatValue; } static CGFloat height(void* ref) { NSMutableDictionary* style=(__bridge NSMutableDictionary*)ref; NSNumber* height=style[@"height"]; if (height==nil) { UIImage *image = style[@"image"]; height=@([image size].height); [style setValue:height forKey:@"height"]; } return height.floatValue; } }; NSMutableDictionary* attributes=[NSMutableDictionary dictionaryWithDictionary:parser->m_style.lastObject]; [attributes setValue:CFBridgingRelease(image_delegate().create(attributes)) forKey:(id)kCTRunDelegateAttributeName]; [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"\ufffc" attributes:attributes]]; } else if (xmlStrcasecmp(name,BAD_CAST"div")==0) { struct div_delegate : RunDelegateT<div_delegate> { static CGFloat descent(void* ref) { id<HTMLRenderer> renderer=(__bridge id<HTMLRenderer>)ref; return [renderer respondsToSelector:@selector(descent)] ? renderer.descent : 0.0f; } static CGFloat ascent(void* ref) { id<HTMLRenderer> renderer=(__bridge id<HTMLRenderer>)ref; return [renderer respondsToSelector:@selector(ascent)] ? renderer.ascent : (height(ref)-descent(ref)); } static CGFloat width(void* ref) { return ((__bridge id<HTMLRenderer>)ref).size.width; } static CGFloat height(void* ref) { return ((__bridge id<HTMLRenderer>)ref).size.height; } }; NSMutableDictionary* attributes=[NSMutableDictionary dictionaryWithDictionary:parser->m_style.lastObject]; id<HTMLRenderer> render=parser->m_rendererHandler ? parser->m_rendererHandler(attributes) : nil; if (render) { [attributes setValue:CFBridgingRelease(div_delegate().create(render)) forKey:(id)kCTRunDelegateAttributeName]; [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"\ufffc" attributes:attributes]]; } } } static void endElement(HTMLParser* parser, const xmlChar* name) { [parser->m_style removeLastObject]; } static void characters(HTMLParser* parser, const xmlChar *chars, int len) { [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:[[NSString alloc] initWithBytes:chars length:len encoding:NSUTF8StringEncoding] attributes:parser->m_style.lastObject]]; } static void endDocument(HTMLParser* parser) { if (parser->m_attributedString.length && [parser->m_attributedString attributesAtIndex:parser->m_attributedString.length-1 effectiveRange:NULL][(id)kCTRunDelegateAttributeName]!=nil) { // special case - need to add a dummy character or else we'll lose the latest item [parser->m_attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@"\r" attributes:nil]]; } } static void error(HTMLParser* parser, const char* msg, ...) { va_list va; va_start(va, msg); vfprintf(stderr, msg, va); va_end(va); } }; m_handler.startDocument=NULL; m_handler.endDocument=(endDocumentSAXFunc)callbacks::endDocument; m_handler.startElement=(startElementSAXFunc)callbacks::startElement; m_handler.endElement=(endElementSAXFunc)callbacks::endElement; m_handler.characters=(charactersSAXFunc)callbacks::characters; m_handler.comment=NULL; m_handler.cdataBlock=NULL; m_handler.error=(errorSAXFunc)callbacks::error; m_style=[NSMutableArray arrayWithCapacity:4]; } return self; } -(NSAttributedString*)parse:(NSString*)html { NSData* data=[html dataUsingEncoding:NSUTF8StringEncoding]; if (data) { htmlParserCtxtPtr context=htmlCreatePushParserCtxt(&m_handler, (__bridge void *)(self), (const char*)data.bytes, data.length, NULL, XML_CHAR_ENCODING_UTF8); if (context) { htmlCtxtUseOptions(context, HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING|HTML_PARSE_NONET|HTML_PARSE_COMPACT|HTML_PARSE_NOBLANKS); m_attributedString=[[NSMutableAttributedString alloc] init]; [m_style setArray:@[@{@"font":@"Helvetica",@"size":@([UIFont systemFontSize])}]]; if (htmlParseDocument(context)==0) { htmlFreeParserCtxt(context); return m_attributedString; } htmlFreeParserCtxt(context); } } return nil; } @end @implementation NSAttributedString (CTHTML) +(NSAttributedString*)attributedStringWithHTML:(NSString*)html { return [self attributedStringWithHTML:html renderer:nil]; } +(NSAttributedString*)attributedStringWithHTML:(NSString*)html renderer:(id<HTMLRenderer> (^)(NSMutableDictionary* attributes))renderer { HTMLParser* parser=[[HTMLParser alloc] init]; parser.rendererHandler=renderer; return [parser parse:html]; } @end