From 94221671c0b86226e2719a080a288e1bc66cb181 Mon Sep 17 00:00:00 2001 From: Gabriel Chittolina Date: Thu, 16 Apr 2026 13:54:22 -0300 Subject: [PATCH 1/4] 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/4] 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); - } } }); } From 56377a14248fb19c76ff3eb1feede7834935dfb9 Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 28 Apr 2026 21:11:23 -0300 Subject: [PATCH 3/4] fix(pm-adapter): prefer child node TOC instruction in custom-gallery SDTs Word stores TOC field codes on the child tableOfContents node, not the wrapper SDT. The new branch was passing the wrapper's tocInstruction, which is undefined for Custom TOC docs, silently dropping per-TOC options like '\o "1-3"'. Mirror processTocChildren's existing recursion (toc.ts:162-172) and prefer the child's instruction. Adds 3 unit tests for the new branch covering instruction preference, fallback to the wrapper, and the Array.isArray guard. --- .../src/sdt/document-part-object.test.ts | 82 +++++++++++++++++++ .../src/sdt/document-part-object.ts | 13 ++- 2 files changed, 93 insertions(+), 2 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 72c4de5159..f7e359bb21 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 @@ -464,6 +464,88 @@ describe('document-part-object', () => { expect(callArgs[0]).toEqual(tocNode.content); expect(callArgs[1]).toMatchObject({ docPartGallery: 'Custom Table of Contents' }); }); + + it('should prefer the child tableOfContents instruction over the wrapper SDT instruction', () => { + // In real "Custom Table of Contents" docs, Word stores the TOC field codes on + // the child node, not the wrapper SDT. The new branch must read from the child + // first, otherwise per-TOC options like '\\o "1-3"' are silently dropped. + const childInstruction = 'TOC \\o "1-1" \\h \\z \\u'; + const tocNode: PMNode = { + type: 'tableOfContents', + content: [{ type: 'paragraph', content: [] }], + attrs: { instruction: childInstruction }, + }; + 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'); + // Wrapper SDT has no instruction; child carries the TOC field codes + vi.mocked(metadataModule.getNodeInstruction).mockImplementation((n: PMNode) => + n.type === 'tableOfContents' ? childInstruction : 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[1]).toMatchObject({ tocInstruction: childInstruction }); + }); + + it('should fall back to the wrapper SDT instruction when the child tableOfContents has none', () => { + const wrapperInstruction = 'TOC \\o "1-3"'; + const tocNode: PMNode = { + type: 'tableOfContents', + content: [{ type: 'paragraph', content: [] }], + }; + 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'); + // Only the wrapper SDT carries an instruction; the child doesn't + vi.mocked(metadataModule.getNodeInstruction).mockImplementation((n: PMNode) => + n.type === 'documentPartObject' ? wrapperInstruction : 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[1]).toMatchObject({ tocInstruction: wrapperInstruction }); + }); + + it('should not call processTocChildren when the tableOfContents child has no content array', () => { + // Guards against the Array.isArray check that the new branch added; without + // it, processTocChildren would be invoked with a non-array and crash. + const tocNode: PMNode = { + type: 'tableOfContents', + // no 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).not.toHaveBeenCalled(); + }); }); // ==================== Edge Cases ==================== 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 5e54fe5a1f..35d95221e1 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 @@ -77,10 +77,19 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC } } 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) + // the TOC field codes were preprocessed into an sd:tableOfContents element). + // Word stores the TOC field codes on the child node, not the wrapper SDT - prefer + // the child's instruction so per-TOC options aren't lost (mirrors the recursion + // inside processTocChildren in toc.ts). + const childTocInstruction = getNodeInstruction(child) ?? tocInstruction; processTocChildren( child.content, - { docPartGallery: docPartGallery ?? '', docPartObjectId, tocInstruction, sdtMetadata: docPartSdtMetadata }, + { + docPartGallery: docPartGallery ?? '', + docPartObjectId, + tocInstruction: childTocInstruction, + sdtMetadata: docPartSdtMetadata, + }, { nextBlockId, positions, From b5012f10d26cb5ea6e57e39356f56701525ffe3d Mon Sep 17 00:00:00 2001 From: Gabriel Chittolina Date: Fri, 1 May 2026 10:21:34 -0300 Subject: [PATCH 4/4] fix: add themeColors and sectionState to tableOfContents processing --- .../layout-engine/pm-adapter/src/sdt/document-part-object.ts | 2 ++ 1 file changed, 2 insertions(+) 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 dc9be63eb6..1c086a33b3 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 @@ -115,8 +115,10 @@ export function handleDocumentPartObjectNode(node: PMNode, context: NodeHandlerC hyperlinkConfig, enableComments, trackedChangesConfig, + themeColors, converters, converterContext, + sectionState, }, { blocks, recordBlockKind }, );