// // DTHTMLElement.m // DTCoreText (WeRead custom fork) // // Reverse-engineered pseudo-implementation from WeChat Reading binary. // Each DTHTMLElement node represents an HTML tag; it holds resolved style // properties and can produce an NSAttributedString via -attributedString. // // Key responsibilities: // 1. Store the tag name, attributes, and parent/child tree pointers. // 2. Apply CSS style dictionaries (from stylesheet or inline style). // 3. Resolve font descriptors and paragraph styles. // 4. Convert itself + children into NSAttributedString (recursive). // 5. Handle WeRead-specific page layout attributes. // #import "DTHTMLElement.h" #import "DTTextAttachment.h" #import "DTBorderStyle.h" #import "DTBackgroundImageStyle.h" #import "DTTableStyle.h" #import "DTCoreTextFontDescriptor.h" #import "DTCoreTextParagraphStyle.h" #import "DTCSSStylesheet.h" // DTCoreText standard attribute keys (defined elsewhere in DTCoreText) // extern NSString *const DTTextListsAttribute; // extern NSString *const DTStrikeOutAttribute; // extern NSString *const DTUnderlineStyleAttribute; // extern NSString *const DTLinkAttribute; // ... // Void elements: tags that have no closing tag and no children. static NSSet *_voidElements = nil; @implementation DTHTMLElement { // Mutable text accumulator used during parsing. NSMutableString *_textBuffer; } // -------------------------------------------------- # pragma mark Class initialization // -------------------------------------------------- + (void)initialize { if (self == [DTHTMLElement class]) { _voidElements = [NSSet setWithArray:@[ @"area", @"base", @"br", @"col", @"embed", @"hr", @"img", @"input", @"link", @"meta", @"param", @"source", @"track", @"wbr" ]]; } } // -------------------------------------------------- # pragma mark Initialization // -------------------------------------------------- - (instancetype)initWithTagName:(NSString *)tagName attributes:(NSDictionary *)attributes { self = [super init]; if (self) { _tagName = [tagName lowercaseString]; _attributes = [attributes copy] ?: @{}; _children = [NSMutableArray array]; // Parse id and class from attributes. _elementId = attributes[@"id"]; NSString *classStr = attributes[@"class"]; if (classStr) { _classNames = [classStr componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; } // Default display mode: block for most tags, inline for span/em/etc. _isBlockElement = [self _isDefaultBlockElement:_tagName]; } return self; } - (instancetype)init { return [self initWithTagName:nil attributes:nil]; } // -------------------------------------------------- # pragma mark Default block/inline classification // -------------------------------------------------- /** Returns YES for tags that are block-level by default in HTML. */ - (BOOL)_isDefaultBlockElement:(NSString *)tag { static NSSet *blockTags = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ blockTags = [NSSet setWithArray:@[ @"div", @"p", @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", @"blockquote", @"ul", @"ol", @"li", @"table", @"tr", @"td", @"th", @"thead", @"tbody", @"tfoot", @"section", @"article", @"header", @"footer", @"nav", @"main", @"aside", @"figure", @"figcaption", @"address", @"pre", @"hr", @"form", @"fieldset", @"dl", @"dt", @"dd" ]]; }); return [blockTags containsObject:tag]; } // -------------------------------------------------- # pragma mark Text content management // -------------------------------------------------- /** Appends character data to this element's text buffer. Called by the builder during SAX foundCharacters: events. */ - (void)appendText:(NSString *)text { if (!text || text.length == 0) return; if (!_textBuffer) { _textBuffer = [NSMutableString stringWithString:text]; } else { [_textBuffer appendString:text]; } _text = _textBuffer; } // -------------------------------------------------- # pragma mark Style application // -------------------------------------------------- /** Applies a CSS style dictionary to this element. This is the core style resolution method. It translates CSS property names into DTCoreText property objects (font descriptors, paragraph styles, colors). @param styleDict Dictionary of CSS property → value. @param isLatin YES if the book language is Latin-script (affects font fallback and line-height calculations). */ - (void)applyStyleDictionary:(NSDictionary *)styleDict isLatinLanguageBook:(BOOL)isLatin { if (!styleDict || styleDict.count == 0) return; _styleDictionary = styleDict; // ---- Font properties ---- [self _applyFontProperties:styleDict isLatin:isLatin]; // ---- Text properties ---- [self _applyTextProperties:styleDict]; // ---- Color properties ---- [self _applyColorProperties:styleDict]; // ---- Display / layout properties ---- [self _applyDisplayProperties:styleDict]; // ---- Margin / padding (for paragraph style) ---- [self _applyBoxModelProperties:styleDict]; // ---- WeRead-specific properties ---- [self _applyWeReadProperties:styleDict]; // ---- Border properties ---- [self _applyBorderProperties:styleDict]; // ---- Background properties ---- [self _applyBackgroundProperties:styleDict]; } // -------------------------------------------------- # pragma mark Font property resolution // -------------------------------------------------- - (void)_applyFontProperties:(NSDictionary *)dict isLatin:(BOOL)isLatin { // font-family NSString *fontFamily = dict[@"font-family"]; if (fontFamily) { // Strip quotes, handle generic families (serif, sans-serif, monospace). fontFamily = [fontFamily stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@" '\""]]; if (!_fontDescriptor) { _fontDescriptor = [[DTCoreTextFontDescriptor alloc] init]; } _fontDescriptor.fontFamily = fontFamily; } // font-size NSString *fontSizeStr = dict[@"font-size"]; if (fontSizeStr) { CGFloat size = [self _floatFromCSSValue:fontSizeStr]; if (size > 0) { if (!_fontDescriptor) { _fontDescriptor = [[DTCoreTextFontDescriptor alloc] init]; } _fontDescriptor.pointSize = size; } } // font-weight NSString *fontWeight = dict[@"font-weight"]; if (fontWeight) { if (!_fontDescriptor) { _fontDescriptor = [[DTCoreTextFontDescriptor alloc] init]; } if ([fontWeight isEqualToString:@"bold"] || [fontWeight integerValue] >= 700) { _fontDescriptor.boldTrait = YES; } } // font-style (italic / normal) NSString *fontStyle = dict[@"font-style"]; if (fontStyle) { if (!_fontDescriptor) { _fontDescriptor = [[DTCoreTextFontDescriptor alloc] init]; } if ([fontStyle isEqualToString:@"italic"] || [fontStyle isEqualToString:@"oblique"]) { _fontDescriptor.italicTrait = YES; } } } // -------------------------------------------------- # pragma mark Text property resolution // -------------------------------------------------- - (void)_applyTextProperties:(NSDictionary *)dict { // text-decoration NSString *decoration = dict[@"text-decoration"]; if (decoration) { // underline, line-through, none // stored for later attributed string construction } // text-align NSString *align = dict[@"text-align"]; if (align) { _textAlign = align; if (!_paragraphStyle) { _paragraphStyle = [[DTCoreTextParagraphStyle alloc] init]; } if ([align isEqualToString:@"center"]) { _paragraphStyle.textAlignment = kCTCenterTextAlignment; } else if ([align isEqualToString:@"right"]) { _paragraphStyle.textAlignment = kCTRightTextAlignment; } else if ([align isEqualToString:@"justify"]) { _paragraphStyle.textAlignment = kCTJustifiedTextAlignment; } else { _paragraphStyle.textAlignment = kCTLeftTextAlignment; } } // text-indent NSString *indent = dict[@"text-indent"]; if (indent) { if (!_paragraphStyle) { _paragraphStyle = [[DTCoreTextParagraphStyle alloc] init]; } _paragraphStyle.firstLineHeadIndent = [self _floatFromCSSValue:indent]; } // line-height NSString *lineHeight = dict[@"line-height"]; if (lineHeight) { if (!_paragraphStyle) { _paragraphStyle = [[DTCoreTextParagraphStyle alloc] init]; } _paragraphStyle.lineHeightMultiple = [self _floatFromCSSValue:lineHeight]; } // letter-spacing NSString *letterSpacing = dict[@"letter-spacing"]; if (letterSpacing) { // Applied as kern in the attributed string. } // white-space NSString *ws = dict[@"white-space"]; if (ws) { _whiteSpace = ws; } } // -------------------------------------------------- # pragma mark Color property resolution // -------------------------------------------------- - (void)_applyColorProperties:(NSDictionary *)dict { // color NSString *colorStr = dict[@"color"]; if (colorStr) { _textColor = [self _colorFromCSSValue:colorStr]; } // background-color NSString *bgColorStr = dict[@"background-color"]; if (bgColorStr) { _backgroundColor = [self _colorFromCSSValue:bgColorStr]; } } // -------------------------------------------------- # pragma mark Display / layout properties // -------------------------------------------------- - (void)_applyDisplayProperties:(NSDictionary *)dict { NSString *display = dict[@"display"]; if (display) { if ([display isEqualToString:@"block"] || [display isEqualToString:@"flex"] || [display isEqualToString:@"grid"]) { _isBlockElement = YES; } else if ([display isEqualToString:@"inline"] || [display isEqualToString:@"inline-block"]) { _isBlockElement = NO; } else if ([display isEqualToString:@"none"]) { // Element should be hidden; mark for skipping. } } // vertical-align NSString *vAlign = dict[@"vertical-align"]; if (vAlign) { // sub, super, top, middle, bottom, etc. } } // -------------------------------------------------- # pragma mark Box model (margin / padding) // -------------------------------------------------- - (void)_applyBoxModelProperties:(NSDictionary *)dict { if (!_paragraphStyle) { _paragraphStyle = [[DTCoreTextParagraphStyle alloc] init]; } // margin-top NSString *marginTop = dict[@"margin-top"]; if (marginTop) { _paragraphStyle.paragraphSpacingBefore = [self _floatFromCSSValue:marginTop]; } // margin-bottom NSString *marginBottom = dict[@"margin-bottom"]; if (marginBottom) { _paragraphStyle.paragraphSpacing = [self _floatFromCSSValue:marginBottom]; } // padding-left NSString *paddingLeft = dict[@"padding-left"]; if (paddingLeft) { _paragraphStyle.headIndent = [self _floatFromCSSValue:paddingLeft]; } } // -------------------------------------------------- # pragma mark WeRead-specific CSS properties // -------------------------------------------------- /** Handles WeRead's proprietary CSS attributes that control page-level layout. These are used by WeRead's paging engine (not standard web rendering). */ - (void)_applyWeReadProperties:(NSDictionary *)dict { // wr-vertical-center-style: vertically center content within a page. NSString *vCenter = dict[@"wr-vertical-center-style"]; if (vCenter) { _verticalCenterStyle = vCenter; } // weread-page-relate: marks element as related to page-level layout. NSString *pageRelate = dict[@"weread-page-relate"]; if (pageRelate) { _pageRelate = pageRelate; } // avoidPageBreakInside: prevent page breaks within this element. NSString *avoidBreak = dict[@"avoidPageBreakInside"]; if (avoidBreak) { _shouldAvoidPageBreakInside = YES; } // DTPageBreakAfter: force a page break after this element. NSString *breakAfter = dict[@"DTPageBreakAfter"]; if (breakAfter && ([breakAfter boolValue] || [breakAfter isEqualToString:@"always"])) { _pageBreakAfter = YES; } // DTPageBreakBefore: force a page break before this element. NSString *breakBefore = dict[@"DTPageBreakBefore"]; if (breakBefore && ([breakBefore boolValue] || [breakBefore isEqualToString:@"always"])) { _pageBreakBefore = YES; } // DTPageBackgroundColor: per-page background color (for styled pages). NSString *bgColor = dict[@"DTPageBackgroundColor"]; if (bgColor) { _pageBackgroundColor = [self _colorFromCSSValue:bgColor]; } // DTPageBackgroundImage: per-page background image URL/path. NSString *bgImage = dict[@"DTPageBackgroundImage"]; if (bgImage) { _pageBackgroundImage = bgImage; } } // -------------------------------------------------- # pragma mark Border properties // -------------------------------------------------- - (void)_applyBorderProperties:(NSDictionary *)dict { // border-width, border-style, border-color NSString *borderWidth = dict[@"border-width"]; NSString *borderStyle = dict[@"border-style"]; NSString *borderColor = dict[@"border-color"]; if (borderWidth || borderStyle || borderColor) { if (!_borderStyle) { _borderStyle = [[DTBorderStyle alloc] init]; } if (borderWidth) { _borderStyle.borderWidth = [self _floatFromCSSValue:borderWidth]; } if (borderColor) { _borderStyle.borderColor = [self _colorFromCSSValue:borderColor]; } } } // -------------------------------------------------- # pragma mark Background properties // -------------------------------------------------- - (void)_applyBackgroundProperties:(NSDictionary *)dict { NSString *bgImage = dict[@"background-image"]; if (bgImage && [bgImage hasPrefix:@"url("]) { // Extract URL from url('...') NSRange start = [bgImage rangeOfString:@"'"]; NSRange end = [bgImage rangeOfString:@"'" options:NSBackwardsSearch]; if (start.location != NSNotFound && end.location != NSNotFound) { NSString *urlStr = [bgImage substringWithRange: NSMakeRange(start.location + 1, end.location - start.location - 1)]; if (!_backgroundImageStyle) { _backgroundImageStyle = [[DTBackgroundImageStyle alloc] init]; } _backgroundImageStyle.imageURL = [NSURL URLWithString:urlStr]; } } } // -------------------------------------------------- # pragma mark Interpret attributes (finalization) // -------------------------------------------------- /** Called after the closing tag is encountered. Finalizes computed properties that depend on children. */ - (void)interpretAttributes { // For block elements, ensure a paragraph style exists. if (_isBlockElement && !_paragraphStyle) { _paragraphStyle = [[DTCoreTextParagraphStyle alloc] init]; } // Resolve font descriptor from tag name if not set by CSS. if (!_fontDescriptor) { _fontDescriptor = [self _defaultFontDescriptorForTag:_tagName]; } // Process children (recursive interpret). for (DTHTMLElement *child in _children) { [child interpretAttributes]; } } /** Returns a default font descriptor based on the HTML tag name. */ - (DTCoreTextFontDescriptor *)_defaultFontDescriptorForTag:(NSString *)tag { DTCoreTextFontDescriptor *desc = [[DTCoreTextFontDescriptor alloc] init]; if ([tag isEqualToString:@"b"] || [tag isEqualToString:@"strong"]) { desc.boldTrait = YES; } else if ([tag isEqualToString:@"i"] || [tag isEqualToString:@"em"]) { desc.italicTrait = YES; } else if ([tag isEqualToString:@"big"]) { desc.pointSize = 18.0; } else if ([tag isEqualToString:@"small"]) { desc.pointSize = 10.0; } else if ([tag isEqualToString:@"h1"]) { desc.pointSize = 24.0; desc.boldTrait = YES; } else if ([tag isEqualToString:@"h2"]) { desc.pointSize = 20.0; desc.boldTrait = YES; } else if ([tag isEqualToString:@"h3"]) { desc.pointSize = 16.0; desc.boldTrait = YES; } else if ([tag isEqualToString:@"h4"]) { desc.pointSize = 14.0; desc.boldTrait = YES; } else if ([tag isEqualToString:@"code"] || [tag isEqualToString:@"tt"] || [tag isEqualToString:@"pre"]) { desc.monospaceFamily = YES; } return desc; } // -------------------------------------------------- # pragma mark Attributed string generation // -------------------------------------------------- /** Recursively converts this element and all its children into an NSAttributedString. This is the core rendering method. Algorithm: 1. Create a mutable attributed string from this element's text content. 2. Apply font, color, paragraph style, and link attributes. 3. For each child, call -attributedString recursively and append. 4. Handle special elements (img, br, table, etc.). 5. Return the assembled string. */ - (NSAttributedString *)attributedString { NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init]; // Step 1: Handle void elements first. if ([self isVoidElement]) { return [self _attributedStringForVoidElement]; } // Step 2: Emit page break before marker if needed. if (_pageBreakBefore) { NSDictionary *breakAttrs = @{ DTPageBreakBeforeAttribute: @YES }; NSAttributedString *breakStr = [[NSAttributedString alloc] initWithString:@"
" // LINE SEPARATOR as page break marker attributes:breakAttrs]; [output appendAttributedString:breakStr]; } // Step 3: Process text content. if (_text && _text.length > 0) { NSAttributedString *textStr = [self _attributedStringForText:_text]; [output appendAttributedString:textStr]; } // Step 4: Process children recursively. for (DTHTMLElement *child in _children) { NSAttributedString *childStr = [child attributedString]; if (childStr) { [output appendAttributedString:childStr]; } } // Step 5: Wrap in paragraph style if this is a block element. if (_isBlockElement && output.length > 0) { [self _applyParagraphStyleToString:output]; } // Step 6: Emit page break after marker if needed. if (_pageBreakAfter) { NSDictionary *breakAttrs = @{ DTPageBreakAfterAttribute: @YES }; NSAttributedString *breakStr = [[NSAttributedString alloc] initWithString:@"
" attributes:breakAttrs]; [output appendAttributedString:breakStr]; } // Step 7: Apply WeRead page-level attributes. [self _applyWeReadPageAttributesToString:output]; return output; } // -------------------------------------------------- # pragma mark Text → NSAttributedString // -------------------------------------------------- /** Creates an NSAttributedString from text with the element's resolved styles. */ - (NSAttributedString *)_attributedStringForText:(NSString *)text { NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; // Font CTFontRef font = [_fontDescriptor newMatchingFont]; if (font) { attrs[(id)kCTFontAttributeName] = (__bridge id)font; CFRelease(font); } // Text color if (_textColor) { attrs[(id)kCTForegroundColorAttributeName] = (__bridge id)_textColor.CGColor; } // Background color (highlight) if (_backgroundColor) { attrs[@"DTBackgroundColor"] = _backgroundColor; } // Link if (_linkURL) { attrs[@"DTLink"] = _linkURL; } // Kern (letter-spacing) // if (_letterSpacing) { attrs[(id)kCTKernAttributeName] = ...; } return [[NSAttributedString alloc] initWithString:text attributes:attrs]; } // -------------------------------------------------- # pragma mark Void element handling // -------------------------------------------------- - (BOOL)isVoidElement { return [_voidElements containsObject:_tagName]; } /** Produces the attributed string for void elements (br, img, hr, etc.) */ - (NSAttributedString *)_attributedStringForVoidElement { if ([_tagName isEqualToString:@"br"]) { // Line break: insert newline with current paragraph style. NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; if (_paragraphStyle) { attrs[@"DTParagraphStyle"] = _paragraphStyle; } return [[NSAttributedString alloc] initWithString:@"\n" attributes:attrs]; } else if ([_tagName isEqualToString:@"img"]) { // Image: create a text attachment and wrap in attributed string. return [self _attributedStringForImage]; } else if ([_tagName isEqualToString:@"hr"]) { // Horizontal rule: treated as a paragraph separator. return [[NSAttributedString alloc] initWithString:@"\n"]; } return [[NSAttributedString alloc] initWithString:@""]; } /** Creates an NSAttributedString containing an image attachment. */ - (NSAttributedString *)_attributedStringForImage { if (!_textAttachment) { _textAttachment = [[DTTextAttachment alloc] init]; _textAttachment.contentURL = _imageURL; } // The attachment is represented by the Unicode object replacement character. unichar objectChar = 0xFFFC; NSString *objectStr = [NSString stringWithCharacters:&objectChar length:1]; NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; attrs[@"DTTextAttachment"] = _textAttachment; if (_linkURL) { attrs[@"DTLink"] = _linkURL; } return [[NSAttributedString alloc] initWithString:objectStr attributes:attrs]; } // -------------------------------------------------- # pragma mark Paragraph style application // -------------------------------------------------- /** Applies the element's paragraph style to the entire range of the string. */ - (void)_applyParagraphStyleToString:(NSMutableAttributedString *)str { if (!_paragraphStyle || str.length == 0) return; CTParagraphStyleRef ctStyle = [_paragraphStyle createCTParagraphStyle]; if (ctStyle) { [str addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)ctStyle range:NSMakeRange(0, str.length)]; CFRelease(ctStyle); } } // -------------------------------------------------- # pragma mark WeRead page attribute application // -------------------------------------------------- /** Applies WeRead-specific page layout attributes to the string. These attributes are consumed by WeRead's paging engine to control page breaks, backgrounds, and vertical centering. */ - (void)_applyWeReadPageAttributesToString:(NSMutableAttributedString *)str { if (str.length == 0) return; NSRange fullRange = NSMakeRange(0, str.length); // Vertical center style if (_verticalCenterStyle) { [str addAttribute:DTHTMLVerticalCenterAttribute value:_verticalCenterStyle range:fullRange]; } // Page relate if (_pageRelate) { [str addAttribute:DTPageRelateAttribute value:_pageRelate range:fullRange]; } // Avoid page break inside if (_shouldAvoidPageBreakInside) { [str addAttribute:DTPageBreakInsideAvoidAttribute value:@YES range:fullRange]; } // Page background color if (_pageBackgroundColor) { [str addAttribute:DTPageBackgroundColorAttribute value:_pageBackgroundColor range:fullRange]; } // Page background image if (_pageBackgroundImage) { [str addAttribute:DTPageBackgroundImageAttribute value:_pageBackgroundImage range:fullRange]; } } // -------------------------------------------------- # pragma mark CSS value parsing helpers // -------------------------------------------------- /** Converts a CSS length value string (e.g. "14px", "1.2em", "100%") into a CGFloat in points. */ - (CGFloat)_floatFromCSSValue:(NSString *)value { if (!value || value.length == 0) return 0.0; // Strip whitespace. value = [value stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; // Handle special values. if ([value isEqualToString:@"inherit"] || [value isEqualToString:@"auto"]) { return 0.0; } // Extract numeric part. NSString *numericPart = value; NSString *unit = @""; // Check for known units. NSArray *units = @[@"px", @"em", @"rem", @"pt", @"%", @"ex"]; for (NSString *u in units) { if ([value hasSuffix:u]) { numericPart = [value substringToIndex:value.length - u.length]; unit = u; break; } } CGFloat floatValue = [numericPart floatValue]; // Convert to points (simplified; real implementation handles em/% relative // to parent). if ([unit isEqualToString:@"em"] || [unit isEqualToString:@"rem"]) { // Assume 1em = parent font size (default 16px). floatValue *= 16.0; } else if ([unit isEqualToString:@"pt"]) { // 1pt = 1pt (no conversion needed on iOS). } else if ([unit isEqualToString:@"%"]) { // Percentage: caller must interpret relative to container. } // "px" → points (1:1 on non-retina, but iOS uses points natively). return floatValue; } /** Converts a CSS color value string to UIColor. Supports: - #RRGGBB hex notation - #RGB shorthand - rgb(r,g,b) functional notation - Named colors (red, blue, etc.) */ - (UIColor *)_colorFromCSSValue:(NSString *)cssValue { if (!cssValue || cssValue.length == 0) return nil; cssValue = [cssValue stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; // Hex colors if ([cssValue hasPrefix:@"#"]) { NSString *hex = [cssValue substringFromIndex:1]; // Expand shorthand #RGB → #RRGGBB if (hex.length == 3) { hex = [NSString stringWithFormat:@"%C%C%C%C%C%C", [hex characterAtIndex:0], [hex characterAtIndex:0], [hex characterAtIndex:1], [hex characterAtIndex:1], [hex characterAtIndex:2], [hex characterAtIndex:2]]; } if (hex.length == 6) { unsigned int rgb = 0; [[NSScanner scannerWithString:hex] scanHexInt:&rgb]; return [UIColor colorWithRed:((rgb >> 16) & 0xFF) / 255.0 green:((rgb >> 8) & 0xFF) / 255.0 blue:((rgb >> 0) & 0xFF) / 255.0 alpha:1.0]; } } // rgb(r,g,b) notation if ([cssValue hasPrefix:@"rgb("] || [cssValue hasPrefix:@"rgba("]) { NSString *inner = cssValue; inner = [inner stringByReplacingOccurrencesOfString:@"rgb(" withString:@""]; inner = [inner stringByReplacingOccurrencesOfString:@"rgba(" withString:@""]; inner = [inner stringByReplacingOccurrencesOfString:@")" withString:@""]; NSArray *components = [inner componentsSeparatedByString:@","]; if (components.count >= 3) { CGFloat r = [components[0] floatValue] / 255.0; CGFloat g = [components[1] floatValue] / 255.0; CGFloat b = [components[2] floatValue] / 255.0; CGFloat a = components.count >= 4 ? [components[3] floatValue] : 1.0; return [UIColor colorWithRed:r green:g blue:b alpha:a]; } } // Named colors static NSDictionary *namedColors = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ namedColors = @{ @"black" : [UIColor blackColor], @"white" : [UIColor whiteColor], @"red" : [UIColor redColor], @"green" : [UIColor greenColor], @"blue" : [UIColor blueColor], @"yellow" : [UIColor yellowColor], @"gray" : [UIColor grayColor], @"grey" : [UIColor grayColor], @"cyan" : [UIColor cyanColor], @"magenta" : [UIColor magentaColor], @"orange" : [UIColor orangeColor], @"purple" : [UIColor purpleColor], @"clear" : [UIColor clearColor], }; }); UIColor *named = namedColors[cssValue.lowercaseString]; if (named) return named; return nil; } @end