//
// 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