Support strikethrough

This commit is contained in:
jpirnay
2026-04-29 09:08:07 +02:00
parent 93f5bdb492
commit b83482e732
11 changed files with 242 additions and 44 deletions
+1 -1
View File
@@ -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)
+19 -17
View File
@@ -17,26 +17,28 @@ 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);
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) { 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; const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
renderer.drawLine(startX, underlineY, startX + lineWidth, underlineY, true);
}
int startX = wordX; if ((currentStyle & EpdFontFamily::STRIKETHROUGH) != 0) {
int underlineWidth = fullWordWidth; const int strikeY = y + renderer.getFontAscenderSize(fontId) / 2 + 1;
renderer.drawLine(startX, strikeY, startX + lineWidth, strikeY, true);
// 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);
} }
} }
} }
+6 -4
View File
@@ -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) {
+1 -1
View File
@@ -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 };
+69 -15
View File
@@ -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,30 @@ 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.hasFontWeight()) { if (cssStyle.hasFontWeight()) {
entry.hasBold = true; entry.hasBold = true;
entry.bold = cssStyle.fontWeight == CssFontWeight::Bold; 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; 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 +950,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 +981,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();
@@ -1188,8 +1234,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 +1256,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 +1331,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;
+26 -6
View File
@@ -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();
} }
}; };
@@ -111,6 +123,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)) {
+5
View File
@@ -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
+62
View File
@@ -0,0 +1,62 @@
#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 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();
testInlineTextDecorationNormalization();
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
return testsFailed > 0 ? 1 : 0;
}
+10
View File
@@ -69,6 +69,15 @@ 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 testUnderscoreBoldWorks() { void testUnderscoreBoldWorks() {
printf("testUnderscoreBoldWorks...\n"); printf("testUnderscoreBoldWorks...\n");
auto spans = MdParser::parseInline("foo __bar__ baz"); auto spans = MdParser::parseInline("foo __bar__ baz");
@@ -105,6 +114,7 @@ int main() {
testUnderscoreWithinExpressionRemainsLiteral(); testUnderscoreWithinExpressionRemainsLiteral();
testUnderscoreEmphasisStillWorks(); testUnderscoreEmphasisStillWorks();
testAsteriskEmphasisStillWorks(); testAsteriskEmphasisStillWorks();
testStrikethroughWorks();
testUnderscoreBoldWorks(); testUnderscoreBoldWorks();
testNestedUnorderedListIndentLevel(); testNestedUnorderedListIndentLevel();
testNestedOrderedListIndentLevel(); testNestedOrderedListIndentLevel();
+40
View File
@@ -0,0 +1,40 @@
#!/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"
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" "$@"