From 94221671c0b86226e2719a080a288e1bc66cb181 Mon Sep 17 00:00:00 2001 From: Gabriel Chittolina Date: Thu, 16 Apr 2026 13:54:22 -0300 Subject: [PATCH 1/2] fix: toc not being displayed inside w:sdt --- .../src/sdt/document-part-object.test.ts | 92 +++++++++++++++++++ .../src/sdt/document-part-object.ts | 36 +++++++- .../pm-adapter/src/sdt/toc.test.ts | 70 ++++++++++++++ .../layout-engine/pm-adapter/src/sdt/toc.ts | 16 ++++ 4 files changed, 213 insertions(+), 1 deletion(-) diff --git a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts index 767398b0f1..f77921eb89 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts @@ -438,6 +438,98 @@ describe('document-part-object', () => { }); }); + // ==================== Table Children Tests ==================== + describe('Table children', () => { + it('should process table children for non-TOC docPartGallery types', () => { + const tableNode: PMNode = { + type: 'table', + content: [{ type: 'tableRow', content: [] }], + attrs: {}, + }; + const node: PMNode = { + type: 'documentPartObject', + content: [tableNode], + attrs: { docPartGallery: 'Building Block Gallery' }, + }; + + const tableBlock = { kind: 'table' as const, id: 'tbl-1', rows: [] }; + const mockTableNodeToBlock = vi.fn(() => tableBlock); + + vi.mocked(metadataModule.getDocPartGallery).mockReturnValue('Building Block Gallery'); + + const contextWithTable: NodeHandlerContext = { + ...mockContext, + converters: { + ...mockContext.converters, + tableNodeToBlock: mockTableNodeToBlock, + }, + }; + + handleDocumentPartObjectNode(node, contextWithTable); + + expect(mockTableNodeToBlock).toHaveBeenCalledWith( + tableNode, + expect.objectContaining({ + nextBlockId: mockBlockIdGenerator, + positions: mockPositionMap, + hyperlinkConfig: mockHyperlinkConfig, + converterContext: mockConverterContext, + enableComments: mockEnableComments, + }), + ); + expect(contextWithTable.blocks).toHaveLength(1); + expect(contextWithTable.blocks[0]).toBe(tableBlock); + }); + + it('should not push block when tableNodeToBlock returns null for non-TOC type', () => { + const node: PMNode = { + type: 'documentPartObject', + content: [{ type: 'table', content: [], attrs: {} }], + attrs: { docPartGallery: 'Building Block Gallery' }, + }; + + const mockTableNodeToBlock = vi.fn(() => null); + vi.mocked(metadataModule.getDocPartGallery).mockReturnValue('Building Block Gallery'); + + const contextWithTable: NodeHandlerContext = { + ...mockContext, + converters: { + ...mockContext.converters, + tableNodeToBlock: mockTableNodeToBlock, + }, + }; + + handleDocumentPartObjectNode(node, contextWithTable); + + expect(contextWithTable.blocks).toHaveLength(0); + }); + + it('should process tableOfContents children for non-"Table of Contents" gallery types (e.g. "Custom Table of Contents")', () => { + const tocNode: PMNode = { + type: 'tableOfContents', + content: [{ type: 'paragraph', content: [] }], + attrs: { instruction: 'TOC \\o "1-3"' }, + }; + const node: PMNode = { + type: 'documentPartObject', + content: [tocNode], + attrs: { docPartGallery: 'Custom Table of Contents' }, + }; + + vi.mocked(metadataModule.getDocPartGallery).mockReturnValue('Custom Table of Contents'); + vi.mocked(metadataModule.getDocPartObjectId).mockReturnValue('toc-1'); + vi.mocked(metadataModule.getNodeInstruction).mockReturnValue(undefined); + vi.mocked(metadataModule.resolveNodeSdtMetadata).mockReturnValue(undefined as never); + + handleDocumentPartObjectNode(node, mockContext); + + expect(tocModule.processTocChildren).toHaveBeenCalledOnce(); + const callArgs = vi.mocked(tocModule.processTocChildren).mock.calls[0]; + expect(callArgs[0]).toEqual(tocNode.content); + expect(callArgs[1]).toMatchObject({ docPartGallery: 'Custom Table of Contents' }); + }); + }); + // ==================== Edge Cases ==================== describe('Edge cases', () => { it('should handle docPartGallery with different case sensitivity', () => { diff --git a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts index 045ca3c91b..4d8aba004a 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts @@ -56,7 +56,7 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC { blocks, recordBlockKind }, ); } else if (paragraphToFlowBlocks) { - // For non-ToC gallery types (page numbers, etc.), process child paragraphs normally + // For non-ToC gallery types (page numbers, etc.), process child paragraphs and tables normally for (const child of node.content) { if (child.type === 'paragraph') { const childBlocks = paragraphToFlowBlocks({ @@ -75,6 +75,40 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC blocks.push(block); recordBlockKind?.(block.kind); } + } else if (child.type === 'table') { + const tableBlock = converters.tableNodeToBlock(child, { + nextBlockId, + positions, + trackedChangesConfig, + bookmarks, + hyperlinkConfig, + themeColors, + converterContext, + converters, + enableComments, + }); + if (tableBlock) { + blocks.push(tableBlock); + recordBlockKind?.(tableBlock.kind); + } + } else if (child.type === 'tableOfContents' && Array.isArray(child.content)) { + // A nested tableOfContents node (e.g. from a "Custom Table of Contents" SDT where + // the TOC field codes were preprocessed into an sd:tableOfContents element) + processTocChildren( + child.content, + { docPartGallery: docPartGallery ?? '', docPartObjectId, tocInstruction, sdtMetadata: docPartSdtMetadata }, + { + nextBlockId, + positions, + bookmarks, + hyperlinkConfig, + enableComments, + trackedChangesConfig, + converters, + converterContext, + }, + { blocks, recordBlockKind }, + ); } } } diff --git a/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts b/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts index 86a6e2a70a..4cb1068456 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts @@ -421,6 +421,76 @@ describe('toc', () => { expect(blocks[2].attrs?.isTocEntry).toBe(true); }); + it('processes table children inside a docPartObj SDT', () => { + const tableNode: PMNode = { + type: 'table', + content: [{ type: 'tableRow', content: [] }], + attrs: {}, + }; + const children: PMNode[] = [tableNode]; + + const blocks: FlowBlock[] = []; + const recordBlockKind = vi.fn(); + const tableBlock = { kind: 'table' as const, id: 'tbl-1', rows: [] }; + const mockTableNodeToBlock = vi.fn(() => tableBlock); + + processTocChildren( + children, + { + docPartGallery: 'Table of Contents', + docPartObjectId: 'toc-123', + }, + { + nextBlockId: mockBlockIdGenerator, + positions: mockPositionMap, + hyperlinkConfig: mockHyperlinkConfig, + enableComments: true, + converters: { tableNodeToBlock: mockTableNodeToBlock } as never, + converterContext: mockConverterContext, + }, + { blocks, recordBlockKind }, + ); + + expect(mockTableNodeToBlock).toHaveBeenCalledWith( + tableNode, + expect.objectContaining({ + nextBlockId: mockBlockIdGenerator, + positions: mockPositionMap, + hyperlinkConfig: mockHyperlinkConfig, + enableComments: true, + converterContext: mockConverterContext, + }), + ); + expect(blocks).toHaveLength(1); + expect(blocks[0]).toBe(tableBlock); + expect(recordBlockKind).toHaveBeenCalledWith('table'); + }); + + it('skips table child when tableNodeToBlock returns null', () => { + const children: PMNode[] = [{ type: 'table', content: [], attrs: {} }]; + + const blocks: FlowBlock[] = []; + const recordBlockKind = vi.fn(); + const mockTableNodeToBlock = vi.fn(() => null); + + processTocChildren( + children, + { docPartGallery: 'Table of Contents' }, + { + nextBlockId: mockBlockIdGenerator, + positions: mockPositionMap, + hyperlinkConfig: mockHyperlinkConfig, + enableComments: true, + converters: { tableNodeToBlock: mockTableNodeToBlock } as never, + converterContext: mockConverterContext, + }, + { blocks, recordBlockKind }, + ); + + expect(blocks).toHaveLength(0); + expect(recordBlockKind).not.toHaveBeenCalled(); + }); + it('passes all context parameters to paragraph converter', () => { const children: PMNode[] = [ { diff --git a/packages/layout-engine/pm-adapter/src/sdt/toc.ts b/packages/layout-engine/pm-adapter/src/sdt/toc.ts index dd5246dccb..81d9fe2a08 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/toc.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/toc.ts @@ -151,6 +151,22 @@ export function processTocChildren( context, outputArrays, ); + } else if (child.type === 'table') { + // Table child (e.g. a TOC rendered as a plain table) - convert and emit as-is + const tableBlock = context.converters.tableNodeToBlock(child, { + nextBlockId: context.nextBlockId, + positions: context.positions, + trackedChangesConfig: context.trackedChangesConfig, + bookmarks: context.bookmarks, + hyperlinkConfig: context.hyperlinkConfig, + converterContext: context.converterContext, + converters: context.converters, + enableComments: context.enableComments, + }); + if (tableBlock) { + blocks.push(tableBlock); + recordBlockKind?.(tableBlock.kind); + } } }); } From ad510e09aa0aa90423b36ced55f3951c7692c642 Mon Sep 17 00:00:00 2001 From: Gabriel Chittolina Date: Thu, 16 Apr 2026 15:38:45 -0300 Subject: [PATCH 2/2] refactor: simplified code --- .../src/sdt/document-part-object.test.ts | 64 ----------------- .../src/sdt/document-part-object.ts | 18 +---- .../pm-adapter/src/sdt/toc.test.ts | 70 ------------------- .../layout-engine/pm-adapter/src/sdt/toc.ts | 16 ----- 4 files changed, 1 insertion(+), 167 deletions(-) diff --git a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts index f77921eb89..72c4de5159 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.test.ts @@ -440,70 +440,6 @@ describe('document-part-object', () => { // ==================== Table Children Tests ==================== describe('Table children', () => { - it('should process table children for non-TOC docPartGallery types', () => { - const tableNode: PMNode = { - type: 'table', - content: [{ type: 'tableRow', content: [] }], - attrs: {}, - }; - const node: PMNode = { - type: 'documentPartObject', - content: [tableNode], - attrs: { docPartGallery: 'Building Block Gallery' }, - }; - - const tableBlock = { kind: 'table' as const, id: 'tbl-1', rows: [] }; - const mockTableNodeToBlock = vi.fn(() => tableBlock); - - vi.mocked(metadataModule.getDocPartGallery).mockReturnValue('Building Block Gallery'); - - const contextWithTable: NodeHandlerContext = { - ...mockContext, - converters: { - ...mockContext.converters, - tableNodeToBlock: mockTableNodeToBlock, - }, - }; - - handleDocumentPartObjectNode(node, contextWithTable); - - expect(mockTableNodeToBlock).toHaveBeenCalledWith( - tableNode, - expect.objectContaining({ - nextBlockId: mockBlockIdGenerator, - positions: mockPositionMap, - hyperlinkConfig: mockHyperlinkConfig, - converterContext: mockConverterContext, - enableComments: mockEnableComments, - }), - ); - expect(contextWithTable.blocks).toHaveLength(1); - expect(contextWithTable.blocks[0]).toBe(tableBlock); - }); - - it('should not push block when tableNodeToBlock returns null for non-TOC type', () => { - const node: PMNode = { - type: 'documentPartObject', - content: [{ type: 'table', content: [], attrs: {} }], - attrs: { docPartGallery: 'Building Block Gallery' }, - }; - - const mockTableNodeToBlock = vi.fn(() => null); - vi.mocked(metadataModule.getDocPartGallery).mockReturnValue('Building Block Gallery'); - - const contextWithTable: NodeHandlerContext = { - ...mockContext, - converters: { - ...mockContext.converters, - tableNodeToBlock: mockTableNodeToBlock, - }, - }; - - handleDocumentPartObjectNode(node, contextWithTable); - - expect(contextWithTable.blocks).toHaveLength(0); - }); - it('should process tableOfContents children for non-"Table of Contents" gallery types (e.g. "Custom Table of Contents")', () => { const tocNode: PMNode = { type: 'tableOfContents', diff --git a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts index 4d8aba004a..5e54fe5a1f 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/document-part-object.ts @@ -56,7 +56,7 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC { blocks, recordBlockKind }, ); } else if (paragraphToFlowBlocks) { - // For non-ToC gallery types (page numbers, etc.), process child paragraphs and tables normally + // For non-ToC gallery types (page numbers, etc.), process child paragraphs normally for (const child of node.content) { if (child.type === 'paragraph') { const childBlocks = paragraphToFlowBlocks({ @@ -75,22 +75,6 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC blocks.push(block); recordBlockKind?.(block.kind); } - } else if (child.type === 'table') { - const tableBlock = converters.tableNodeToBlock(child, { - nextBlockId, - positions, - trackedChangesConfig, - bookmarks, - hyperlinkConfig, - themeColors, - converterContext, - converters, - enableComments, - }); - if (tableBlock) { - blocks.push(tableBlock); - recordBlockKind?.(tableBlock.kind); - } } else if (child.type === 'tableOfContents' && Array.isArray(child.content)) { // A nested tableOfContents node (e.g. from a "Custom Table of Contents" SDT where // the TOC field codes were preprocessed into an sd:tableOfContents element) diff --git a/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts b/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts index 4cb1068456..86a6e2a70a 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/toc.test.ts @@ -421,76 +421,6 @@ describe('toc', () => { expect(blocks[2].attrs?.isTocEntry).toBe(true); }); - it('processes table children inside a docPartObj SDT', () => { - const tableNode: PMNode = { - type: 'table', - content: [{ type: 'tableRow', content: [] }], - attrs: {}, - }; - const children: PMNode[] = [tableNode]; - - const blocks: FlowBlock[] = []; - const recordBlockKind = vi.fn(); - const tableBlock = { kind: 'table' as const, id: 'tbl-1', rows: [] }; - const mockTableNodeToBlock = vi.fn(() => tableBlock); - - processTocChildren( - children, - { - docPartGallery: 'Table of Contents', - docPartObjectId: 'toc-123', - }, - { - nextBlockId: mockBlockIdGenerator, - positions: mockPositionMap, - hyperlinkConfig: mockHyperlinkConfig, - enableComments: true, - converters: { tableNodeToBlock: mockTableNodeToBlock } as never, - converterContext: mockConverterContext, - }, - { blocks, recordBlockKind }, - ); - - expect(mockTableNodeToBlock).toHaveBeenCalledWith( - tableNode, - expect.objectContaining({ - nextBlockId: mockBlockIdGenerator, - positions: mockPositionMap, - hyperlinkConfig: mockHyperlinkConfig, - enableComments: true, - converterContext: mockConverterContext, - }), - ); - expect(blocks).toHaveLength(1); - expect(blocks[0]).toBe(tableBlock); - expect(recordBlockKind).toHaveBeenCalledWith('table'); - }); - - it('skips table child when tableNodeToBlock returns null', () => { - const children: PMNode[] = [{ type: 'table', content: [], attrs: {} }]; - - const blocks: FlowBlock[] = []; - const recordBlockKind = vi.fn(); - const mockTableNodeToBlock = vi.fn(() => null); - - processTocChildren( - children, - { docPartGallery: 'Table of Contents' }, - { - nextBlockId: mockBlockIdGenerator, - positions: mockPositionMap, - hyperlinkConfig: mockHyperlinkConfig, - enableComments: true, - converters: { tableNodeToBlock: mockTableNodeToBlock } as never, - converterContext: mockConverterContext, - }, - { blocks, recordBlockKind }, - ); - - expect(blocks).toHaveLength(0); - expect(recordBlockKind).not.toHaveBeenCalled(); - }); - it('passes all context parameters to paragraph converter', () => { const children: PMNode[] = [ { diff --git a/packages/layout-engine/pm-adapter/src/sdt/toc.ts b/packages/layout-engine/pm-adapter/src/sdt/toc.ts index 81d9fe2a08..dd5246dccb 100644 --- a/packages/layout-engine/pm-adapter/src/sdt/toc.ts +++ b/packages/layout-engine/pm-adapter/src/sdt/toc.ts @@ -151,22 +151,6 @@ export function processTocChildren( context, outputArrays, ); - } else if (child.type === 'table') { - // Table child (e.g. a TOC rendered as a plain table) - convert and emit as-is - const tableBlock = context.converters.tableNodeToBlock(child, { - nextBlockId: context.nextBlockId, - positions: context.positions, - trackedChangesConfig: context.trackedChangesConfig, - bookmarks: context.bookmarks, - hyperlinkConfig: context.hyperlinkConfig, - converterContext: context.converterContext, - converters: context.converters, - enableComments: context.enableComments, - }); - if (tableBlock) { - blocks.push(tableBlock); - recordBlockKind?.(tableBlock.kind); - } } }); }