Merge pull request #152 from jpirnay/feat-strikethrough
feat: Support strikethrough
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
class EpdFontFamily {
|
class EpdFontFamily {
|
||||||
public:
|
public:
|
||||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
|
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4, STRIKETHROUGH = 8 };
|
||||||
|
|
||||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||||
const EpdFont* boldItalic = nullptr)
|
const EpdFont* boldItalic = nullptr)
|
||||||
|
|||||||
@@ -17,26 +17,33 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
|||||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||||
|
|
||||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
const std::string& w = words[i];
|
||||||
const std::string& w = words[i];
|
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;
|
||||||
|
const bool hasDecoration = (currentStyle & (EpdFontFamily::UNDERLINE | EpdFontFamily::STRIKETHROUGH)) != 0;
|
||||||
|
int startX = wordX;
|
||||||
|
int lineWidth = 0;
|
||||||
|
|
||||||
|
if (hasEmSpacePrefix || hasDecoration) {
|
||||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
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
|
lineWidth = fullWordWidth;
|
||||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
if (hasEmSpacePrefix) {
|
||||||
|
|
||||||
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 char* visiblePtr = w.c_str() + 3;
|
||||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
const int prefixWidth = renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||||
startX = wordX + prefixWidth;
|
startX = wordX + prefixWidth;
|
||||||
underlineWidth = visibleWidth;
|
lineWidth = visibleWidth;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
|
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||||
|
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||||
|
renderer.drawLine(startX, underlineY, startX + lineWidth, 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);
|
const std::string v = normalized(val);
|
||||||
|
|
||||||
// text-decoration can have multiple space-separated values
|
// text-decoration can have multiple space-separated values
|
||||||
if (v.find("underline") != std::string::npos) {
|
bool underline = v.find("underline") != std::string::npos;
|
||||||
return CssTextDecoration::Underline;
|
bool lineThrough = v.find("line-through") != std::string::npos;
|
||||||
}
|
uint8_t result = 0;
|
||||||
return CssTextDecoration::None;
|
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) {
|
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 };
|
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||||
|
|
||||||
// Text decoration options
|
// 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
|
// Display options - only None and Block are relevant for e-ink rendering
|
||||||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
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"};
|
const char* UNDERLINE_TAGS[] = {"u", "ins"};
|
||||||
constexpr int NUM_UNDERLINE_TAGS = sizeof(UNDERLINE_TAGS) / sizeof(UNDERLINE_TAGS[0]);
|
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"};
|
const char* IMAGE_TAGS[] = {"img"};
|
||||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||||
|
|
||||||
@@ -135,8 +138,11 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|||||||
// Start with block-level styles
|
// Start with block-level styles
|
||||||
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
effectiveUnderline =
|
effectiveUnderline = currentCssStyle.hasTextDecoration() && (static_cast<uint8_t>(currentCssStyle.textDecoration) &
|
||||||
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
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
|
// Apply inline style stack in order
|
||||||
for (const auto& entry : inlineStyleStack) {
|
for (const auto& entry : inlineStyleStack) {
|
||||||
@@ -149,6 +155,9 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
|||||||
if (entry.hasUnderline) {
|
if (entry.hasUnderline) {
|
||||||
effectiveUnderline = entry.underline;
|
effectiveUnderline = entry.underline;
|
||||||
}
|
}
|
||||||
|
if (entry.hasStrikethrough) {
|
||||||
|
effectiveStrikethrough = entry.strikethrough;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +167,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
const bool isBold = boldUntilDepth < depth || effectiveBold;
|
||||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
||||||
|
const bool isStrikethrough = strikethroughUntilDepth < depth || effectiveStrikethrough;
|
||||||
|
|
||||||
// Combine style flags using bitwise OR
|
// Combine style flags using bitwise OR
|
||||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
@@ -170,6 +180,9 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|||||||
if (isUnderline) {
|
if (isUnderline) {
|
||||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||||
}
|
}
|
||||||
|
if (isStrikethrough) {
|
||||||
|
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::STRIKETHROUGH);
|
||||||
|
}
|
||||||
|
|
||||||
// flush the buffer
|
// flush the buffer
|
||||||
partWordBuffer[partWordBufferIndex] = '\0';
|
partWordBuffer[partWordBufferIndex] = '\0';
|
||||||
@@ -857,18 +870,43 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->preUntilDepth = std::min(self->preUntilDepth, self->depth);
|
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
|
// Flush buffer before style change so preceding text gets current style
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
self->nextWordContinues = true;
|
self->nextWordContinues = true;
|
||||||
}
|
}
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||||
// Push inline style entry for underline tag
|
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;
|
StyleStackEntry entry;
|
||||||
entry.depth = self->depth; // Track depth for matching pop
|
entry.depth = self->depth; // Track depth for matching pop
|
||||||
entry.hasUnderline = true;
|
if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||||
entry.underline = true;
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = true;
|
||||||
|
}
|
||||||
|
if (matches(name, STRIKETHROUGH_TAGS, NUM_STRIKETHROUGH_TAGS)) {
|
||||||
|
entry.hasStrikethrough = true;
|
||||||
|
entry.strikethrough = true;
|
||||||
|
}
|
||||||
|
if (cssStyle.hasTextDecoration()) {
|
||||||
|
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||||
|
if (dec & static_cast<uint8_t>(CssTextDecoration::Underline)) {
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = true;
|
||||||
|
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||||
|
}
|
||||||
|
if (dec & static_cast<uint8_t>(CssTextDecoration::LineThrough)) {
|
||||||
|
entry.hasStrikethrough = true;
|
||||||
|
entry.strikethrough = true;
|
||||||
|
self->strikethroughUntilDepth = std::min(self->strikethroughUntilDepth, self->depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (cssStyle.hasFontWeight()) {
|
if (cssStyle.hasFontWeight()) {
|
||||||
entry.hasBold = true;
|
entry.hasBold = true;
|
||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
@@ -896,8 +934,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
if (cssStyle.hasTextDecoration()) {
|
||||||
entry.hasUnderline = true;
|
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
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->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -918,8 +963,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
if (cssStyle.hasTextDecoration()) {
|
||||||
entry.hasUnderline = true;
|
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
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->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -942,8 +994,22 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
entry.italic = cssStyle.fontStyle == CssFontStyle::Italic;
|
||||||
}
|
}
|
||||||
if (cssStyle.hasTextDecoration()) {
|
if (cssStyle.hasTextDecoration()) {
|
||||||
entry.hasUnderline = true;
|
const uint8_t dec = static_cast<uint8_t>(cssStyle.textDecoration);
|
||||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
if (dec == static_cast<uint8_t>(CssTextDecoration::None)) {
|
||||||
|
entry.hasUnderline = true;
|
||||||
|
entry.underline = false;
|
||||||
|
entry.hasStrikethrough = true;
|
||||||
|
entry.strikethrough = false;
|
||||||
|
} else {
|
||||||
|
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->inlineStyleStack.push_back(entry);
|
||||||
self->updateEffectiveInlineStyle();
|
self->updateEffectiveInlineStyle();
|
||||||
@@ -1188,8 +1254,10 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
const bool willClearBold = self->boldUntilDepth == self->depth - 1;
|
||||||
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
const bool willClearItalic = self->italicUntilDepth == self->depth - 1;
|
||||||
const bool willClearUnderline = self->underlineUntilDepth == 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 headerOrBlockTag = isHeaderOrBlock(name);
|
||||||
const bool tableStructuralTag = isTableStructuralTag(name);
|
const bool tableStructuralTag = isTableStructuralTag(name);
|
||||||
|
|
||||||
@@ -1208,7 +1276,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
!headerOrBlockTag && !tableStructuralTag && !matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
!headerOrBlockTag && !tableStructuralTag && !matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1;
|
||||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||||
matches(name, ITALIC_TAGS, NUM_ITALIC_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;
|
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
@@ -1282,6 +1351,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
|||||||
self->underlineUntilDepth = INT_MAX;
|
self->underlineUntilDepth = INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leaving strikethrough tag
|
||||||
|
if (self->strikethroughUntilDepth == self->depth) {
|
||||||
|
self->strikethroughUntilDepth = INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
// Leaving pre tag
|
// Leaving pre tag
|
||||||
if (self->preUntilDepth == self->depth) {
|
if (self->preUntilDepth == self->depth) {
|
||||||
self->preUntilDepth = INT_MAX;
|
self->preUntilDepth = INT_MAX;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class ChapterHtmlSlimParser final : public Print {
|
|||||||
int boldUntilDepth = INT_MAX;
|
int boldUntilDepth = INT_MAX;
|
||||||
int italicUntilDepth = INT_MAX;
|
int italicUntilDepth = INT_MAX;
|
||||||
int underlineUntilDepth = 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
|
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
|
// buffer for building up words from characters, will auto break if longer than this
|
||||||
// leave one char at end for null pointer
|
// leave one char at end for null pointer
|
||||||
@@ -63,12 +64,14 @@ class ChapterHtmlSlimParser final : public Print {
|
|||||||
bool hasBold = false, bold = false;
|
bool hasBold = false, bold = false;
|
||||||
bool hasItalic = false, italic = false;
|
bool hasItalic = false, italic = false;
|
||||||
bool hasUnderline = false, underline = false;
|
bool hasUnderline = false, underline = false;
|
||||||
|
bool hasStrikethrough = false, strikethrough = false;
|
||||||
};
|
};
|
||||||
std::vector<StyleStackEntry> inlineStyleStack;
|
std::vector<StyleStackEntry> inlineStyleStack;
|
||||||
CssStyle currentCssStyle;
|
CssStyle currentCssStyle;
|
||||||
bool effectiveBold = false;
|
bool effectiveBold = false;
|
||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = false;
|
bool effectiveUnderline = false;
|
||||||
|
bool effectiveStrikethrough = false;
|
||||||
int tableDepth = 0;
|
int tableDepth = 0;
|
||||||
int tableRowIndex = 0;
|
int tableRowIndex = 0;
|
||||||
int tableColIndex = 0;
|
int tableColIndex = 0;
|
||||||
|
|||||||
+35
-12
@@ -4,11 +4,22 @@
|
|||||||
|
|
||||||
namespace MdParser {
|
namespace MdParser {
|
||||||
|
|
||||||
static EpdFontFamily::Style combineFlags(bool bold, bool italic) {
|
static EpdFontFamily::Style combineFlags(bool bold, bool italic, bool strike) {
|
||||||
if (bold && italic) return EpdFontFamily::BOLD_ITALIC;
|
EpdFontFamily::Style result = EpdFontFamily::REGULAR;
|
||||||
if (bold) return EpdFontFamily::BOLD;
|
if (bold)
|
||||||
if (italic) return EpdFontFamily::ITALIC;
|
result =
|
||||||
return EpdFontFamily::REGULAR;
|
static_cast<EpdFontFamily::Style>(static_cast<uint8_t>(result) | static_cast<uint8_t>(EpdFontFamily::BOLD));
|
||||||
|
if (italic)
|
||||||
|
result =
|
||||||
|
static_cast<EpdFontFamily::Style>(static_cast<uint8_t>(result) | static_cast<uint8_t>(EpdFontFamily::ITALIC));
|
||||||
|
if (bold && italic)
|
||||||
|
result = static_cast<EpdFontFamily::Style>(
|
||||||
|
static_cast<uint8_t>(EpdFontFamily::BOLD_ITALIC) |
|
||||||
|
(static_cast<uint8_t>(result) & static_cast<uint8_t>(EpdFontFamily::STRIKETHROUGH)));
|
||||||
|
if (strike)
|
||||||
|
result = static_cast<EpdFontFamily::Style>(static_cast<uint8_t>(result) |
|
||||||
|
static_cast<uint8_t>(EpdFontFamily::STRIKETHROUGH));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr int TAB_WIDTH = 4;
|
static constexpr int TAB_WIDTH = 4;
|
||||||
@@ -63,11 +74,12 @@ std::vector<Span> parseInline(const std::string& text) {
|
|||||||
std::string current;
|
std::string current;
|
||||||
bool bold = false;
|
bool bold = false;
|
||||||
bool italic = false;
|
bool italic = false;
|
||||||
|
bool strike = false;
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
|
|
||||||
auto emitSpan = [&]() {
|
auto emitSpan = [&]() {
|
||||||
if (!current.empty()) {
|
if (!current.empty()) {
|
||||||
spans.push_back({std::move(current), combineFlags(bold, italic)});
|
spans.push_back({std::move(current), combineFlags(bold, italic, strike)});
|
||||||
current.clear();
|
current.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -81,7 +93,12 @@ std::vector<Span> parseInline(const std::string& text) {
|
|||||||
// Escaped character
|
// Escaped character
|
||||||
if (c == '\\' && i + 1 < text.size()) {
|
if (c == '\\' && i + 1 < text.size()) {
|
||||||
char next = text[i + 1];
|
char next = text[i + 1];
|
||||||
if (next == '*' || next == '_' || next == '`' || next == '[' || next == '!' || next == '\\') {
|
if (next == '~' && i + 2 < text.size() && text[i + 2] == '~') {
|
||||||
|
current.append("~~");
|
||||||
|
i += 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (next == '*' || next == '_' || next == '`' || next == '[' || next == '!' || next == '~' || next == '\\') {
|
||||||
current += next;
|
current += next;
|
||||||
i += 2;
|
i += 2;
|
||||||
continue;
|
continue;
|
||||||
@@ -111,6 +128,14 @@ std::vector<Span> parseInline(const std::string& text) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ~~ — toggle strikethrough
|
||||||
|
if (c == '~' && i + 1 < text.size() && text[i + 1] == '~') {
|
||||||
|
emitSpan();
|
||||||
|
strike = !strike;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// ** or __ — toggle bold
|
// ** or __ — toggle bold
|
||||||
if ((c == '*' || c == '_') && i + 1 < text.size() && text[i + 1] == c) {
|
if ((c == '*' || c == '_') && i + 1 < text.size() && text[i + 1] == c) {
|
||||||
if (c == '_' && !isUnderscoreEmphasis(text, i, 2)) {
|
if (c == '_' && !isUnderscoreEmphasis(text, i, 2)) {
|
||||||
@@ -309,12 +334,10 @@ ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) {
|
|||||||
result.blockType = BlockType::Header3;
|
result.blockType = BlockType::Header3;
|
||||||
|
|
||||||
result.spans = parseInline(content);
|
result.spans = parseInline(content);
|
||||||
// Force bold on all header spans
|
// Force bold on all header spans while preserving any existing decoration bits.
|
||||||
for (auto& span : result.spans) {
|
for (auto& span : result.spans) {
|
||||||
if (span.style == EpdFontFamily::REGULAR)
|
span.style = static_cast<EpdFontFamily::Style>(static_cast<uint8_t>(span.style) |
|
||||||
span.style = EpdFontFamily::BOLD;
|
static_cast<uint8_t>(EpdFontFamily::BOLD));
|
||||||
else if (span.style == EpdFontFamily::ITALIC)
|
|
||||||
span.style = EpdFontFamily::BOLD_ITALIC;
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ in the development cycle.
|
|||||||
|
|
||||||
More information about PlatformIO Unit Testing:
|
More information about PlatformIO Unit Testing:
|
||||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||||
|
|
||||||
|
Example test runners in this repository:
|
||||||
|
- test/run_url_utils_test.sh
|
||||||
|
- test/run_opds_parser_test.sh
|
||||||
|
- test/run_epub_css_test.sh
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ static const EpdFontData kTestFontData = {
|
|||||||
.kernRightClassCount = 2,
|
.kernRightClassCount = 2,
|
||||||
.ligaturePairs = nullptr,
|
.ligaturePairs = nullptr,
|
||||||
.ligaturePairCount = 0,
|
.ligaturePairCount = 0,
|
||||||
|
.glyphMissHandler = nullptr,
|
||||||
|
.glyphMissCtx = nullptr,
|
||||||
};
|
};
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "../../lib/Epub/Epub/css/CssParser.h"
|
||||||
|
|
||||||
|
static int testsPassed = 0;
|
||||||
|
static int testsFailed = 0;
|
||||||
|
|
||||||
|
#define ASSERT_EQ(a, b) \
|
||||||
|
do { \
|
||||||
|
if ((a) != (b)) { \
|
||||||
|
fprintf(stderr, " FAIL: %s:%d: %s != %s\n", __FILE__, __LINE__, #a, #b); \
|
||||||
|
testsFailed++; \
|
||||||
|
return; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define ASSERT_TRUE(cond) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
fprintf(stderr, " FAIL: %s:%d: %s is false\n", __FILE__, __LINE__, #cond); \
|
||||||
|
testsFailed++; \
|
||||||
|
return; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define PASS() testsPassed++
|
||||||
|
|
||||||
|
void testInlineLineThrough() {
|
||||||
|
printf("testInlineLineThrough...\n");
|
||||||
|
const CssStyle style = CssParser::parseInlineStyle("text-decoration: line-through");
|
||||||
|
ASSERT_TRUE(style.hasTextDecoration());
|
||||||
|
ASSERT_EQ(style.textDecoration, CssTextDecoration::LineThrough);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testInlineUnderlineLineThrough() {
|
||||||
|
printf("testInlineUnderlineLineThrough...\n");
|
||||||
|
const CssStyle style = CssParser::parseInlineStyle("text-decoration: underline line-through");
|
||||||
|
ASSERT_TRUE(style.hasTextDecoration());
|
||||||
|
ASSERT_EQ(style.textDecoration, CssTextDecoration::UnderlineLineThrough);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testInlineLineThroughUnderlineOrderInsensitive() {
|
||||||
|
printf("testInlineLineThroughUnderlineOrderInsensitive...\n");
|
||||||
|
const CssStyle style = CssParser::parseInlineStyle("text-decoration: line-through underline");
|
||||||
|
ASSERT_TRUE(style.hasTextDecoration());
|
||||||
|
ASSERT_EQ(style.textDecoration, CssTextDecoration::UnderlineLineThrough);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testInlineTextDecorationNormalization() {
|
||||||
|
printf("testInlineTextDecorationNormalization...\n");
|
||||||
|
const CssStyle style = CssParser::parseInlineStyle("TEXT-DECORATION : LINE-THROUGH ;");
|
||||||
|
ASSERT_TRUE(style.hasTextDecoration());
|
||||||
|
ASSERT_EQ(style.textDecoration, CssTextDecoration::LineThrough);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("=== EPUB CSS Parser Tests ===\n\n");
|
||||||
|
|
||||||
|
testInlineLineThrough();
|
||||||
|
testInlineUnderlineLineThrough();
|
||||||
|
testInlineLineThroughUnderlineOrderInsensitive();
|
||||||
|
testInlineTextDecorationNormalization();
|
||||||
|
|
||||||
|
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
|
||||||
|
return testsFailed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -69,6 +69,24 @@ void testAsteriskEmphasisStillWorks() {
|
|||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testStrikethroughWorks() {
|
||||||
|
printf("testStrikethroughWorks...\n");
|
||||||
|
auto spans = MdParser::parseInline("foo ~~bar~~ baz");
|
||||||
|
ASSERT_EQ(flattenText(spans), "foo bar baz");
|
||||||
|
ASSERT_EQ(spans.size(), 3);
|
||||||
|
ASSERT_EQ(spans[1].style, EpdFontFamily::STRIKETHROUGH);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
|
void testEscapedTildeDoesNotToggleStrikethrough() {
|
||||||
|
printf("testEscapedTildeDoesNotToggleStrikethrough...\n");
|
||||||
|
auto spans = MdParser::parseInline("foo \\~~bar~~ baz");
|
||||||
|
ASSERT_EQ(flattenText(spans), "foo ~~bar~~ baz");
|
||||||
|
ASSERT_EQ(spans.size(), 1);
|
||||||
|
ASSERT_EQ(spans[0].style, EpdFontFamily::REGULAR);
|
||||||
|
PASS();
|
||||||
|
}
|
||||||
|
|
||||||
void testUnderscoreBoldWorks() {
|
void testUnderscoreBoldWorks() {
|
||||||
printf("testUnderscoreBoldWorks...\n");
|
printf("testUnderscoreBoldWorks...\n");
|
||||||
auto spans = MdParser::parseInline("foo __bar__ baz");
|
auto spans = MdParser::parseInline("foo __bar__ baz");
|
||||||
@@ -105,7 +123,9 @@ int main() {
|
|||||||
testUnderscoreWithinExpressionRemainsLiteral();
|
testUnderscoreWithinExpressionRemainsLiteral();
|
||||||
testUnderscoreEmphasisStillWorks();
|
testUnderscoreEmphasisStillWorks();
|
||||||
testAsteriskEmphasisStillWorks();
|
testAsteriskEmphasisStillWorks();
|
||||||
|
testStrikethroughWorks();
|
||||||
testUnderscoreBoldWorks();
|
testUnderscoreBoldWorks();
|
||||||
|
testEscapedTildeDoesNotToggleStrikethrough();
|
||||||
testNestedUnorderedListIndentLevel();
|
testNestedUnorderedListIndentLevel();
|
||||||
testNestedOrderedListIndentLevel();
|
testNestedOrderedListIndentLevel();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BUILD_DIR="$ROOT_DIR/build/epub_css"
|
||||||
|
BINARY="$BUILD_DIR/CssParserTest"
|
||||||
|
PLATFORMIO_DIR="${PLATFORMIO_CORE_DIR:-$HOME/.platformio}"
|
||||||
|
ARDUINO_FRAMEWORK_DIR="$PLATFORMIO_DIR/packages/framework-arduinoespressif32"
|
||||||
|
|
||||||
|
PIO_CMD=""
|
||||||
|
if command -v pio >/dev/null 2>&1; then
|
||||||
|
PIO_CMD="pio"
|
||||||
|
elif command -v platformio >/dev/null 2>&1; then
|
||||||
|
PIO_CMD="platformio"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$PLATFORMIO_DIR" ] && [ -n "$PIO_CMD" ]; then
|
||||||
|
PLATFORMIO_DIR="$($PIO_CMD settings get home_dir 2>/dev/null || true)"
|
||||||
|
if [ -n "$PLATFORMIO_DIR" ] && [ -d "$PLATFORMIO_DIR" ]; then
|
||||||
|
ARDUINO_FRAMEWORK_DIR="$PLATFORMIO_DIR/packages/framework-arduinoespressif32"
|
||||||
|
echo "Using PLATFORMIO_DIR from $PIO_CMD: $PLATFORMIO_DIR"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$PLATFORMIO_DIR" ]; then
|
||||||
|
echo "SKIP: PLATFORMIO_DIR does not exist and PlatformIO could not be located; skipping EPUB CSS test." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$ARDUINO_FRAMEWORK_DIR" ]; then
|
||||||
|
echo "SKIP: ARDUINO_FRAMEWORK_DIR does not exist: $ARDUINO_FRAMEWORK_DIR; skipping EPUB CSS test." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
SOURCES=(
|
||||||
|
"$ROOT_DIR/test/epub_css/CssParserTest.cpp"
|
||||||
|
"$ROOT_DIR/lib/Epub/Epub/css/CssParser.cpp"
|
||||||
|
"$ROOT_DIR/lib/hal/HalStorage.cpp"
|
||||||
|
"$ROOT_DIR/lib/Logging/Logging.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
CXXFLAGS=(
|
||||||
|
-std=c++20
|
||||||
|
-O2
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
-pedantic
|
||||||
|
-fno-exceptions
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-DDESTRUCTOR_CLOSES_FILE=1
|
||||||
|
-I"$ROOT_DIR/test/shims"
|
||||||
|
-I"$ROOT_DIR"
|
||||||
|
-I"$ROOT_DIR/lib"
|
||||||
|
-I"$ROOT_DIR/lib/hal"
|
||||||
|
-I"$ROOT_DIR/lib/Logging"
|
||||||
|
-I"$ARDUINO_FRAMEWORK_DIR/cores/esp32"
|
||||||
|
-I"$ARDUINO_FRAMEWORK_DIR/variants/esp32c3"
|
||||||
|
)
|
||||||
|
|
||||||
|
c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY"
|
||||||
|
|
||||||
|
"$BINARY" "$@"
|
||||||
Reference in New Issue
Block a user