mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
4 Commits
9dac5bf27e
...
d445eb0bb0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d445eb0bb0 | ||
|
|
394fc41819 | ||
|
|
a6d6e5e770 | ||
|
|
6796989247 |
@ -19,6 +19,23 @@ 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.
|
||||||
@ -369,10 +386,20 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
? blockStyle.textIndent
|
? blockStyle.textIndent
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// Calculate total word width for this line
|
// Calculate total word width for this line and count actual word gaps
|
||||||
|
// (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;
|
||||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
size_t actualGapCount = 0;
|
||||||
lineWordWidthSum += wordWidths[i];
|
auto countWordIt = words.begin();
|
||||||
|
|
||||||
|
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)
|
||||||
@ -382,24 +409,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;
|
||||||
|
|
||||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
// For justified text, calculate spacing based on actual gap count
|
||||||
spacing = spareSpace / (lineWordCount - 1);
|
if (style == TextBlock::JUSTIFIED && !isLastLine && actualGapCount >= 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 (style == TextBlock::RIGHT_ALIGN) {
|
||||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
||||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
xpos = (spareSpace - static_cast<int>(actualGapCount) * 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;
|
||||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
auto wordIt = words.begin();
|
||||||
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
|
||||||
|
|||||||
@ -253,15 +253,12 @@ CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
|
|||||||
return CssTextDecoration::None;
|
return CssTextDecoration::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
float CssParser::interpretLength(const std::string& val, const float emSize) {
|
CssLength CssParser::interpretLength(const std::string& val) {
|
||||||
const std::string v = normalized(val);
|
const std::string v = normalized(val);
|
||||||
if (v.empty()) return 0.0f;
|
if (v.empty()) return CssLength{};
|
||||||
|
|
||||||
// Determine unit and multiplier
|
|
||||||
float multiplier = 1.0f;
|
|
||||||
size_t unitStart = v.size();
|
|
||||||
|
|
||||||
// Find where the number ends
|
// Find where the number ends
|
||||||
|
size_t unitStart = v.size();
|
||||||
for (size_t i = 0; i < v.size(); ++i) {
|
for (size_t i = 0; i < v.size(); ++i) {
|
||||||
const char c = v[i];
|
const char c = v[i];
|
||||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||||
@ -273,20 +270,23 @@ float CssParser::interpretLength(const std::string& val, const float emSize) {
|
|||||||
const std::string numPart = v.substr(0, unitStart);
|
const std::string numPart = v.substr(0, unitStart);
|
||||||
const std::string unitPart = v.substr(unitStart);
|
const std::string unitPart = v.substr(unitStart);
|
||||||
|
|
||||||
// Handle units
|
// Parse numeric value
|
||||||
if (unitPart == "em" || unitPart == "rem") {
|
|
||||||
multiplier = emSize;
|
|
||||||
} else if (unitPart == "pt") {
|
|
||||||
multiplier = 1.33f; // Approximate pt to px conversion
|
|
||||||
}
|
|
||||||
// px is default (multiplier = 1.0)
|
|
||||||
|
|
||||||
char* endPtr = nullptr;
|
char* endPtr = nullptr;
|
||||||
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
const float numericValue = std::strtof(numPart.c_str(), &endPtr);
|
||||||
|
if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed
|
||||||
|
|
||||||
if (endPtr == numPart.c_str()) return 0.0f; // No number parsed
|
// Determine unit type (preserve for deferred resolution)
|
||||||
|
auto unit = CssUnit::Pixels;
|
||||||
|
if (unitPart == "em") {
|
||||||
|
unit = CssUnit::Em;
|
||||||
|
} else if (unitPart == "rem") {
|
||||||
|
unit = CssUnit::Rem;
|
||||||
|
} else if (unitPart == "pt") {
|
||||||
|
unit = CssUnit::Points;
|
||||||
|
}
|
||||||
|
// px and unitless default to Pixels
|
||||||
|
|
||||||
return numericValue * multiplier;
|
return CssLength{numericValue, unit};
|
||||||
}
|
}
|
||||||
|
|
||||||
int8_t CssParser::interpretSpacing(const std::string& val) {
|
int8_t CssParser::interpretSpacing(const std::string& val) {
|
||||||
@ -367,28 +367,28 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
|||||||
style.decoration = interpretDecoration(propValue);
|
style.decoration = interpretDecoration(propValue);
|
||||||
style.defined.decoration = 1;
|
style.defined.decoration = 1;
|
||||||
} else if (propName == "text-indent") {
|
} else if (propName == "text-indent") {
|
||||||
style.indentPixels = interpretLength(propValue);
|
style.indent = interpretLength(propValue);
|
||||||
style.defined.indent = 1;
|
style.defined.indent = 1;
|
||||||
} else if (propName == "margin-top") {
|
} else if (propName == "margin-top") {
|
||||||
style.marginTop = static_cast<int16_t>(interpretLength(propValue));
|
style.marginTop = interpretLength(propValue);
|
||||||
style.defined.marginTop = 1;
|
style.defined.marginTop = 1;
|
||||||
} else if (propName == "margin-bottom") {
|
} else if (propName == "margin-bottom") {
|
||||||
style.marginBottom = static_cast<int16_t>(interpretLength(propValue));
|
style.marginBottom = interpretLength(propValue);
|
||||||
style.defined.marginBottom = 1;
|
style.defined.marginBottom = 1;
|
||||||
} else if (propName == "margin-left") {
|
} else if (propName == "margin-left") {
|
||||||
style.marginLeft = static_cast<int16_t>(interpretLength(propValue));
|
style.marginLeft = interpretLength(propValue);
|
||||||
style.defined.marginLeft = 1;
|
style.defined.marginLeft = 1;
|
||||||
} else if (propName == "margin-right") {
|
} else if (propName == "margin-right") {
|
||||||
style.marginRight = static_cast<int16_t>(interpretLength(propValue));
|
style.marginRight = interpretLength(propValue);
|
||||||
style.defined.marginRight = 1;
|
style.defined.marginRight = 1;
|
||||||
} else if (propName == "margin") {
|
} else if (propName == "margin") {
|
||||||
// Shorthand: 1-4 values for top, right, bottom, left
|
// Shorthand: 1-4 values for top, right, bottom, left
|
||||||
const auto values = splitWhitespace(propValue);
|
const auto values = splitWhitespace(propValue);
|
||||||
if (!values.empty()) {
|
if (!values.empty()) {
|
||||||
const auto top = static_cast<int16_t>(interpretLength(values[0]));
|
const CssLength top = interpretLength(values[0]);
|
||||||
const int16_t right = values.size() >= 2 ? static_cast<int16_t>(interpretLength(values[1])) : top;
|
const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top;
|
||||||
const int16_t bottom = values.size() >= 3 ? static_cast<int16_t>(interpretLength(values[2])) : top;
|
const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top;
|
||||||
const int16_t left = values.size() >= 4 ? static_cast<int16_t>(interpretLength(values[3])) : right;
|
const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right;
|
||||||
style.marginTop = top;
|
style.marginTop = top;
|
||||||
style.marginRight = right;
|
style.marginRight = right;
|
||||||
style.marginBottom = bottom;
|
style.marginBottom = bottom;
|
||||||
@ -396,25 +396,25 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
|||||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||||
}
|
}
|
||||||
} else if (propName == "padding-top") {
|
} else if (propName == "padding-top") {
|
||||||
style.paddingTop = static_cast<int16_t>(interpretLength(propValue));
|
style.paddingTop = interpretLength(propValue);
|
||||||
style.defined.paddingTop = 1;
|
style.defined.paddingTop = 1;
|
||||||
} else if (propName == "padding-bottom") {
|
} else if (propName == "padding-bottom") {
|
||||||
style.paddingBottom = static_cast<int16_t>(interpretLength(propValue));
|
style.paddingBottom = interpretLength(propValue);
|
||||||
style.defined.paddingBottom = 1;
|
style.defined.paddingBottom = 1;
|
||||||
} else if (propName == "padding-left") {
|
} else if (propName == "padding-left") {
|
||||||
style.paddingLeft = static_cast<int16_t>(interpretLength(propValue));
|
style.paddingLeft = interpretLength(propValue);
|
||||||
style.defined.paddingLeft = 1;
|
style.defined.paddingLeft = 1;
|
||||||
} else if (propName == "padding-right") {
|
} else if (propName == "padding-right") {
|
||||||
style.paddingRight = static_cast<int16_t>(interpretLength(propValue));
|
style.paddingRight = interpretLength(propValue);
|
||||||
style.defined.paddingRight = 1;
|
style.defined.paddingRight = 1;
|
||||||
} else if (propName == "padding") {
|
} else if (propName == "padding") {
|
||||||
// Shorthand: 1-4 values for top, right, bottom, left
|
// Shorthand: 1-4 values for top, right, bottom, left
|
||||||
const auto values = splitWhitespace(propValue);
|
const auto values = splitWhitespace(propValue);
|
||||||
if (!values.empty()) {
|
if (!values.empty()) {
|
||||||
const auto top = static_cast<int16_t>(interpretLength(values[0]));
|
const CssLength top = interpretLength(values[0]);
|
||||||
const int16_t right = values.size() >= 2 ? static_cast<int16_t>(interpretLength(values[1])) : top;
|
const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top;
|
||||||
const int16_t bottom = values.size() >= 3 ? static_cast<int16_t>(interpretLength(values[2])) : top;
|
const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top;
|
||||||
const int16_t left = values.size() >= 4 ? static_cast<int16_t>(interpretLength(values[3])) : right;
|
const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right;
|
||||||
style.paddingTop = top;
|
style.paddingTop = top;
|
||||||
style.paddingRight = right;
|
style.paddingRight = right;
|
||||||
style.paddingBottom = bottom;
|
style.paddingBottom = bottom;
|
||||||
|
|||||||
@ -89,7 +89,7 @@ class CssParser {
|
|||||||
static CssFontStyle interpretFontStyle(const std::string& val);
|
static CssFontStyle interpretFontStyle(const std::string& val);
|
||||||
static CssFontWeight interpretFontWeight(const std::string& val);
|
static CssFontWeight interpretFontWeight(const std::string& val);
|
||||||
static CssTextDecoration interpretDecoration(const std::string& val);
|
static CssTextDecoration interpretDecoration(const std::string& val);
|
||||||
static float interpretLength(const std::string& val, float emSize = 16.0f);
|
static CssLength interpretLength(const std::string& val);
|
||||||
static int8_t interpretSpacing(const std::string& val);
|
static int8_t interpretSpacing(const std::string& val);
|
||||||
|
|
||||||
// String utilities
|
// String utilities
|
||||||
|
|||||||
@ -5,6 +5,37 @@
|
|||||||
// Text alignment options matching CSS text-align property
|
// Text alignment options matching CSS text-align property
|
||||||
enum class TextAlign : uint8_t { None = 0, Left = 1, Right = 2, Center = 3, Justify = 4 };
|
enum class TextAlign : uint8_t { None = 0, Left = 1, Right = 2, Center = 3, Justify = 4 };
|
||||||
|
|
||||||
|
// CSS length unit types
|
||||||
|
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 };
|
||||||
|
|
||||||
|
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
|
||||||
|
struct CssLength {
|
||||||
|
float value = 0.0f;
|
||||||
|
CssUnit unit = CssUnit::Pixels;
|
||||||
|
|
||||||
|
CssLength() = default;
|
||||||
|
CssLength(const float v, const CssUnit u) : value(v), unit(u) {}
|
||||||
|
|
||||||
|
// Convenience constructor for pixel values (most common case)
|
||||||
|
explicit CssLength(const float pixels) : value(pixels) {}
|
||||||
|
|
||||||
|
// Resolve to pixels given the current em size (font line height)
|
||||||
|
[[nodiscard]] float toPixels(const float emSize) const {
|
||||||
|
switch (unit) {
|
||||||
|
case CssUnit::Em:
|
||||||
|
case CssUnit::Rem:
|
||||||
|
return value * emSize;
|
||||||
|
case CssUnit::Points:
|
||||||
|
return value * 1.33f; // Approximate pt to px conversion
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve to int16_t pixels (for BlockStyle fields)
|
||||||
|
[[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast<int16_t>(toPixels(emSize)); }
|
||||||
|
};
|
||||||
|
|
||||||
// Font style options matching CSS font-style property
|
// Font style options matching CSS font-style property
|
||||||
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||||||
|
|
||||||
@ -61,21 +92,22 @@ struct CssPropertyFlags {
|
|||||||
|
|
||||||
// Represents a collection of CSS style properties
|
// Represents a collection of CSS style properties
|
||||||
// Only stores properties relevant to e-ink text rendering
|
// Only stores properties relevant to e-ink text rendering
|
||||||
|
// Length values are stored as CssLength (value + unit) for deferred resolution
|
||||||
struct CssStyle {
|
struct CssStyle {
|
||||||
TextAlign alignment = TextAlign::None;
|
TextAlign alignment = TextAlign::None;
|
||||||
CssFontStyle fontStyle = CssFontStyle::Normal;
|
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||||||
CssFontWeight fontWeight = CssFontWeight::Normal;
|
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||||||
CssTextDecoration decoration = CssTextDecoration::None;
|
CssTextDecoration decoration = CssTextDecoration::None;
|
||||||
|
|
||||||
float indentPixels = 0.0f; // First-line indent in pixels
|
CssLength indent; // First-line indent (deferred resolution)
|
||||||
int16_t marginTop = 0; // Vertical spacing before block (in pixels)
|
CssLength marginTop; // Vertical spacing before block
|
||||||
int16_t marginBottom = 0; // Vertical spacing after block (in pixels)
|
CssLength marginBottom; // Vertical spacing after block
|
||||||
int16_t marginLeft = 0; // Horizontal spacing left of block (in pixels)
|
CssLength marginLeft; // Horizontal spacing left of block
|
||||||
int16_t marginRight = 0; // Horizontal spacing right of block (in pixels)
|
CssLength marginRight; // Horizontal spacing right of block
|
||||||
int16_t paddingTop = 0; // Padding before (in pixels)
|
CssLength paddingTop; // Padding before
|
||||||
int16_t paddingBottom = 0; // Padding after (in pixels)
|
CssLength paddingBottom; // Padding after
|
||||||
int16_t paddingLeft = 0; // Padding left (in pixels)
|
CssLength paddingLeft; // Padding left
|
||||||
int16_t paddingRight = 0; // Padding right (in pixels)
|
CssLength paddingRight; // Padding right
|
||||||
|
|
||||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||||
|
|
||||||
@ -99,7 +131,7 @@ struct CssStyle {
|
|||||||
defined.decoration = 1;
|
defined.decoration = 1;
|
||||||
}
|
}
|
||||||
if (base.defined.indent) {
|
if (base.defined.indent) {
|
||||||
indentPixels = base.indentPixels;
|
indent = base.indent;
|
||||||
defined.indent = 1;
|
defined.indent = 1;
|
||||||
}
|
}
|
||||||
if (base.defined.marginTop) {
|
if (base.defined.marginTop) {
|
||||||
@ -159,9 +191,9 @@ struct CssStyle {
|
|||||||
fontStyle = CssFontStyle::Normal;
|
fontStyle = CssFontStyle::Normal;
|
||||||
fontWeight = CssFontWeight::Normal;
|
fontWeight = CssFontWeight::Normal;
|
||||||
decoration = CssTextDecoration::None;
|
decoration = CssTextDecoration::None;
|
||||||
indentPixels = 0.0f;
|
indent = CssLength{};
|
||||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||||
defined.clearAll();
|
defined.clearAll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,21 +43,28 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a BlockStyle from CSS style properties
|
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||||
BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle) {
|
// emSize is the current font line height, used for em/rem unit conversion
|
||||||
|
BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle, const float emSize) {
|
||||||
BlockStyle blockStyle;
|
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
|
// Vertical: combine margin and padding for top/bottom spacing
|
||||||
blockStyle.marginTop = static_cast<int16_t>(cssStyle.marginTop + cssStyle.paddingTop);
|
blockStyle.marginTop = static_cast<int16_t>(marginTopPx + paddingTopPx);
|
||||||
blockStyle.marginBottom = static_cast<int16_t>(cssStyle.marginBottom + cssStyle.paddingBottom);
|
blockStyle.marginBottom = static_cast<int16_t>(marginBottomPx + paddingBottomPx);
|
||||||
blockStyle.paddingTop = cssStyle.paddingTop;
|
blockStyle.paddingTop = paddingTopPx;
|
||||||
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
blockStyle.paddingBottom = paddingBottomPx;
|
||||||
// Horizontal: store margin and padding separately for layout calculations
|
// Horizontal: store margin and padding separately for layout calculations
|
||||||
blockStyle.marginLeft = cssStyle.marginLeft;
|
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize);
|
||||||
blockStyle.marginRight = cssStyle.marginRight;
|
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize);
|
||||||
blockStyle.paddingLeft = cssStyle.paddingLeft;
|
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize);
|
||||||
blockStyle.paddingRight = cssStyle.paddingRight;
|
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize);
|
||||||
// Text indent
|
// Text indent
|
||||||
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
blockStyle.textIndent = cssStyle.indent.toPixelsInt16(emSize);
|
||||||
blockStyle.textIndentDefined = cssStyle.defined.indent;
|
blockStyle.textIndentDefined = cssStyle.defined.indent;
|
||||||
return blockStyle;
|
return blockStyle;
|
||||||
}
|
}
|
||||||
@ -106,13 +113,44 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
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 TextBlock::Style style, 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);
|
currentTextBlock->setStyle(style);
|
||||||
currentTextBlock->setBlockStyle(blockStyle);
|
// Merge with existing block style to accumulate margins from parent block elements
|
||||||
|
// 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
|
||||||
|
const BlockStyle merged = mergeBlockStyles(currentTextBlock->getBlockStyle(), blockStyle);
|
||||||
|
currentTextBlock->setBlockStyle(merged);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,7 +282,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
self->currentBlockStyle = cssStyle;
|
self->currentBlockStyle = cssStyle;
|
||||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle));
|
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId)));
|
||||||
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)) {
|
||||||
@ -277,7 +315,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
}
|
}
|
||||||
|
|
||||||
self->currentBlockStyle = cssStyle;
|
self->currentBlockStyle = cssStyle;
|
||||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user