// // DTHTMLElement.m // CoreTextExtensions // // Created by Oliver Drobnik on 4/14/11. // Copyright 2011 Drobnik.com. All rights reserved. // #import "DTCoreText.h" #import "DTHTMLElement.h" #import "DTAnchorHTMLElement.h" #import "DTTextAttachmentHTMLElement.h" #import "DTBreakHTMLElement.h" #import "DTHorizontalRuleHTMLElement.h" #import "DTListItemHTMLElement.h" #import "DTStylesheetHTMLElement.h" #import "DTTextHTMLElement.h" #import "NSString+DTUtilities.h" #import "DTColorFunctions.h" #import "DTLog.h" @interface DTHTMLElement () @property (nonatomic, strong) NSString *linkGUID; // internal initializer - (id)initWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options; @end // global variables BOOL ___shouldUseiOS6Attributes = NO; NSDictionary *_classesForNames = nil; @implementation DTHTMLElement { NSSet *_CSSClassNamesToIgnoreForCustomAttributes; } + (void)initialize { // prevent calling from children if (self != [DTHTMLElement class]) { return; } // lookup table so that we quickly get the correct class to instantiate for special tags NSMutableDictionary *tmpDict = [[NSMutableDictionary alloc] init]; [tmpDict setObject:[DTAnchorHTMLElement class] forKey:@"a"]; [tmpDict setObject:[DTBreakHTMLElement class] forKey:@"br"]; [tmpDict setObject:[DTHorizontalRuleHTMLElement class] forKey:@"hr"]; [tmpDict setObject:[DTListItemHTMLElement class] forKey:@"li"]; [tmpDict setObject:[DTStylesheetHTMLElement class] forKey:@"style"]; [tmpDict setObject:[DTTextAttachmentHTMLElement class] forKey:@"img"]; [tmpDict setObject:[DTTextAttachmentHTMLElement class] forKey:@"object"]; [tmpDict setObject:[DTTextAttachmentHTMLElement class] forKey:@"video"]; [tmpDict setObject:[DTTextAttachmentHTMLElement class] forKey:@"iframe"]; _classesForNames = [tmpDict copy]; } + (instancetype)elementWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options { // look for specialized class Class class = [_classesForNames objectForKey:name]; if (!class) { // see if this is a custom attachment class Class attachmentClass = [DTTextAttachment registeredClassForTagName:name]; if (attachmentClass) { class = [DTTextAttachmentHTMLElement class]; } else { // use generic of none found class = [DTHTMLElement class]; } } DTHTMLElement *element = [[class alloc] initWithName:name attributes:attributes options:options]; return element; } - (instancetype)initWithName:(NSString *)name attributes:(NSDictionary *)attributes options:(NSDictionary *)options { // node does not need the options, but it needs the name and attributes self = [super initWithName:name attributes:attributes]; if (self) { } return self; } #pragma mark - Creating Attributed Strings - (NSDictionary *)attributesForAttributedStringRepresentation { NSMutableDictionary *tmpDict = [NSMutableDictionary dictionary]; // add text attachment if (_textAttachment) { #if TARGET_OS_IPHONE // need run delegate for sizing (only supported on iOS) CTRunDelegateRef embeddedObjectRunDelegate = createEmbeddedObjectRunDelegate(_textAttachment); [tmpDict setObject:CFBridgingRelease(embeddedObjectRunDelegate) forKey:(id)kCTRunDelegateAttributeName]; #endif // add attachment [tmpDict setObject:_textAttachment forKey:NSAttachmentAttributeName]; // remember original paragraphSpacing [tmpDict setObject:DTNSNumberFromCGFloat(self.paragraphStyle.paragraphSpacing) forKey:DTAttachmentParagraphSpacingAttribute]; } CTFontRef font = [_fontDescriptor newMatchingFont]; if (font) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES && __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_5_1 if (___useiOS6Attributes) { UIFont *uiFont = [UIFont fontWithCTFont:font]; [tmpDict setObject:uiFont forKey:NSFontAttributeName]; } else #endif { // __bridge since its already retained elsewhere [tmpDict setObject:(__bridge id)(font) forKey:(id)kCTFontAttributeName]; } // use this font to adjust the values needed for the run delegate during layout time [_textAttachment adjustVerticalAlignmentForFont:font]; CFRelease(font); } // add hyperlink if (_link) { [tmpDict setObject:_link forKey:DTLinkAttribute]; // add a GUID to group multiple glyph runs belonging to same link [tmpDict setObject:_linkGUID forKey:DTGUIDAttribute]; } // add anchor if (_anchorName) { [tmpDict setObject:_anchorName forKey:DTAnchorAttribute]; } // add strikout if applicable if (_strikeOut) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { [tmpDict setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSStrikethroughStyleAttributeName]; } else #endif { [tmpDict setObject:[NSNumber numberWithBool:YES] forKey:DTStrikeOutAttribute]; } } // set underline style if (_underlineStyle) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { [tmpDict setObject:[NSNumber numberWithInteger:NSUnderlineStyleSingle] forKey:NSUnderlineStyleAttributeName]; } else #endif { [tmpDict setObject:[NSNumber numberWithInteger:_underlineStyle] forKey:(id)kCTUnderlineStyleAttributeName]; } // we could set an underline color as well if we wanted, but not supported by HTML // [attributes setObject:(id)[DTImage redColor].CGColor forKey:(id)kCTUnderlineColorAttributeName]; } if (_textColor) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { [tmpDict setObject:_textColor forKey:NSForegroundColorAttributeName]; } else #endif { [tmpDict setObject:(id)[_textColor CGColor] forKey:(id)kCTForegroundColorAttributeName]; } } if (_backgroundColor) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { [tmpDict setObject:_backgroundColor forKey:NSBackgroundColorAttributeName]; } else #endif { [tmpDict setObject:(id)[_backgroundColor CGColor] forKey:DTBackgroundColorAttribute]; } } if (_superscriptStyle) { [tmpDict setObject:(id)[NSNumber numberWithInteger:_superscriptStyle] forKey:(id)kCTSuperscriptAttributeName]; } // add paragraph style if (_paragraphStyle) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { NSParagraphStyle *style = [self.paragraphStyle NSParagraphStyle]; [tmpDict setObject:style forKey:NSParagraphStyleAttributeName]; } else #endif { CTParagraphStyleRef newParagraphStyle = [self.paragraphStyle createCTParagraphStyle]; [tmpDict setObject:CFBridgingRelease(newParagraphStyle) forKey:(id)kCTParagraphStyleAttributeName]; } } // add shadow array if applicable if (_shadows) { #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { // only a single shadow supported NSDictionary *firstShadow = [_shadows objectAtIndex:0]; NSShadow *shadow = [[NSShadow alloc] init]; shadow.shadowOffset = [[firstShadow objectForKey:@"Offset"] CGSizeValue]; shadow.shadowColor = [firstShadow objectForKey:@"Color"]; shadow.shadowBlurRadius = [[firstShadow objectForKey:@"Blur"] floatValue]; [tmpDict setObject:shadow forKey:NSShadowAttributeName]; } else #endif { [tmpDict setObject:_shadows forKey:DTShadowsAttribute]; } } if (_letterSpacing) { NSNumber *letterSpacingNum = DTNSNumberFromCGFloat(_letterSpacing); #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { [tmpDict setObject:letterSpacingNum forKey:NSKernAttributeName]; } else #endif { [tmpDict setObject:letterSpacingNum forKey:(id)kCTKernAttributeName]; } } if (_headerLevel) { [tmpDict setObject:[NSNumber numberWithInteger:_headerLevel] forKey:DTHeaderLevelAttribute]; } if (_paragraphStyle.textLists) { [tmpDict setObject:_paragraphStyle.textLists forKey:DTTextListsAttribute]; } if (_paragraphStyle.textBlocks) { [tmpDict setObject:_paragraphStyle.textBlocks forKey:DTTextBlocksAttribute]; } if (_backgroundStrokeColor) { [tmpDict setObject:(id)[_backgroundStrokeColor CGColor] forKey:DTBackgroundStrokeColorAttribute]; } if (_backgroundStrokeWidth) { [tmpDict setObject:DTNSNumberFromCGFloat(_backgroundStrokeWidth) forKey:DTBackgroundStrokeWidthAttribute]; } if (_backgroundCornerRadius) { [tmpDict setObject:DTNSNumberFromCGFloat(_backgroundCornerRadius) forKey:DTBackgroundCornerRadiusAttribute]; } return tmpDict; } - (BOOL)needsOutput { @synchronized(self) { if ([self.childNodes count]) { for (DTHTMLElement *oneChild in self.childNodes) { // ignore children that have display:none if (oneChild.displayStyle == DTHTMLElementDisplayStyleNone) { continue; } if (!oneChild.didOutput) { return YES; } } return NO; } return YES; } } - (BOOL)_isNotChildOfList { DTHTMLElement *parent = self.parentElement; if (parent.displayStyle == DTHTMLElementDisplayStyleListItem) { return NO; } if (parent.displayStyle == DTHTMLElementDisplayStyleBlock) { if ([parent.name isEqualToString:@"ol"] || [parent.name isEqualToString:@"ul"]) { return NO; } } return YES; } // adds the attributes that have not been "dealt with" to the DTCustomAttributesAttribute - (void)_addCustomHTMLAttributesToAttributedString:(NSMutableAttributedString *)attributedString { NSSet *attributesToIgnore = [[self class] attributesToIgnoreForCustomAttributesAttribute]; NSRange entireString = NSMakeRange(0, [attributedString length]); [_attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { if ([attributesToIgnore containsObject:key]) return; if (_CSSClassNamesToIgnoreForCustomAttributes && [key isEqualToString:@"class"]) { NSMutableArray *classNamesToKeep = [NSMutableArray array]; NSArray *components = nil; if ([value isKindOfClass:[NSString class]]) { // split the class string components = [value componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; } else if ([value isKindOfClass:[NSArray class]]) { // already an array components = value; } for (NSString *oneClassName in components) { if (![_CSSClassNamesToIgnoreForCustomAttributes containsObject:oneClassName]) { [classNamesToKeep addObject:oneClassName]; } } if ([classNamesToKeep count]) { value = [classNamesToKeep componentsJoinedByString:@" "]; } else { return; // continue enumeration, class attribute would be empty } } // we preserve existing because they are from children [attributedString addHTMLAttribute:key value:value range:entireString replaceExisting:NO]; }]; } - (NSAttributedString *)attributedString { @synchronized(self) { if (_displayStyle == DTHTMLElementDisplayStyleNone || self.didOutput) { return nil; } NSDictionary *attributes = [self attributesForAttributedStringRepresentation]; NSMutableAttributedString *tmpString; if (_textAttachment) { // ignore text, use unicode object placeholder tmpString = [[NSMutableAttributedString alloc] initWithString:UNICODE_OBJECT_PLACEHOLDER attributes:attributes]; } else { // walk through children tmpString = [[NSMutableAttributedString alloc] init]; DTHTMLElement *previousChild = nil; for (DTHTMLElement *oneChild in self.childNodes) { if (oneChild.displayStyle == DTHTMLElementDisplayStyleNone) { continue; } // if previous node was inline and this child is block then we need a newline if (previousChild && previousChild.displayStyle == DTHTMLElementDisplayStyleInline) { if (oneChild.displayStyle == DTHTMLElementDisplayStyleBlock) { // trim off whitespace suffix while ([[tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet ignorableWhitespaceCharacterSet]]) { [tmpString deleteCharactersInRange:NSMakeRange([tmpString length]-1, 1)]; } // paragraph break [tmpString appendString:@"\n"]; } } NSAttributedString *nodeString = [oneChild attributedString]; if (nodeString) { if (!oneChild.containsAppleConvertedSpace) { // we already have a white space in the string so far if ([[tmpString string] hasSuffixCharacterFromSet:[NSCharacterSet ignorableWhitespaceCharacterSet]]) { // following e.g. a BR we don't want a space or NL NSCharacterSet *charactersToIgnore = [NSCharacterSet characterSetWithCharactersInString:@" \n\t"]; while ([[nodeString string] hasPrefixCharacterFromSet:charactersToIgnore]) { NSString *field = [nodeString attribute:DTFieldAttribute atIndex:0 effectiveRange:NULL]; // do not trim off field if ([field isEqualToString:DTListPrefixField]) { break; } // do not trim off HR character BOOL isHR = [[nodeString attribute:DTHorizontalRuleStyleAttribute atIndex:0 effectiveRange:NULL] boolValue]; if (isHR) { break; } nodeString = [nodeString attributedSubstringFromRange:NSMakeRange(1, [nodeString length]-1)]; } } } [tmpString appendAttributedString:nodeString]; } previousChild = oneChild; } } // block-level elements get space trimmed and a newline if (_displayStyle != DTHTMLElementDisplayStyleInline) { // trim off whitespace prefix while ([[tmpString string] hasPrefix:@" "]) { [tmpString deleteCharactersInRange:NSMakeRange(0, 1)]; } // trim off whitespace suffix while ([[tmpString string] hasSuffix:@" "]) { [tmpString deleteCharactersInRange:NSMakeRange([tmpString length]-1, 1)]; } if (![self.name isEqualToString:@"html"] && ![self.name isEqualToString:@"body"]) { if (![[tmpString string] hasSuffix:@"\n"]) { if ([tmpString length]) { // extend font and paragraph style with the \n [tmpString appendEndOfParagraph]; } else { // string is empty, need a new attributed string so that we have the attributes attributes = [self attributesForAttributedStringRepresentation]; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes]; [tmpString appendAttributedString:attributedString]; } } } } // make sure the last sub-paragraph of this has no less than the specified paragraph spacing of this element // e.g. last LI needs to inherit the margin-after of the UL if (self.displayStyle == DTHTMLElementDisplayStyleBlock && [tmpString length]>0) { // if this is latest if ([self _isNotChildOfList]) { NSRange paragraphRange = [[tmpString string] rangeOfParagraphAtIndex:[tmpString length]-1]; #if DTCORETEXT_SUPPORT_NS_ATTRIBUTES if (___useiOS6Attributes) { NSParagraphStyle *paraStyle = [tmpString attribute:NSParagraphStyleAttributeName atIndex:paragraphRange.location effectiveRange:NULL]; DTCoreTextParagraphStyle *paragraphStyle = [DTCoreTextParagraphStyle paragraphStyleWithNSParagraphStyle:paraStyle]; if (paragraphStyle.paragraphSpacing < self.paragraphStyle.paragraphSpacing) { paragraphStyle.paragraphSpacing = self.paragraphStyle.paragraphSpacing; // make new paragraph style NSParagraphStyle *newParaStyle = [paragraphStyle NSParagraphStyle]; #if DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX if (NSFoundationVersionNumber <= NSFoundationVersionNumber10_6_8) // less than OS X 10.7 and less than iOS 5 { // remove old (works around iOS 4.3 leak) [tmpString removeAttribute:NSParagraphStyleAttributeName range:paragraphRange]; } #endif // set new [tmpString addAttribute:NSParagraphStyleAttributeName value:newParaStyle range:paragraphRange]; } } else #endif { CTParagraphStyleRef paraStyle = (__bridge CTParagraphStyleRef)[tmpString attribute:(id)kCTParagraphStyleAttributeName atIndex:paragraphRange.location effectiveRange:NULL]; DTCoreTextParagraphStyle *paragraphStyle = [DTCoreTextParagraphStyle paragraphStyleWithCTParagraphStyle:paraStyle]; if (paragraphStyle.paragraphSpacing < self.paragraphStyle.paragraphSpacing) { paragraphStyle.paragraphSpacing = self.paragraphStyle.paragraphSpacing; // make new paragraph style CTParagraphStyleRef newParaStyle = [paragraphStyle createCTParagraphStyle]; #if DTCORETEXT_NEEDS_ATTRIBUTE_REPLACEMENT_LEAK_FIX if (NSFoundationVersionNumber <= NSFoundationVersionNumber10_6_8) // less than OS X 10.7 and less than iOS 5 { // remove old (works around iOS 4.3 leak) [tmpString removeAttribute:(id)kCTParagraphStyleAttributeName range:paragraphRange]; } #endif // set new [tmpString addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge_transfer id)newParaStyle range:paragraphRange]; } } } } // add the custom attributes if (_shouldProcessCustomHTMLAttributes) { [self _addCustomHTMLAttributesToAttributedString:tmpString]; } return tmpString; } } #pragma mark - Working with CSS Styles // decodes the edgeInsets for padding or margin - (BOOL)_parseEdgeInsetsFromStyleDictionary:(NSDictionary *)styles forAttributesWithPrefix:(NSString *)prefix writingDirection:(CTWritingDirection)writingDirection intoEdgeInsets:(DTEdgeInsets *)intoEdgeInsets { DTEdgeInsets edgeInsets = {0,0,0,0}; // preserve previous values in insets if (intoEdgeInsets) { edgeInsets = *intoEdgeInsets; } BOOL didModify = NO; if (![styles count]) { return didModify; } BOOL isWebKitAttribute = NO; if ([prefix hasPrefix:@"-webkit"]) { isWebKitAttribute = YES; } NSString *leftKey = @"-left"; if (isWebKitAttribute) { if (writingDirection==kCTWritingDirectionRightToLeft) { // RTL leftKey = @"-end"; } else { // LTR leftKey = @"-start"; } } NSString *rightKey = @"-right"; if (isWebKitAttribute) { if (writingDirection==kCTWritingDirectionRightToLeft) { // RTL rightKey = @"-start"; } else { // LTR rightKey = @"-end"; } } NSString *topKey = isWebKitAttribute?@"-before":@"-top"; NSString *bottomKey = isWebKitAttribute?@"-after":@"-bottom"; for (NSString *oneKey in styles) { if (![oneKey hasPrefix:prefix]) { continue; } NSString *attributeValue = [styles objectForKey:oneKey]; NSRange dashRange = [oneKey rangeOfString:@"-"]; if (dashRange.length) { if ([oneKey hasSuffix:leftKey]) { edgeInsets.left = [attributeValue pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; didModify = YES; } else if ([oneKey hasSuffix:bottomKey]) { edgeInsets.bottom = [attributeValue pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; didModify = YES; } else if ([oneKey hasSuffix:rightKey]) { edgeInsets.right = [attributeValue pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; didModify = YES; } else if ([oneKey hasSuffix:topKey]) { edgeInsets.top = [attributeValue pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; didModify = YES; } } else { // shortcut with multiple values edgeInsets = [attributeValue DTEdgeInsetsRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; didModify = YES; } } if (didModify && intoEdgeInsets) { *intoEdgeInsets = edgeInsets; } return didModify; } - (void)applyStyleDictionary:(NSDictionary *)styles { if (![styles count]) { return; } // keep that for later lookup _styles = styles; // writing direction NSString *directionStr = [_styles objectForKey:@"direction"]; if (directionStr) { if ([directionStr isEqualToString:@"rtl"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionRightToLeft; } else if ([directionStr isEqualToString:@"ltr"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; } else if ([directionStr isEqualToString:@"auto"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionNatural; // that's also default } else { // other values are invalid and will be ignored } } // register pseudo-selector contents self.beforeContent = [[_styles objectForKey:@"before:content"] stringByDecodingCSSContentAttribute]; NSString *fontSize = [styles objectForKey:@"font-size"]; if (fontSize) { // absolute sizes based on 12.0 CoreText default size, Safari has 16.0 if ([fontSize isEqualToString:@"smaller"]) { _fontDescriptor.pointSize /= 1.2f; } else if ([fontSize isEqualToString:@"larger"]) { _fontDescriptor.pointSize *= 1.2f; } else if ([fontSize isEqualToString:@"xx-small"]) { _fontDescriptor.pointSize = 9.0f * _textScale; } else if ([fontSize isEqualToString:@"x-small"]) { _fontDescriptor.pointSize = 10.0f * _textScale; } else if ([fontSize isEqualToString:@"small"]) { _fontDescriptor.pointSize = 13.0f * _textScale; } else if ([fontSize isEqualToString:@"medium"]) { _fontDescriptor.pointSize = 16.0f * _textScale; } else if ([fontSize isEqualToString:@"large"]) { _fontDescriptor.pointSize = 18.0f * _textScale; } else if ([fontSize isEqualToString:@"x-large"]) { _fontDescriptor.pointSize = 24.0f * _textScale; } else if ([fontSize isEqualToString:@"xx-large"]) { _fontDescriptor.pointSize = 32.0f * _textScale; } else if ([fontSize isEqualToString:@"-webkit-xxx-large"]) { _fontDescriptor.pointSize = 48.0f * _textScale; } else if ([fontSize isEqualToString:@"inherit"]) { _fontDescriptor.pointSize = self.parentElement.fontDescriptor.pointSize; } else if ([fontSize isCSSLengthValue]) { _fontDescriptor.pointSize = [fontSize pixelSizeOfCSSMeasureRelativeToCurrentTextSize:_currentTextSize textScale:_textScale]; } } NSString *color = [styles objectForKey:@"color"]; if (color) { self.textColor = DTColorCreateWithHTMLName(color); } NSString *bgColor = [styles objectForKey:@"background-color"]; if (bgColor) { self.backgroundColor = DTColorCreateWithHTMLName(bgColor); } NSString *floatString = [styles objectForKey:@"float"]; if (floatString) { if ([floatString isEqualToString:@"left"]) { _floatStyle = DTHTMLElementFloatStyleLeft; } else if ([floatString isEqualToString:@"right"]) { _floatStyle = DTHTMLElementFloatStyleRight; } else if ([floatString isEqualToString:@"none"]) { _floatStyle = DTHTMLElementFloatStyleNone; } } id fontFamilyStyle = [styles objectForKey:@"font-family"]; if (fontFamilyStyle) { NSArray *fontFamilies; if ([fontFamilyStyle isKindOfClass:[NSString class]]) { fontFamilies = [NSArray arrayWithObject:fontFamilyStyle]; } else if ([fontFamilyStyle isKindOfClass:[NSArray class]]) { fontFamilies = fontFamilyStyle; } BOOL foundFontFamily = NO; for (NSString *fontFamily in fontFamilies) { _fontDescriptor.fontFamily = fontFamily; // check if this is a known font family CTFontRef font = [_fontDescriptor newMatchingFont]; NSString *foundFamily = CFBridgingRelease(CTFontCopyFamilyName(font)); if ([foundFamily isEqualToString:fontFamily]) { foundFontFamily = YES; break; } NSString *lowercaseFontFamily = [fontFamily lowercaseString]; if ([lowercaseFontFamily rangeOfString:@"geneva"].length) { _fontDescriptor.fontFamily = @"Helvetica"; foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"cursive"].length) { _fontDescriptor.stylisticClass = kCTFontScriptsClass; _fontDescriptor.fontFamily = nil; foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"sans-serif"].length) { // too many matches (24) // fontDescriptor.stylisticClass = kCTFontSansSerifClass; _fontDescriptor.fontFamily = @"Helvetica"; foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"serif"].length) { // kCTFontTransitionalSerifsClass = Baskerville // kCTFontClarendonSerifsClass = American Typewriter // kCTFontSlabSerifsClass = Courier New // // strangely none of the classes yields Times _fontDescriptor.fontFamily = @"Times New Roman"; foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"fantasy"].length) { _fontDescriptor.fontFamily = @"Papyrus"; // only available on iPad foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"monospace"].length) { _fontDescriptor.monospaceTrait = YES; _fontDescriptor.fontFamily = @"Courier"; foundFontFamily = YES; } else if ([lowercaseFontFamily rangeOfString:@"times"].length) { _fontDescriptor.fontFamily = @"Times New Roman"; foundFontFamily = YES; } else if ([lowercaseFontFamily isEqualToString:@"inherit"]) { _fontDescriptor.fontFamily = self.parentElement.fontDescriptor.fontFamily; foundFontFamily = YES; } if (foundFontFamily) { break; } } if (!foundFontFamily) { // probably custom font registered in info.plist _fontDescriptor.fontFamily = [fontFamilies objectAtIndex:0]; } } NSString *fontStyle = [[styles objectForKey:@"font-style"] lowercaseString]; if (fontStyle) { // remove font name since this would cause font creation to ignore the trait _fontDescriptor.fontName = nil; if ([fontStyle isEqualToString:@"normal"]) { _fontDescriptor.italicTrait = NO; } else if ([fontStyle isEqualToString:@"italic"] || [fontStyle isEqualToString:@"oblique"]) { _fontDescriptor.italicTrait = YES; } else if ([fontStyle isEqualToString:@"inherit"]) { // nothing to do } } NSString *fontWeight = [[styles objectForKey:@"font-weight"] lowercaseString]; if (fontWeight) { // remove font name since this would cause font creation to ignore the trait _fontDescriptor.fontName = nil; if ([fontWeight isEqualToString:@"normal"]) { _fontDescriptor.boldTrait = NO; } else if ([fontWeight isEqualToString:@"bold"]) { _fontDescriptor.boldTrait = YES; } else if ([fontWeight isEqualToString:@"bolder"]) { _fontDescriptor.boldTrait = YES; } else if ([fontWeight isEqualToString:@"lighter"]) { _fontDescriptor.boldTrait = NO; } else { // can be 100 - 900 NSInteger value = [fontWeight intValue]; if (value<=600) { _fontDescriptor.boldTrait = NO; } else { _fontDescriptor.boldTrait = YES; } } } NSString *decoration = [[styles objectForKey:@"text-decoration"] lowercaseString]; if (decoration) { if ([decoration isEqualToString:@"underline"]) { self.underlineStyle = kCTUnderlineStyleSingle; } else if ([decoration isEqualToString:@"line-through"]) { self.strikeOut = YES; } else if ([decoration isEqualToString:@"none"]) { // remove all self.underlineStyle = kCTUnderlineStyleNone; self.strikeOut = NO; } else if ([decoration isEqualToString:@"overline"]) { DTLogInfo(@"Note: 'overline' text decoration not supported"); } else if ([decoration isEqualToString:@"blink"]) { DTLogInfo(@"Note: 'blink' text decoration not supported"); } else if ([decoration isEqualToString:@"inherit"]) { // nothing to do } } NSString *alignment = [[styles objectForKey:@"text-align"] lowercaseString]; if (alignment) { if ([alignment isEqualToString:@"left"]) { self.paragraphStyle.alignment = kCTLeftTextAlignment; } else if ([alignment isEqualToString:@"right"]) { self.paragraphStyle.alignment = kCTRightTextAlignment; } else if ([alignment isEqualToString:@"center"]) { self.paragraphStyle.alignment = kCTCenterTextAlignment; } else if ([alignment isEqualToString:@"justify"]) { self.paragraphStyle.alignment = kCTJustifiedTextAlignment; } else if ([alignment isEqualToString:@"inherit"]) { // nothing to do } } NSString *verticalAlignment = [[styles objectForKey:@"vertical-align"] lowercaseString]; if (verticalAlignment) { if ([verticalAlignment isEqualToString:@"sub"]) { self.superscriptStyle = -1; } else if ([verticalAlignment isEqualToString:@"super"]) { self.superscriptStyle = +1; } else if ([verticalAlignment isEqualToString:@"baseline"]) { self.superscriptStyle = 0; } else if ([verticalAlignment isEqualToString:@"inherit"]) { // nothing to do } else if ([verticalAlignment isEqualToString:@"text-top"]) { _textAttachmentAlignment = DTTextAttachmentVerticalAlignmentTop; } else if ([verticalAlignment isEqualToString:@"middle"]) { _textAttachmentAlignment = DTTextAttachmentVerticalAlignmentCenter; } else if ([verticalAlignment isEqualToString:@"text-bottom"]) { _textAttachmentAlignment = DTTextAttachmentVerticalAlignmentBottom; } else if ([verticalAlignment isEqualToString:@"baseline"]) { _textAttachmentAlignment = DTTextAttachmentVerticalAlignmentBaseline; } } NSString *letterSpacing = [[styles objectForKey:@"letter-spacing"] lowercaseString]; if (letterSpacing) { if ([letterSpacing isEqualToString:@"normal"]) { _letterSpacing = 0; } else if ([letterSpacing isEqualToString:@"inherit"]) { // no op, we already inherited it } else // interpret as length { _letterSpacing = [letterSpacing pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; } } // if there is a text attachment we transfer the aligment we got _textAttachment.verticalAlignment = _textAttachmentAlignment; id shadow = [styles objectForKey:@"text-shadow"]; if (shadow) { self.shadows = [shadow arrayOfCSSShadowsWithCurrentTextSize:_fontDescriptor.pointSize currentColor:_textColor]; } NSString *lineHeight = [[styles objectForKey:@"line-height"] lowercaseString]; if (lineHeight) { if ([lineHeight isEqualToString:@"normal"]) { self.paragraphStyle.lineHeightMultiple = 0.0; // default self.paragraphStyle.minimumLineHeight = 0.0; // default self.paragraphStyle.maximumLineHeight = 0.0; // default } else if ([lineHeight isEqualToString:@"inherit"]) { // no op, we already inherited it } else if ([lineHeight isNumeric]) { self.paragraphStyle.lineHeightMultiple = [lineHeight floatValue]; } else // interpret as length { CGFloat lineHeightValue = [lineHeight pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; self.paragraphStyle.minimumLineHeight = lineHeightValue; self.paragraphStyle.maximumLineHeight = lineHeightValue; } } NSString *fontVariantStr = [[styles objectForKey:@"font-variant"] lowercaseString]; if (fontVariantStr) { if ([fontVariantStr isEqualToString:@"small-caps"]) { _fontVariant = DTHTMLElementFontVariantSmallCaps; } else if ([fontVariantStr isEqualToString:@"inherit"]) { _fontVariant = DTHTMLElementFontVariantInherit; } else { _fontVariant = DTHTMLElementFontVariantNormal; } } NSString *widthString = [styles objectForKey:@"width"]; if (widthString && ![widthString isEqualToString:@"auto"]) { _size.width = [widthString pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; } NSString *heightString = [styles objectForKey:@"height"]; if (heightString && ![heightString isEqualToString:@"auto"]) { _size.height = [heightString pixelSizeOfCSSMeasureRelativeToCurrentTextSize:self.fontDescriptor.pointSize textScale:_textScale]; } NSString *whitespaceString = [styles objectForKey:@"white-space"]; if ([whitespaceString hasPrefix:@"pre"]) { _preserveNewlines = YES; } else { _preserveNewlines = NO; } NSString *displayString = [styles objectForKey:@"display"]; if (displayString) { if ([displayString isEqualToString:@"none"]) { _displayStyle = DTHTMLElementDisplayStyleNone; } else if ([displayString isEqualToString:@"block"]) { _displayStyle = DTHTMLElementDisplayStyleBlock; } else if ([displayString isEqualToString:@"inline"]) { _displayStyle = DTHTMLElementDisplayStyleInline; } else if ([displayString isEqualToString:@"list-item"]) { _displayStyle = DTHTMLElementDisplayStyleListItem; } else if ([displayString isEqualToString:@"table"]) { _displayStyle = DTHTMLElementDisplayStyleTable; } else if ([verticalAlignment isEqualToString:@"inherit"]) { // nothing to do } } NSString *borderColor = [styles objectForKey:@"border-color"]; if (borderColor) { self.backgroundStrokeColor = DTColorCreateWithHTMLName(borderColor); } NSString *borderWidth = [[styles objectForKey:@"border-width"] lowercaseString]; if (borderWidth) { _backgroundStrokeWidth = [borderWidth floatValue]; } else { _backgroundStrokeWidth = 0.0f; } NSString *cornerRadius = [[styles objectForKey:@"border-radius"] lowercaseString]; if (cornerRadius) { _backgroundCornerRadius = [cornerRadius floatValue]; } else { _backgroundCornerRadius = 0.0f; } BOOL needsTextBlock = (_backgroundColor!=nil || _backgroundStrokeColor!=nil || _backgroundCornerRadius > 0 || _backgroundStrokeWidth > 0); BOOL hasMargins = NO; NSString *allKeys = [[styles allKeys] componentsJoinedByString:@";"]; // there can only be padding if the word "margin" occurs in the styles keys if ([allKeys rangeOfString:@"-webkit-margin"].length) { hasMargins = ([self _parseEdgeInsetsFromStyleDictionary:styles forAttributesWithPrefix:@"-webkit-margin" writingDirection:self.paragraphStyle.baseWritingDirection intoEdgeInsets:&_margins] || hasMargins); } if ([allKeys rangeOfString:@"margin"].length) { hasMargins = ([self _parseEdgeInsetsFromStyleDictionary:styles forAttributesWithPrefix:@"margin" writingDirection:self.paragraphStyle.baseWritingDirection intoEdgeInsets:&_margins] || hasMargins); } BOOL hasPadding = NO; // there can only be padding if the word "padding" occurs in the styles keys if ([allKeys rangeOfString:@"-webkit-padding"].length) { hasPadding = ([self _parseEdgeInsetsFromStyleDictionary:styles forAttributesWithPrefix:@"-webkit-padding" writingDirection:self.paragraphStyle.baseWritingDirection intoEdgeInsets:&_padding] || hasPadding); } if ([allKeys rangeOfString:@"padding"].length) { hasPadding = ([self _parseEdgeInsetsFromStyleDictionary:styles forAttributesWithPrefix:@"padding" writingDirection:self.paragraphStyle.baseWritingDirection intoEdgeInsets:&_padding] || hasPadding); } if (hasPadding) { if ([self.name isEqualToString:@"ul"] || [self.name isEqualToString:@"ol"]) { _listIndent = _padding.left; _padding.left = 0; } // if we still have padding we need a block if (_padding.left>0 || _padding.right>0 || _padding.top>0 || _padding.bottom>0) { needsTextBlock = YES; } } if (_displayStyle == DTHTMLElementDisplayStyleBlock) { // we only care for margins of block level elements if (hasMargins) { self.paragraphStyle.paragraphSpacing = _margins.bottom; // we increase the inherited values for the time being self.paragraphStyle.headIndent += _margins.left; self.paragraphStyle.firstLineHeadIndent = self.paragraphStyle.headIndent; // tailIndent from right side is negative self.paragraphStyle.tailIndent -= _margins.right; } if (needsTextBlock) { // need a block DTTextBlock *newBlock = [[DTTextBlock alloc] init]; newBlock.padding = _padding; // transfer background color to block newBlock.backgroundColor = _backgroundColor; _backgroundColor = nil; if (self.paragraphStyle.textBlocks) { // make mutable version NSMutableArray *mutableBlocks = [self.paragraphStyle.textBlocks mutableCopy]; // add new block to the array [mutableBlocks addObject:newBlock]; // set non-mutable version self.paragraphStyle.textBlocks = [mutableBlocks copy]; } else { // didn't have any blocks before, start new array NSArray *blocks = [NSArray arrayWithObject:newBlock]; self.paragraphStyle.textBlocks = blocks; } } } else if (_displayStyle == DTHTMLElementDisplayStyleListItem) { self.paragraphStyle.paragraphSpacing = _margins.bottom; } } - (DTCSSListStyle *)listStyle { DTCSSListStyle *style = [[DTCSSListStyle alloc] initWithStyles:_styles]; NSString *startingIndex = [_attributes objectForKey:@"start"]; // set the starting index if there is one specified if (startingIndex) { style.startingItemNumber = [startingIndex integerValue]; } return style; } #pragma mark - Calulcating Properties - (id)valueForKeyPathWithInheritance:(NSString *)keyPath { id value = [self valueForKeyPath:keyPath]; // if property is not set we also go to parent if (!value && self.parentElement) { return [self.parentElement valueForKeyPathWithInheritance:keyPath]; } // enum properties have 0 for inherit if ([value isKindOfClass:[NSNumber class]]) { NSNumber *number = value; if (([number integerValue]==0) && self.parentElement) { return [self.parentElement valueForKeyPathWithInheritance:keyPath]; } } // string properties have 'inherit' for inheriting if ([value isKindOfClass:[NSString class]]) { NSString *string = value; if ([string isEqualToString:@"inherit"] && self.parentElement) { return [self.parentElement valueForKeyPathWithInheritance:keyPath]; } } // obviously not inherited return value; } #pragma mark - Working with HTML Attributes + (NSSet *)attributesToIgnoreForCustomAttributesAttribute { return [NSSet setWithObjects:@"style", @"dir", @"align", @"src", @"href", @"color", @"face", @"size", @"name", @"height", @"width", nil]; } - (NSString *)attributeForKey:(NSString *)key { return [_attributes objectForKey:key]; } - (void)inheritAttributesFromElement:(DTHTMLElement *)element { _fontDescriptor = [element.fontDescriptor copy]; _paragraphStyle = [element.paragraphStyle copy]; _headerLevel = element.headerLevel; _fontVariant = element.fontVariant; _underlineStyle = element.underlineStyle; _strikeOut = element.strikeOut; _superscriptStyle = element.superscriptStyle; _letterSpacing = element.letterSpacing; _shadows = [element.shadows copy]; _link = [element.link copy]; _anchorName = [element.anchorName copy]; _linkGUID = element.linkGUID; _textColor = element.textColor; _isColorInherited = YES; _preserveNewlines = element.preserveNewlines; _currentTextSize = element.currentTextSize; _textScale = element.textScale; _backgroundColor = element.backgroundColor; _backgroundStrokeColor = element.backgroundStrokeColor; _backgroundStrokeWidth = element.backgroundStrokeWidth; _backgroundCornerRadius = element.backgroundCornerRadius; // only inherit background-color from inline elements if (element.displayStyle == DTHTMLElementDisplayStyleInline || element.displayStyle == DTHTMLElementDisplayStyleListItem) { self.backgroundColor = element.backgroundColor; } _containsAppleConvertedSpace = element.containsAppleConvertedSpace; // we copy the link because we might need for it making the custom view _textAttachment.hyperLinkURL = element.link; } - (void)interpretAttributes { if (!_attributes) { // nothing to interpret return; } // transfer Apple Converted Space tag if ([[self attributeForKey:@"class"] isEqualToString:@"Apple-converted-space"]) { _containsAppleConvertedSpace = YES; } // detect writing direction if set NSString *directionStr = [self attributeForKey:@"dir"]; if (directionStr) { NSAssert(_paragraphStyle, @"Found dir attribute, but missing paragraph style on element"); if ([directionStr isEqualToString:@"rtl"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionRightToLeft; } else if ([directionStr isEqualToString:@"ltr"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionLeftToRight; } else if ([directionStr isEqualToString:@"auto"]) { _paragraphStyle.baseWritingDirection = NSWritingDirectionNatural; // that's also default } else { // other values are invalid and will be ignored } } // handles align="justify" NSString *align = [self attributeForKey:@"align"]; if (align) { if ([align isEqualToString: @"justify"]) { _paragraphStyle.alignment = kCTTextAlignmentJustified; } else if ([align isEqualToString: @"left"]) { _paragraphStyle.alignment = kCTTextAlignmentLeft; } else if ([align isEqualToString: @"center"]) { _paragraphStyle.alignment = kCTTextAlignmentCenter; } else if ([align isEqualToString: @"right"]) { _paragraphStyle.alignment = kCTTextAlignmentRight; } } } #pragma mark - Properties - (void)setTextColor:(DTColor *)textColor { if (_textColor != textColor) { _textColor = textColor; _isColorInherited = NO; } } - (DTHTMLElementFontVariant)fontVariant { if (_fontVariant == DTHTMLElementFontVariantInherit) { if (self.parentElement) { return self.parentElement.fontVariant; } return DTHTMLElementFontVariantNormal; } return _fontVariant; } - (void)setAttributes:(NSDictionary *)attributes { [super setAttributes:[attributes copy]]; // decode size contained in attributes, might be overridden later by CSS size _size = CGSizeMake([[self attributeForKey:@"width"] floatValue], [[self attributeForKey:@"height"] floatValue]); } - (void)setTextAttachment:(DTTextAttachment *)textAttachment { textAttachment.verticalAlignment = _textAttachmentAlignment; _textAttachment = textAttachment; // transfer link GUID _textAttachment.hyperLinkGUID = _linkGUID; // transfer size _textAttachment.displaySize = _size; } - (void)setLink:(NSURL *)link { _linkGUID = [NSString stringWithUUID]; _link = [link copy]; if (_textAttachment) { _textAttachment.hyperLinkGUID = _linkGUID; } } - (void)setDidOutput:(BOOL)didOutput { @synchronized(self) { _didOutput = didOutput; } } - (BOOL)didOutput { @synchronized(self) { return _didOutput; } } - (void)setCurrentTextSize:(CGFloat)currentTextSize { _currentTextSize = currentTextSize; } - (CGFloat)currentTextSize { CGFloat textSize = _currentTextSize; if ((_currentTextSize == 0) && self.parentElement) { textSize = self.parentElement.currentTextSize; } return textSize; } - (DTHTMLElement *)parentElement { return (DTHTMLElement *)self.parentNode; } @synthesize fontDescriptor = _fontDescriptor; @synthesize paragraphStyle = _paragraphStyle; @synthesize textColor = _textColor; @synthesize backgroundColor = _backgroundColor; @synthesize beforeContent = _beforeContent; @synthesize link = _link; @synthesize anchorName = _anchorName; @synthesize underlineStyle = _underlineStyle; @synthesize textAttachment = _textAttachment; @synthesize strikeOut = _strikeOut; @synthesize superscriptStyle = _superscriptStyle; @synthesize headerLevel = _headerLevel; @synthesize shadows = _shadows; @synthesize floatStyle = _floatStyle; @synthesize isColorInherited = _isColorInherited; @synthesize preserveNewlines = _preserveNewlines; @synthesize displayStyle = _displayStyle; @synthesize fontVariant = _fontVariant; @synthesize currentTextSize = _currentTextSize; @synthesize textScale = _textScale; @synthesize size = _size; @synthesize margins = _margins; @synthesize padding = _padding; @synthesize linkGUID = _linkGUID; @synthesize containsAppleConvertedSpace = _containsAppleConvertedSpace; @synthesize CSSClassNamesToIgnoreForCustomAttributes = _CSSClassNamesToIgnoreForCustomAttributes; @synthesize shouldProcessCustomHTMLAttributes = _shouldProcessCustomHTMLAttributes; @synthesize backgroundStrokeColor = _backgroundStrokeColor; @synthesize backgroundStrokeWidth = _backgroundStrokeWidth; @synthesize backgroundCornerRadius = _backgroundCornerRadius; @synthesize letterSpacing = _letterSpacing; @end