|
|
|
@@ -22,6 +22,9 @@ constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
|
|
|
|
const char* ITALIC_TAGS[] = {"i", "em"};
|
|
|
|
|
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
|
|
|
|
|
|
|
|
|
const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
|
|
|
|
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
|
|
|
|
|
|
|
|
|
|
const char* IMAGE_TAGS[] = {"img"};
|
|
|
|
|
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
|
|
|
|
|
|
|
|
@@ -40,17 +43,51 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isHeaderOrBlock(const char* name) {
|
|
|
|
|
return matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update effective bold/italic/underline based on block style and inline style stack
|
|
|
|
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|
|
|
|
// Start with block-level styles
|
|
|
|
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
|
|
|
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
|
|
|
|
effectiveUnderline =
|
|
|
|
|
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
|
|
|
|
|
|
|
|
|
// Apply inline style stack in order
|
|
|
|
|
for (const auto& entry : inlineStyleStack) {
|
|
|
|
|
if (entry.hasBold) {
|
|
|
|
|
effectiveBold = entry.bold;
|
|
|
|
|
}
|
|
|
|
|
if (entry.hasItalic) {
|
|
|
|
|
effectiveItalic = entry.italic;
|
|
|
|
|
}
|
|
|
|
|
if (entry.hasUnderline) {
|
|
|
|
|
effectiveUnderline = entry.underline;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// flush the contents of partWordBuffer to currentTextBlock
|
|
|
|
|
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|
|
|
|
// determine font style
|
|
|
|
|
// Determine font style from depth-based tracking and CSS effective style
|
|
|
|
|
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
|
|
|
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
|
|
|
|
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
|
|
|
|
|
|
|
|
|
// Combine style flags using bitwise OR
|
|
|
|
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
|
|
|
|
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
|
|
|
|
} else if (boldUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::BOLD;
|
|
|
|
|
} else if (italicUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::ITALIC;
|
|
|
|
|
if (isBold) {
|
|
|
|
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::BOLD);
|
|
|
|
|
}
|
|
|
|
|
if (isItalic) {
|
|
|
|
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
|
|
|
|
}
|
|
|
|
|
if (isUnderline) {
|
|
|
|
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// flush the buffer
|
|
|
|
|
partWordBuffer[partWordBufferIndex] = '\0';
|
|
|
|
|
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
|
|
|
@@ -58,17 +95,20 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// start a new text block if needed
|
|
|
|
|
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
|
|
|
|
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
|
|
|
|
if (currentTextBlock) {
|
|
|
|
|
// already have a text block running and it is empty - just reuse it
|
|
|
|
|
if (currentTextBlock->isEmpty()) {
|
|
|
|
|
currentTextBlock->setStyle(style);
|
|
|
|
|
// Merge with existing block style to accumulate CSS styling from parent block elements.
|
|
|
|
|
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
|
|
|
|
|
// div's margin should be preserved, even though it has no direct text content.
|
|
|
|
|
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
makePages();
|
|
|
|
|
}
|
|
|
|
|
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled));
|
|
|
|
|
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
|
|
|
@@ -80,13 +120,30 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract class and style attributes for CSS processing
|
|
|
|
|
std::string classAttr;
|
|
|
|
|
std::string styleAttr;
|
|
|
|
|
if (atts != nullptr) {
|
|
|
|
|
for (int i = 0; atts[i]; i += 2) {
|
|
|
|
|
if (strcmp(atts[i], "class") == 0) {
|
|
|
|
|
classAttr = atts[i + 1];
|
|
|
|
|
} else if (strcmp(atts[i], "style") == 0) {
|
|
|
|
|
styleAttr = atts[i + 1];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto centeredBlockStyle = BlockStyle();
|
|
|
|
|
centeredBlockStyle.textAlignDefined = true;
|
|
|
|
|
centeredBlockStyle.alignment = CssTextAlign::Center;
|
|
|
|
|
|
|
|
|
|
// Special handling for tables - show placeholder text instead of dropping silently
|
|
|
|
|
if (strcmp(name, "table") == 0) {
|
|
|
|
|
// Add placeholder text
|
|
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
|
|
|
self->startNewTextBlock(centeredBlockStyle);
|
|
|
|
|
|
|
|
|
|
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
|
|
|
|
// Advance depth before processing character data (like you would for a element with text)
|
|
|
|
|
// Advance depth before processing character data (like you would for an element with text)
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
|
|
|
|
|
|
|
|
@@ -111,9 +168,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|
|
|
|
|
|
|
|
|
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
|
|
|
|
|
|
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
|
|
|
self->startNewTextBlock(centeredBlockStyle);
|
|
|
|
|
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
|
|
|
|
// Advance depth before processing character data (like you would for a element with text)
|
|
|
|
|
// Advance depth before processing character data (like you would for an element with text)
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
self->characterData(userData, alt.c_str(), alt.length());
|
|
|
|
|
|
|
|
|
@@ -141,43 +198,113 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
|
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
|
|
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
// Compute CSS style for this element
|
|
|
|
|
CssStyle cssStyle;
|
|
|
|
|
if (self->cssParser) {
|
|
|
|
|
// Get combined tag + class styles
|
|
|
|
|
cssStyle = self->cssParser->resolveStyle(name, classAttr);
|
|
|
|
|
// Merge inline style (highest priority)
|
|
|
|
|
if (!styleAttr.empty()) {
|
|
|
|
|
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
|
|
|
|
cssStyle.applyOver(inlineStyle);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
|
|
|
|
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
|
|
|
|
|
const auto userAlignment = static_cast<CssTextAlign>(self->paragraphAlignment);
|
|
|
|
|
|
|
|
|
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
|
|
|
|
self->currentCssStyle = cssStyle;
|
|
|
|
|
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
|
|
|
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
|
|
|
|
if (strcmp(name, "br") == 0) {
|
|
|
|
|
if (self->partWordBufferIndex > 0) {
|
|
|
|
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
|
|
|
|
self->flushPartWordBuffer();
|
|
|
|
|
}
|
|
|
|
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
|
|
|
|
|
} else {
|
|
|
|
|
self->currentCssStyle = cssStyle;
|
|
|
|
|
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
|
|
|
|
|
if (strcmp(name, "li") == 0) {
|
|
|
|
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->startNewTextBlock(static_cast<TextBlock::Style>(self->paragraphAlignment));
|
|
|
|
|
if (strcmp(name, "li") == 0) {
|
|
|
|
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
|
|
|
|
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
|
|
|
|
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
|
|
|
|
// Push inline style entry for underline tag
|
|
|
|
|
StyleStackEntry entry;
|
|
|
|
|
entry.depth = self->depth; // Track depth for matching pop
|
|
|
|
|
entry.hasUnderline = true;
|
|
|
|
|
entry.underline = true;
|
|
|
|
|
if (cssStyle.hasFontWeight()) {
|
|
|
|
|
entry.hasBold = true;
|
|
|
|
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
|
|
|
|
if (cssStyle.hasFontStyle()) {
|
|
|
|
|
entry.hasItalic = true;
|
|
|
|
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
|
|
|
|
}
|
|
|
|
|
self->inlineStyleStack.push_back(entry);
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
|
|
|
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
|
|
|
|
// Push inline style entry for bold tag
|
|
|
|
|
StyleStackEntry entry;
|
|
|
|
|
entry.depth = self->depth; // Track depth for matching pop
|
|
|
|
|
entry.hasBold = true;
|
|
|
|
|
entry.bold = true;
|
|
|
|
|
if (cssStyle.hasFontStyle()) {
|
|
|
|
|
entry.hasItalic = true;
|
|
|
|
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
|
|
|
|
}
|
|
|
|
|
if (cssStyle.hasTextDecoration()) {
|
|
|
|
|
entry.hasUnderline = true;
|
|
|
|
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
|
|
|
}
|
|
|
|
|
self->inlineStyleStack.push_back(entry);
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
|
|
|
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
// Push inline style entry for italic tag
|
|
|
|
|
StyleStackEntry entry;
|
|
|
|
|
entry.depth = self->depth; // Track depth for matching pop
|
|
|
|
|
entry.hasItalic = true;
|
|
|
|
|
entry.italic = true;
|
|
|
|
|
if (cssStyle.hasFontWeight()) {
|
|
|
|
|
entry.hasBold = true;
|
|
|
|
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
|
|
|
|
}
|
|
|
|
|
if (cssStyle.hasTextDecoration()) {
|
|
|
|
|
entry.hasUnderline = true;
|
|
|
|
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
|
|
|
}
|
|
|
|
|
self->inlineStyleStack.push_back(entry);
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
} else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) {
|
|
|
|
|
// Handle span and other inline elements for CSS styling
|
|
|
|
|
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
|
|
|
|
StyleStackEntry entry;
|
|
|
|
|
entry.depth = self->depth; // Track depth for matching pop
|
|
|
|
|
if (cssStyle.hasFontWeight()) {
|
|
|
|
|
entry.hasBold = true;
|
|
|
|
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
|
|
|
|
}
|
|
|
|
|
if (cssStyle.hasFontStyle()) {
|
|
|
|
|
entry.hasItalic = true;
|
|
|
|
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
|
|
|
|
}
|
|
|
|
|
if (cssStyle.hasTextDecoration()) {
|
|
|
|
|
entry.hasUnderline = true;
|
|
|
|
|
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
|
|
|
|
}
|
|
|
|
|
self->inlineStyleStack.push_back(entry);
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unprocessed tag, just increasing depth and continue forward
|
|
|
|
@@ -239,17 +366,27 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|
|
|
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
|
|
|
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
|
|
|
|
|
|
|
|
|
if (self->partWordBufferIndex > 0) {
|
|
|
|
|
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
|
|
|
|
// We don't want to flush out content when closing inline tags like <span>.
|
|
|
|
|
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
|
|
|
|
|
// text styling needs to be overhauled to fix it.
|
|
|
|
|
const bool shouldBreakText =
|
|
|
|
|
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
|
|
|
|
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
|
|
|
|
strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
|
|
|
|
// Check if any style state will change after we decrement depth
|
|
|
|
|
// If so, we MUST flush the partWordBuffer with the CURRENT style first
|
|
|
|
|
// Note: depth hasn't been decremented yet, so we check against (depth - 1)
|
|
|
|
|
const bool willPopStyleStack =
|
|
|
|
|
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
|
|
|
|
|
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
|
|
|
|
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
|
|
|
|
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
|
|
|
|
|
|
|
|
|
if (shouldBreakText) {
|
|
|
|
|
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
|
|
|
|
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
|
|
|
|
|
|
|
|
|
// Flush buffer with current style BEFORE any style changes
|
|
|
|
|
if (self->partWordBufferIndex > 0) {
|
|
|
|
|
// Flush if style will change OR if we're closing a block/structural element
|
|
|
|
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
|
|
|
|
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
|
|
|
|
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
|
|
|
|
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
|
|
|
|
|
|
|
|
|
if (shouldFlush) {
|
|
|
|
|
self->flushPartWordBuffer();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -261,19 +398,40 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|
|
|
|
self->skipUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaving bold
|
|
|
|
|
// Leaving bold tag
|
|
|
|
|
if (self->boldUntilDepth == self->depth) {
|
|
|
|
|
self->boldUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaving italic
|
|
|
|
|
// Leaving italic tag
|
|
|
|
|
if (self->italicUntilDepth == self->depth) {
|
|
|
|
|
self->italicUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaving underline tag
|
|
|
|
|
if (self->underlineUntilDepth == self->depth) {
|
|
|
|
|
self->underlineUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pop from inline style stack if we pushed an entry at this depth
|
|
|
|
|
// This handles all inline elements: b, i, u, span, etc.
|
|
|
|
|
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
|
|
|
|
|
self->inlineStyleStack.pop_back();
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear block style when leaving header or block elements
|
|
|
|
|
if (headerOrBlockTag) {
|
|
|
|
|
self->currentCssStyle.reset();
|
|
|
|
|
self->updateEffectiveInlineStyle();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|
|
|
|
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
|
|
|
|
auto paragraphAlignmentBlockStyle = BlockStyle();
|
|
|
|
|
paragraphAlignmentBlockStyle.textAlignDefined = true;
|
|
|
|
|
paragraphAlignmentBlockStyle.alignment = static_cast<CssTextAlign>(this->paragraphAlignment);
|
|
|
|
|
startNewTextBlock(paragraphAlignmentBlockStyle);
|
|
|
|
|
|
|
|
|
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
|
|
|
|
int done;
|
|
|
|
@@ -362,7 +520,9 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
|
|
|
|
currentPageNextY = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentPage->elements.push_back(std::make_shared<PageLine>(line, 0, currentPageNextY));
|
|
|
|
|
// Apply horizontal left inset (margin + padding) as x position offset
|
|
|
|
|
const int16_t xOffset = line->getBlockStyle().leftInset();
|
|
|
|
|
currentPage->elements.push_back(std::make_shared<PageLine>(line, xOffset, currentPageNextY));
|
|
|
|
|
currentPageNextY += lineHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -378,10 +538,34 @@ void ChapterHtmlSlimParser::makePages() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
|
|
|
|
|
|
|
|
|
// Apply top spacing before the paragraph (stored in pixels)
|
|
|
|
|
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
|
|
|
|
if (blockStyle.marginTop > 0) {
|
|
|
|
|
currentPageNextY += blockStyle.marginTop;
|
|
|
|
|
}
|
|
|
|
|
if (blockStyle.paddingTop > 0) {
|
|
|
|
|
currentPageNextY += blockStyle.paddingTop;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate effective width accounting for horizontal margins/padding
|
|
|
|
|
const int horizontalInset = blockStyle.totalHorizontalInset();
|
|
|
|
|
const uint16_t effectiveWidth =
|
|
|
|
|
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
|
|
|
|
|
|
|
|
|
|
currentTextBlock->layoutAndExtractLines(
|
|
|
|
|
renderer, fontId, viewportWidth,
|
|
|
|
|
renderer, fontId, effectiveWidth,
|
|
|
|
|
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
|
|
|
|
// Extra paragraph spacing if enabled
|
|
|
|
|
|
|
|
|
|
// Apply bottom spacing after the paragraph (stored in pixels)
|
|
|
|
|
if (blockStyle.marginBottom > 0) {
|
|
|
|
|
currentPageNextY += blockStyle.marginBottom;
|
|
|
|
|
}
|
|
|
|
|
if (blockStyle.paddingBottom > 0) {
|
|
|
|
|
currentPageNextY += blockStyle.paddingBottom;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extra paragraph spacing if enabled (default behavior)
|
|
|
|
|
if (extraParagraphSpacing) {
|
|
|
|
|
currentPageNextY += lineHeight / 2;
|
|
|
|
|
}
|
|
|
|
|