Improve EPUB rendering and add end-of-book Start Over
EPUB rendering improvements: - Add margin-left/padding-left CSS parsing for block indentation - Add vertical bar and italic styling for blockquotes - Add left margin indentation for list items (ol/ul) - Fix ordered lists showing bullets instead of numbers - Fix nested <p> inside <li> causing marker on separate line End-of-book improvements: - Add "Start Over" option to wrap to first page when pressing next - Show "Start Over" button hint on finished book prompt
This commit is contained in:
parent
ef705d3ac6
commit
dd630dcf72
@ -68,7 +68,9 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
// Apply fixed transforms before any per-line layout work.
|
// Apply fixed transforms before any per-line layout work.
|
||||||
applyParagraphIndent();
|
applyParagraphIndent();
|
||||||
|
|
||||||
const int pageWidth = viewportWidth;
|
// Apply horizontal margin (for blockquotes, nested content, etc.)
|
||||||
|
const int leftMargin = blockStyle.marginLeft;
|
||||||
|
const int pageWidth = viewportWidth - leftMargin;
|
||||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||||
std::vector<size_t> lineBreakIndices;
|
std::vector<size_t> lineBreakIndices;
|
||||||
@ -81,7 +83,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
for (size_t i = 0; i < lineCount; ++i) {
|
for (size_t i = 0; i < lineCount; ++i) {
|
||||||
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
extractLine(i, pageWidth, spaceWidth, leftMargin, wordWidths, lineBreakIndices, processLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +338,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, const int leftMargin,
|
||||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||||
@ -359,12 +361,12 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
spacing = spareSpace / (lineWordCount - 1);
|
spacing = spareSpace / (lineWordCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate initial x position
|
// Calculate initial x position (offset by left margin for blockquotes, etc.)
|
||||||
uint16_t xpos = 0;
|
uint16_t xpos = static_cast<uint16_t>(leftMargin);
|
||||||
if (style == TextBlock::RIGHT_ALIGN) {
|
if (style == TextBlock::RIGHT_ALIGN) {
|
||||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
xpos += spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
xpos += (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-calculate X positions for words
|
// Pre-calculate X positions for words
|
||||||
|
|||||||
@ -28,8 +28,8 @@ class ParsedText {
|
|||||||
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
||||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, int leftMargin,
|
||||||
const std::vector<size_t>& lineBreakIndices,
|
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Version 12: Added content offsets to LUT for position restoration after re-indexing
|
// Version 13: Added marginLeft and hasLeftBorder to BlockStyle serialization
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
||||||
sizeof(uint32_t);
|
sizeof(uint32_t);
|
||||||
|
|||||||
@ -13,5 +13,7 @@ struct BlockStyle {
|
|||||||
int8_t marginBottom = 0; // 0-2 lines
|
int8_t marginBottom = 0; // 0-2 lines
|
||||||
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
||||||
int8_t paddingBottom = 0; // 0-2 lines (treated same as margin)
|
int8_t paddingBottom = 0; // 0-2 lines (treated same as margin)
|
||||||
int16_t textIndent = 0; // pixels
|
int16_t textIndent = 0; // pixels (first line indent)
|
||||||
|
int16_t marginLeft = 0; // pixels (horizontal indent for entire block)
|
||||||
|
bool hasLeftBorder = false; // draw vertical bar in left margin (for blockquotes)
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,17 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw left border (vertical bar) for blockquotes
|
||||||
|
if (blockStyle.hasLeftBorder && blockStyle.marginLeft > 0) {
|
||||||
|
const int lineHeight = renderer.getLineHeight(fontId);
|
||||||
|
const int barX = x + 4; // Small offset from left edge
|
||||||
|
const int barTop = y;
|
||||||
|
const int barBottom = y + lineHeight;
|
||||||
|
// Draw a 2-pixel wide vertical bar
|
||||||
|
renderer.drawLine(barX, barTop, barX, barBottom, true);
|
||||||
|
renderer.drawLine(barX + 1, barTop, barX + 1, barBottom, true);
|
||||||
|
}
|
||||||
|
|
||||||
auto wordIt = words.begin();
|
auto wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
@ -92,6 +103,8 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
serialization::writePod(file, blockStyle.paddingTop);
|
serialization::writePod(file, blockStyle.paddingTop);
|
||||||
serialization::writePod(file, blockStyle.paddingBottom);
|
serialization::writePod(file, blockStyle.paddingBottom);
|
||||||
serialization::writePod(file, blockStyle.textIndent);
|
serialization::writePod(file, blockStyle.textIndent);
|
||||||
|
serialization::writePod(file, blockStyle.marginLeft);
|
||||||
|
serialization::writePod(file, blockStyle.hasLeftBorder);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -144,6 +157,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
serialization::readPod(file, blockStyle.paddingTop);
|
serialization::readPod(file, blockStyle.paddingTop);
|
||||||
serialization::readPod(file, blockStyle.paddingBottom);
|
serialization::readPod(file, blockStyle.paddingBottom);
|
||||||
serialization::readPod(file, blockStyle.textIndent);
|
serialization::readPod(file, blockStyle.textIndent);
|
||||||
|
serialization::readPod(file, blockStyle.marginLeft);
|
||||||
|
serialization::readPod(file, blockStyle.hasLeftBorder);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
||||||
blockStyle, std::move(wordUnderlines)));
|
blockStyle, std::move(wordUnderlines)));
|
||||||
|
|||||||
@ -393,6 +393,32 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
|||||||
style.paddingBottom = spacing;
|
style.paddingBottom = spacing;
|
||||||
style.defined.paddingBottom = 1;
|
style.defined.paddingBottom = 1;
|
||||||
}
|
}
|
||||||
|
} else if (propName == "margin-left" || propName == "padding-left") {
|
||||||
|
// Horizontal indentation for blockquotes and nested content
|
||||||
|
const float pixels = interpretLength(propValue);
|
||||||
|
if (pixels > 0) {
|
||||||
|
style.marginLeft += pixels; // Accumulate margin-left and padding-left
|
||||||
|
style.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
} else if (propName == "margin") {
|
||||||
|
// Shorthand: margin: top right bottom left OR margin: vertical horizontal
|
||||||
|
const auto values = splitWhitespace(propValue);
|
||||||
|
if (values.size() >= 2) {
|
||||||
|
// At least 2 values: first is vertical (top/bottom), second is horizontal (left/right)
|
||||||
|
const float horizontal = interpretLength(values[1]);
|
||||||
|
if (horizontal > 0) {
|
||||||
|
style.marginLeft = horizontal;
|
||||||
|
style.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (values.size() == 4) {
|
||||||
|
// 4 values: top right bottom left - use the 4th value for left
|
||||||
|
const float left = interpretLength(values[3]);
|
||||||
|
if (left > 0) {
|
||||||
|
style.marginLeft = left;
|
||||||
|
style.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,8 @@ struct CssPropertyFlags {
|
|||||||
uint16_t marginBottom : 1;
|
uint16_t marginBottom : 1;
|
||||||
uint16_t paddingTop : 1;
|
uint16_t paddingTop : 1;
|
||||||
uint16_t paddingBottom : 1;
|
uint16_t paddingBottom : 1;
|
||||||
uint16_t reserved : 7;
|
uint16_t marginLeft : 1;
|
||||||
|
uint16_t reserved : 6;
|
||||||
|
|
||||||
CssPropertyFlags()
|
CssPropertyFlags()
|
||||||
: alignment(0),
|
: alignment(0),
|
||||||
@ -37,16 +38,17 @@ struct CssPropertyFlags {
|
|||||||
marginBottom(0),
|
marginBottom(0),
|
||||||
paddingTop(0),
|
paddingTop(0),
|
||||||
paddingBottom(0),
|
paddingBottom(0),
|
||||||
|
marginLeft(0),
|
||||||
reserved(0) {}
|
reserved(0) {}
|
||||||
|
|
||||||
[[nodiscard]] bool anySet() const {
|
[[nodiscard]] bool anySet() const {
|
||||||
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
||||||
paddingBottom;
|
paddingBottom || marginLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearAll() {
|
void clearAll() {
|
||||||
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
||||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
marginTop = marginBottom = paddingTop = paddingBottom = marginLeft = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,6 +65,7 @@ struct CssStyle {
|
|||||||
int8_t marginBottom = 0; // Vertical spacing after block (in lines, 0-2)
|
int8_t marginBottom = 0; // Vertical spacing after block (in lines, 0-2)
|
||||||
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
||||||
int8_t paddingBottom = 0; // Padding after (in lines, 0-2)
|
int8_t paddingBottom = 0; // Padding after (in lines, 0-2)
|
||||||
|
float marginLeft = 0.0f; // Horizontal indent in pixels (for blockquotes, etc.)
|
||||||
|
|
||||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||||
|
|
||||||
@ -105,6 +108,10 @@ struct CssStyle {
|
|||||||
paddingBottom = base.paddingBottom;
|
paddingBottom = base.paddingBottom;
|
||||||
defined.paddingBottom = 1;
|
defined.paddingBottom = 1;
|
||||||
}
|
}
|
||||||
|
if (base.defined.marginLeft) {
|
||||||
|
marginLeft = base.marginLeft;
|
||||||
|
defined.marginLeft = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatibility accessors for existing code that uses hasX pattern
|
// Compatibility accessors for existing code that uses hasX pattern
|
||||||
@ -117,6 +124,7 @@ struct CssStyle {
|
|||||||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||||
|
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||||
|
|
||||||
// Merge another style (alias for applyOver for compatibility)
|
// Merge another style (alias for applyOver for compatibility)
|
||||||
void merge(const CssStyle& other) { applyOver(other); }
|
void merge(const CssStyle& other) { applyOver(other); }
|
||||||
@ -128,6 +136,7 @@ struct CssStyle {
|
|||||||
decoration = CssTextDecoration::None;
|
decoration = CssTextDecoration::None;
|
||||||
indentPixels = 0.0f;
|
indentPixels = 0.0f;
|
||||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
||||||
|
marginLeft = 0.0f;
|
||||||
defined.clearAll();
|
defined.clearAll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,9 @@ constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|||||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
|
|
||||||
|
const char* LIST_TAGS[] = {"ol", "ul"};
|
||||||
|
constexpr int NUM_LIST_TAGS = sizeof(LIST_TAGS) / sizeof(LIST_TAGS[0]);
|
||||||
|
|
||||||
const char* BOLD_TAGS[] = {"b", "strong"};
|
const char* BOLD_TAGS[] = {"b", "strong"};
|
||||||
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||||
|
|
||||||
@ -55,6 +58,7 @@ BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle) {
|
|||||||
blockStyle.paddingTop = cssStyle.paddingTop;
|
blockStyle.paddingTop = cssStyle.paddingTop;
|
||||||
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
||||||
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
||||||
|
blockStyle.marginLeft = static_cast<int16_t>(cssStyle.marginLeft);
|
||||||
return blockStyle;
|
return blockStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +324,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
// Determine if this is a block element
|
// Determine if this is a block element
|
||||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||||
|
bool isListTag = matches(name, LIST_TAGS, NUM_LIST_TAGS);
|
||||||
|
|
||||||
|
// Handle list container tags (ol, ul)
|
||||||
|
if (isListTag) {
|
||||||
|
ListContext ctx;
|
||||||
|
ctx.isOrdered = strcmp(name, "ol") == 0;
|
||||||
|
ctx.counter = 0;
|
||||||
|
ctx.depth = self->depth;
|
||||||
|
self->listStack.push_back(ctx);
|
||||||
|
self->depth += 1;
|
||||||
|
return; // Lists themselves don't create text blocks
|
||||||
|
}
|
||||||
|
|
||||||
// Compute CSS style for this element
|
// Compute CSS style for this element
|
||||||
CssStyle cssStyle;
|
CssStyle cssStyle;
|
||||||
@ -365,6 +381,20 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
// This fixes issue where <br/> incorrectly wrapped the preceding word to a new line
|
// This fixes issue where <br/> incorrectly wrapped the preceding word to a new line
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||||
|
} else if (strcmp(name, "li") == 0) {
|
||||||
|
// For list items, DON'T create a text block yet - wait for the first content element
|
||||||
|
// This prevents the marker from being on its own line when <li><p>content</p></li>
|
||||||
|
self->insideListItem = true;
|
||||||
|
self->listItemDepth = self->depth;
|
||||||
|
self->listItemHasContent = false;
|
||||||
|
|
||||||
|
// Increment counter now (so nested lists work correctly)
|
||||||
|
if (!self->listStack.empty()) {
|
||||||
|
self->listStack.back().counter++;
|
||||||
|
}
|
||||||
|
// Don't create text block or add marker yet - will be done when first content arrives
|
||||||
|
self->depth += 1;
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Determine alignment from CSS or default
|
// Determine alignment from CSS or default
|
||||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
||||||
@ -387,12 +417,71 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply default styling for blockquote if no CSS margin is specified
|
||||||
|
const bool isBlockquote = strcmp(name, "blockquote") == 0;
|
||||||
|
if (isBlockquote) {
|
||||||
|
if (!cssStyle.hasMarginLeft()) {
|
||||||
|
// Default left indent for blockquotes (~1.5em at 16px base = 24px)
|
||||||
|
cssStyle.marginLeft = 24.0f;
|
||||||
|
cssStyle.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
// Also make blockquotes italic by default if not specified
|
||||||
|
if (!cssStyle.hasFontStyle()) {
|
||||||
|
cssStyle.fontStyle = CssFontStyle::Italic;
|
||||||
|
cssStyle.defined.fontStyle = 1;
|
||||||
|
}
|
||||||
|
// Track blockquote context for child elements
|
||||||
|
self->insideBlockquote = true;
|
||||||
|
self->blockquoteDepth = self->depth;
|
||||||
|
self->blockquoteMarginLeft = cssStyle.marginLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply blockquote styling to child block elements
|
||||||
|
if (self->insideBlockquote && !isBlockquote) {
|
||||||
|
// Inherit margin and border from parent blockquote
|
||||||
|
if (!cssStyle.hasMarginLeft()) {
|
||||||
|
cssStyle.marginLeft = self->blockquoteMarginLeft;
|
||||||
|
cssStyle.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply left margin to list items (indent the whole block)
|
||||||
|
if (self->insideListItem && !cssStyle.hasMarginLeft()) {
|
||||||
|
// Default left indent for list items (~1.5em at 16px base = 24px)
|
||||||
|
cssStyle.marginLeft = 24.0f;
|
||||||
|
cssStyle.defined.marginLeft = 1;
|
||||||
|
}
|
||||||
|
|
||||||
self->currentBlockStyle = cssStyle;
|
self->currentBlockStyle = cssStyle;
|
||||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle));
|
BlockStyle blockStyleForElement = createBlockStyleFromCss(cssStyle);
|
||||||
|
if (isBlockquote || self->insideBlockquote) {
|
||||||
|
blockStyleForElement.hasLeftBorder = true; // Draw vertical bar for blockquotes
|
||||||
|
}
|
||||||
|
self->startNewTextBlock(alignment, blockStyleForElement);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
|
|
||||||
if (strcmp(name, "li") == 0) {
|
// If this is a blockquote, apply italic styling
|
||||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
if (isBlockquote && cssStyle.hasFontStyle() && cssStyle.fontStyle == CssFontStyle::Italic) {
|
||||||
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first block element inside a list item, add the marker
|
||||||
|
if (self->insideListItem && !self->listItemHasContent) {
|
||||||
|
if (!self->listStack.empty()) {
|
||||||
|
const ListContext& ctx = self->listStack.back();
|
||||||
|
if (ctx.isOrdered) {
|
||||||
|
// Ordered list: use number (counter was already incremented)
|
||||||
|
std::string marker = std::to_string(ctx.counter) + ". ";
|
||||||
|
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||||
|
} else {
|
||||||
|
// Unordered list: use bullet
|
||||||
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No list context (orphan li), use bullet as fallback
|
||||||
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||||
|
}
|
||||||
|
self->listItemHasContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||||
@ -566,7 +655,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
||||||
matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || self->depth == 1;
|
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||||
|
matches(name, LIST_TAGS, NUM_LIST_TAGS) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
// Use combined depth-based and CSS-based style
|
// Use combined depth-based and CSS-based style
|
||||||
@ -596,6 +686,27 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->skipUntilDepth = INT_MAX;
|
self->skipUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaving list container (ol, ul)
|
||||||
|
if (matches(name, LIST_TAGS, NUM_LIST_TAGS)) {
|
||||||
|
if (!self->listStack.empty() && self->listStack.back().depth == self->depth) {
|
||||||
|
self->listStack.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaving list item (li)
|
||||||
|
if (strcmp(name, "li") == 0 && self->listItemDepth == self->depth) {
|
||||||
|
self->insideListItem = false;
|
||||||
|
self->listItemDepth = INT_MAX;
|
||||||
|
self->listItemHasContent = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leaving blockquote
|
||||||
|
if (strcmp(name, "blockquote") == 0 && self->blockquoteDepth == self->depth) {
|
||||||
|
self->insideBlockquote = false;
|
||||||
|
self->blockquoteDepth = INT_MAX;
|
||||||
|
self->blockquoteMarginLeft = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
// Leaving bold tag
|
// Leaving bold tag
|
||||||
if (self->boldUntilDepth == self->depth) {
|
if (self->boldUntilDepth == self->depth) {
|
||||||
self->boldUntilDepth = INT_MAX;
|
self->boldUntilDepth = INT_MAX;
|
||||||
|
|||||||
@ -59,6 +59,22 @@ class ChapterHtmlSlimParser {
|
|||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
bool effectiveUnderline = false;
|
||||||
|
|
||||||
|
// List context tracking for ordered/unordered lists
|
||||||
|
struct ListContext {
|
||||||
|
bool isOrdered = false; // true for <ol>, false for <ul>
|
||||||
|
int counter = 0; // Current item number (for ordered lists)
|
||||||
|
int depth = 0; // Depth at which list was opened
|
||||||
|
};
|
||||||
|
std::vector<ListContext> listStack;
|
||||||
|
bool insideListItem = false; // True when we're inside an <li> element
|
||||||
|
int listItemDepth = INT_MAX; // Depth at which <li> was opened
|
||||||
|
bool listItemHasContent = false; // True if we've added content to the current list item
|
||||||
|
|
||||||
|
// Blockquote context tracking (for left border on child elements)
|
||||||
|
bool insideBlockquote = false;
|
||||||
|
int blockquoteDepth = INT_MAX;
|
||||||
|
float blockquoteMarginLeft = 0.0f; // Inherit margin from blockquote to child elements
|
||||||
|
|
||||||
// Byte offset tracking for position restoration after re-indexing
|
// Byte offset tracking for position restoration after re-indexing
|
||||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
||||||
|
|||||||
@ -236,6 +236,15 @@ void EpubReaderActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
|
// Start over from beginning
|
||||||
|
currentSpineIndex = 0;
|
||||||
|
nextPageNumber = 0;
|
||||||
|
showingEndOfBookPrompt = false;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -988,7 +997,7 @@ void EpubReaderActivity::renderEndOfBookPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button hints
|
// Button hints
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "Start Over");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -171,6 +171,14 @@ void TxtReaderActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
|
// Start over from beginning
|
||||||
|
currentPage = 0;
|
||||||
|
showingEndOfBookPrompt = false;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,7 +917,7 @@ void TxtReaderActivity::renderEndOfBookPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button hints
|
// Button hints
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "Start Over");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user