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..fe844cf --- /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); + + + // 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'); + }); + }); +});