diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.js index 21d94521aa..3ca33cca40 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.js @@ -33,16 +33,26 @@ const encode = (params) => { const { nodes = [], nodeListHandler } = params || {}; const node = nodes[0]; - const processedContent = nodeListHandler.handler({ + let processedContent = nodeListHandler.handler({ ...params, nodes: node.elements || [], }); + const hasParagraphBlocks = (processedContent || []).some((child) => child?.type === 'paragraph'); + if (!hasParagraphBlocks) { + processedContent = [ + { + type: 'paragraph', + content: processedContent.filter((child) => Boolean(child && child.type)), + }, + ]; + } + const attrs = { + instruction: node.attributes?.instruction || '', + }; + attrs.rightAlignPageNumbers = deriveRightAlignPageNumbers(processedContent); const processedNode = { type: 'tableOfContents', - attrs: { - instruction: node.attributes?.instruction || '', - rightAlignPageNumbers: deriveRightAlignPageNumbers(processedContent), - }, + attrs, content: processedContent, }; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.test.js index f1d01a7001..272504b157 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/sd/tableOfContents/tableOfContents-translator.test.js @@ -90,6 +90,73 @@ describe('sd:tableOfContents translator', () => { const result = config.encode(params); expect(result.attrs.rightAlignPageNumbers).toBe(false); }); + + it('wraps inline children into a paragraph when parent accepts blocks', () => { + const mockNodeListHandler = { + handler: vi.fn(() => [{ type: 'text', text: 'Inline content' }]), + }; + const params = { + nodes: [ + { + name: 'sd:tableOfContents', + attributes: { instruction: 'TOC \\h' }, + elements: [{ name: 'w:r', elements: [] }], + }, + ], + nodeListHandler: mockNodeListHandler, + }; + + const result = config.encode(params); + expect(result).toEqual({ + type: 'tableOfContents', + attrs: { instruction: 'TOC \\h', rightAlignPageNumbers: true }, + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Inline content' }] }], + }); + }); + + it('does not wrap when content already contains paragraph blocks', () => { + const mockNodeListHandler = { + handler: vi.fn(() => [ + { type: 'paragraph', content: [{ type: 'text', text: 'Para' }] }, + { type: 'text', text: 'trailing inline' }, + ]), + }; + const params = { + nodes: [ + { + name: 'sd:tableOfContents', + attributes: { instruction: 'TOC \\o "1-3"' }, + elements: [{ name: 'w:p', elements: [] }], + }, + ], + nodeListHandler: mockNodeListHandler, + }; + + const result = config.encode(params); + expect(result.content).toEqual([ + { type: 'paragraph', content: [{ type: 'text', text: 'Para' }] }, + { type: 'text', text: 'trailing inline' }, + ]); + }); + + it('filters out null and typeless children when wrapping', () => { + const mockNodeListHandler = { + handler: vi.fn(() => [null, { type: 'text', text: 'valid' }, undefined, {}]), + }; + const params = { + nodes: [ + { + name: 'sd:tableOfContents', + attributes: { instruction: 'TOC \\h' }, + elements: [{ name: 'w:r', elements: [] }], + }, + ], + nodeListHandler: mockNodeListHandler, + }; + + const result = config.encode(params); + expect(result.content).toEqual([{ type: 'paragraph', content: [{ type: 'text', text: 'valid' }] }]); + }); }); describe('decode', () => { diff --git a/packages/super-editor/src/editors/v1/extensions/index.js b/packages/super-editor/src/editors/v1/extensions/index.js index bac9866d8a..4e879bdac0 100644 --- a/packages/super-editor/src/editors/v1/extensions/index.js +++ b/packages/super-editor/src/editors/v1/extensions/index.js @@ -274,6 +274,7 @@ export { TableCell, TableHeader, DocumentIndex, + TableOfContents, IndexEntry, TableOfContentsEntry, TocPageNumber, diff --git a/tests/behavior/tests/importing/sd-2440-field-based-toc.spec.ts b/tests/behavior/tests/importing/sd-2440-field-based-toc.spec.ts new file mode 100644 index 0000000000..411028c047 --- /dev/null +++ b/tests/behavior/tests/importing/sd-2440-field-based-toc.spec.ts @@ -0,0 +1,29 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test, expect } from '../../fixtures/superdoc.js'; +import { assertDocumentApiReady, getDocumentText } from '../../helpers/document-api.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DOC_PATH = path.resolve( + __dirname, + '../../test-data/rendering/sd-2440-field-based-toc-list-of-tables-figures.docx', +); + +test.skip(!fs.existsSync(DOC_PATH), 'Test document not available — run pnpm corpus:pull'); + +test.use({ config: { toolbar: 'full', comments: 'off' } }); + +test('loads document with field-based TOC list of tables/figures without schema errors (SD-2440)', async ({ + superdoc, +}) => { + await superdoc.loadDocument(DOC_PATH); + await superdoc.waitForStable(); + await assertDocumentApiReady(superdoc.page); + + const text = await getDocumentText(superdoc.page); + expect(text.length).toBeGreaterThan(0); + + await expect(superdoc.page.locator('.superdoc-page').first()).toBeVisible(); + await expect(superdoc.page.locator('.superdoc-line').first()).toBeVisible(); +});