// // DTHTMLAttributedStringBuilder.m // DTCoreText // // Created by Oliver Drobnik on 21.01.12. // Copyright (c) 2012 Drobnik.com. All rights reserved. // #import "DTCoreText.h" #import "DTHTMLAttributedStringBuilder.h" #import "DTTextHTMLElement.h" #import "DTBreakHTMLElement.h" #import "DTStylesheetHTMLElement.h" #import "DTTextAttachmentHTMLElement.h" #import "DTLog.h" #if DEBUG_LOG_METRICS #import "NSString+DTFormatNumbers.h" #endif @interface DTHTMLAttributedStringBuilder () - (void)_registerTagStartHandlers; - (void)_registerTagEndHandlers; @end @implementation DTHTMLAttributedStringBuilder { NSData *_data; NSDictionary *_options; BOOL _shouldKeepDocumentNodeTree; // settings for parsing CGFloat _textScale; DTColor *_defaultLinkColor; DTCSSStylesheet *_globalStyleSheet; NSURL *_baseURL; DTCoreTextFontDescriptor *_defaultFontDescriptor; DTCoreTextParagraphStyle *_defaultParagraphStyle; // root node inherits these defaults DTHTMLElement *_defaultTag; // parsing state, accessed from inside blocks NSMutableAttributedString *_tmpString; // GCD dispatch_queue_t _stringAssemblyQueue; dispatch_group_t _stringAssemblyGroup; dispatch_queue_t _dataParsingQueue; dispatch_group_t _dataParsingGroup; dispatch_queue_t _treeBuildingQueue;; dispatch_group_t _treeBuildingGroup; // lookup table for blocks that deal with begin and end tags NSMutableDictionary *_tagStartHandlers; NSMutableDictionary *_tagEndHandlers; DTHTMLAttributedStringBuilderWillFlushCallback _willFlushCallback; BOOL _shouldProcessCustomHTMLAttributes; // new parsing DTHTMLElement *_rootNode; DTHTMLElement *_bodyElement; DTHTMLElement *_currentTag; BOOL _ignoreParseEvents; // ignores events from parser after first HTML tag was finished BOOL _ignoreInlineStyles; // ignores style blocks attached on elements } - (id)initWithHTML:(NSData *)data options:(NSDictionary *)options documentAttributes:(NSDictionary * __autoreleasing*)docAttributes { self = [super init]; if (self) { _data = data; _options = options; // documentAttributes ignored for now // GCD setup _stringAssemblyQueue = dispatch_queue_create("DTHTMLAttributedStringBuilder", 0); _stringAssemblyGroup = dispatch_group_create(); _dataParsingQueue = dispatch_queue_create("DTHTMLAttributedStringBuilderParser", 0); _dataParsingGroup = dispatch_group_create(); _treeBuildingQueue = dispatch_queue_create("DTHTMLAttributedStringBuilderParser Tree Queue", 0); _treeBuildingGroup = dispatch_group_create(); } return self; } - (void)dealloc { #if !OS_OBJECT_USE_OBJC dispatch_release(_stringAssemblyQueue); dispatch_release(_stringAssemblyGroup); dispatch_release(_dataParsingQueue); dispatch_release(_dataParsingGroup); dispatch_release(_treeBuildingQueue); dispatch_release(_treeBuildingGroup); #endif } - (BOOL)_buildString { #if DEBUG_LOG_METRICS // metrics: get start time CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); #endif // only with valid data if (![_data length]) { return NO; } // register default handlers [self _registerTagStartHandlers]; [self _registerTagEndHandlers]; // Specify the appropriate text encoding for the passed data, default is UTF8 NSString *textEncodingName = [_options objectForKey:NSTextEncodingNameDocumentOption]; NSStringEncoding encoding = NSUTF8StringEncoding; // default if (textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((__bridge CFStringRef)textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES // custom option to use iOS 6 attributes if running on iOS 6 if ([[_options objectForKey:DTUseiOS6Attributes] boolValue]) { #if TARGET_OS_IPHONE // NS-attributes only supported running on iOS 6.0 or greater if (floor(NSFoundationVersionNumber) >= DTNSFoundationVersionNumber_iOS_6_0) { ___useiOS6Attributes = YES; } else { ___useiOS6Attributes = NO; } #else // Mac generally supports it ___useiOS6Attributes = YES; #endif } #endif // custom option to scale text _textScale = [[_options objectForKey:NSTextSizeMultiplierDocumentOption] floatValue]; if (!_textScale) { _textScale = 1.0f; } // use baseURL from options if present _baseURL = [_options objectForKey:NSBaseURLDocumentOption]; // the combined style sheet for entire document _globalStyleSheet = [[DTCSSStylesheet defaultStyleSheet] copy]; // do we have a default style sheet passed as option? DTCSSStylesheet *defaultStylesheet = [_options objectForKey:DTDefaultStyleSheet]; if (defaultStylesheet) { // merge the default styles to the combined style sheet [_globalStyleSheet mergeStylesheet:defaultStylesheet]; } // for performance reasons we will return this mutable string _tmpString = [[NSMutableAttributedString alloc] init]; // base tag with font defaults _defaultFontDescriptor = [[DTCoreTextFontDescriptor alloc] initWithFontAttributes:nil]; // set the default font size CGFloat defaultFontSize = 12.0f; NSNumber *defaultFontSizeNumber = [_options objectForKey:DTDefaultFontSize]; if (defaultFontSizeNumber) { defaultFontSize = [defaultFontSizeNumber floatValue]; } _defaultFontDescriptor.pointSize = defaultFontSize * _textScale; NSString *defaultFontFamily = [_options objectForKey:DTDefaultFontFamily]; if (defaultFontFamily) { _defaultFontDescriptor.fontFamily = defaultFontFamily; } else { _defaultFontDescriptor.fontFamily = @"Times New Roman"; } NSString *defaultFontName = [_options objectForKey:DTDefaultFontName]; if (defaultFontName) { _defaultFontDescriptor.fontName = defaultFontName; } _defaultLinkColor = [_options objectForKey:DTDefaultLinkColor]; if (_defaultLinkColor) { if ([_defaultLinkColor isKindOfClass:[NSString class]]) { // convert from string to color _defaultLinkColor = DTColorCreateWithHTMLName((NSString *)_defaultLinkColor); } // get hex code for the passed color NSString *colorHex = DTHexStringFromDTColor(_defaultLinkColor); // overwrite the style NSString *styleBlock = [NSString stringWithFormat:@"a {color:#%@;}", colorHex]; [_globalStyleSheet parseStyleBlock:styleBlock]; } // default is to have A underlined NSNumber *linkDecorationDefault = [_options objectForKey:DTDefaultLinkDecoration]; if (linkDecorationDefault) { if (![linkDecorationDefault boolValue]) { // remove default decoration [_globalStyleSheet parseStyleBlock:@"a {text-decoration:none;}"]; } } DTColor *defaultLinkHighlightColor = [_options objectForKey:DTDefaultLinkHighlightColor]; if (defaultLinkHighlightColor) { if ([defaultLinkHighlightColor isKindOfClass:[NSString class]]) { // convert from string to color defaultLinkHighlightColor = DTColorCreateWithHTMLName((NSString *)defaultLinkHighlightColor); } // get hex code for the passed color NSString *colorHex = DTHexStringFromDTColor(defaultLinkHighlightColor); // overwrite the style NSString *styleBlock = [NSString stringWithFormat:@"a:active {color:#%@;}", colorHex]; [_globalStyleSheet parseStyleBlock:styleBlock]; } // default paragraph style _defaultParagraphStyle = [DTCoreTextParagraphStyle defaultParagraphStyle]; NSNumber *defaultLineHeightMultiplierNum = [_options objectForKey:DTDefaultLineHeightMultiplier]; if (defaultLineHeightMultiplierNum) { CGFloat defaultLineHeightMultiplier = [defaultLineHeightMultiplierNum floatValue]; _defaultParagraphStyle.lineHeightMultiple = defaultLineHeightMultiplier; } NSNumber *defaultTextAlignmentNum = [_options objectForKey:DTDefaultTextAlignment]; if (defaultTextAlignmentNum) { _defaultParagraphStyle.alignment = (CTTextAlignment)[defaultTextAlignmentNum integerValue]; } NSNumber *defaultFirstLineHeadIndent = [_options objectForKey:DTDefaultFirstLineHeadIndent]; if (defaultFirstLineHeadIndent) { _defaultParagraphStyle.firstLineHeadIndent = [defaultFirstLineHeadIndent integerValue]; } NSNumber *defaultHeadIndent = [_options objectForKey:DTDefaultHeadIndent]; if (defaultHeadIndent) { _defaultParagraphStyle.headIndent = [defaultHeadIndent integerValue]; } _defaultTag = [[DTHTMLElement alloc] init]; _defaultTag.fontDescriptor = _defaultFontDescriptor; _defaultTag.paragraphStyle = _defaultParagraphStyle; _defaultTag.textScale = _textScale; _defaultTag.currentTextSize = _defaultFontDescriptor.pointSize; #if DTCORETEXT_FIX_14684188 // workaround, only necessary while rdar://14684188 is not fixed _defaultTag.textColor = [UIColor blackColor]; #endif id defaultColor = [_options objectForKey:DTDefaultTextColor]; if (defaultColor) { if ([defaultColor isKindOfClass:[DTColor class]]) { // already a DTColor _defaultTag.textColor = defaultColor; } else { // need to convert first _defaultTag.textColor = DTColorCreateWithHTMLName(defaultColor); } } _shouldProcessCustomHTMLAttributes = [[_options objectForKey:DTProcessCustomHTMLAttributes] boolValue]; // ignore inline styles if option is passed _ignoreInlineStyles = [[_options objectForKey:DTIgnoreInlineStylesOption] boolValue]; // create a parser DTHTMLParser *parser = [[DTHTMLParser alloc] initWithData:_data encoding:encoding]; parser.delegate = (id)self; __block BOOL result; dispatch_group_async(_dataParsingGroup, _dataParsingQueue, ^{ result = [parser parse]; }); // wait until all string assembly is complete dispatch_group_wait(_dataParsingGroup, DISPATCH_TIME_FOREVER); dispatch_group_wait(_treeBuildingGroup, DISPATCH_TIME_FOREVER); dispatch_group_wait(_stringAssemblyGroup, DISPATCH_TIME_FOREVER); // clean up handlers because they retained self _tagStartHandlers = nil; _tagEndHandlers = nil; #if DEBUG_LOG_METRICS // metrics: get end time CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); // output metrics DTLogInfo((@"DTCoreText created string from %@ HTML in %.2f sec", [NSString stringByFormattingBytes:[_data length]], endTime-startTime); #endif return result; } - (NSAttributedString *)generatedAttributedString { if (!_tmpString) { [self _buildString]; } return _tmpString; } #pragma mark GCD - (void)_registerTagStartHandlers { if (_tagStartHandlers) { return; } _tagStartHandlers = [[NSMutableDictionary alloc] init]; void (^blockquoteBlock)(void) = ^ { _currentTag.paragraphStyle.headIndent += (CGFloat)25.0 * _textScale; _currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent; _currentTag.paragraphStyle.paragraphSpacing = _defaultFontDescriptor.pointSize; }; [_tagStartHandlers setObject:[blockquoteBlock copy] forKey:@"blockquote"]; void (^aBlock)(void) = ^ { if (_currentTag.isColorInherited || !_currentTag.textColor) { _currentTag.textColor = _defaultLinkColor; _currentTag.isColorInherited = NO; } // the name attribute of A becomes an anchor _currentTag.anchorName = [_currentTag attributeForKey:@"name"]; // remove line breaks and whitespace in links NSString *cleanString = [[_currentTag attributeForKey:@"href"] stringByReplacingOccurrencesOfString:@"\n" withString:@""]; cleanString = [cleanString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (![cleanString length]) { // no valid href return; } NSURL *link = [NSURL URLWithString:cleanString]; // deal with relative URL if (![link scheme]) { if ([cleanString length]) { link = [NSURL URLWithString:cleanString relativeToURL:_baseURL]; if (!link) { // NSURL did not like the link, so let's encode it cleanString = [cleanString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; link = [NSURL URLWithString:cleanString relativeToURL:_baseURL]; } } else { link = _baseURL; } } _currentTag.link = link; }; [_tagStartHandlers setObject:[aBlock copy] forKey:@"a"]; void (^listBlock)(void) = ^ { _currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent; // create the appropriate list style from CSS DTCSSListStyle *newListStyle = [_currentTag listStyle]; // append this list style to the current paragraph style text lists NSMutableArray *textLists = [_currentTag.paragraphStyle.textLists mutableCopy]; if (!textLists) { textLists = [NSMutableArray array]; } [textLists addObject:newListStyle]; _currentTag.paragraphStyle.textLists = textLists; }; [_tagStartHandlers setObject:[listBlock copy] forKey:@"ul"]; [_tagStartHandlers setObject:[listBlock copy] forKey:@"ol"]; void (^h1Block)(void) = ^ { _currentTag.headerLevel = 1; }; [_tagStartHandlers setObject:[h1Block copy] forKey:@"h1"]; void (^h2Block)(void) = ^ { _currentTag.headerLevel = 2; }; [_tagStartHandlers setObject:[h2Block copy] forKey:@"h2"]; void (^h3Block)(void) = ^ { _currentTag.headerLevel = 3; }; [_tagStartHandlers setObject:[h3Block copy] forKey:@"h3"]; void (^h4Block)(void) = ^ { _currentTag.headerLevel = 4; }; [_tagStartHandlers setObject:[h4Block copy] forKey:@"h4"]; void (^h5Block)(void) = ^ { _currentTag.headerLevel = 5; }; [_tagStartHandlers setObject:[h5Block copy] forKey:@"h5"]; void (^h6Block)(void) = ^ { _currentTag.headerLevel = 6; }; [_tagStartHandlers setObject:[h6Block copy] forKey:@"h6"]; void (^fontBlock)(void) = ^ { CGFloat pointSize; NSString *sizeAttribute = [_currentTag attributeForKey:@"size"]; if (sizeAttribute) { NSInteger sizeValue = [sizeAttribute intValue]; switch (sizeValue) { case 1: pointSize = _textScale * 10.0f; break; case 2: pointSize = _textScale * 13.0f; break; case 3: pointSize = _textScale * 16.0f; break; case 4: pointSize = _textScale * 18.0f; break; case 5: pointSize = _textScale * 24.0f; break; case 6: pointSize = _textScale * 32.0f; break; case 7: pointSize = _textScale * 48.0f; break; default: pointSize = _defaultFontDescriptor.pointSize; break; } } else { // size is inherited pointSize = _currentTag.fontDescriptor.pointSize; } NSString *face = [_currentTag attributeForKey:@"face"]; if (face) { // create a temp font with this face CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)face, pointSize, NULL); _currentTag.fontDescriptor = [DTCoreTextFontDescriptor fontDescriptorForCTFont:font]; CFRelease(font); } else { // modify inherited descriptor _currentTag.fontDescriptor.pointSize = pointSize; } NSString *color = [_currentTag attributeForKey:@"color"]; if (color) { _currentTag.textColor = DTColorCreateWithHTMLName(color); } }; [_tagStartHandlers setObject:[fontBlock copy] forKey:@"font"]; void (^pBlock)(void) = ^ { _currentTag.paragraphStyle.firstLineHeadIndent = _currentTag.paragraphStyle.headIndent + _defaultParagraphStyle.firstLineHeadIndent; }; [_tagStartHandlers setObject:[pBlock copy] forKey:@"p"]; } - (void)_registerTagEndHandlers { if (_tagEndHandlers) { return; } _tagEndHandlers = [[NSMutableDictionary alloc] init]; void (^objectBlock)(void) = ^ { if ([_currentTag isKindOfClass:[DTTextAttachmentHTMLElement class]]) { if ([_currentTag.textAttachment isKindOfClass:[DTObjectTextAttachment class]]) { DTObjectTextAttachment *objectAttachment = (DTObjectTextAttachment *)_currentTag.textAttachment; // transfer the child nodes to the attachment objectAttachment.childNodes = [_currentTag.childNodes copy]; } } }; [_tagEndHandlers setObject:[objectBlock copy] forKey:@"object"]; void (^videoBlock)(void) = ^ { if ([_currentTag isKindOfClass:[DTTextAttachmentHTMLElement class]]) { DTTextAttachmentHTMLElement *attachmentElement = (DTTextAttachmentHTMLElement *)_currentTag; if ([attachmentElement.textAttachment isKindOfClass:[DTVideoTextAttachment class]]) { DTVideoTextAttachment *videoAttachment = (DTVideoTextAttachment *)attachmentElement.textAttachment; // find first child that has a source if (!videoAttachment.contentURL) { for (DTHTMLElement *child in attachmentElement.childNodes) { if ([child.name isEqualToString:@"source"]) { NSString *src = [child attributeForKey:@"src"]; // content URL videoAttachment.contentURL = [NSURL URLWithString:src relativeToURL:_baseURL]; break; } } } } } }; [_tagEndHandlers setObject:[videoBlock copy] forKey:@"video"]; void (^styleBlock)(void) = ^ { DTCSSStylesheet *localSheet = [(DTStylesheetHTMLElement *)_currentTag stylesheet]; [_globalStyleSheet mergeStylesheet:localSheet]; }; [_tagEndHandlers setObject:[styleBlock copy] forKey:@"style"]; void (^linkBlock)(void) = ^ { NSString *href = [_currentTag attributeForKey:@"href"]; NSString *type = [[_currentTag attributeForKey:@"type"] lowercaseString]; if ([type isEqualToString:@"text/css"]) { NSURL *stylesheetURL = [NSURL URLWithString:href relativeToURL:_baseURL]; if ([stylesheetURL isFileURL]) { NSString *stylesheetContent = [NSString stringWithContentsOfURL:stylesheetURL encoding:NSUTF8StringEncoding error:nil]; if (stylesheetContent) { DTCSSStylesheet *localSheet = [[DTCSSStylesheet alloc] initWithStyleBlock:stylesheetContent]; [_globalStyleSheet mergeStylesheet:localSheet]; } } else { DTLogWarning(@"CSS link referencing a non-local target, ignored"); } } }; [ _tagEndHandlers setObject:[linkBlock copy] forKey:@"link"]; } #pragma mark DTHTMLParser Delegate - (void)parser:(DTHTMLParser *)parser didStartElement:(NSString *)elementName attributes:(NSDictionary *)attributeDict { dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{ if (_ignoreParseEvents) { return; } DTHTMLElement *newNode = [DTHTMLElement elementWithName:elementName attributes:attributeDict options:_options]; DTHTMLElement *previousLastChild = nil; if (_currentTag) { // inherit stuff [newNode inheritAttributesFromElement:_currentTag]; [newNode interpretAttributes]; previousLastChild = [_currentTag.childNodes lastObject]; // add as new child of current node [_currentTag addChildNode:newNode]; // remember body node if (!_bodyElement && [newNode.name isEqualToString:@"body"]) { _bodyElement = newNode; } if (_shouldProcessCustomHTMLAttributes) { newNode.shouldProcessCustomHTMLAttributes = _shouldProcessCustomHTMLAttributes; } } else { NSAssert(!_rootNode, @"Something went wrong, second root node found in document and not ignored."); // might be first node ever if (!_rootNode) { _rootNode = newNode; [_rootNode inheritAttributesFromElement:_defaultTag]; [_rootNode interpretAttributes]; } } // apply style from merged style sheet NSSet *matchedSelectors; NSDictionary *mergedStyles = [_globalStyleSheet mergedStyleDictionaryForElement:newNode matchedSelectors:&matchedSelectors ignoreInlineStyle:_ignoreInlineStyles]; if (mergedStyles) { [newNode applyStyleDictionary:mergedStyles]; // do not add the matched class names to 'class' custom attribute if (matchedSelectors) { NSMutableSet *classNamesToIgnoreForCustomAttributes = [NSMutableSet set]; for (NSString *oneSelector in matchedSelectors) { // class selectors have a period NSRange periodRange = [oneSelector rangeOfString:@"."]; if (periodRange.location != NSNotFound) { NSString *className = [oneSelector substringFromIndex:periodRange.location+1]; // add this to ignored classes [classNamesToIgnoreForCustomAttributes addObject:className]; } } if ([classNamesToIgnoreForCustomAttributes count]) { newNode.CSSClassNamesToIgnoreForCustomAttributes = classNamesToIgnoreForCustomAttributes; } } } // adding a block element eliminates previous trailing white space text node // because a new block starts on a new line if (previousLastChild && newNode.displayStyle != DTHTMLElementDisplayStyleInline) { if ([previousLastChild isKindOfClass:[DTTextHTMLElement class]]) { DTTextHTMLElement *textElement = (DTTextHTMLElement *)previousLastChild; if ([[textElement text] isIgnorableWhitespace]) { [_currentTag removeChildNode:textElement]; } } } _currentTag = newNode; // find block to execute for this tag if any void (^tagBlock)(void) = [_tagStartHandlers objectForKey:elementName]; if (tagBlock) { tagBlock(); } }); } - (void)parser:(DTHTMLParser *)parser didEndElement:(NSString *)elementName { dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{ @autoreleasepool { if (_ignoreParseEvents) { return; } // output the element if it is direct descendant of body tag, or close of body in case there are direct text nodes // find block to execute for this tag if any void (^tagBlock)(void) = [_tagEndHandlers objectForKey:elementName]; if (tagBlock) { tagBlock(); } if (_currentTag.displayStyle != DTHTMLElementDisplayStyleNone) { if (_currentTag == _bodyElement || _currentTag.parentElement == _bodyElement) { DTHTMLElement *theTag = _currentTag; dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{ // has children that have not been output yet if ([theTag needsOutput]) { // caller gets opportunity to modify tag before it is written if (_willFlushCallback) { _willFlushCallback(theTag); } NSAttributedString *nodeString = [theTag attributedString]; if (nodeString) { // if this is a block element then we need a paragraph break before it if (theTag.displayStyle != DTHTMLElementDisplayStyleInline) { if ([_tmpString length] && ![[_tmpString string] hasSuffix:@"\n"]) { // trim off whitespace while ([[_tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet ignorableWhitespaceCharacterSet]]) { [_tmpString deleteCharactersInRange:NSMakeRange([_tmpString length]-1, 1)]; } [_tmpString appendString:@"\n"]; } } [_tmpString appendAttributedString:nodeString]; theTag.didOutput = YES; if (!_shouldKeepDocumentNodeTree) { // we don't need the children any more [theTag removeAllChildNodes]; } } } }); } } while (![_currentTag.name isEqualToString:elementName]) { // missing end of element, attempt to recover _currentTag = [_currentTag parentElement]; } // closing the root node, ignore everything afterwards if (_currentTag == _rootNode) { _ignoreParseEvents = YES; } // go back up a level _currentTag = [_currentTag parentElement]; } }); } - (void)parser:(DTHTMLParser *)parser foundCharacters:(NSString *)string { dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{ if (_ignoreParseEvents) { return; } NSAssert(_currentTag, @"Cannot add text node without a current node"); if (!_currentTag.preserveNewlines && [string isIgnorableWhitespace]) { // ignore whitespace as first element of block element if (_currentTag.displayStyle!=DTHTMLElementDisplayStyleInline && ![_currentTag.childNodes count]) { return; } // ignore whitespace following a block element DTHTMLElement *previousTag = [_currentTag.childNodes lastObject]; if (previousTag.displayStyle != DTHTMLElementDisplayStyleInline) { return; } // ignore whitespace following a BR if ([previousTag isKindOfClass:[DTBreakHTMLElement class]]) { return; } } // adds a text node to the current node DTTextHTMLElement *textNode = [[DTTextHTMLElement alloc] init]; textNode.text = string; [textNode inheritAttributesFromElement:_currentTag]; [textNode interpretAttributes]; // save it for later output [_currentTag addChildNode:textNode]; DTHTMLElement *theTag = _currentTag; // text directly contained in body needs to be output right away if (theTag == _bodyElement) { dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{ [_tmpString appendAttributedString:[textNode attributedString]]; theTag.didOutput = YES; }); // only add it to current tag if we need it if (_shouldKeepDocumentNodeTree) { [theTag addChildNode:textNode]; } return; } }); } - (void)parser:(DTHTMLParser *)parser foundCDATA:(NSData *)CDATABlock { dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{ if (_ignoreParseEvents) { return; } NSAssert(_currentTag, @"Cannot add text node without a current node"); NSString *styleBlock = [[NSString alloc] initWithData:CDATABlock encoding:NSUTF8StringEncoding]; // adds a text node to the current node DTHTMLParserTextNode *textNode = [[DTHTMLParserTextNode alloc] initWithCharacters:styleBlock]; [_currentTag addChildNode:textNode]; }); } - (void)parserDidEndDocument:(DTHTMLParser *)parser { dispatch_group_async(_treeBuildingGroup, _treeBuildingQueue, ^{ NSAssert(!_currentTag, @"Something went wrong, at end of document there is still an open node"); dispatch_group_async(_stringAssemblyGroup, _stringAssemblyQueue, ^{ // trim off white space at end while ([[_tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]) { [_tmpString deleteCharactersInRange:NSMakeRange([_tmpString length]-1, 1)]; } }); }); } #pragma mark Properties @synthesize willFlushCallback = _willFlushCallback; @synthesize shouldKeepDocumentNodeTree = _shouldKeepDocumentNodeTree; @end