Skip to content

Commit a14004d

Browse files
authored
fix: declare w15 namespace when bootstrapping numbering.xml (#2470)
1 parent c315599 commit a14004d

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

packages/super-editor/src/core/parts/adapters/numbering-part-descriptor.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import { describe, it, expect } from 'vitest';
2-
import { syncNumberingToXmlTree } from './numbering-part-descriptor.js';
2+
import { numberingPartDescriptor, syncNumberingToXmlTree } from './numbering-part-descriptor.js';
3+
4+
describe('numberingPartDescriptor.ensurePart', () => {
5+
it('declares xmlns:w15 so w15:* attributes in list definitions are namespace-valid (SD-2252)', () => {
6+
const part = numberingPartDescriptor.ensurePart() as {
7+
elements: Array<{ attributes: Record<string, string> }>;
8+
};
9+
const root = part.elements[0];
10+
11+
expect(root.attributes['xmlns:w']).toBe('http://schemas.openxmlformats.org/wordprocessingml/2006/main');
12+
expect(root.attributes['xmlns:w15']).toBe('http://schemas.microsoft.com/office/word/2012/wordml');
13+
expect(root.attributes['xmlns:mc']).toBe('http://schemas.openxmlformats.org/markup-compatibility/2006');
14+
expect(root.attributes['mc:Ignorable']).toContain('w15');
15+
});
16+
});
317

418
describe('syncNumberingToXmlTree', () => {
519
it('preserves non-abstract/definition children like w:numPicBullet', () => {

packages/super-editor/src/core/parts/adapters/numbering-part-descriptor.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,19 @@ import { isPartCacheStale, clearPartCacheStale } from '../cache-staleness.js';
1717

1818
const NUMBERING_PART_ID = 'word/numbering.xml' as const;
1919

20-
const NUMBERING_XMLNS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
20+
/**
21+
* Namespace attributes for the `<w:numbering>` root element.
22+
*
23+
* Includes `xmlns:w15` because base list definitions use
24+
* `w15:restartNumberingAfterBreak` — without this declaration the
25+
* numbering part is namespace-invalid and Word shows a repair prompt.
26+
*/
27+
const NUMBERING_ROOT_ATTRS: Record<string, string> = {
28+
'xmlns:w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
29+
'xmlns:w15': 'http://schemas.microsoft.com/office/word/2012/wordml',
30+
'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
31+
'mc:Ignorable': 'w15',
32+
};
2133

2234
// ---------------------------------------------------------------------------
2335
// Converter shape (minimal interface to avoid importing SuperConverter)
@@ -147,7 +159,7 @@ export const numberingPartDescriptor: PartDescriptor = {
147159
{
148160
type: 'element',
149161
name: 'w:numbering',
150-
attributes: { 'xmlns:w': NUMBERING_XMLNS },
162+
attributes: { ...NUMBERING_ROOT_ATTRS },
151163
elements: [],
152164
},
153165
],

tests/doc-api-stories/tests/lists/numbering-metadata-regression.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ function countMatches(source: string, pattern: RegExp): number {
130130
return source.match(pattern)?.length ?? 0;
131131
}
132132

133+
function getRootStartTag(xml: string, tagName: string): string {
134+
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
135+
const match = xml.match(new RegExp(`<${escapedTagName}\\b[^>]*>`));
136+
if (!match) {
137+
throw new Error(`Missing root start tag <${tagName}>.`);
138+
}
139+
return match[0];
140+
}
141+
133142
describe('document-api story: lists.create numbering metadata regression', () => {
134143
it('registers numbering metadata when bullet list creation adds numbering to a numbering-less source docx', async () => {
135144
const resultsDir = path.join(STORIES_ROOT, 'results', 'lists', 'numbering-metadata-regression');
@@ -180,5 +189,21 @@ describe('document-api story: lists.create numbering metadata regression', () =>
180189
),
181190
).toBe(1);
182191
expect(countMatches(resultDocumentRels, /Target="numbering\.xml"/g)).toBe(1);
192+
193+
// SD-2252: every namespace prefix used in the numbering part must be
194+
// declared on the root element, otherwise Word flags the file as
195+
// unreadable. Check the actual <w:numbering ...> start tag so we do not
196+
// false-pass on an xmlns declaration that appears later or in a narrower scope.
197+
const numberingRootStartTag = getRootStartTag(resultNumbering, 'w:numbering');
198+
const usedPrefixes = new Set([...resultNumbering.matchAll(/(?:^|[\s<])(\w+):/g)].map((m) => m[1]));
199+
// xml and xmlns are built-in prefixes that never need an explicit declaration.
200+
usedPrefixes.delete('xml');
201+
usedPrefixes.delete('xmlns');
202+
203+
for (const prefix of usedPrefixes) {
204+
expect(numberingRootStartTag, `missing xmlns:${prefix} declaration on <w:numbering>`).toMatch(
205+
new RegExp(`xmlns:${prefix}=`),
206+
);
207+
}
183208
});
184209
});

0 commit comments

Comments
 (0)