## Summary - Add CSS `display: none` support to the EPUB rendering pipeline (fixes #1431) - Parse `display` property in stylesheets and inline styles, with full cascade resolution (element, class, element.class, inline) - Skip hidden elements and all their descendants in `ChapterHtmlSlimParser` - Separate display:none check for `<img>` tags (image code path is independent of the general element handler) - Flush pending text blocks before placing images to fix layout ordering (text preceding an image now correctly renders above it) - Bump CSS cache version to 4 to invalidate stale caches - Add test EPUB (`test_display_none.epub`) covering class selectors, element selectors, combined selectors, inline styles, nested hidden content, hidden images, style priority/override, and realistic use cases
234 lines
8.3 KiB
C++
234 lines
8.3 KiB
C++
#pragma once
|
||
|
||
#include <cstdint>
|
||
|
||
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
|
||
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 };
|
||
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 };
|
||
|
||
// 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) {}
|
||
|
||
// Returns true if this length can be resolved to pixels with the given context.
|
||
// Percentage units require a non-zero containerWidth to resolve.
|
||
[[nodiscard]] bool isResolvable(const float containerWidth = 0) const {
|
||
return unit != CssUnit::Percent || containerWidth > 0;
|
||
}
|
||
|
||
// Resolve to pixels given the current em size (font line height)
|
||
// containerWidth is needed for percentage units (e.g. viewport width)
|
||
[[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const {
|
||
switch (unit) {
|
||
case CssUnit::Em:
|
||
case CssUnit::Rem:
|
||
return value * emSize;
|
||
case CssUnit::Points:
|
||
return value * 1.33f; // Approximate pt to px conversion
|
||
case CssUnit::Percent:
|
||
return value * containerWidth / 100.0f;
|
||
default:
|
||
return value;
|
||
}
|
||
}
|
||
|
||
// Resolve to int16_t pixels (for BlockStyle fields)
|
||
[[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const {
|
||
return static_cast<int16_t>(toPixels(emSize, containerWidth));
|
||
}
|
||
};
|
||
|
||
// Font style options matching CSS font-style property
|
||
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||
|
||
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||
|
||
// Text decoration options
|
||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
||
|
||
// Display options - only None and Block are relevant for e-ink rendering
|
||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||
|
||
// Bitmask for tracking which properties have been explicitly set
|
||
struct CssPropertyFlags {
|
||
uint16_t textAlign : 1;
|
||
uint16_t fontStyle : 1;
|
||
uint16_t fontWeight : 1;
|
||
uint16_t textDecoration : 1;
|
||
uint16_t textIndent : 1;
|
||
uint16_t marginTop : 1;
|
||
uint16_t marginBottom : 1;
|
||
uint16_t marginLeft : 1;
|
||
uint16_t marginRight : 1;
|
||
uint16_t paddingTop : 1;
|
||
uint16_t paddingBottom : 1;
|
||
uint16_t paddingLeft : 1;
|
||
uint16_t paddingRight : 1;
|
||
uint16_t imageHeight : 1;
|
||
uint16_t imageWidth : 1;
|
||
uint16_t display : 1;
|
||
|
||
CssPropertyFlags()
|
||
: textAlign(0),
|
||
fontStyle(0),
|
||
fontWeight(0),
|
||
textDecoration(0),
|
||
textIndent(0),
|
||
marginTop(0),
|
||
marginBottom(0),
|
||
marginLeft(0),
|
||
marginRight(0),
|
||
paddingTop(0),
|
||
paddingBottom(0),
|
||
paddingLeft(0),
|
||
paddingRight(0),
|
||
imageHeight(0),
|
||
imageWidth(0),
|
||
display(0) {}
|
||
|
||
[[nodiscard]] bool anySet() const {
|
||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight ||
|
||
imageWidth || display;
|
||
}
|
||
|
||
void clearAll() {
|
||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||
imageHeight = imageWidth = display = 0;
|
||
}
|
||
};
|
||
|
||
// Represents a collection of CSS style properties
|
||
// Only stores properties relevant to e-ink text rendering
|
||
// Length values are stored as CssLength (value + unit) for deferred resolution
|
||
struct CssStyle {
|
||
CssTextAlign textAlign = CssTextAlign::Left;
|
||
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||
|
||
CssLength textIndent; // First-line indent (deferred resolution)
|
||
CssLength marginTop; // Vertical spacing before block
|
||
CssLength marginBottom; // Vertical spacing after block
|
||
CssLength marginLeft; // Horizontal spacing left of block
|
||
CssLength marginRight; // Horizontal spacing right of block
|
||
CssLength paddingTop; // Padding before
|
||
CssLength paddingBottom; // Padding after
|
||
CssLength paddingLeft; // Padding left
|
||
CssLength paddingRight; // Padding right
|
||
CssLength imageHeight; // Height for img (e.g. 2em) – width derived from aspect ratio when only height set
|
||
CssLength imageWidth; // Width for img when both or only width set
|
||
CssDisplay display = CssDisplay::Block; // display property (Block or None)
|
||
|
||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||
|
||
// Apply properties from another style, only overwriting if the other style
|
||
// has that property explicitly defined
|
||
void applyOver(const CssStyle& base) {
|
||
if (base.hasTextAlign()) {
|
||
textAlign = base.textAlign;
|
||
defined.textAlign = 1;
|
||
}
|
||
if (base.hasFontStyle()) {
|
||
fontStyle = base.fontStyle;
|
||
defined.fontStyle = 1;
|
||
}
|
||
if (base.hasFontWeight()) {
|
||
fontWeight = base.fontWeight;
|
||
defined.fontWeight = 1;
|
||
}
|
||
if (base.hasTextDecoration()) {
|
||
textDecoration = base.textDecoration;
|
||
defined.textDecoration = 1;
|
||
}
|
||
if (base.hasTextIndent()) {
|
||
textIndent = base.textIndent;
|
||
defined.textIndent = 1;
|
||
}
|
||
if (base.hasMarginTop()) {
|
||
marginTop = base.marginTop;
|
||
defined.marginTop = 1;
|
||
}
|
||
if (base.hasMarginBottom()) {
|
||
marginBottom = base.marginBottom;
|
||
defined.marginBottom = 1;
|
||
}
|
||
if (base.hasMarginLeft()) {
|
||
marginLeft = base.marginLeft;
|
||
defined.marginLeft = 1;
|
||
}
|
||
if (base.hasMarginRight()) {
|
||
marginRight = base.marginRight;
|
||
defined.marginRight = 1;
|
||
}
|
||
if (base.hasPaddingTop()) {
|
||
paddingTop = base.paddingTop;
|
||
defined.paddingTop = 1;
|
||
}
|
||
if (base.hasPaddingBottom()) {
|
||
paddingBottom = base.paddingBottom;
|
||
defined.paddingBottom = 1;
|
||
}
|
||
if (base.hasPaddingLeft()) {
|
||
paddingLeft = base.paddingLeft;
|
||
defined.paddingLeft = 1;
|
||
}
|
||
if (base.hasPaddingRight()) {
|
||
paddingRight = base.paddingRight;
|
||
defined.paddingRight = 1;
|
||
}
|
||
if (base.hasImageHeight()) {
|
||
imageHeight = base.imageHeight;
|
||
defined.imageHeight = 1;
|
||
}
|
||
if (base.hasImageWidth()) {
|
||
imageWidth = base.imageWidth;
|
||
defined.imageWidth = 1;
|
||
}
|
||
if (base.hasDisplay()) {
|
||
display = base.display;
|
||
defined.display = 1;
|
||
}
|
||
}
|
||
|
||
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||
[[nodiscard]] bool hasFontStyle() const { return defined.fontStyle; }
|
||
[[nodiscard]] bool hasFontWeight() const { return defined.fontWeight; }
|
||
[[nodiscard]] bool hasTextDecoration() const { return defined.textDecoration; }
|
||
[[nodiscard]] bool hasTextIndent() const { return defined.textIndent; }
|
||
[[nodiscard]] bool hasMarginTop() const { return defined.marginTop; }
|
||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||
[[nodiscard]] bool hasMarginRight() const { return defined.marginRight; }
|
||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
|
||
[[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; }
|
||
[[nodiscard]] bool hasDisplay() const { return defined.display; }
|
||
|
||
void reset() {
|
||
textAlign = CssTextAlign::Left;
|
||
fontStyle = CssFontStyle::Normal;
|
||
fontWeight = CssFontWeight::Normal;
|
||
textDecoration = CssTextDecoration::None;
|
||
textIndent = CssLength{};
|
||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||
imageHeight = imageWidth = CssLength{};
|
||
display = CssDisplay::Block;
|
||
defined.clearAll();
|
||
}
|
||
};
|