From 8b006d22a8845a2c709bd27433ab5e6812aee02d Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Thu, 3 Sep 2026 10:14:28 +0200 Subject: [PATCH] test: the corpus gains a document whose chapters are numbered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 15-chapters carries what only a stylesheet holds: numbering on the heading styles and two heading styles of the document's own. The round-trip legs now carry that stylesheet too — a style the file names but the built-ins never heard of can only be written from there — and one absolute assertion says the chapters read as headings, which a round trip cannot: both its sides go through the same importer, so a consistent misreading compares equal. A numbered heading style rides the package's factory slot; a second definition under the same id leaves two, and LibreOffice reads the first. --- tests/corpus.test.ts | 25 ++++++++-- tests/corpus/15-chapters.docx | Bin 0 -> 9025 bytes tests/corpus/15-chapters.odt | Bin 0 -> 15168 bytes tests/render-parity/make-fixtures.mjs | 67 +++++++++++++++++++++++--- 4 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 tests/corpus/15-chapters.docx create mode 100644 tests/corpus/15-chapters.odt diff --git a/tests/corpus.test.ts b/tests/corpus.test.ts index b7eb2e5..729a929 100644 --- a/tests/corpus.test.ts +++ b/tests/corpus.test.ts @@ -53,16 +53,35 @@ describe.skipIf(!files.length)('the authored corpus', () => { for (const f of files) { it(`${f} imports and survives its own round trip`, async () => { - const doc = importAny(f, load(f)); + const read = readAny(f, load(f)); + const doc = read.content; expect(outline(doc).length).toBeGreaterThan(0); const margins = { top: 2, bottom: 2, left: 2, right: 2 }; + // The file's own stylesheet goes back out with it, as it does when the app saves: + // a style the document names but builtinStyleSheet() has never heard of can only + // be written from there. const again = f.endsWith('.odt') - ? importOdt(await buildOdt(doc, margins, 'portrait')) - : importDocx(await buildDocx(doc, margins, 'portrait')); + ? importOdt(await buildOdt(doc, margins, 'portrait', undefined, undefined, undefined, read.styles)) + : importDocx(await buildDocx(doc, margins, 'portrait', undefined, undefined, undefined, read.styles)); expect(outline(again.content)).toEqual(outline(doc)); }); } + // What the legs above cannot see: both sides of a round trip go through the same + // importer, so a file read wrongly but consistently still compares equal. 15-chapters + // carries chapter numbering on its heading styles and two heading styles of its own — + // read as a list, or as body text, either would round-trip happily. + const CHAPTERS = ['heading1+brk:Introduction', 'heading2:Motivation', + 'heading1+brk:Results', 'heading2:Method', + 'heading1[Appendix 1]+brk:Appendix', 'heading2[Appendix 2]:List of Tables']; + for (const f of files.filter((n) => n.startsWith('15-chapters'))) { + it(`${f} reads its numbered chapters as headings`, () => { + const doc = importAny(f, load(f)); + expect(outline(doc).filter((l) => l.startsWith('heading'))).toEqual(CHAPTERS); + expect(outline(doc).filter((l) => /^(ordered|bullet)List|^listItem/.test(l))).toEqual([]); + }); + } + // Word re-saves of the same documents (tests/corpus/word/): Word rewrites the file // in its own dialect — rsids, proofErr, separator notes, theme docDefaults — which // neither the docx lib nor LibreOffice produce, so this is the third author here. diff --git a/tests/corpus/15-chapters.docx b/tests/corpus/15-chapters.docx new file mode 100644 index 0000000000000000000000000000000000000000..89441ef913f60cf26e547fb396ea0a1a6e5f6e97 GIT binary patch literal 9025 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 G{(k`9skt=( literal 0 HcmV?d00001 diff --git a/tests/corpus/15-chapters.odt b/tests/corpus/15-chapters.odt new file mode 100644 index 0000000000000000000000000000000000000000..ac5c59b7e3e57d52bfb1bbf3949d53dcc552f1f2 GIT binary patch literal 15168 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 new Paragraph({ children: [new TextRun({ text, .. // Spacing, font and outline level are declared: LibreOffice fills a built-in style's // gaps from its own defaults where Word uses only what the file declares, and without // the level it converts a heading to a merely carrying the heading's style. -const heading = (id, name, halfPt, before, after, level) => ({ +const heading = (id, name, halfPt, before, after, level, numbering) => ({ id, name, basedOn: 'Normal', next: 'Normal', quickFormat: true, run: { font: 'Arial', size: halfPt, bold: true }, - paragraph: { spacing: { before, after }, outlineLevel: level }, + paragraph: { spacing: { before, after }, outlineLevel: level, ...(numbering ? { numbering } : {}) }, }); -async function write(name, sections, defaultRun = { font: 'Times New Roman', size: 24 }, extra = {}) { +// `chapters` names a numbering the heading styles carry: chapter numbering lives on the +// style in both products, never on the paragraph. `extra.styles` adds the document's own +// styles beside the built-ins rather than replacing the whole block. +async function write(name, sections, defaultRun = { font: 'Times New Roman', size: 24 }, extra = {}, chapters = null) { if (only && !only.test(name)) return; + const { styles: ownStyles, ...rest } = extra; + const h1 = heading('Heading1', 'Heading 1', 36, 240, 120, 0, chapters && { reference: chapters, level: 0 }); + const h2 = heading('Heading2', 'Heading 2', 32, 200, 100, 1, chapters && { reference: chapters, level: 1 }); + // The package writes its own Heading1-6 regardless, so a second definition under the + // same id leaves two — and LibreOffice reads the first, dropping everything declared + // here. A numbered heading therefore rides the factory slot instead (see FACTORY_SLOTS + // in src/lib/export/CLAUDE.md); the plain fixtures keep the shape they were built with. const doc = new Document({ styles: { - default: { document: { run: defaultRun } }, + default: { document: { run: defaultRun }, ...(chapters ? { heading1: h1, heading2: h2 } : {}) }, paragraphStyles: [ // Word always writes an explicit Normal; the docx lib would emit docDefaults only. { id: 'Normal', name: 'Normal', run: defaultRun, paragraph: { spacing: { after: 0 } } }, - heading('Heading1', 'Heading 1', 36, 240, 120, 0), - heading('Heading2', 'Heading 2', 32, 200, 100, 1), + ...(chapters ? [] : [h1, h2]), + ...(ownStyles?.paragraphStyles ?? []), ], }, - ...extra, + ...rest, sections, }); writeFileSync(join(OUT, name), await Packer.toBuffer(doc)); @@ -286,6 +296,49 @@ await write('14-formulas.docx', [{ ], }]); +// 15. Chapter numbering on the heading styles, plus heading styles of the document's +// own ("Appendix 1"/"2", named nothing like "Heading n"). Both live in the stylesheet +// and nowhere in the document tree, so only a leg carrying it exercises them. +await write('15-chapters.docx', [{ + properties: { page }, + children: [ + para('Introduction', { p: { style: 'Heading1', pageBreakBefore: true } }), + para(LOREM), + para('Motivation', { p: { style: 'Heading2' } }), + para(LOREM), + para('Results', { p: { style: 'Heading1', pageBreakBefore: true } }), + para(LOREM), + para('Method', { p: { style: 'Heading2' } }), + para(LOREM), + para('Appendix', { p: { style: 'Appendix1', pageBreakBefore: true } }), + para(LOREM), + para('List of Tables', { p: { style: 'Appendix2' } }), + para(LOREM), + ], +}], undefined, { + styles: { + paragraphStyles: [ + { id: 'Appendix1', name: 'Appendix 1', basedOn: 'Heading1', next: 'Normal', quickFormat: true, + paragraph: { outlineLevel: 0 } }, + { id: 'Appendix2', name: 'Appendix 2', basedOn: 'Heading2', next: 'Normal', quickFormat: true, + paragraph: { outlineLevel: 1 } }, + ], + }, + numbering: { + config: [{ + reference: 'chapter-numbering', + levels: [ + // The level names the style it numbers and the style points back at the level: + // both halves, or LibreOffice reads the pair as an ordinary list. + { level: 0, format: LevelFormat.DECIMAL, text: '%1.', alignment: AlignmentType.START, + style: { paragraph: { indent: { left: 0, hanging: 360 } }, style: 'Heading1' } }, + { level: 1, format: LevelFormat.DECIMAL, text: '%1.%2', alignment: AlignmentType.START, + style: { paragraph: { indent: { left: 0, hanging: 432 } }, style: 'Heading2' } }, + ], + }], + }, +}, 'chapter-numbering'); + // ODT twins, written by LibreOffice itself — the dominant ODT producer, so they carry // its own conventions (percentage font sizes, Text Body, list styles) and exercise the // foreign-document path our own exporter never produces.