feat(epub): improve text-decoration support (#2397)

Co-authored-by: JiangoJ <jiangj2620@gmail.com>
This commit is contained in:
Leopoldo Pla Sempere
2026-07-02 12:35:24 +03:00
committed by GitHub
co-authored by JiangoJ
parent 5c86bfe1fd
commit 57447a56a6
11 changed files with 183 additions and 81 deletions
+5 -3
View File
@@ -90,13 +90,13 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 25
### Version 28
Each file in `sections/*.bin` stores one laid-out spine section. The header is
also the cache-busting key: if any layout-affecting setting differs from the
current reader settings, the section is discarded and rebuilt.
Version 25 includes:
Version 28 includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading
@@ -105,6 +105,8 @@ Version 25 includes:
- paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata
- per-page footnote entries
- serialized word style bits for underline, strikethrough, superscript, and
subscript
ImHex pattern:
@@ -113,7 +115,7 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 25
#define EXPECTED_VERSION 28
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
+1 -1
View File
@@ -1,7 +1,7 @@
#include "EpdFontFamily.h"
const EpdFont* EpdFontFamily::getFont(const Style style) const {
// Extract font style bits (ignore UNDERLINE bit for font selection)
// Extract font style bits; render-time overlay bits do not affect font selection.
const bool hasBold = (style & BOLD) != 0;
const bool hasItalic = (style & ITALIC) != 0;
+4
View File
@@ -17,6 +17,7 @@ class EpdFontFamily {
SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender
SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender
};
static constexpr uint8_t TEXT_DECORATION_MASK = static_cast<uint8_t>(UNDERLINE | STRIKETHROUGH);
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
const EpdFont* boldItalic = nullptr)
@@ -27,6 +28,9 @@ class EpdFontFamily {
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;
int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const;
uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const;
static constexpr bool hasTextDecoration(const Style style) {
return (static_cast<uint8_t>(style) & TEXT_DECORATION_MASK) != 0;
}
private:
const EpdFont* regular;
+2 -2
View File
@@ -10,8 +10,8 @@
#include "parsers/ChapterHtmlSlimParser.h"
namespace {
// v27: words NFC-composed at layout time; bump invalidates NFD section caches.
constexpr uint8_t SECTION_FILE_VERSION = 27;
// v28: text decoration bits now include line-through in serialized wordStyles.
constexpr uint8_t SECTION_FILE_VERSION = 28;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
+71 -5
View File
@@ -21,6 +21,39 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
const bool scanning = renderer.isFontCacheScanning();
const int ascender = renderer.getFontAscenderSize(fontId);
struct DecorationLineTracker {
EpdFontFamily::Style style;
int yOffset;
int startX = -1;
int endX = -1;
int yPos = 0;
bool active() const { return startX != -1; }
void reset() {
startX = -1;
endX = -1;
yPos = 0;
}
};
DecorationLineTracker decorationLines[] = {
{EpdFontFamily::UNDERLINE, ascender + 2},
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
};
const auto flushDecoration = [&](DecorationLineTracker& line) {
if (line.active()) {
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
line.reset();
}
};
const auto flushDecorations = [&]() {
for (auto& line : decorationLines) {
flushDecoration(line);
}
};
for (size_t i = 0; i < words.size(); i++) {
const int wordX = wordXpos[i] + x;
const EpdFontFamily::Style currentStyle = wordStyles[i];
@@ -59,18 +92,51 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir);
}
if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) {
if (scanning) {
continue;
}
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
const std::string& w = words[i];
int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
const int underlineY = wordY + ascender + 2;
int lineStartX = wordX;
int lineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
underlineWidth = (underlineWidth + 1) / 2;
lineWidth = (lineWidth + 1) / 2;
}
renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true);
// Do not decorate the synthetic em-space used for paragraph indentation.
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
static_cast<uint8_t>(w[2]) == 0x83) {
const char* visibleText = w.c_str() + 3;
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
lineWidth = (lineWidth + 1) / 2;
}
}
for (auto& line : decorationLines) {
if ((currentStyle & line.style) == 0) {
flushDecoration(line);
continue;
}
const int lineY = wordY + line.yOffset;
if (line.active() && line.yPos != lineY) {
flushDecoration(line);
}
if (!line.active()) {
line.startX = lineStartX;
line.yPos = lineY;
}
line.endX = lineStartX + lineWidth;
}
} else {
flushDecorations();
}
}
flushDecorations();
}
bool TextBlock::serialize(HalFile& file) const {
+15 -13
View File
@@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe
[](char a, char b) { return asciiToLower(a) == b; });
}
// Case-insensitive ASCII substring search. Only needed by text-decoration,
// which accepts multi-value strings like "underline solid red".
constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) {
return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
[](char a, char b) { return asciiToLower(a) == b; }) != value.end();
}
// Walk s and invoke fn(token) for each non-empty run between delimiters.
// Tokens are boundary-trimmed and yielded as string_views into s; no
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
@@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
}
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
// text-decoration can have multiple space-separated values
if (icontainsAscii(val, "underline")) {
return CssTextDecoration::Underline;
}
return CssTextDecoration::None;
// text-decoration can have multiple space-separated values. Compare whole tokens
// so malformed values like "notunderline" do not accidentally enable a line.
CssTextDecoration result = CssTextDecoration::None;
bool explicitNone = false;
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
if (iequalsAscii(token, "none")) {
explicitNone = true;
} else if (iequalsAscii(token, "underline")) {
result = result | CssTextDecoration::Underline;
} else if (iequalsAscii(token, "line-through")) {
result = result | CssTextDecoration::LineThrough;
}
});
return explicitNone ? CssTextDecoration::None : result;
}
CssLength CssParser::interpretLength(std::string_view val) {
@@ -868,7 +870,7 @@ bool CssParser::loadFromCache() {
rulesBySelector_.clear();
return false;
}
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
style.textDecoration = static_cast<CssTextDecoration>(enumVal & CSS_TEXT_DECORATION_MASK);
if (file.read(&enumVal, 1) != 1) {
rulesBySelector_.clear();
+1 -1
View File
@@ -33,7 +33,7 @@
class CssParser {
public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 6;
static constexpr uint8_t CSS_CACHE_VERSION = 7;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default;
+13 -2
View File
@@ -52,8 +52,19 @@ 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 };
// Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
}
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
// Display options - only None and Block are relevant for e-ink rendering
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
+65 -51
View File
@@ -34,6 +34,7 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr const char* BOLD_TAGS[] = {"b", "strong"};
constexpr const char* ITALIC_TAGS[] = {"i", "em"};
constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"};
constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"};
constexpr const char* IMAGE_TAGS[] = {"img"};
constexpr const char* SKIP_TAGS[] = {"head"};
@@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const
}
}
// Update effective bold/italic/underline based on block style and inline style stack
EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) {
EpdFontFamily::Style style = EpdFontFamily::REGULAR;
if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::UNDERLINE);
}
if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) {
style = static_cast<EpdFontFamily::Style>(style | EpdFontFamily::STRIKETHROUGH);
}
return style;
}
void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) {
if (css.hasTextDecoration()) {
entry.hasTextDecoration = true;
entry.textDecoration = css.textDecoration;
}
}
void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration,
const CssStyle& cssStyle) {
StyleStackEntry entry;
entry.depth = depth;
entry.hasTextDecoration = true;
entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
}
if (cssStyle.hasFontStyle()) {
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
inlineStyleStack.push_back(entry);
updateEffectiveInlineStyle();
}
// Update effective bold/italic/decorations based on block style and inline style stack
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
// Start with block-level styles
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
effectiveUnderline =
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
effectiveTextDecoration =
currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None;
effectiveDirectionDefined = currentCssStyle.hasDirection();
effectiveDirection = currentCssStyle.direction;
effectiveSup = false;
@@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
if (entry.hasItalic) {
effectiveItalic = entry.italic;
}
if (entry.hasUnderline) {
effectiveUnderline = entry.underline;
// CSS line decorations propagate through descendants; child entries add
// their own lines but cannot cancel an ancestor's already active line.
if (entry.hasTextDecoration) {
effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration;
}
if (entry.hasDirection) {
effectiveDirectionDefined = true;
@@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
// Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
// Combine style flags using bitwise OR
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
@@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
if (isItalic) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
}
if (isUnderline) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
}
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration));
if (effectiveSup) {
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::SUP);
} else if (effectiveSub) {
@@ -420,14 +457,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
headerStyle.bold = false;
headerStyle.hasItalic = true;
headerStyle.italic = true;
headerStyle.hasUnderline = true;
headerStyle.underline = false;
self->inlineStyleStack.push_back(headerStyle);
self->updateEffectiveInlineStyle();
const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration;
self->effectiveTextDecoration = CssTextDecoration::None;
self->characterData(userData, headerText.c_str(), static_cast<int>(headerText.length()));
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
}
self->effectiveTextDecoration = savedTextDecoration;
self->nextWordContinues = false;
self->inlineStyleStack.pop_back();
self->updateEffectiveInlineStyle();
@@ -760,12 +798,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentFootnote.number[0] = '\0';
self->currentFootnoteLinkTextLen = 0;
// Apply underline style to visually indicate the link
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
// Apply underline style to visually indicate the link.
StyleStackEntry entry;
entry.depth = self->depth;
entry.hasUnderline = true;
entry.underline = true;
entry.hasTextDecoration = true;
entry.textDecoration = CssTextDecoration::Underline;
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -837,23 +874,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->flushPartWordBuffer();
self->nextWordContinues = true;
}
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
// Push inline style entry for underline tag
StyleStackEntry entry;
entry.depth = self->depth; // Track depth for matching pop
entry.hasUnderline = true;
entry.underline = true;
if (cssStyle.hasFontWeight()) {
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle);
} else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
self->flushPartWordBuffer();
self->nextWordContinues = true;
}
if (cssStyle.hasFontStyle()) {
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle);
} else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) {
// Flush buffer before style change so preceding text gets current style
if (self->partWordBufferIndex > 0) {
@@ -870,10 +898,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -893,10 +918,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
self->inlineStyleStack.push_back(entry);
self->updateEffectiveInlineStyle();
@@ -935,10 +957,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
entry.hasItalic = true;
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
}
if (cssStyle.hasTextDecoration()) {
entry.hasUnderline = true;
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
}
applyTextDecorationToEntry(entry, cssStyle);
applyDirectionToEntry(entry, cssStyle);
if (cssStyle.hasVerticalAlign()) {
if (cssStyle.verticalAlign == CssVerticalAlign::Super) {
@@ -1150,9 +1169,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1;
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic;
const bool headerOrBlockTag = isHeaderOrBlock(name);
const bool tableStructuralTag = isTableStructuralTag(name);
@@ -1171,7 +1189,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
!matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1;
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) ||
matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag ||
matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) ||
matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag ||
matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1;
if (shouldFlush) {
@@ -1230,11 +1249,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->italicUntilDepth = INT_MAX;
}
// Leaving underline tag
if (self->underlineUntilDepth == self->depth) {
self->underlineUntilDepth = INT_MAX;
}
// Pop from inline style stack if we pushed an entry at this depth
// This handles all inline elements: b, i, u, span, etc.
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
@@ -31,7 +31,6 @@ class ChapterHtmlSlimParser {
int skipUntilDepth = INT_MAX;
int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX;
int underlineUntilDepth = INT_MAX;
// buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
@@ -60,7 +59,8 @@ class ChapterHtmlSlimParser {
int depth = 0;
bool hasBold = false, bold = false;
bool hasItalic = false, italic = false;
bool hasUnderline = false, underline = false;
bool hasTextDecoration = false;
CssTextDecoration textDecoration = CssTextDecoration::None;
bool hasDirection = false;
CssTextDirection direction = CssTextDirection::Ltr;
bool hasSup = false, sup = false;
@@ -71,7 +71,7 @@ class ChapterHtmlSlimParser {
CssStyle currentCssStyle;
bool effectiveBold = false;
bool effectiveItalic = false;
bool effectiveUnderline = false;
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
bool effectiveDirectionDefined = false;
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
bool effectiveSup = false;
@@ -101,7 +101,10 @@ class ChapterHtmlSlimParser {
void flushPendingAnchor();
void flushPartWordBuffer();
void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
void emitHorizontalRule(const BlockStyle& blockStyle);
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
Binary file not shown.