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.

feat: Add percentage support to CSS properties (#738)

## Summary
- Closes #730

**What is the goal of this PR?**
- Adds percentage-based value support to CSS properties that accept
percentages (padding, margin, text-indent)

**What changes are included?**
- Adds `Percent` as another CSS unit
- Passes the viewport width to `fromCssStyle` so that we can resolve
percentage-based values
- Adds a fallback of using an emspace for text-indent if we have an
unresolvable value for whatever reason

## Additional Context

- This was missed in my CSS support feature, and the fallback when we
encounter a percentage value is to use px instead. This means 5% (which
would be ~30px on the screen) turns into 5px. When percentages are used
in `text-indent`, this fallback behavior makes the indent look like a
single space character. Whoops! 😬

My test EPUB has been updated
[here](https://github.com/jdk2pq/css-test-epub) with percentage based
CSS values at the end of the book.

---

### 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**_, Claude Code

authored by

Jake Kenneally and committed by
GitHub
9b04c2ec ffddc247

+38 -19
+18 -12
lib/Epub/Epub/blocks/BlockStyle.h
··· 64 64 // Create a BlockStyle from CSS style properties, resolving CssLength values to pixels 65 65 // emSize is the current font line height, used for em/rem unit conversion 66 66 // paragraphAlignment is the user's paragraphAlignment setting preference 67 - static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment) { 67 + static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment, 68 + const uint16_t viewportWidth = 0) { 68 69 BlockStyle blockStyle; 69 - // Resolve all CssLength values to pixels using the current font's em size 70 - blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize); 71 - blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize); 72 - blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize); 73 - blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize); 70 + const float vw = viewportWidth; 71 + // Resolve all CssLength values to pixels using the current font's em size and viewport width 72 + blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw); 73 + blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw); 74 + blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw); 75 + blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw); 74 76 75 - blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize); 76 - blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize); 77 - blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize); 78 - blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize); 77 + blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw); 78 + blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw); 79 + blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw); 80 + blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw); 79 81 80 - blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); 81 - blockStyle.textIndentDefined = cssStyle.hasTextIndent(); 82 + // For textIndent: if it's a percentage we can't resolve (no viewport width), 83 + // leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used 84 + if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) { 85 + blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw); 86 + blockStyle.textIndentDefined = true; 87 + } 82 88 blockStyle.textAlignDefined = cssStyle.hasTextAlign(); 83 89 // User setting overrides CSS, unless "Book's Style" alignment setting is selected 84 90 if (paragraphAlignment == CssTextAlign::None) {
+3 -1
lib/Epub/Epub/css/CssParser.cpp
··· 283 283 unit = CssUnit::Rem; 284 284 } else if (unitPart == "pt") { 285 285 unit = CssUnit::Points; 286 + } else if (unitPart == "%") { 287 + unit = CssUnit::Percent; 286 288 } 287 289 // px and unitless default to Pixels 288 290 ··· 518 520 // Cache serialization 519 521 520 522 // Cache format version - increment when format changes 521 - constexpr uint8_t CSS_CACHE_VERSION = 1; 523 + constexpr uint8_t CSS_CACHE_VERSION = 2; 522 524 523 525 bool CssParser::saveToCache(FsFile& file) const { 524 526 if (!file) {
+14 -3
lib/Epub/Epub/css/CssStyle.h
··· 4 4 5 5 // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings 6 6 enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; 7 - enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; 7 + enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 }; 8 8 9 9 // Represents a CSS length value with its unit, allowing deferred resolution to pixels 10 10 struct CssLength { ··· 17 17 // Convenience constructor for pixel values (most common case) 18 18 explicit CssLength(const float pixels) : value(pixels) {} 19 19 20 + // Returns true if this length can be resolved to pixels with the given context. 21 + // Percentage units require a non-zero containerWidth to resolve. 22 + [[nodiscard]] bool isResolvable(const float containerWidth = 0) const { 23 + return unit != CssUnit::Percent || containerWidth > 0; 24 + } 25 + 20 26 // Resolve to pixels given the current em size (font line height) 21 - [[nodiscard]] float toPixels(const float emSize) const { 27 + // containerWidth is needed for percentage units (e.g. viewport width) 28 + [[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const { 22 29 switch (unit) { 23 30 case CssUnit::Em: 24 31 case CssUnit::Rem: 25 32 return value * emSize; 26 33 case CssUnit::Points: 27 34 return value * 1.33f; // Approximate pt to px conversion 35 + case CssUnit::Percent: 36 + return value * containerWidth / 100.0f; 28 37 default: 29 38 return value; 30 39 } 31 40 } 32 41 33 42 // Resolve to int16_t pixels (for BlockStyle fields) 34 - [[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast<int16_t>(toPixels(emSize)); } 43 + [[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const { 44 + return static_cast<int16_t>(toPixels(emSize, containerWidth)); 45 + } 35 46 }; 36 47 37 48 // Font style options matching CSS font-style property
+3 -3
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 213 213 } 214 214 215 215 const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; 216 - const auto userAlignmentBlockStyle = 217 - BlockStyle::fromCssStyle(cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment)); 216 + const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( 217 + cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth); 218 218 219 219 if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { 220 220 self->currentCssStyle = cssStyle; 221 - auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); 221 + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth); 222 222 headerBlockStyle.textAlignDefined = true; 223 223 if (self->embeddedStyle && cssStyle.hasTextAlign()) { 224 224 headerBlockStyle.alignment = cssStyle.textAlign;