A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Hanging indent (negative text-indent) and em-unit sizing (#1229)

## Summary

* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:

* **What changes are included?**
1. Negative text-indent was silently ignored

Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.

2. extraParagraphSpacing suppressed hanging indents

Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.

3. em unit was calibrated against line height, not font size

emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).

## Additional Context

Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }

<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).

After: the bullet marker hangs at 2 em; continuation lines align at 3
em.

<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**YES**_

authored by

jpirnay and committed by
GitHub
aff93f1d f0a549b6

+25 -16
+19 -9
lib/Epub/Epub/ParsedText.cpp
··· 144 144 return {}; 145 145 } 146 146 147 - // Calculate first line indent (only for left/justified text without extra paragraph spacing) 147 + // Calculate first line indent (only for left/justified text). 148 + // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. 149 + // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — 150 + // it is structural (positions the bullet/marker), not decorative. 148 151 const int firstLineIndent = 149 - blockStyle.textIndent > 0 && !extraParagraphSpacing && 152 + blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && 150 153 (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) 151 154 ? blockStyle.textIndent 152 155 : 0; ··· 275 278 const int pageWidth, const int spaceWidth, 276 279 std::vector<uint16_t>& wordWidths, 277 280 std::vector<bool>& continuesVec) { 278 - // Calculate first line indent (only for left/justified text without extra paragraph spacing) 281 + // Calculate first line indent (only for left/justified text). 282 + // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. 283 + // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — 284 + // it is structural (positions the bullet/marker), not decorative. 279 285 const int firstLineIndent = 280 - blockStyle.textIndent > 0 && !extraParagraphSpacing && 286 + blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && 281 287 (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) 282 288 ? blockStyle.textIndent 283 289 : 0; ··· 443 449 const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; 444 450 const size_t lineWordCount = lineBreak - lastBreakAt; 445 451 446 - // Calculate first line indent (only for left/justified text without extra paragraph spacing) 452 + // Calculate first line indent (only for left/justified text). 453 + // Positive text-indent (paragraph indent) is suppressed when extraParagraphSpacing is on. 454 + // Negative text-indent (hanging indent, e.g. margin-left:3em; text-indent:-1em) always applies — 455 + // it is structural (positions the bullet/marker), not decorative. 447 456 const bool isFirstLine = breakIndex == 0; 448 457 const int firstLineIndent = 449 - isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing && 458 + isFirstLine && blockStyle.textIndentDefined && (blockStyle.textIndent < 0 || !extraParagraphSpacing) && 450 459 (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) 451 460 ? blockStyle.textIndent 452 461 : 0; ··· 485 494 ? spareSpace / static_cast<int>(actualGapCount) 486 495 : 0; 487 496 488 - // Calculate initial x position (first line starts at indent for left/justified text) 489 - auto xpos = static_cast<uint16_t>(firstLineIndent); 497 + // Calculate initial x position (first line starts at indent for left/justified text; 498 + // may be negative for hanging indents, e.g. margin-left:3em; text-indent:-1em). 499 + auto xpos = static_cast<int16_t>(firstLineIndent); 490 500 if (blockStyle.alignment == CssTextAlign::Right) { 491 501 xpos = effectivePageWidth - lineWordWidthSum - totalNaturalGaps; 492 502 } else if (blockStyle.alignment == CssTextAlign::Center) { ··· 495 505 496 506 // Pre-calculate X positions for words 497 507 // Continuation words attach to the previous word with no space before them 498 - std::vector<uint16_t> lineXPos; 508 + std::vector<int16_t> lineXPos; 499 509 lineXPos.reserve(lineWordCount); 500 510 501 511 for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
+1 -1
lib/Epub/Epub/Section.cpp
··· 10 10 #include "parsers/ChapterHtmlSlimParser.h" 11 11 12 12 namespace { 13 - constexpr uint8_t SECTION_FILE_VERSION = 14; 13 + constexpr uint8_t SECTION_FILE_VERSION = 16; 14 14 constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + 15 15 sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + 16 16 sizeof(uint32_t);
+1 -1
lib/Epub/Epub/blocks/TextBlock.cpp
··· 74 74 std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) { 75 75 uint16_t wc; 76 76 std::vector<std::string> words; 77 - std::vector<uint16_t> wordXpos; 77 + std::vector<int16_t> wordXpos; 78 78 std::vector<EpdFontFamily::Style> wordStyles; 79 79 BlockStyle blockStyle; 80 80
+2 -2
lib/Epub/Epub/blocks/TextBlock.h
··· 13 13 class TextBlock final : public Block { 14 14 private: 15 15 std::vector<std::string> words; 16 - std::vector<uint16_t> wordXpos; 16 + std::vector<int16_t> wordXpos; 17 17 std::vector<EpdFontFamily::Style> wordStyles; 18 18 BlockStyle blockStyle; 19 19 20 20 public: 21 - explicit TextBlock(std::vector<std::string> words, std::vector<uint16_t> word_xpos, 21 + explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos, 22 22 std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle()) 23 23 : words(std::move(words)), 24 24 wordXpos(std::move(word_xpos)),
+2 -3
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 278 278 279 279 int displayWidth = 0; 280 280 int displayHeight = 0; 281 - const float emSize = 282 - static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; 281 + const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId)); 283 282 CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{}; 284 283 // Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules 285 284 if (!styleAttr.empty()) { ··· 505 504 } 506 505 } 507 506 508 - const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; 507 + const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId)); 509 508 const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( 510 509 cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth); 511 510