From 3d305a9920199f2d66fa06283c9014ae54c031f0 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Sat, 29 Aug 2026 05:49:58 +0000 Subject: [PATCH] fix: write STYLEREF by outline level, not style name Word resolves the quoted style name against the localized built-in name, so 'STYLEREF "Heading 1"' shows a field error in any non-English Word. The numeric argument means outline level in Word and LibreOffice alike (probed), and the importer already reads it. --- src/lib/export/docx.ts | 6 ++++-- tests/unit/chapter-field.test.ts | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/export/docx.ts b/src/lib/export/docx.ts index aaf04bc..8e07f6e 100644 --- a/src/lib/export/docx.ts +++ b/src/lib/export/docx.ts @@ -670,8 +670,10 @@ function inlineToRuns(content: TiptapNode[] = [], force: TextProps = {}): Inline } else if (node.type === 'pageCount') { out.push(new TextRun({ children: [PageNumber.TOTAL_PAGES], ...runPropsFromMarks(node.marks) })); } else if (node.type === 'chapterField') { - // Word's running head: STYLEREF picks the heading of that level in force on the page. - out.push(new SimpleField(`STYLEREF "Heading ${Number(node.attrs?.level) || 1}" \\* MERGEFORMAT`, String(node.attrs?.text ?? ''))); + // Word's running head: STYLEREF picks the heading of that level in force on the + // page. The numeric form means outline level; a style name would be looked up + // localized ("Heading 1" errors in a German Word, which wants "Überschrift 1"). + out.push(new SimpleField(`STYLEREF ${Number(node.attrs?.level) || 1} \\* MERGEFORMAT`, String(node.attrs?.text ?? ''))); } else if (node.type === 'dateTimeField') { out.push(dateTimeRun(node)); } else if (node.type === 'sequenceField') { diff --git a/tests/unit/chapter-field.test.ts b/tests/unit/chapter-field.test.ts index 1a23a15..b9cc543 100644 --- a/tests/unit/chapter-field.test.ts +++ b/tests/unit/chapter-field.test.ts @@ -36,6 +36,10 @@ describe('chapter field', () => { it('round-trips through DOCX as a STYLEREF field', async () => { const bytes = await buildDocx(doc, margins, 'portrait', hf); + // The numeric form (outline level) — Word resolves a quoted style name against the + // localized name, so "Heading 1" errors in any non-English Word. + const ftr = strFromU8(unzipSync(bytes)['word/footer1.xml']); + expect(ftr).toContain('w:instr="STYLEREF 1 \\* MERGEFORMAT"'); const back = importDocx(bytes); // Word caches the shown name in the field result, which the import drops — the // level is what the live field needs.