Skip to content

Commit 79395b6

Browse files
committed
fix: child export app.xml stats to use main document counts
1 parent 37c4841 commit 79395b6

2 files changed

Lines changed: 59 additions & 13 deletions

File tree

packages/super-editor/src/core/super-converter/SuperConverter.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from './v2/exporter/commentsExporter.js';
2222
import { prepareFootnotesXmlForExport } from './v2/exporter/footnotesExporter.js';
2323
import { writeAppStatistics } from '../../document-api-adapters/helpers/app-properties.js';
24-
import { getWordStatistics } from '../../document-api-adapters/helpers/word-statistics.js';
24+
import { getWordStatistics, resolveMainBodyEditor } from '../../document-api-adapters/helpers/word-statistics.js';
2525
import { refreshAllStatFields } from '../../document-api-adapters/helpers/refresh-stat-fields.js';
2626
import { ensureSettingsRoot, hasUpdateFields, setUpdateFields } from '../../document-api-adapters/document-settings.js';
2727
import { importFootnoteData, importEndnoteData } from './v2/importer/documentFootnotesImporter.js';
@@ -1334,7 +1334,12 @@ class SuperConverter {
13341334
if (!editor) return;
13351335

13361336
try {
1337-
const stats = getWordStatistics(editor);
1337+
// docProps/app.xml is document-scoped metadata. When export runs from a
1338+
// linked child editor (for example a header/footer editor), compute the
1339+
// statistics from the main body editor so package-level counts stay
1340+
// aligned with Word's document-level stat-field semantics.
1341+
const statsEditor = resolveMainBodyEditor(editor);
1342+
const stats = getWordStatistics(statsEditor);
13381343
writeAppStatistics(this.convertedXml, stats);
13391344

13401345
// Only set w:updateFields when the document actually contains a

packages/super-editor/src/tests/regression/opc-package-metadata-roundtrip.test.js

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,30 @@ const TEST_DOC = 'blank-doc.docx';
1818
const CT_CUSTOM = 'application/vnd.openxmlformats-officedocument.custom-properties+xml';
1919
const REL_CUSTOM = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties';
2020
const WORD_STAT_TEXT = 'Alpha beta gamma';
21+
const PARENT_WORD_STAT_TEXT = 'Alpha beta gamma delta';
22+
const CHILD_ONLY_TEXT = 'Header words only';
2123

2224
function readXmlTagValue(xml, tagName) {
2325
const match = xml.match(new RegExp(`<${tagName}>([^<]*)</${tagName}>`));
2426
return match?.[1] ?? null;
2527
}
2628

29+
function readAppStatistics(xml) {
30+
return {
31+
words: readXmlTagValue(xml, 'Words'),
32+
characters: readXmlTagValue(xml, 'Characters'),
33+
charactersWithSpaces: readXmlTagValue(xml, 'CharactersWithSpaces'),
34+
};
35+
}
36+
37+
async function createHeadlessEditor(testDoc = TEST_DOC) {
38+
const { docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(testDoc);
39+
return initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true });
40+
}
41+
2742
describe('OPC package metadata: custom-properties registration', () => {
2843
it('getUpdatedDocs includes correct [Content_Types].xml and _rels/.rels for new custom.xml', async () => {
29-
const { docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(TEST_DOC);
30-
const { editor } = initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true });
44+
const { editor } = await createHeadlessEditor();
3145

3246
try {
3347
const updatedDocs = await editor.exportDocx({ getUpdatedDocs: true });
@@ -54,8 +68,7 @@ describe('OPC package metadata: custom-properties registration', () => {
5468
});
5569

5670
it('zipped export includes valid package metadata for new custom.xml', async () => {
57-
const { docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(TEST_DOC);
58-
const { editor } = initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true });
71+
const { editor } = await createHeadlessEditor();
5972

6073
try {
6174
const exportedBuffer = await editor.exportDocx({ compression: 'STORE' });
@@ -92,8 +105,7 @@ describe('OPC package metadata: custom-properties registration', () => {
92105
});
93106

94107
it('preserves existing managed registrations without duplication', async () => {
95-
const { docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(TEST_DOC);
96-
const { editor } = initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true });
108+
const { editor } = await createHeadlessEditor();
97109

98110
try {
99111
const updatedDocs = await editor.exportDocx({ getUpdatedDocs: true });
@@ -130,8 +142,7 @@ describe('OPC package metadata: custom-properties registration', () => {
130142
});
131143

132144
it('getUpdatedDocs includes refreshed docProps/app.xml statistics', async () => {
133-
const { docx, media, mediaFiles, fonts } = await loadTestDataForEditorTests(TEST_DOC);
134-
const { editor } = initTestEditor({ content: docx, media, mediaFiles, fonts, isHeadless: true });
145+
const { editor } = await createHeadlessEditor();
135146

136147
try {
137148
editor.commands.insertContent(WORD_STAT_TEXT);
@@ -140,10 +151,40 @@ describe('OPC package metadata: custom-properties registration', () => {
140151
const appXml = updatedDocs['docProps/app.xml'];
141152

142153
expect(appXml).toBeTruthy();
143-
expect(readXmlTagValue(appXml, 'Words')).toBe('3');
144-
expect(readXmlTagValue(appXml, 'Characters')).toBe('14');
145-
expect(readXmlTagValue(appXml, 'CharactersWithSpaces')).toBe('16');
154+
expect(readAppStatistics(appXml)).toEqual({
155+
words: '3',
156+
characters: '14',
157+
charactersWithSpaces: '16',
158+
});
159+
} finally {
160+
editor.destroy();
161+
}
162+
});
163+
164+
it('linked child exports keep docProps/app.xml statistics scoped to the main document', async () => {
165+
const { editor } = await createHeadlessEditor();
166+
let childEditor = null;
167+
168+
try {
169+
editor.commands.insertContent(PARENT_WORD_STAT_TEXT);
170+
171+
childEditor = editor.createChildEditor({
172+
isHeadless: true,
173+
isHeaderOrFooter: true,
174+
});
175+
childEditor.commands.insertContent(CHILD_ONLY_TEXT);
176+
177+
const updatedDocs = await childEditor.exportDocx({ getUpdatedDocs: true });
178+
const appXml = updatedDocs['docProps/app.xml'];
179+
180+
expect(appXml).toBeTruthy();
181+
expect(readAppStatistics(appXml)).toEqual({
182+
words: '4',
183+
characters: '19',
184+
charactersWithSpaces: '22',
185+
});
146186
} finally {
187+
childEditor?.destroy();
147188
editor.destroy();
148189
}
149190
});

0 commit comments

Comments
 (0)