Skip to content

fix(list): render <table> nested inside <li> instead of dropping it - #229

Merged
K-Kumar-01 merged 3 commits into
TurboDocx:developfrom
JasonColapietro:fix/table-inside-list-item
Sep 30, 2026
Merged

K-Kumar-01 merged 3 commits into
TurboDocx:developfrom
JasonColapietro:fix/table-inside-list-item

Conversation

@JasonColapietro

Copy link
Copy Markdown
Contributor

Summary

Fixes #198 β€” a <table> nested directly inside an <li> was silently dropped from the DOCX.

classifyListItemChildren() buckets <table> as a block element (LIST_ITEM_BLOCK_TAGS). The block branch pushes the vNode onto the list queue, and when it was popped the buildList loop sent it into xmlBuilder.buildParagraph() β€” which has no way to translate a table vNode into a paragraph, so it emitted nothing.

The failure mode is the bad kind: the wrapper paragraphs render, the surrounding list items render, the file is well-formed OOXML and opens cleanly in Word and LibreOffice. The table is just gone. Nothing warns the caller.

Change

buildList() now detects a <table> vNode and routes it through xmlBuilder.buildTable(), using the same options as the top-level <table> branch of findXMLEquivalent() (maximumWidth, rowCantSplit, and the table.addSpacingAfter trailing paragraph). A table renders the same way whether or not it sits inside a list.

Word has no concept of a table carrying a bullet or number marker, so the table renders between the list item's paragraphs β€” matching the direction suggested in the issue (option 1).

29 lines in src/helpers/render-document-file.js. No change to xml-builder.js.

Result

Input from the issue:

<ul>
  <li>
    <p>before table</p>
    <table border="1">
      <tr><td>r1c1</td><td>r1c2</td></tr>
      <tr><td>r2c1</td><td>r2c2</td></tr>
    </table>
    <p>after table</p>
  </li>
  <li>second item</li>
</ul>

Reading the generated .docx back with mammoth (an independent DOCX reader, not this library):

Before β€” no table, no cells:

<ul><li>before table</li></ul><p>after table</p><ul><li>second item</li></ul>

After β€” all four cells present, in source order:

<ul><li>before table</li></ul><table><tr><td><p>r1c1</p></td><td><p>r1c2</p></td></tr><tr><td><p>r2c1</p></td><td><p>r2c2</p></td></tr></table><p>after table</p><ul><li>second item</li></ul>

Tests

The known-limitation test in tests/list-multiple-paragraphs.test.js ('<table> inside <li> does not crash and preserves wrapper paragraphs', which documented the drop and invited a future fix to flip it) now asserts the table renders.

A new Tables inside list items - Issue #198 suite covers:

  • the exact issue reproduction β€” <w:tbl> present, 4 <w:tc>, all cell text
  • source ordering β€” table sits between the wrapper paragraphs
  • a table as an <li>'s only child
  • tables in nested <ol>/<ul> without disturbing numbering (numbering-definition count and following-item order asserted)
  • <thead>/<tbody> rows
  • the table.addSpacingAfter option honored on this path

All 7 fail on develop and pass with the fix.

Verification

Command Result
npx jest 624 passed, 20 suites, 0 failed
npm run build exit 0
node example/example-node.js exit 0 (Docx file created successfully)
npx eslint src/helpers/render-document-file.js 0 problems, unchanged from develop
npx prettier --check on changed files 4 pre-existing warnings in the test file, identical count before and after

Also checked: every XML part of the generated package parses, and the document body still ends with a paragraph rather than a table (OOXML requires a table to be followed by one) β€” including when the table is the last content in the document.

Scope notes

Two related things I deliberately left alone, happy to follow up if you'd like them:

  • Indentation. A table inside a list item renders at the same width and alignment as a top-level table rather than indenting to line up with the item's continuation paragraphs. Indenting means emitting w:tblInd, and the correct ECMA-376 CT_TblPrBase position for it is between w:tblCellSpacing and w:tblBorders β€” but buildTableProperties() currently emits w:tblPr children in Object.keys() order, so placing it correctly means sequencing that element list. Given fix: sort <w:rPr> children to ECMA-376 EG_RPrBase spec orderΒ #202 (sorting w:rPr children to spec order) landed for exactly this class of bug, that felt like it deserves its own PR rather than riding along here.
  • <table></table> with no rows produces an empty <w:tbl>. That is pre-existing and identical on the top-level table path β€” this change neither introduces nor fixes it.

@github-actions

Copy link
Copy Markdown

TurboDocx DOCX Diff Report

Automated HTML to DOCX regression testing | Powered by TurboDocx

Summary

  • βœ… Identical files: 71
  • πŸ”„ Changed files: 0
  • βž• New files: 0
  • βž– Deleted files: 0

πŸš€ Powered by TurboDocx | html-to-docx

Automated DOCX regression testing β€’ Catch document generation bugs before they ship β€’ 100% open source

Generated by TurboDocx DOCX Diff workflow β€’ Learn more

@nicolasiscoding

Copy link
Copy Markdown
Member

@JasonColapietro !! 1909 friend thanks

@JasonColapietro

Copy link
Copy Markdown
Contributor Author

Thanks Nicolas! One heads-up in case it saves you a step: the Test PR and CodeQL workflows are waiting on first-time-contributor approval, so only the docx-diff check ran automatically.

To de-risk the merge, I pulled current develop (eab9490) into the branch locally β€” clean merge, and npm run test:all is fully green: 21/21 suites, 641/641 tests, plus the example/TS/RTL generation steps. Happy to rebase or adjust anything if useful.

@nicolasiscoding

Copy link
Copy Markdown
Member

@JasonColapietro sorry for this last minute, if you can, go into the examples folder https://github.com/TurboDocx/html-to-docx/tree/develop/example and add examples where you deem appropriate for this but need a few outside of the unit tests

@JasonColapietro
JasonColapietro force-pushed the fix/table-inside-list-item branch from 20d0472 to c2ca1ca Compare August 24, 2026 19:05
@JasonColapietro

Copy link
Copy Markdown
Contributor Author

@nicolasiscoding Examples added β€” example/example-table-in-list.js covers a <table> inside a <ul>, inside an <ol>, and inside a nested list, plus the table.addSpacingAfter option so the spacing behaviour is visible too. I also rebased onto current develop so this picks up #225 and #231.

One thing worth flagging separately: docx-diff is currently failing on every PR in the repo, not just this one. It isn't detecting a real output change.

b69dd31 (#225) added a guard to scripts/diff-docx.js:

Output path must be within the current working directory

but .github/workflows/docx-diff.yml still invokes it as:

node scripts/diff-docx.js ../baseline.docx ../current.docx --output ../diff-report.md

../diff-report.md is outside the working directory, so the script exits non-zero, the workflow sets diff_failed=true, and the verify step reports the misleading "DOCX diff detected unexpected changes."

Evidence:

  • I generated baseline (develop) and current (this branch) DOCX with DETERMINISTIC_IDS=true npm test and ran your diff script with an in-tree output path. Result: 71 identical files, 0 changed, 0 new, 0 deleted β€” this PR does not alter example-node.docx at all.
  • bugfix/dependabot-security-patches-2026-08 failed the same check three times on 2026-08-24, and every docx-diff run before 2026-08-23 succeeded.

Two ways to fix it, whichever you prefer:

  1. Write the report inside the working directory and adjust the later steps, e.g. --output diff-report.md run from the repo root.
  2. Keep the guard but resolve --output against process.cwd() before the containment check, so a path that stays inside the runner workspace is allowed.

Happy to open a separate PR for that if it's useful β€” I left it out of this one to keep the change focused.

@nicolasiscoding

Copy link
Copy Markdown
Member

you rock, if you can open the other PR would be super helpful then rebase it into this change

@JasonColapietro

Copy link
Copy Markdown
Contributor Author

Done β€” the CI repair is now in #233 and ready for review, and I rebased this branch onto that repair as requested. The stack is now current with develop through #232. Fresh local validation on the combined history is green: 22/22 suites, 645/645 tests, all build/example/TypeScript/RTL generation steps, and example/example-table-in-list.js itself. The existing docx-diff check may remain red until #233 lands because the current pull_request_target event loads the workflow from develop; after #233 merges, the next synchronization of this PR will use the corrected workflow.

@JasonColapietro
JasonColapietro force-pushed the fix/table-inside-list-item branch from c2ca1ca to db9cc03 Compare August 31, 2026 02:29
A <table> inside an <li> is classified as a block element and was handed to
buildParagraph(), which has no way to translate a table into a paragraph, so
it emitted nothing. The wrapper paragraphs still rendered and the DOCX opened
cleanly in Word, so the missing table was silent data loss.

Route table vNodes in the list flow through buildTable() using the same
options as the top-level <table> branch of findXMLEquivalent(), so a table
renders the same way whether or not it sits inside a list. Word has no
concept of a table carrying a bullet or number marker, so the table renders
between the list item's paragraphs.

The known-limitation test that documented the drop is flipped to assert the
table renders, and a suite covers the issue reproduction, source ordering,
a table as an <li>'s only child, tables in nested lists with numbering
continuity, thead/tbody, and the table.addSpacingAfter option.

Closes TurboDocx#198
Covers a <table> nested in <ul>, <ol>, and a nested list, plus the
table.addSpacingAfter option, so the behaviour fixed for issue TurboDocx#198 is
runnable from the example folder.
@JasonColapietro
JasonColapietro force-pushed the fix/table-inside-list-item branch from db9cc03 to 29bcef2 Compare September 23, 2026 21:01
@JasonColapietro

Copy link
Copy Markdown
Contributor Author

Rebased onto current develop (571d2e4). Since #233's CI repair is superseded by your docx-diff rework in 5250db8 and 297adee, I dropped that commit from this stack. This PR is now just the two list/table commits.

Fresh local validation on 29bcef2: 21/21 Jest suites (642/642 tests), npm test (build + node example), example/example-table-in-list.js, and ESLint on the changed source/example files.

@K-Kumar-01 K-Kumar-01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @JasonColapietro for this PR.
Looks good to me.
Tested locally and the Pr is working.
Attached the files below

example-table-in-list_after.docx
example-table-inside-list_before.docx

Just one suggestion:

"test:example-table": "npm run build && node example/example-table-in-list.js",

Can we add the above script in package.json?

@JasonColapietro

Copy link
Copy Markdown
Contributor Author

Thanks for the review and for testing it locally, @K-Kumar-01. Added the test:example-table script to package.json in 0501a43. It runs npm run build && node example/example-table-in-list.js, and I confirmed it builds and generates the docx.

@K-Kumar-01

Copy link
Copy Markdown
Collaborator

Merging it @nicolasiscoding

@K-Kumar-01
K-Kumar-01 merged commit 7b73a9d into TurboDocx:develop Sep 30, 2026
JasonColapietro added a commit to JasonColapietro/.github that referenced this pull request Oct 13, 2026
…ai.ai (#9)

The weekly claim guard had failed on main since 2026-09-28: the live
count is 54 merged pull requests across 46 repositories, and the README
said 50 across 44. The social kit had drifted further (50 in the
LinkedIn headline, 49 on the X founder header) because nothing checked
it.

- Upstream record: adds coreos/ignition#2301 and #2302,
  TurboDocx/html-to-docx#229 and XRPLF/xrpl.js#3430; +3,168 / -704
  lines; 33 substantive + 21 listings; fifteen skills catalogs; kernel
  series v2-v7.
- Aligned with seo.suedeai.ai and suedeai.ai: 30-minute call, West Palm
  Beach, info@suedeai.ai, the teardown / sprint / retainer ladder,
  thirteen press placements across seven outlets, five provisional
  patent applications, seven current books (The Signal Chain sold
  direct and incorporating The Guitar Without a Number), proof of
  creation at suedeai.ai/proof-of-creation.
- Social kit: bios corrected, character counts re-measured, images
  re-rendered in Inter.
- Guard: audits the social kit alongside the README, regenerates and
  diffs the image sources, and checks each PNG was rendered from its
  current source via images/rendered-from.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FkZ8KzBvhznp6XGikUqPa
nicolasiscoding pushed a commit that referenced this pull request Oct 30, 2026
Fixes dropped tables nested in list items (#229) and raises the runtime
floors for axios and probe-image-size, plus the fast-uri override.
Version, CHANGELOG and tag follow on main after this is merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nicolasiscoding added a commit that referenced this pull request Oct 30, 2026
Fixes dropped tables nested in list items (#229) and raises the runtime
floors for axios and probe-image-size, plus the fast-uri override.
Version, CHANGELOG and tag follow on main after this is merged.

Co-authored-by: Nicolas Fry <nicolas@turbodocx.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
nicolasiscoding pushed a commit that referenced this pull request Oct 30, 2026
OOXML requires every <w:tc> to end in a <w:p>, and Word refuses the file
otherwise ("The file appears to be corrupted"). buildTableCell only added one
after a directly nested table, so these cells had none:

- a list ending in a table, since #229, when table.addSpacingAfter is off
- children that emit nothing: an empty <li>, <ul> or <figure>, or an image
  that fails to load, with any options

Check the cell's actual last child instead of tracking it by hand. A trailing
table gets the hairline sentinel; an otherwise empty cell gets the same plain
paragraph as <td></td>, so its row keeps normal height.

Committed with --no-verify: the hook lints the whole file, which already has
31 ESLint errors, none on these lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nicolasiscoding added a commit that referenced this pull request Oct 30, 2026
OOXML requires every <w:tc> to end in a <w:p>, and Word refuses the file
otherwise ("The file appears to be corrupted"). buildTableCell only added one
after a directly nested table, so these cells had none:

- a list ending in a table, since #229, when table.addSpacingAfter is off
- children that emit nothing: an empty <li>, <ul> or <figure>, or an image
  that fails to load, with any options

Check the cell's actual last child instead of tracking it by hand. A trailing
table gets the hairline sentinel; an otherwise empty cell gets the same plain
paragraph as <td></td>, so its row keeps normal height.

Committed with --no-verify: the hook lints the whole file, which already has
31 ESLint errors, none on these lines.

Co-authored-by: Nicolas Fry <nicolas@turbodocx.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
nicolasiscoding added a commit that referenced this pull request Oct 30, 2026
OOXML requires every <w:tc> to end in a <w:p>, and Word refuses the file
otherwise ("The file appears to be corrupted"). buildTableCell only added one
after a directly nested table, so these cells had none:

- a list ending in a table, since #229, when table.addSpacingAfter is off
- children that emit nothing: an empty <li>, <ul> or <figure>, or an image
  that fails to load, with any options

Check the cell's actual last child instead of tracking it by hand. A trailing
table gets the hairline sentinel; an otherwise empty cell gets the same plain
paragraph as <td></td>, so its row keeps normal height.

Committed with --no-verify: the hook lints the whole file, which already has
31 ESLint errors, none on these lines.

Co-authored-by: Nicolas Fry <nicolas@turbodocx.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

Table inside <li> is silently dropped from DOCX output

3 participants