Skip to content

fix(odt): continue list numbering across loop copies, process header/footer regions, drop dead {{#empty}} markers - #223

Merged
vaceslav merged 3 commits into
mainfrom
fix/odt-review-engine
Sep 26, 2026
Merged

vaceslav merged 3 commits into
mainfrom
fix/odt-review-engine

Conversation

@vaceslav

Copy link
Copy Markdown
Contributor

Follow-up to #138 (closed), from the independent review of the OpenDocument support.

Problem

  1. Numbered lists restart in loops (HIGH). A paragraph-level loop over a numbered item (in LibreOffice a text:list of its own) cloned the list once per iteration. Every clone was a separate list, so LibreOffice rendered 1. A / 1. B / 1. C, while the same DOCX template renders 1. / 2. / 3.. This also affected DOCX templates converted to ODT by LibreOffice (soffice --convert-to odt) and numbered lists in repeated table rows.
  2. Header/footer regions ignored (MEDIUM). style:region-left / -center / -right inside style:header / style:footer were neither processed nor validated. Placeholders, conditionals and loops in them were left as they were.
  3. Dead {{#empty}} handling (LOW). OdtTemplateEngine and the DOCX DocumentWalker still treated paragraphs containing {{#empty}} / {{/empty}} as marker paragraphs, although the feature was removed in refactor: dedupe marker text and quote normalization, remove dead internal code (#155) #190.

Fix

  • OdtTemplateEngine.ExpandLoop marks every clone of a list with the template list it comes from (an in-memory XObject annotation, also through nested loops). After OdtUniqueNames.EnsureUnique, the new ContinueListNumbering step gives the first copy still in the document an xml:id (it keeps the template's id or gets an unused templify-listN). Every later copy gets text:continue-list="<that id>", and any copied continue-numbering or xml:id is removed from it. continue-list (ODF 1.2) links the copies even when other lists come between them. Only copies of the same source list are linked, so a list after the loop still restarts. Sub-lists (lists inside a list, including list-item loops) are not changed. Nested loops continue across outer iterations, like the shared numId in DOCX.
  • style:region-* are containers in OdtTemplateEngine.ProcessBlock and in OdtTemplateValidator.GetChildSequences (new OdfNames.IsHeaderFooterRegion).
  • Removed the {{#empty}} / {{/empty}} checks in both engines. Behavior change: a paragraph that contains the literal text {{#empty}} now gets placeholder replacement like any other paragraph. Before, its placeholders were skipped silently.
  • docs/for-template-authors/libreoffice.md: list numbering continues across loop iterations, and header/footer regions are supported.

LibreOffice results (headless LibreOffice 26.8, txt export). Paragraph loop with a numbered list, a second list and text between them: 1. A / 2. B / 3. C, and the second list separately 1. / 2. / 3.. A list after the loop: 1.. Nested loops: 1. 2. | 3. 4.. Table-row loop with a numbered list in a cell: 1. 2. 3.. For a DOCX template converted by LibreOffice, the ODT output now matches the DOCX output line for line, including nested loops. Note: LibreOffice Writer drops header regions on load, so for regions the round trip only checks that the processed document opens and converts.

Tests

  • OdtLoopTests: 8 new tests covering continuation, generated ids, unrelated and single-iteration lists left alone, a clone removed by a conditional, nested loops, table-row loops, nested sub-lists and list-item loops.
  • OdtContainerTests.HeaderAndFooterRegions_AreProcessed covers placeholders, a conditional and a loop in regions. OdtValidationTests.HeaderRegions_AreValidated covers a syntax error and loop-scope missing variables in regions.
  • OdtPlaceholderTests / DocumentTemplateProcessorEdgeCaseTests: a paragraph with {{#empty}} text is ordinary text (ODT and DOCX).
  • LibreOffice round trips (Category=LibreOffice, run locally): NumberedListsInLoops_ContinueTheirNumberingInLibreOffice, DocxConvertedByLibreOffice_ProcessedAsOdt_MatchesDocxProcessing (ported from the review probe, now expects 1/2/3) and HeaderRegions_ProcessedDocumentOpensInLibreOffice.
  • All 14 new tests fail without the fix. The full suite passes on net10/9/8 with the LibreOffice tests included, and so do the Release CI build, dotnet format --verify-no-changes, dotnet pack and mkdocs build --strict.

Public API impact: none. There is one behavior change for the edge case of literal {{#empty}} text, described above.

vaceslav and others added 2 commits September 26, 2026 16:48
…footer regions, drop dead {{#empty}} markers (#138)

- Loop copies of a text:list now continue the numbering of the first copy
  (text:continue-list + xml:id), as DOCX does; also across nested loops and in
  table-row loops. Other lists are not changed.
- style:region-left/center/right in headers and footers are processed and validated.
- Remove the {{#empty}}/{{/empty}} marker-paragraph checks (feature removed in #190)
  from the ODT engine and DocumentWalker.

Found by the independent review of the OpenDocument support.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.61017% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...riasDev.Templify/OpenDocument/OdtTemplateEngine.cs 98.07% 0 Missing and 1 partial ⚠️
TriasDev.Templify/Visitors/DocumentWalker.cs 0.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@vaceslav
vaceslav merged commit 92a9b19 into main Sep 26, 2026
12 checks passed
@vaceslav
vaceslav deleted the fix/odt-review-engine branch September 26, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants