From ef70d1d8c8eb3e4cb4401a54b8877da68566b4b8 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Fri, 15 May 2026 22:44:54 +0200 Subject: [PATCH] List handling fixes --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 31 +++++-- scripts/generate_test_epub.py | 83 ++++++++++++++++++ test/epubs/test_text_rendering.epub | Bin 3568 -> 5225 bytes 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 79ae15fb..4b11f6e3 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -976,7 +976,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } if (strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0) { - self->listStack.push_back({self->depth, name[0] == 'o', 0}); + int startCounter = 0; + if (name[0] == 'o') { + const char* startAttr = getAttribute(atts, "start"); + if (startAttr) { + int v = atoi(startAttr); + if (v > 0) startCounter = v - 1; // counter is pre-incremented on each
  • + } + } + self->listStack.push_back({self->depth, name[0] == 'o', startCounter}); } const float emSize = static_cast(self->renderer.getFontAscenderSize(self->fontId)); @@ -1050,14 +1058,21 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->updateEffectiveInlineStyle(); if (strcmp(name, "li") == 0) { - char marker[12]; - if (!self->listStack.empty() && self->listStack.back().isOrdered) { - self->listStack.back().counter += 1; - snprintf(marker, sizeof(marker), "%d.", self->listStack.back().counter); - } else { - strcpy(marker, "\xe2\x80\xa2"); + if (!self->listStack.empty()) { + char marker[12]; + if (self->listStack.back().isOrdered) { + const char* valueAttr = getAttribute(atts, "value"); + if (valueAttr) { + int v = atoi(valueAttr); + if (v > 0) self->listStack.back().counter = v - 1; + } + self->listStack.back().counter += 1; + snprintf(marker, sizeof(marker), "%d.", self->listStack.back().counter); + } else { + strcpy(marker, "\xe2\x80\xa2"); + } + self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR); } - self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR); } else if (strcmp(name, "pre") == 0) { // Record depth so characterData can treat \n as a hard line break inside
    .
             // depth has not been incremented yet here; it will be after startElement returns.
    diff --git a/scripts/generate_test_epub.py b/scripts/generate_test_epub.py
    index 76c199a5..1b12e1ff 100644
    --- a/scripts/generate_test_epub.py
    +++ b/scripts/generate_test_epub.py
    @@ -1016,6 +1016,7 @@ def main():
     
  • pre with inline code element
  • horizontal rules between paragraphs
  • superscript and subscript rendering
  • +
  • list rendering: ul, ol, nested, start/value attributes, bare li
  • """, ), @@ -1086,6 +1087,88 @@ greet("World");

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.


    End of horizontal rule tests.

    +""", + ), + [], + ), + ( + "6. Lists", + make_chapter( + "List Rendering Tests", + """ +

    Unordered list (bullets)

    +
      +
    • First item
    • +
    • Second item
    • +
    • Third item
    • +
    + +

    Ordered list (numbers 1, 2, 3)

    +
      +
    1. One
    2. +
    3. Two
    4. +
    5. Three
    6. +
    + +

    Ordered list with start="5" (should begin at 5)

    +
      +
    1. Five
    2. +
    3. Six
    4. +
    5. Seven
    6. +
    + +

    Ordered list with li value override (should show 1, 2, 5, 6)

    +
      +
    1. One
    2. +
    3. Two
    4. +
    5. Five
    6. +
    7. Six
    8. +
    + +

    Nested lists

    +
      +
    • Fruit +
        +
      • Apple
      • +
      • Banana
      • +
      +
    • +
    • Vegetables +
        +
      • Carrot
      • +
      • Pea
      • +
      +
    • +
    + +

    Nested ordered lists (outer 1,2 / inner 1,2,3)

    +
      +
    1. Chapter one +
        +
      1. Section A
      2. +
      3. Section B
      4. +
      5. Section C
      6. +
      +
    2. +
    3. Chapter two +
        +
      1. Section A
      2. +
      3. Section B
      4. +
      +
    4. +
    + +

    Bare li outside any list (no bullet should appear)

    +

    The line below is a bare <li> with no enclosing <ul> or <ol>. No bullet or number should appear before it.

    +
  • This bare li should have no marker
  • +

    Normal paragraph resumed.

    + +

    List with inline formatting

    + """, ), [], diff --git a/test/epubs/test_text_rendering.epub b/test/epubs/test_text_rendering.epub index 74be4b31dcab7af67b255eae394a951486de2b17..95673e00d2f95c10c0659de493ef24f8014f0ef7 100644 GIT binary patch delta 3036 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 Z`m4@gO2%Qqd|Ln^?$f~)d-9L8{{j2`LtOv> delta 1354 zcmaE<@j;p=z?+#xgaHJ~V<+-hFab%o2leH#>tlpw2y>`0GB7*?Vvq`dSEqnr{p5_q zf|ArCL%oWOlH8oqi3j}-8}PKfujRUI%_}NymO7KAM_c$6pUmZ}S!NZsW?jVTRTmmU|PVd5-9YNT_C& zzRNV7Jab*`=ZQs6?JL4(>0EAJY1Osi<~upZAZC?|551BvE4|4*5|Lp#W6FiVrsvmG z1asy-T^crhxp9%tX-gw#2ZwgsO*)McOodBqV=R(dtQOe1NHDqasOM}c`r>|Sp>KGI zkj_pM|F=5wX|8`{B|WVa<~^;xA0ulY@|?N0O6;|QjI7&=KsT4RgKW3itV`~DO*K2Q zW!Ig6v;V)c!a|t?7|NS>GKDe%!+Em+iyN4ZV9f#3$Jq3k_&_25_qbei0x;AKStj@J zDc1WXRz^26t7M5vZhFnQ>I;LO+=&$lF~%xizKNbU2?>o#ZU0(rcm9M}(fw(x){Uz- z8X5BcF-=dnqAk9<6%gBpAeak})btKbX_{jH{Q1X{oU={js(cv;MGb+`7`1_!4G z0iK?`N7uq)t&7%qo?h?Xd6I$oRm$tTzwgaGckF0k{=&e8X7`@zc)RmV(XPq7W0o%e z{`++E97Ul$J8C2M*6+L5xNNT|XBoT9_iI-zFS}fw^LbnD<+4e?HaxwVWA^y?xx@8~ zGS{s-{A>EUFk%ABUufTHmE&HijPGR_~DPR+aCKBv{6y+-KcwFzy9roWQ; zk=3wV)8EM<3