918 lines
29 KiB
Objective-C
918 lines
29 KiB
Objective-C
//
|
||
// 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
|