From dc88b27f402c0e9b17dfb2daafef55c37c47d112 Mon Sep 17 00:00:00 2001 From: Nicolas Fry Date: Fri, 14 Nov 2025 11:22:13 -0500 Subject: [PATCH 1/2] feat: add support for nested tables in table cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements fix for issue #147 - renders tables nested inside table cells correctly. Changes: - Added nested table handling in buildTableCell() function (xml-builder.js) - Nested tables now processed with buildTable() instead of buildParagraph() - Preserves table width constraints from parent cell - All styling and borders properly inherited Tests: - Added comprehensive test suite (tests/nested-tables.test.js) - 13 new tests covering: * Basic nested tables * Multiple nesting levels (up to 3 deep) * Styling preservation (backgrounds, borders) * Mixed content (text + tables, lists + tables) * Complex scenarios (multi-row/column nested tables) * Regression tests (normal tables, images, lists) - All 355 tests pass (342 existing + 13 new) - no regressions Note: ESLint errors in xml-builder.js are pre-existing (36 errors in develop branch) Fixes #147 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/helpers/xml-builder.js | 12 ++ tests/nested-tables.test.js | 348 ++++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 tests/nested-tables.test.js diff --git a/src/helpers/xml-builder.js b/src/helpers/xml-builder.js index 8ead4a2..1e37eff 100644 --- a/src/helpers/xml-builder.js +++ b/src/helpers/xml-builder.js @@ -2797,6 +2797,18 @@ const buildTableCell = async ( if (vNodeHasChildren(childVNode)) { await buildList(childVNode, docxDocumentInstance, tableCellFragment); } + } else if (isVNode(childVNode) && childVNode.tagName === 'table') { + // FIX for Issue #147: render nested table in table cell + // eslint-disable-next-line no-use-before-define + const nestedTableFragment = await buildTable( + childVNode, + { + ...modifiedAttributes, + maximumWidth: modifiedAttributes.maximumWidth || parentWidth, + }, + docxDocumentInstance + ); + tableCellFragment.import(nestedTableFragment); } else { const paragraphFragment = await buildParagraph( childVNode, diff --git a/tests/nested-tables.test.js b/tests/nested-tables.test.js new file mode 100644 index 0000000..6b8a3cb --- /dev/null +++ b/tests/nested-tables.test.js @@ -0,0 +1,348 @@ +import HTMLtoDOCX from '../index.js'; +import { parseDOCX, assertParagraphCount } from './helpers/docx-assertions.js'; + +describe('Nested Tables - Issue #147', () => { + describe('Basic nested table support', () => { + test('should render table nested inside table cell', async () => { + // Exact HTML from issue #147 + const htmlString = ` +

Outer

+ + + + +
+

Inner

+ + + + +
Cell Value
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + // Should have at least 3 paragraphs: + // 1. "Outer" (h2) + // 2. "Inner" (h3 inside outer table cell) + // 3. "Cell Value" (inside nested table) + expect(parsed.paragraphs.length).toBeGreaterThanOrEqual(3); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Outer'); + expect(allText).toContain('Inner'); + expect(allText).toContain('Cell Value'); + }); + + test('should render simple nested table without extra content', async () => { + const htmlString = ` + + + + +
+ + + + +
Nested content
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + expect(parsed.paragraphs.length).toBeGreaterThanOrEqual(1); + expect(parsed.paragraphs[0].text).toContain('Nested content'); + }); + }); + + describe('Multiple levels of nesting', () => { + test('should handle three levels of nested tables', async () => { + const htmlString = ` + + + + +
Level 1 + + + + +
Level 2 + + + + +
Level 3
+
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Level 1'); + expect(allText).toContain('Level 2'); + expect(allText).toContain('Level 3'); + }); + }); + + describe('Nested table styling', () => { + test('should preserve background color in nested table cells', async () => { + const htmlString = ` + + + + +
+ Outer green cell + + + + +
Inner red cell
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + // Both cells should render + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Outer green cell'); + expect(allText).toContain('Inner red cell'); + + // Check that shading/background colors are present in XML + expect(parsed.xml).toContain(' { + const htmlString = ` + + + + +
+ + + + +
Nested with border
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + expect(parsed.paragraphs[0].text).toContain('Nested with border'); + + // Both tables should have borders + expect(parsed.xml).toContain(' { + test('should handle text before and after nested table', async () => { + const htmlString = ` + + + + +
+

Before table

+ + + + +
Inside nested table
+

After table

+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Before table'); + expect(allText).toContain('Inside nested table'); + expect(allText).toContain('After table'); + }); + + test('should handle multiple nested tables in same cell', async () => { + const htmlString = ` + + + + +
+ + +
First nested table
+ + +
Second nested table
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('First nested table'); + expect(allText).toContain('Second nested table'); + }); + + test('should handle nested table with list', async () => { + const htmlString = ` + + + + +
+
    +
  • List item
  • +
+ + +
Nested table
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('List item'); + expect(allText).toContain('Nested table'); + }); + }); + + describe('Complex nested table scenarios', () => { + test('should handle nested table with multiple rows and columns', async () => { + const htmlString = ` + + + + +
+ + + + + + + + + +
Row 1, Col 1Row 1, Col 2
Row 2, Col 1Row 2, Col 2
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Row 1, Col 1'); + expect(allText).toContain('Row 1, Col 2'); + expect(allText).toContain('Row 2, Col 1'); + expect(allText).toContain('Row 2, Col 2'); + }); + + test('should handle outer table with multiple cells containing nested tables', async () => { + const htmlString = ` + + + + + +
+ + +
Nested in cell 1
+
+ + +
Nested in cell 2
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Nested in cell 1'); + expect(allText).toContain('Nested in cell 2'); + }); + }); + + describe('Regression tests', () => { + test('should not break normal tables without nesting', async () => { + const htmlString = ` + + + + + + + + + +
Cell 1Cell 2
Cell 3Cell 4
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + // Should have at least 4 cells (may have extra empty paragraphs for spacing) + expect(parsed.paragraphs.length).toBeGreaterThanOrEqual(4); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Cell 1'); + expect(allText).toContain('Cell 2'); + expect(allText).toContain('Cell 3'); + expect(allText).toContain('Cell 4'); + }); + + test('should not break tables with images', async () => { + const htmlString = ` + + + + +
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + // Should render without errors + expect(docx).toBeDefined(); + }); + + test('should not break tables with lists', async () => { + const htmlString = ` + + + + +
+
    +
  • Item 1
  • +
  • Item 2
  • +
+
+ `; + + const docx = await HTMLtoDOCX(htmlString); + const parsed = await parseDOCX(docx); + + const allText = parsed.paragraphs.map((p) => p.text).join(' '); + expect(allText).toContain('Item 1'); + expect(allText).toContain('Item 2'); + }); + }); +}); From 9d05acd19624035f8e69e25c5feb0b492bc02533 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 14 Nov 2025 20:32:09 -0500 Subject: [PATCH 2/2] Potential fix for code scanning alert no. 38: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- tests/nested-tables.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/nested-tables.test.js b/tests/nested-tables.test.js index 6b8a3cb..fe844cf 100644 --- a/tests/nested-tables.test.js +++ b/tests/nested-tables.test.js @@ -317,7 +317,7 @@ describe('Nested Tables - Issue #147', () => { `; const docx = await HTMLtoDOCX(htmlString); - const parsed = await parseDOCX(docx); + // Should render without errors expect(docx).toBeDefined();