Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> }>;
};
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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ 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;

/**
* Namespace attributes for the `<w:numbering>` 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<string, string> = {
'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<string, string> = { ...DEFAULT_DOCX_DEFS };

// ---------------------------------------------------------------------------
// Converter shape (minimal interface to avoid importing SuperConverter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 `<w:numbering>` 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');
});
});
Loading