mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
refactor: merge TextBlock::Style into BlockStyle; use bitflag underlines
Major consolidation of styling infrastructure: - Remove TextBlock::Style enum (JUSTIFIED, LEFT_ALIGN, etc.) Alignment is now stored in BlockStyle.alignment using CssTextAlign - Remove wordUnderlines list from TextBlock and ParsedText Underline state is now encoded in EpdFontFamily::Style via UNDERLINE bitflag - Use BlockStyle::fromCssStyle() and getCombinedBlockStyle() in parser Removes duplicated createBlockStyleFromCss() and mergeBlockStyles() - Simplify text block rendering to check style bitflag for underlines - Revert spurious spaces handling (isAttachingPunctuation logic) The actualGapCount approach had issues; using standard word gaps This reduces code duplication and simplifies the style inheritance model.
This commit is contained in:
parent
f0f6618246
commit
d564173949
@ -19,23 +19,6 @@ namespace {
|
|||||||
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
||||||
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
||||||
|
|
||||||
// Check if a character is punctuation that should attach to the previous word
|
|
||||||
// (no space before it). Includes sentence punctuation and closing quotes.
|
|
||||||
// Excludes brackets/parens to avoid false positives with decorative patterns like "[ 1 ]".
|
|
||||||
bool isAttachingPunctuation(const char c) {
|
|
||||||
return c == '.' || c == ',' || c == '!' || c == '?' || c == ';' || c == ':' || c == '"' || c == '\'';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a word consists entirely of punctuation that should attach to the previous word
|
|
||||||
bool isAttachingPunctuationWord(const std::string& word) {
|
|
||||||
if (word.empty()) return false;
|
|
||||||
// Check if word starts with attaching punctuation and is short (to avoid false positives)
|
|
||||||
if (isAttachingPunctuation(word[0]) && word.size() <= 3) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
||||||
|
|
||||||
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
||||||
@ -66,12 +49,15 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline) {
|
void ParsedText::addWord(std::string word, const EpdFontFamily::Style style, const bool underline) {
|
||||||
if (word.empty()) return;
|
if (word.empty()) return;
|
||||||
|
|
||||||
words.push_back(std::move(word));
|
words.push_back(std::move(word));
|
||||||
wordStyles.push_back(fontStyle);
|
EpdFontFamily::Style combinedStyle = style;
|
||||||
wordUnderlines.push_back(underline);
|
if (underline) {
|
||||||
|
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
||||||
|
}
|
||||||
|
wordStyles.push_back(combinedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consumes data to minimize memory usage
|
// Consumes data to minimize memory usage
|
||||||
@ -112,8 +98,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
|||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
|
|
||||||
while (wordsIt != words.end()) {
|
while (wordsIt != words.end()) {
|
||||||
uint16_t width = measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt);
|
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||||
wordWidths.push_back(width);
|
|
||||||
|
|
||||||
std::advance(wordsIt, 1);
|
std::advance(wordsIt, 1);
|
||||||
std::advance(wordStylesIt, 1);
|
std::advance(wordStylesIt, 1);
|
||||||
@ -129,8 +114,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
const int firstLineIndent = blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
const int firstLineIndent =
|
||||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
? blockStyle.textIndent
|
? blockStyle.textIndent
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
@ -233,7 +219,7 @@ void ParsedText::applyParagraphIndent() {
|
|||||||
if (blockStyle.textIndentDefined) {
|
if (blockStyle.textIndentDefined) {
|
||||||
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
||||||
// The actual indent positioning is handled in extractLine()
|
// The actual indent positioning is handled in extractLine()
|
||||||
} else if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
|
||||||
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
||||||
words.front().insert(0, "\xe2\x80\x83");
|
words.front().insert(0, "\xe2\x80\x83");
|
||||||
}
|
}
|
||||||
@ -244,8 +230,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
|||||||
const int pageWidth, const int spaceWidth,
|
const int pageWidth, const int spaceWidth,
|
||||||
std::vector<uint16_t>& wordWidths) {
|
std::vector<uint16_t>& wordWidths) {
|
||||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
const int firstLineIndent = blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
const int firstLineIndent =
|
||||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
? blockStyle.textIndent
|
? blockStyle.textIndent
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
@ -381,25 +368,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
|
|
||||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||||
const bool isFirstLine = breakIndex == 0;
|
const bool isFirstLine = breakIndex == 0;
|
||||||
const int firstLineIndent = isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
const int firstLineIndent =
|
||||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||||
|
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||||
? blockStyle.textIndent
|
? blockStyle.textIndent
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Calculate total word width for this line and count actual word gaps
|
// Calculate total word width for this line
|
||||||
// (punctuation that attaches to previous word doesn't count as a gap)
|
|
||||||
// Note: words list starts at the beginning because previous lines were spliced out
|
|
||||||
int lineWordWidthSum = 0;
|
int lineWordWidthSum = 0;
|
||||||
size_t actualGapCount = 0;
|
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||||
auto countWordIt = words.begin();
|
lineWordWidthSum += wordWidths[i];
|
||||||
|
|
||||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
|
||||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
|
||||||
// Count gaps: each word after the first creates a gap, unless it's attaching punctuation
|
|
||||||
if (wordIdx > 0 && !isAttachingPunctuationWord(*countWordIt)) {
|
|
||||||
actualGapCount++;
|
|
||||||
}
|
|
||||||
++countWordIt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||||
@ -409,54 +387,37 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
int spacing = spaceWidth;
|
int spacing = spaceWidth;
|
||||||
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
// For justified text, calculate spacing based on actual gap count
|
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && lineWordCount >= 2) {
|
||||||
if (style == TextBlock::JUSTIFIED && !isLastLine && actualGapCount >= 1) {
|
spacing = spareSpace / (lineWordCount - 1);
|
||||||
spacing = spareSpace / static_cast<int>(actualGapCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate initial x position (first line starts at indent for left/justified text)
|
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||||
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||||
if (style == TextBlock::RIGHT_ALIGN) {
|
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||||
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||||
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
|
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-calculate X positions for words
|
// Pre-calculate X positions for words
|
||||||
// Punctuation that attaches to the previous word doesn't get space before it
|
|
||||||
// Note: words list starts at the beginning because previous lines were spliced out
|
|
||||||
std::list<uint16_t> lineXPos;
|
std::list<uint16_t> lineXPos;
|
||||||
auto wordIt = words.begin();
|
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||||
|
const uint16_t currentWordWidth = wordWidths[i];
|
||||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
|
||||||
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
|
||||||
|
|
||||||
lineXPos.push_back(xpos);
|
lineXPos.push_back(xpos);
|
||||||
|
xpos += currentWordWidth + spacing;
|
||||||
// Add spacing after this word, unless the next word is attaching punctuation
|
|
||||||
auto nextWordIt = wordIt;
|
|
||||||
++nextWordIt;
|
|
||||||
const bool nextIsAttachingPunctuation = wordIdx + 1 < lineWordCount && isAttachingPunctuationWord(*nextWordIt);
|
|
||||||
|
|
||||||
xpos += currentWordWidth + (nextIsAttachingPunctuation ? 0 : spacing);
|
|
||||||
++wordIt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterators always start at the beginning as we are moving content with splice below
|
// Iterators always start at the beginning as we are moving content with splice below
|
||||||
auto wordEndIt = words.begin();
|
auto wordEndIt = words.begin();
|
||||||
auto wordStyleEndIt = wordStyles.begin();
|
auto wordStyleEndIt = wordStyles.begin();
|
||||||
auto wordUnderlineEndIt = wordUnderlines.begin();
|
|
||||||
std::advance(wordEndIt, lineWordCount);
|
std::advance(wordEndIt, lineWordCount);
|
||||||
std::advance(wordStyleEndIt, lineWordCount);
|
std::advance(wordStyleEndIt, lineWordCount);
|
||||||
std::advance(wordUnderlineEndIt, lineWordCount);
|
|
||||||
|
|
||||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||||
std::list<std::string> lineWords;
|
std::list<std::string> lineWords;
|
||||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||||
std::list<bool> lineWordUnderlines;
|
|
||||||
lineWordUnderlines.splice(lineWordUnderlines.begin(), wordUnderlines, wordUnderlines.begin(), wordUnderlineEndIt);
|
|
||||||
|
|
||||||
for (auto& word : lineWords) {
|
for (auto& word : lineWords) {
|
||||||
if (containsSoftHyphen(word)) {
|
if (containsSoftHyphen(word)) {
|
||||||
@ -464,6 +425,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style,
|
processLine(
|
||||||
blockStyle, std::move(lineWordUnderlines)));
|
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,6 @@ class GfxRenderer;
|
|||||||
class ParsedText {
|
class ParsedText {
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
std::list<bool> wordUnderlines; // Track underline per word
|
|
||||||
TextBlock::Style style;
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
bool hyphenationEnabled;
|
bool hyphenationEnabled;
|
||||||
@ -35,19 +33,14 @@ class ParsedText {
|
|||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||||
const bool hyphenationEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
const BlockStyle& blockStyle = BlockStyle())
|
||||||
: style(style),
|
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||||
blockStyle(blockStyle),
|
|
||||||
extraParagraphSpacing(extraParagraphSpacing),
|
|
||||||
hyphenationEnabled(hyphenationEnabled) {}
|
|
||||||
~ParsedText() = default;
|
~ParsedText() = default;
|
||||||
|
|
||||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false);
|
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false);
|
||||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
|
||||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
TextBlock::Style getStyle() const { return style; }
|
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
|
||||||
size_t size() const { return words.size(); }
|
size_t size() const { return words.size(); }
|
||||||
bool isEmpty() const { return words.empty(); }
|
bool isEmpty() const { return words.empty(); }
|
||||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||||
|
|||||||
@ -14,15 +14,14 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
auto wordIt = words.begin();
|
auto wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
auto wordUnderlineIt = wordUnderlines.begin();
|
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
for (size_t i = 0; i < words.size(); i++) {
|
||||||
const int wordX = *wordXposIt + x;
|
const int wordX = *wordXposIt + x;
|
||||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, *wordStylesIt);
|
const EpdFontFamily::Style currentStyle = *wordStylesIt;
|
||||||
|
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, currentStyle);
|
||||||
|
|
||||||
// Draw underline if word is underlined
|
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||||
if (wordUnderlineIt != wordUnderlines.end() && *wordUnderlineIt) {
|
|
||||||
const std::string& w = *wordIt;
|
const std::string& w = *wordIt;
|
||||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), *wordStylesIt);
|
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||||
const char* visiblePtr = w.c_str() + 3;
|
const char* visiblePtr = w.c_str() + 3;
|
||||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, std::string("\xe2\x80\x83").c_str());
|
const int prefixWidth = renderer.getTextAdvanceX(fontId, std::string("\xe2\x80\x83").c_str());
|
||||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, *wordStylesIt);
|
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||||
startX = wordX + prefixWidth;
|
startX = wordX + prefixWidth;
|
||||||
underlineWidth = visibleWidth;
|
underlineWidth = visibleWidth;
|
||||||
}
|
}
|
||||||
@ -45,9 +44,6 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
std::advance(wordIt, 1);
|
std::advance(wordIt, 1);
|
||||||
std::advance(wordStylesIt, 1);
|
std::advance(wordStylesIt, 1);
|
||||||
std::advance(wordXposIt, 1);
|
std::advance(wordXposIt, 1);
|
||||||
if (wordUnderlineIt != wordUnderlines.end()) {
|
|
||||||
std::advance(wordUnderlineIt, 1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,29 +60,9 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||||
|
|
||||||
// Underline flags (packed as bytes, 8 words per byte)
|
// Style (alignment + margins/padding/indent)
|
||||||
uint8_t underlineByte = 0;
|
serialization::writePod(file, blockStyle.alignment);
|
||||||
int bitIndex = 0;
|
serialization::writePod(file, blockStyle.textAlignDefined);
|
||||||
auto underlineIt = wordUnderlines.begin();
|
|
||||||
for (size_t i = 0; i < words.size(); i++) {
|
|
||||||
if (underlineIt != wordUnderlines.end() && *underlineIt) {
|
|
||||||
underlineByte |= 1 << bitIndex;
|
|
||||||
}
|
|
||||||
bitIndex++;
|
|
||||||
if (bitIndex == 8 || i == words.size() - 1) {
|
|
||||||
serialization::writePod(file, underlineByte);
|
|
||||||
underlineByte = 0;
|
|
||||||
bitIndex = 0;
|
|
||||||
}
|
|
||||||
if (underlineIt != wordUnderlines.end()) {
|
|
||||||
++underlineIt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block style (alignment)
|
|
||||||
serialization::writePod(file, style);
|
|
||||||
|
|
||||||
// Block style (margins/padding/indent)
|
|
||||||
serialization::writePod(file, blockStyle.marginTop);
|
serialization::writePod(file, blockStyle.marginTop);
|
||||||
serialization::writePod(file, blockStyle.marginBottom);
|
serialization::writePod(file, blockStyle.marginBottom);
|
||||||
serialization::writePod(file, blockStyle.marginLeft);
|
serialization::writePod(file, blockStyle.marginLeft);
|
||||||
@ -106,8 +82,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
std::list<bool> wordUnderlines;
|
|
||||||
Style style;
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
|
||||||
// Word count
|
// Word count
|
||||||
@ -127,23 +101,9 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||||
|
|
||||||
// Underline flags (packed as bytes, 8 words per byte)
|
// Style (alignment + margins/padding/indent)
|
||||||
wordUnderlines.resize(wc, false);
|
serialization::readPod(file, blockStyle.alignment);
|
||||||
auto underlineIt = wordUnderlines.begin();
|
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||||
const int bytesNeeded = (wc + 7) / 8;
|
|
||||||
for (int byteIdx = 0; byteIdx < bytesNeeded; byteIdx++) {
|
|
||||||
uint8_t underlineByte;
|
|
||||||
serialization::readPod(file, underlineByte);
|
|
||||||
for (int bit = 0; bit < 8 && underlineIt != wordUnderlines.end(); bit++) {
|
|
||||||
*underlineIt = (underlineByte & 1 << bit) != 0;
|
|
||||||
++underlineIt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block style (alignment)
|
|
||||||
serialization::readPod(file, style);
|
|
||||||
|
|
||||||
// Block style (margins/padding/indent)
|
|
||||||
serialization::readPod(file, blockStyle.marginTop);
|
serialization::readPod(file, blockStyle.marginTop);
|
||||||
serialization::readPod(file, blockStyle.marginBottom);
|
serialization::readPod(file, blockStyle.marginBottom);
|
||||||
serialization::readPod(file, blockStyle.marginLeft);
|
serialization::readPod(file, blockStyle.marginLeft);
|
||||||
@ -155,6 +115,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
serialization::readPod(file, blockStyle.textIndent);
|
serialization::readPod(file, blockStyle.textIndent);
|
||||||
serialization::readPod(file, blockStyle.textIndentDefined);
|
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||||
|
|
||||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
return std::unique_ptr<TextBlock>(
|
||||||
blockStyle, std::move(wordUnderlines)));
|
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,41 +11,21 @@
|
|||||||
|
|
||||||
// Represents a line of text on a page
|
// Represents a line of text on a page
|
||||||
class TextBlock final : public Block {
|
class TextBlock final : public Block {
|
||||||
public:
|
|
||||||
enum Style : uint8_t {
|
|
||||||
JUSTIFIED = 0,
|
|
||||||
LEFT_ALIGN = 1,
|
|
||||||
CENTER_ALIGN = 2,
|
|
||||||
RIGHT_ALIGN = 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontFamily::Style> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
std::list<bool> wordUnderlines; // Track underline per word
|
|
||||||
Style style;
|
|
||||||
BlockStyle blockStyle;
|
BlockStyle blockStyle;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||||
std::list<EpdFontFamily::Style> word_styles, const Style style,
|
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||||
const BlockStyle& blockStyle = BlockStyle(), std::list<bool> word_underlines = std::list<bool>())
|
|
||||||
: words(std::move(words)),
|
: words(std::move(words)),
|
||||||
wordXpos(std::move(word_xpos)),
|
wordXpos(std::move(word_xpos)),
|
||||||
wordStyles(std::move(word_styles)),
|
wordStyles(std::move(word_styles)),
|
||||||
wordUnderlines(std::move(word_underlines)),
|
blockStyle(blockStyle) {}
|
||||||
style(style),
|
|
||||||
blockStyle(blockStyle) {
|
|
||||||
// Ensure underlines list matches words list size
|
|
||||||
while (this->wordUnderlines.size() < this->words.size()) {
|
|
||||||
this->wordUnderlines.push_back(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
void setStyle(const Style style) { this->style = style; }
|
|
||||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||||
Style getStyle() const { return style; }
|
|
||||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(GfxRenderer& renderer) override {};
|
void layout(GfxRenderer& renderer) override {};
|
||||||
|
|||||||
@ -43,39 +43,17 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
bool isHeaderOrBlock(const char* name) {
|
||||||
// emSize is the current font line height, used for em/rem unit conversion
|
return matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||||
BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle, const float emSize) {
|
|
||||||
BlockStyle blockStyle;
|
|
||||||
// Resolve all CssLength values to pixels using the current font's em size
|
|
||||||
const int16_t marginTopPx = cssStyle.marginTop.toPixelsInt16(emSize);
|
|
||||||
const int16_t marginBottomPx = cssStyle.marginBottom.toPixelsInt16(emSize);
|
|
||||||
const int16_t paddingTopPx = cssStyle.paddingTop.toPixelsInt16(emSize);
|
|
||||||
const int16_t paddingBottomPx = cssStyle.paddingBottom.toPixelsInt16(emSize);
|
|
||||||
|
|
||||||
// Vertical: combine margin and padding for top/bottom spacing
|
|
||||||
blockStyle.marginTop = static_cast<int16_t>(marginTopPx + paddingTopPx);
|
|
||||||
blockStyle.marginBottom = static_cast<int16_t>(marginBottomPx + paddingBottomPx);
|
|
||||||
blockStyle.paddingTop = paddingTopPx;
|
|
||||||
blockStyle.paddingBottom = paddingBottomPx;
|
|
||||||
// Horizontal: store margin and padding separately for layout calculations
|
|
||||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize);
|
|
||||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize);
|
|
||||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize);
|
|
||||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize);
|
|
||||||
// Text indent
|
|
||||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize);
|
|
||||||
blockStyle.textIndentDefined = cssStyle.defined.textIndent;
|
|
||||||
return blockStyle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update effective bold/italic/underline based on block style and inline style stack
|
// Update effective bold/italic/underline based on block style and inline style stack
|
||||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||||
// Start with block-level styles
|
// Start with block-level styles
|
||||||
effectiveBold = currentBlockStyle.hasFontWeight() && currentBlockStyle.fontWeight == CssFontWeight::Bold;
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
effectiveItalic = currentBlockStyle.hasFontStyle() && currentBlockStyle.fontStyle == CssFontStyle::Italic;
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
effectiveUnderline =
|
effectiveUnderline =
|
||||||
currentBlockStyle.hasTextDecoration() && currentBlockStyle.textDecoration == CssTextDecoration::Underline;
|
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
||||||
|
|
||||||
// Apply inline style stack in order
|
// Apply inline style stack in order
|
||||||
for (const auto& entry : inlineStyleStack) {
|
for (const auto& entry : inlineStyleStack) {
|
||||||
@ -98,69 +76,41 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
||||||
|
|
||||||
|
// Combine style flags using bitwise OR
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
if (isBold && isItalic) {
|
if (isBold) {
|
||||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::BOLD);
|
||||||
} else if (isBold) {
|
}
|
||||||
fontStyle = EpdFontFamily::BOLD;
|
if (isItalic) {
|
||||||
} else if (isItalic) {
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||||
fontStyle = EpdFontFamily::ITALIC;
|
}
|
||||||
|
if (isUnderline) {
|
||||||
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// flush the buffer
|
// flush the buffer
|
||||||
partWordBuffer[partWordBufferIndex] = '\0';
|
partWordBuffer[partWordBufferIndex] = '\0';
|
||||||
currentTextBlock->addWord(partWordBuffer, fontStyle, isUnderline);
|
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
||||||
partWordBufferIndex = 0;
|
partWordBufferIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge block styles for nested block elements
|
|
||||||
// When a child block element is inside a parent with no direct text content,
|
|
||||||
// we accumulate their margins so nested containers properly contribute spacing
|
|
||||||
BlockStyle mergeBlockStyles(const BlockStyle& parent, const BlockStyle& child) {
|
|
||||||
BlockStyle merged;
|
|
||||||
// Vertical margins: sum them (nested blocks create additive spacing)
|
|
||||||
merged.marginTop = static_cast<int16_t>(parent.marginTop + child.marginTop);
|
|
||||||
merged.marginBottom = static_cast<int16_t>(parent.marginBottom + child.marginBottom);
|
|
||||||
// Horizontal margins: sum them (nested blocks create cumulative indentation)
|
|
||||||
merged.marginLeft = static_cast<int16_t>(parent.marginLeft + child.marginLeft);
|
|
||||||
merged.marginRight = static_cast<int16_t>(parent.marginRight + child.marginRight);
|
|
||||||
// Padding: sum them
|
|
||||||
merged.paddingTop = static_cast<int16_t>(parent.paddingTop + child.paddingTop);
|
|
||||||
merged.paddingBottom = static_cast<int16_t>(parent.paddingBottom + child.paddingBottom);
|
|
||||||
merged.paddingLeft = static_cast<int16_t>(parent.paddingLeft + child.paddingLeft);
|
|
||||||
merged.paddingRight = static_cast<int16_t>(parent.paddingRight + child.paddingRight);
|
|
||||||
// Text indent: use child's if defined, otherwise inherit parent's
|
|
||||||
if (child.textIndentDefined) {
|
|
||||||
merged.textIndent = child.textIndent;
|
|
||||||
merged.textIndentDefined = true;
|
|
||||||
} else if (parent.textIndentDefined) {
|
|
||||||
merged.textIndent = parent.textIndent;
|
|
||||||
merged.textIndentDefined = true;
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
// start a new text block if needed
|
// start a new text block if needed
|
||||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style, const BlockStyle& blockStyle) {
|
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
// already have a text block running and it is empty - just reuse it
|
// already have a text block running and it is empty - just reuse it
|
||||||
if (currentTextBlock->isEmpty()) {
|
if (currentTextBlock->isEmpty()) {
|
||||||
currentTextBlock->setStyle(style);
|
// Merge with existing block style to accumulate CSS styling from parent block elements.
|
||||||
// Merge with existing block style to accumulate margins from parent block elements
|
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
|
||||||
// This handles cases like <div margin-bottom:2em><h1>text</h1></div> where the
|
// div's margin should be preserved, even though it has no direct text content.
|
||||||
// div's margin should be preserved even though it has no direct text content
|
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
|
||||||
const BlockStyle merged = mergeBlockStyles(currentTextBlock->getBlockStyle(), blockStyle);
|
|
||||||
currentTextBlock->setBlockStyle(merged);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
makePages();
|
makePages();
|
||||||
}
|
}
|
||||||
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { startNewTextBlock(style, BlockStyle{}); }
|
|
||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
|
|
||||||
@ -183,13 +133,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto centeredBlockStyle = BlockStyle();
|
||||||
|
centeredBlockStyle.textAlignDefined = true;
|
||||||
|
centeredBlockStyle.alignment = CssTextAlign::Center;
|
||||||
|
|
||||||
// Special handling for tables - show placeholder text instead of dropping silently
|
// Special handling for tables - show placeholder text instead of dropping silently
|
||||||
if (strcmp(name, "table") == 0) {
|
if (strcmp(name, "table") == 0) {
|
||||||
// Add placeholder text
|
// Add placeholder text
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
self->startNewTextBlock(centeredBlockStyle);
|
||||||
|
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
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->depth += 1;
|
||||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||||
|
|
||||||
@ -214,9 +168,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
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);
|
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->depth += 1;
|
||||||
self->characterData(userData, alt.c_str(), alt.length());
|
self->characterData(userData, alt.c_str(), alt.length());
|
||||||
|
|
||||||
@ -244,9 +198,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this is a block element
|
|
||||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
|
||||||
|
|
||||||
// Compute CSS style for this element
|
// Compute CSS style for this element
|
||||||
CssStyle cssStyle;
|
CssStyle cssStyle;
|
||||||
if (self->cssParser) {
|
if (self->cssParser) {
|
||||||
@ -259,30 +210,12 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
|
||||||
// Headers: center aligned, bold, apply CSS overrides
|
const auto userAlignment = static_cast<CssTextAlign>(self->paragraphAlignment);
|
||||||
TextBlock::Style alignment = TextBlock::CENTER_ALIGN;
|
|
||||||
if (cssStyle.hasTextAlign()) {
|
|
||||||
switch (cssStyle.textAlign) {
|
|
||||||
case CssTextAlign::Left:
|
|
||||||
alignment = TextBlock::LEFT_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Right:
|
|
||||||
alignment = TextBlock::RIGHT_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Center:
|
|
||||||
alignment = TextBlock::CENTER_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Justify:
|
|
||||||
alignment = TextBlock::JUSTIFIED;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self->currentBlockStyle = cssStyle;
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId)));
|
self->currentCssStyle = cssStyle;
|
||||||
|
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
||||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
@ -291,31 +224,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
}
|
}
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
|
||||||
} else {
|
} else {
|
||||||
// Determine alignment from CSS or default
|
self->currentCssStyle = cssStyle;
|
||||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
||||||
if (cssStyle.hasTextAlign()) {
|
|
||||||
switch (cssStyle.textAlign) {
|
|
||||||
case CssTextAlign::Left:
|
|
||||||
alignment = TextBlock::LEFT_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Right:
|
|
||||||
alignment = TextBlock::RIGHT_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Center:
|
|
||||||
alignment = TextBlock::CENTER_ALIGN;
|
|
||||||
break;
|
|
||||||
case CssTextAlign::Justify:
|
|
||||||
alignment = TextBlock::JUSTIFIED;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self->currentBlockStyle = cssStyle;
|
|
||||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId)));
|
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
|
|
||||||
if (strcmp(name, "li") == 0) {
|
if (strcmp(name, "li") == 0) {
|
||||||
@ -373,7 +285,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
self->inlineStyleStack.push_back(entry);
|
self->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
} else if (strcmp(name, "span") == 0 || !isBlockElement) {
|
} else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) {
|
||||||
// Handle span and other inline elements for CSS styling
|
// Handle span and other inline elements for CSS styling
|
||||||
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
||||||
StyleStackEntry entry;
|
StyleStackEntry entry;
|
||||||
@ -464,12 +376,12 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
||||||
|
|
||||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||||
|
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||||
|
|
||||||
// Flush buffer with current style BEFORE any style changes
|
// Flush buffer with current style BEFORE any style changes
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
// Flush if style will change OR if we're closing a block/structural element
|
// Flush if style will change OR if we're closing a block/structural element
|
||||||
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || 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) || strcmp(name, "table") == 0 ||
|
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
||||||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||||
@ -508,15 +420,18 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear block style when leaving block elements
|
// Clear block style when leaving header or block elements
|
||||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
if (headerOrBlockTag) {
|
||||||
self->currentBlockStyle.reset();
|
self->currentCssStyle.reset();
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
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);
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||||
int done;
|
int done;
|
||||||
@ -624,11 +539,14 @@ void ChapterHtmlSlimParser::makePages() {
|
|||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||||
|
|
||||||
// Apply marginTop before the paragraph (stored in pixels)
|
// Apply top spacing before the paragraph (stored in pixels)
|
||||||
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||||
if (blockStyle.marginTop > 0) {
|
if (blockStyle.marginTop > 0) {
|
||||||
currentPageNextY += blockStyle.marginTop;
|
currentPageNextY += blockStyle.marginTop;
|
||||||
}
|
}
|
||||||
|
if (blockStyle.paddingTop > 0) {
|
||||||
|
currentPageNextY += blockStyle.paddingTop;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate effective width accounting for horizontal margins/padding
|
// Calculate effective width accounting for horizontal margins/padding
|
||||||
const int horizontalInset = blockStyle.totalHorizontalInset();
|
const int horizontalInset = blockStyle.totalHorizontalInset();
|
||||||
@ -639,10 +557,13 @@ void ChapterHtmlSlimParser::makePages() {
|
|||||||
renderer, fontId, effectiveWidth,
|
renderer, fontId, effectiveWidth,
|
||||||
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
||||||
|
|
||||||
// Apply marginBottom after the paragraph (stored in pixels)
|
// Apply bottom spacing after the paragraph (stored in pixels)
|
||||||
if (blockStyle.marginBottom > 0) {
|
if (blockStyle.marginBottom > 0) {
|
||||||
currentPageNextY += blockStyle.marginBottom;
|
currentPageNextY += blockStyle.marginBottom;
|
||||||
}
|
}
|
||||||
|
if (blockStyle.paddingBottom > 0) {
|
||||||
|
currentPageNextY += blockStyle.paddingBottom;
|
||||||
|
}
|
||||||
|
|
||||||
// Extra paragraph spacing if enabled (default behavior)
|
// Extra paragraph spacing if enabled (default behavior)
|
||||||
if (extraParagraphSpacing) {
|
if (extraParagraphSpacing) {
|
||||||
|
|||||||
@ -50,14 +50,13 @@ class ChapterHtmlSlimParser {
|
|||||||
bool hasUnderline = false, underline = false;
|
bool hasUnderline = false, underline = false;
|
||||||
};
|
};
|
||||||
std::vector<StyleStackEntry> inlineStyleStack;
|
std::vector<StyleStackEntry> inlineStyleStack;
|
||||||
CssStyle currentBlockStyle;
|
CssStyle currentCssStyle;
|
||||||
bool effectiveBold = false;
|
bool effectiveBold = false;
|
||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
bool effectiveUnderline = false;
|
||||||
|
|
||||||
void updateEffectiveInlineStyle();
|
void updateEffectiveInlineStyle();
|
||||||
void startNewTextBlock(TextBlock::Style style, const BlockStyle& blockStyle);
|
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||||
void startNewTextBlock(TextBlock::Style style);
|
|
||||||
void flushPartWordBuffer();
|
void flushPartWordBuffer();
|
||||||
void makePages();
|
void makePages();
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user