From 5ddab33bc72a0fe1b3afb5c964ba04084ffe038e Mon Sep 17 00:00:00 2001 From: "d.bilukcha" Date: Fri, 3 Jul 2026 09:51:46 +0300 Subject: [PATCH] fix(super-editor): declare full namespace set on freshly-created numbering.xml (GH #3773) Importing a docx with no word/numbering.xml and then adding the first numbered list produced a numbering part missing xmlns:w16cid (and other w16* namespaces), so Word refused to open/repair the exported file. numbering-part-descriptor.ts's ensurePart() hand-maintained a 3-key namespace map (xmlns:w/w15/mc) instead of reusing DEFAULT_DOCX_DEFS, the full namespace + mc:Ignorable map that document.xml, comments.xml, footnotes.xml, and people.xml already apply unconditionally. Since every numbering-part creation goes through this ensurePart() (mutateNumbering -> mutatePart -> executeMutate), the legacy SuperConverter#exportNumberingFile baseNumbering fallback never runs for this path - the part descriptor's own map has to be complete on its own. Reuses DEFAULT_DOCX_DEFS instead of hand-picking namespaces one bug report at a time. Adds a unit test on ensurePart() and an end-to-end regression test (import numbering-less blank-doc.docx -> toggleOrderedList -> export -> assert namespace-complete root). Relaxes one templates-adapter.integration.test.ts assertion that hardcoded the old narrow mc:Ignorable merge result to match the sibling styles.xml test's existing flexible pattern. Co-Authored-By: Claude Sonnet 5 --- .../numbering-part-descriptor.test.ts | 12 +++++ .../adapters/numbering-part-descriptor.ts | 19 +++---- .../templates-adapter.integration.test.ts | 2 +- .../numbering-namespace-on-first-list.test.js | 51 +++++++++++++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 packages/super-editor/src/editors/v1/tests/import-export/numbering-namespace-on-first-list.test.js diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.test.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.test.ts index 0c96028ea6..407815b93a 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.test.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.test.ts @@ -13,6 +13,18 @@ describe('numberingPartDescriptor.ensurePart', () => { expect(root.attributes['xmlns:mc']).toBe('http://schemas.openxmlformats.org/markup-compatibility/2006'); expect(root.attributes['mc:Ignorable']).toContain('w15'); }); + + it('declares xmlns:w16cid so freshly-created numbering parts are namespace-valid (GH #3773)', () => { + const part = numberingPartDescriptor.ensurePart() as { + elements: Array<{ attributes: Record }>; + }; + const root = part.elements[0]; + + expect(root.attributes['xmlns:w16cid']).toBe('http://schemas.microsoft.com/office/word/2016/wordml/cid'); + expect(root.attributes['xmlns:w14']).toBe('http://schemas.microsoft.com/office/word/2010/wordml'); + expect(root.attributes['xmlns:r']).toBe('http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + expect(root.attributes['mc:Ignorable']).toContain('w16cid'); + }); }); describe('syncNumberingToXmlTree', () => { diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.ts index 8805691c80..1a84581427 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/numbering-part-descriptor.ts @@ -13,6 +13,7 @@ import type { Editor } from '../../Editor.js'; import type { PartDescriptor } from '../types.js'; import { translator as wAbstractNumTranslator } from '../../super-converter/v3/handlers/w/abstractNum/index.js'; import { translator as wNumTranslator } from '../../super-converter/v3/handlers/w/num/index.js'; +import { DEFAULT_DOCX_DEFS } from '../../super-converter/exporter-docx-defs.js'; import { isPartCacheStale, clearPartCacheStale } from '../cache-staleness.js'; const NUMBERING_PART_ID = 'word/numbering.xml' as const; @@ -20,16 +21,16 @@ const NUMBERING_PART_ID = 'word/numbering.xml' as const; /** * Namespace attributes for the `` root element. * - * Includes `xmlns:w15` because base list definitions use - * `w15:restartNumberingAfterBreak` — without this declaration the - * numbering part is namespace-invalid and Word shows a repair prompt. + * Reuses the same full namespace + `mc:Ignorable` map that document.xml, + * comments.xml, footnotes.xml, and people.xml apply unconditionally + * (`DEFAULT_DOCX_DEFS`), instead of hand-picking a subset. A prior version + * of this map declared only `xmlns:w`/`xmlns:w15`/`xmlns:mc`, which was + * enough for `w15:restartNumberingAfterBreak` but omitted `xmlns:w16cid` + * (used by list level overrides) — Word refused to open/repair any docx + * where this part was freshly created (i.e. the source docx had no + * numbering.xml before the user added their first list). See GH #3773. */ -const NUMBERING_ROOT_ATTRS: Record = { - 'xmlns:w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', - 'xmlns:w15': 'http://schemas.microsoft.com/office/word/2012/wordml', - 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006', - 'mc:Ignorable': 'w15', -}; +const NUMBERING_ROOT_ATTRS: Record = { ...DEFAULT_DOCX_DEFS }; // --------------------------------------------------------------------------- // Converter shape (minimal interface to avoid importing SuperConverter) diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/templates/templates-adapter.integration.test.ts b/packages/super-editor/src/editors/v1/document-api-adapters/templates/templates-adapter.integration.test.ts index 7f7a657625..cda30a3e8d 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/templates/templates-adapter.integration.test.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/templates/templates-adapter.integration.test.ts @@ -278,7 +278,7 @@ describe('templates.apply adapter integration', () => { expect(numberingXml).toContain('xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"'); expect(numberingXml).toContain('xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"'); expect(numberingXml).toContain('xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"'); - expect(numberingXml).toContain('mc:Ignorable="w15 w14 w16cid"'); + expect(numberingXml).toMatch(/mc:Ignorable="[^"]*\bw16cid\b/); expect(numberingXml).toContain('w15:restartNumberingAfterBreak="0"'); expect(numberingXml).toContain('w16cid:durableId="123456789"'); }); diff --git a/packages/super-editor/src/editors/v1/tests/import-export/numbering-namespace-on-first-list.test.js b/packages/super-editor/src/editors/v1/tests/import-export/numbering-namespace-on-first-list.test.js new file mode 100644 index 0000000000..4767759ff8 --- /dev/null +++ b/packages/super-editor/src/editors/v1/tests/import-export/numbering-namespace-on-first-list.test.js @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'vitest'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'node:url'; +import { promises as fs } from 'fs'; +import { Editor } from '@core/Editor.js'; +import DocxZipper from '@core/DocxZipper.js'; +import { parseXmlToJson } from '@converter/v2/docxHelper.js'; +import { initTestEditor } from '../helpers/helpers.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const findNumberingRoot = (json) => { + if (!json?.elements?.length) return null; + if (json.elements[0]?.name === 'w:numbering') return json.elements[0]; + return json.elements.find((el) => el?.name === 'w:numbering') || null; +}; + +// GH #3773: a docx imported with no `word/numbering.xml` (blank-doc.docx has none — +// see the SD-2911 P2 sanity test) that then gets its first numbered list added via +// `toggleOrderedList` must still export a namespace-complete `` root. +// The part is created on-the-fly by the parts system (numbering-part-descriptor.ts) +// mid-session, so the legacy `baseNumbering` export-time fallback never runs for it — +// the part descriptor's own namespace map has to be complete on its own. +describe('numbering.xml namespaces when the first list is added to a numbering-less doc (GH #3773)', () => { + it('emits the full namespace set (including xmlns:w16cid) on the freshly-created numbering part', async () => { + const docxPath = join(__dirname, '../data', 'blank-doc.docx'); + const docxBuffer = await fs.readFile(docxPath); + + const [docx, media, mediaFiles, fonts] = await Editor.loadXmlData(docxBuffer, true); + const { editor } = await initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true }); + + editor.commands.insertContent('First item'); + editor.commands.toggleOrderedList(); + + const exportedBuffer = await editor.exportDocx({ isFinalDoc: false }); + const exportedZipper = new DocxZipper(); + const exportedFiles = await exportedZipper.getDocxData(exportedBuffer, true); + const exportedNumberingEntry = exportedFiles.find((entry) => entry.name === 'word/numbering.xml'); + + expect(exportedNumberingEntry, 'export must contain word/numbering.xml').toBeDefined(); + + const exportedRoot = findNumberingRoot(parseXmlToJson(exportedNumberingEntry.content)); + + expect(exportedRoot.attributes['xmlns:w16cid']).toBe('http://schemas.microsoft.com/office/word/2016/wordml/cid'); + expect(exportedRoot.attributes['xmlns:w14']).toBe('http://schemas.microsoft.com/office/word/2010/wordml'); + expect(exportedRoot.attributes['xmlns:r']).toBe( + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', + ); + expect(exportedRoot.attributes['mc:Ignorable']).toContain('w16cid'); + }); +});