//
// DTHTMLAttributedStringBuilder.m
// DTCoreText (WeRead custom fork)
//
// Reverse-engineered pseudo-implementation from WeChat Reading binary.
// This file reconstructs the full HTML → NSAttributedString pipeline.
//
// Pipeline overview:
// 1. Parse raw HTML via DTHTMLParser (SAX-style).
// 2. Each SAX event (startElement / endElement / foundCharacters / CDATA)
// builds or mutates DTHTMLElement nodes in a DOM tree.
// 3. After parsing completes, -_buildString walks the DOM tree and calls
// -[DTHTMLElement attributedString] recursively.
// 4. The resulting NSAttributedString is stored in _generatedAttributedString.
//
#import "DTHTMLAttributedStringBuilder.h"
#import "DTHTMLParserDelegate.h"
#import "DTHTMLElement.h"
#import "DTCSSStylesheet.h"
#import "DTCoreTextFontDescriptor.h"
#import "DTCoreTextParagraphStyle.h"
#import "DTHTMLParser.h"
#import "WRBook.h"
#import "WRChapter.h"
// ---------------------------------------------------------------------------
// WeRead custom attribute key definitions
// ---------------------------------------------------------------------------
NSString *const DTPageBackgroundColorAttribute = @"DTPageBackgroundColorAttribute";
NSString *const DTPageBackgroundImageAttribute = @"DTPageBackgroundImageAttribute";
NSString *const DTPageBackgroundImagePathAttribute = @"DTPageBackgroundImagePathAttribute";
NSString *const DTPageBreakAfterAttribute = @"DTPageBreakAfterAttribute";
NSString *const DTPageBreakBeforeAttribute = @"DTPageBreakBeforeAttribute";
NSString *const DTPageBreakInsideAvoidAttribute = @"DTPageBreakInsideAvoidAttribute";
NSString *const DTPageRelateAttribute = @"DTPageRelateAttribute";
NSString *const DTPageSize = @"DTPageSize";
NSString *const DTPageFlippingStyle = @"DTPageFlippingStyle";
NSString *const DTHTMLVerticalCenterAttribute = @"DTHTMLVerticalCenterAttribute";
NSString *const DTHTMLTranslateTagAttribute = @"DTHTMLTranslateTagAttribute";
NSString *const DTHTMLTranslateNoStyleAttribute = @"DTHTMLTranslateNoStyleAttribute";
// ---------------------------------------------------------------------------
#pragma mark - Private interface
// ---------------------------------------------------------------------------
@interface DTHTMLAttributedStringBuilder ()
{
// ---- ivar: the SAX parser delegate that accumulates the DOM tree ----
DTHTMLParserDelegate *_parserDelegate;
// ---- Cached input ----
NSData *_htmlData;
DTCSSStylesheet *_cssStyleSheet;
NSDictionary *_options;
// ---- Result ----
NSAttributedString *_generatedAttributedString;
// ---- WeRead book/chapter context ----
WRBook *_book;
WRChapter *_chapter;
}
@end
// ---------------------------------------------------------------------------
#pragma mark - DTHTMLParserDelegate (internal helper)
// ---------------------------------------------------------------------------
// In the actual binary this is a separate class whose ivars include all the
// mutable state needed during parsing. We define it here to show the fields
// that DTHTMLAttributedStringBuilder delegates to.
@interface DTHTMLParserDelegate : NSObject
{
// Current tag handlers (block-based dispatch tables keyed on tag name)
NSData *_tagStartHandlers; // actually a block map
NSDictionary *_tagEndHandlers;
// CSS stylesheet applied during parsing
DTCSSStylesheet *_cssStyleSheet;
// Base URL for resolving relative links / images
NSURL *_baseURL;
// Default font / paragraph descriptors used when no CSS overrides exist
DTCoreTextFontDescriptor *_defaultFontDescriptor;
DTCoreTextParagraphStyle *_defaultParagraphStyle;
// The root of the DOM tree being built
DTHTMLElement *_rootElement;
// Stack of open elements (for nesting / parent resolution)
NSMutableDictionary *_elementStack; // index → DTHTMLElement
// Accumulated text for the current text run
NSMutableDictionary *_currentTextBuffer;
// "Current" element pointers (set during SAX traversal)
DTHTMLElement *_currentElement;
DTHTMLElement *_parentElement;
DTHTMLElement *_lastInlineElement;
DTHTMLElement *_lastBlockElement;
// WeRead-specific rendering context
WRBook *_book;
WRChapter *_chapter;
// Final output accumulator
NSMutableAttributedString *_outputString;
// Image / view references (for lazy image loading)
UIImageView *_currentImageView;
UIView *_imageContainerView;
UIView *_currentView;
NSString *_currentImageSrc;
UIView *_parentView;
UIView *_rootView;
// Collected tag list for post-processing
NSArray *_tagOrder;
}
// Methods used during SAX parsing
- (void)_registerTagStartHandlers;
- (void)_registerTagEndHandlers;
@end
@implementation DTHTMLParserDelegate
// --------------------------------------------------
# pragma mark Handler registration
// --------------------------------------------------
/**
Registers block handlers for HTML start tags.
Each handler receives the element name, attributes dict, and position,
then creates or configures the appropriate DTHTMLElement subclass.
WeRead registers custom handlers for their proprietary CSS attributes:
- wr-vertical-center-style → DTHTMLVerticalCenterAttribute
- weread-page-relate → DTPageRelateAttribute
- avoidPageBreakInside → DTPageBreakInsideAvoidAttribute
- DTPageBreakAfter / DTPageBreakBefore
- DTPageBackgroundColor / DTPageBackgroundImage
*/
- (void)_registerTagStartHandlers
{
// Pseudo-code: build a dictionary mapping tag names to handler blocks.
//
// _tagStartHandlers = @{
// @"p" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// @"div" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// @"img" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// @"br" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// @"a" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// @"h1" : ^(NSString *tag, NSDictionary *attrs, NSUInteger pos) { ... },
// ...
// // WeRead custom tags / attributes handled here
// };
//
// Each handler typically:
// 1. Creates a new DTHTMLElement.
// 2. Sets its tagName, attributes, parent.
// 3. Calls -[DTHTMLElement applyStyleDictionary:isLatinLanguageBook:].
// 4. Pushes the element onto _elementStack.
// 5. Updates _currentElement.
}
/**
Registers block handlers for HTML end tags.
Responsible for:
- Popping the element from the stack.
- Finalizing inline text runs.
- Applying paragraph-level styles.
- Handling DTPageBreakAfter / Before attributes.
*/
- (void)_registerTagEndHandlers
{
// Pseudo-code: analogous to start handlers but for closing tags.
//
// _tagEndHandlers = @{
// @"p" : ^(NSString *tag) { ... },
// @"div" : ^(NSString *tag) { ... },
// ...
// };
//
// Each handler typically:
// 1. Pops the current element from the stack.
// 2. Calls -[DTHTMLElement interpretAttributes] to finalize.
// 3. If element has DTPageBreakAfter, inserts page break marker.
// 4. Sets _currentElement back to the parent.
}
@end
// ---------------------------------------------------------------------------
#pragma mark - DTHTMLAttributedStringBuilder implementation
// ---------------------------------------------------------------------------
@implementation DTHTMLAttributedStringBuilder
// --------------------------------------------------
# pragma mark Initialization
// --------------------------------------------------
- (instancetype)initWithHTML:(NSData *)htmlData
options:(NSDictionary *)options
{
self = [super init];
if (self) {
_htmlData = [htmlData copy];
_options = [options copy] ?: @{};
// Create the parser delegate and configure it.
_parserDelegate = [[DTHTMLParserDelegate alloc] init];
// If a CSS stylesheet is provided in options, install it.
DTCSSStylesheet *sheet = _options[@"DTDefaultCSSStyleSheet"];
if (sheet) {
_cssStyleSheet = sheet;
_parserDelegate->_cssStyleSheet = sheet;
}
// Base URL for resolving
, , etc.
NSURL *baseURL = _options[@"DTBaseURL"];
if (baseURL) {
_parserDelegate->_baseURL = baseURL;
}
// Default font descriptor (system font fallback).
DTCoreTextFontDescriptor *fontDesc = _options[@"DTDefaultFontDescriptor"];
if (fontDesc) {
_parserDelegate->_defaultFontDescriptor = fontDesc;
}
// Default paragraph style.
DTCoreTextParagraphStyle *paraStyle = _options[@"DTDefaultParagraphStyle"];
if (paraStyle) {
_parserDelegate->_defaultParagraphStyle = paraStyle;
}
// Register the tag handler dispatch tables.
[_parserDelegate _registerTagStartHandlers];
[_parserDelegate _registerTagEndHandlers];
}
return self;
}
- (instancetype)initWithHTML:(NSData *)htmlData
cssStyleSheet:(DTCSSStylesheet *)styleSheet
options:(NSDictionary *)options
{
// Merge stylesheet into options so the designated init picks it up.
NSMutableDictionary *merged = [options mutableCopy] ?: [NSMutableDictionary dictionary];
if (styleSheet) {
merged[@"DTDefaultCSSStyleSheet"] = styleSheet;
}
return [self initWithHTML:htmlData options:merged];
}
// --------------------------------------------------
# pragma mark Build pipeline
// --------------------------------------------------
/**
Main entry point. Kicks off the SAX parser; when parsing completes the
delegate has built a DOM tree of DTHTMLElement nodes. Then -_buildString
converts that tree into the final NSAttributedString.
*/
- (void)buildString
{
// Step 1: Create a SAX parser and point it at our delegate.
DTHTMLParser *parser = [[DTHTMLParser alloc] initWithData:_htmlData];
parser.delegate = _parserDelegate;
// Step 2: Parse. This triggers the delegate callbacks below.
[parser parse];
// Step 3: Convert the DOM tree to an attributed string.
[self _buildString];
}
/**
Walks the DOM tree rooted at _parserDelegate->_rootElement and recursively
calls -[DTHTMLElement attributedString] to produce the final output.
*/
- (void)_buildString
{
DTHTMLElement *root = _parserDelegate->_rootElement;
if (!root) {
_generatedAttributedString = [[NSAttributedString alloc] initWithString:@""];
return;
}
// Recursively convert the DOM tree.
// DTHTMLElement's -attributedString walks children and concatenates.
NSAttributedString *result = [root attributedString];
// If the result is nil (empty document), produce an empty string.
if (!result) {
result = [[NSAttributedString alloc] initWithString:@""];
}
_generatedAttributedString = result;
// Store into the delegate's output for external access if needed.
_parserDelegate->_outputString = [result mutableCopy];
}
// --------------------------------------------------
# pragma mark DTHTMLParser delegate callbacks
// --------------------------------------------------
/**
Called by the SAX parser when an opening HTML tag is encountered.
@param parser The DTHTMLParser instance.
@param elementName Tag name (e.g. "p", "div", "img").
@param attributeDict Parsed attributes from the HTML tag.
@param position Character offset in the original HTML data.
*/
- (void)parser:(id)parser
didStartElement:(NSString *)elementName
attributes:(NSDictionary *)attributeDict
position:(NSUInteger)position
{
// Look up the registered handler block for this tag.
// If found, invoke it. Otherwise, fall through to default handling.
DTHTMLElement *newElement = [[DTHTMLElement alloc] initWithTagName:elementName
attributes:attributeDict];
// Set the parent to the current element on the stack.
DTHTMLElement *parent = _parserDelegate->_currentElement;
newElement.parent = parent;
[parent.children addObject:newElement];
// Push onto the element stack.
_parserDelegate->_currentElement = newElement;
// Apply inline style attribute (style="...") and any CSS rules
// matching this element.
NSDictionary *styleDict = [self _resolveStyleForElement:newElement
attributes:attributeDict];
if (styleDict) {
// Determine if this is a Latin-language book (affects font fallback).
BOOL isLatin = [_parserDelegate->_book isLatinLanguageBook];
[newElement applyStyleDictionary:styleDict isLatinLanguageBook:isLatin];
}
// Handle WeRead-specific custom CSS attributes.
[self _applyWeReadCustomAttributes:newElement fromAttributes:attributeDict];
// Handle special tags.
if ([elementName caseInsensitiveCompare:@"img"] == NSOrderedSame) {
[self _handleImageElement:newElement attributes:attributeDict];
}
else if ([elementName caseInsensitiveCompare:@"br"] == NSOrderedSame) {
[self _handleBRElement:newElement];
}
else if ([elementName caseInsensitiveCompare:@"a"] == NSOrderedSame) {
[self _handleAnchorElement:newElement attributes:attributeDict];
}
// Track last block vs. inline element for layout decisions.
if ([newElement isBlockElement]) {
_parserDelegate->_lastBlockElement = newElement;
} else {
_parserDelegate->_lastInlineElement = newElement;
}
}
/**
Called when character data is found between tags.
@param parser The DTHTMLParser instance.
@param string The character data.
@param position Character offset in the original HTML.
*/
- (void)parser:(id)parser
foundCharacters:(NSString *)string
position:(NSUInteger)position
{
if (!string || string.length == 0) {
return;
}
// Append to the current text buffer.
// The delegate accumulates text until a closing tag flushes it.
DTHTMLElement *current = _parserDelegate->_currentElement;
if (current) {
[current appendText:string];
}
}
/**
Called when a CDATA section is encountered (e.g. inside