ReadViewSDK/Doc/WXRead/decompiled/DTHTMLElement.m
2026-05-21 19:40:51 +08:00

918 lines
29 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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