Support strikethrough
This commit is contained in:
@@ -17,26 +17,28 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||
|
||||
const std::string& w = words[i];
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
int startX = wordX;
|
||||
int lineWidth = fullWordWidth;
|
||||
const bool hasEmSpacePrefix = w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 &&
|
||||
static_cast<uint8_t>(w[1]) == 0x80 && static_cast<uint8_t>(w[2]) == 0x83;
|
||||
if (hasEmSpacePrefix) {
|
||||
const char* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||
startX = wordX + prefixWidth;
|
||||
lineWidth = visibleWidth;
|
||||
}
|
||||
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = words[i];
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
renderer.drawLine(startX, underlineY, startX + lineWidth, underlineY, true);
|
||||
}
|
||||
|
||||
int startX = wordX;
|
||||
int underlineWidth = fullWordWidth;
|
||||
|
||||
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
|
||||
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* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||
startX = wordX + prefixWidth;
|
||||
underlineWidth = visibleWidth;
|
||||
}
|
||||
|
||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
||||
if ((currentStyle & EpdFontFamily::STRIKETHROUGH) != 0) {
|
||||
const int strikeY = y + renderer.getFontAscenderSize(fontId) / 2 + 1;
|
||||
renderer.drawLine(startX, strikeY, startX + lineWidth, strikeY, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,10 +205,12 @@ CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
// text-decoration can have multiple space-separated values
|
||||
if (v.find("underline") != std::string::npos) {
|
||||
return CssTextDecoration::Underline;
|
||||
}
|
||||
return CssTextDecoration::None;
|
||||
bool underline = v.find("underline") != std::string::npos;
|
||||
bool lineThrough = v.find("line-through") != std::string::npos;
|
||||
uint8_t result = 0;
|
||||
if (underline) result |= static_cast<uint8_t>(CssTextDecoration::Underline);
|
||||
if (lineThrough) result |= static_cast<uint8_t>(CssTextDecoration::LineThrough);
|
||||
return static_cast<CssTextDecoration>(result);
|
||||
}
|
||||
|
||||
CssLength CssParser::interpretLength(const std::string& val) {
|
||||
|
||||
@@ -52,7 +52,7 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||
|
||||
// Text decoration options
|
||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2, UnderlineLineThrough = 3 };
|
||||
|
||||
// Display options - only None and Block are relevant for e-ink rendering
|
||||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||||
|
||||
@@ -43,6 +43,9 @@ constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||
const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
|
||||
|
||||
const char* STRIKETHROUGH_TAGS[] = {"s", "del", "strike"};
|
||||
constexpr int NUM_STRIKETHROUGH_TAGS = sizeof(STRIKETHROUGH_TAGS) / sizeof(STRIKETHROUGH_TAGS[0]);
|
||||
|
||||
const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
|
||||
@@ -135,8 +138,11 @@ 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;
|
||||
effectiveUnderline = currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) &
|
||||
static_cast<uint8_t>(CssTextDecoration::Underline)) != 0;
|
||||
effectiveStrikethrough =
|
||||
currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) &
|
||||
static_cast<uint8_t>(CssTextDecoration::LineThrough)) != 0;
|
||||
|
||||
// Apply inline style stack in order
|
||||
for (const auto& entry : inlineStyleStack) {
|
||||
@@ -149,6 +155,9 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
if (entry.hasUnderline) {
|
||||
effectiveUnderline = entry.underline;
|
||||
}
|
||||
if (entry.hasStrikethrough) {
|
||||
effectiveStrikethrough = entry.strikethrough;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +167,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
||||
const bool isStrikethrough = strikethroughUntilDepth < depth || effectiveStrikethrough;
|
||||
|
||||
// Combine style flags using bitwise OR
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
@@ -170,6 +180,9 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
if (isUnderline) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
if (isStrikethrough) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::STRIKETHROUGH);
|
||||
}
|
||||
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
@@ -857,18 +870,30 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->preUntilDepth = std::min(self->preUntilDepth, self->depth);
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||
matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) {
|
||||
// Flush buffer before style change so preceding text gets current style
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
self->flushPartWordBuffer();
|
||||
self->nextWordContinues = true;
|
||||
}
|
||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||
// Push inline style entry for underline tag
|
||||
if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||
}
|
||||
if (matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) {
|
||||
self->strikethroughUntilDepth = std::min(self->strikethroughUntilDepth, self->depth);
|
||||
}
|
||||
// Push inline style entry for underline/strikethrough tag
|
||||
StyleStackEntry entry;
|
||||
entry.depth = self->depth; // Track depth for matching pop
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
}
|
||||
if (matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) {
|
||||
entry.hasStrikethrough = true;
|
||||
entry.strikethrough = true;
|
||||
}
|
||||
if (cssStyle.hasFontWeight()) {
|
||||
entry.hasBold = true;
|
||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||
@@ -896,8 +921,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::Underline)) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
}
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::LineThrough)) {
|
||||
entry.hasStrikethrough = true;
|
||||
entry.strikethrough = true;
|
||||
}
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
@@ -918,8 +950,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::Underline)) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
}
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::LineThrough)) {
|
||||
entry.hasStrikethrough = true;
|
||||
entry.strikethrough = true;
|
||||
}
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
@@ -942,8 +981,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::Underline)) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = true;
|
||||
}
|
||||
if (dec & static_cast<uint8_t>(CssTextDecoration::LineThrough)) {
|
||||
entry.hasStrikethrough = true;
|
||||
entry.strikethrough = true;
|
||||
}
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
@@ -1188,8 +1234,10 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
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 willClearStrikethrough = self->strikethroughUntilDepth == self->depth - 1;
|
||||
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
const bool styleWillChange =
|
||||
willPopStyleStack || willClearBold || willClearItalic || willClearUnderline || willClearStrikethrough;
|
||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||
const bool tableStructuralTag = isTableStructuralTag(name);
|
||||
|
||||
@@ -1208,7 +1256,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
!headerOrBlockTag && !tableStructuralTag && !matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || tableStructuralTag ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||
matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS) || tableStructuralTag ||
|
||||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldFlush) {
|
||||
@@ -1282,6 +1331,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
self->underlineUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving strikethrough tag
|
||||
if (self->strikethroughUntilDepth == self->depth) {
|
||||
self->strikethroughUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving pre tag
|
||||
if (self->preUntilDepth == self->depth) {
|
||||
self->preUntilDepth = INT_MAX;
|
||||
|
||||
@@ -34,6 +34,7 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
int boldUntilDepth = INT_MAX;
|
||||
int italicUntilDepth = INT_MAX;
|
||||
int underlineUntilDepth = INT_MAX;
|
||||
int strikethroughUntilDepth = INT_MAX;
|
||||
int preUntilDepth = INT_MAX; // set when inside a <pre> element; enables \n → line-break handling
|
||||
// buffer for building up words from characters, will auto break if longer than this
|
||||
// leave one char at end for null pointer
|
||||
@@ -63,12 +64,14 @@ class ChapterHtmlSlimParser final : public Print {
|
||||
bool hasBold = false, bold = false;
|
||||
bool hasItalic = false, italic = false;
|
||||
bool hasUnderline = false, underline = false;
|
||||
bool hasStrikethrough = false, strikethrough = false;
|
||||
};
|
||||
std::vector<StyleStackEntry> inlineStyleStack;
|
||||
CssStyle currentCssStyle;
|
||||
bool effectiveBold = false;
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
bool effectiveStrikethrough = false;
|
||||
int tableDepth = 0;
|
||||
int tableRowIndex = 0;
|
||||
int tableColIndex = 0;
|
||||
|
||||
Reference in New Issue
Block a user