// Builds the round-trip fixture: one .docx containing every feature v1 // claims to support, plus a few it doesn't (a header, a footer, a footnote, // a text box) so the preservation half of the deal is tested too. // // node test/fixture.mjs [out.docx] import { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, Table, TableRow, TableCell, WidthType, BorderStyle, ImageRun, ExternalHyperlink, PageBreak, Header, Footer, FootnoteReferenceRun, LevelFormat, Tab, } from "docx"; import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import zlib from "node:zlib"; // A 4x3 red PNG, built rather than checked in so the fixture stays one file. function tinyPng(w = 4, h = 3, rgb = [220, 40, 40]) { const crcTable = (() => { const t = new Int32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c; } return t; })(); const crc = (buf) => { let c = -1; for (const b of buf) c = crcTable[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ -1) >>> 0; }; const chunk = (type, data) => { const len = Buffer.alloc(4); len.writeUInt32BE(data.length); const body = Buffer.concat([Buffer.from(type, "latin1"), data]); const c = Buffer.alloc(4); c.writeUInt32BE(crc(body)); return Buffer.concat([len, body, c]); }; const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(w, 0); ihdr.writeUInt32BE(h, 4); ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0; const raw = Buffer.concat(Array.from({ length: h }, () => Buffer.concat([Buffer.from([0]), Buffer.concat(Array.from({ length: w }, () => Buffer.from(rgb)))]))); return Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(raw)), chunk("IEND", Buffer.alloc(0)), ]); } const border = { style: BorderStyle.SINGLE, size: 4, color: "999999" }; const cell = (text, opts = {}) => new TableCell(Object.assign({ children: [new Paragraph({ children: [new TextRun({ text })] })], }, opts)); export function buildFixture() { const png = tinyPng(); const doc = new Document({ title: "Round-trip fixture", creator: "Silent Mode", description: "Every v1 feature, once.", numbering: { config: [ { reference: "bullets", levels: [ { level: 0, format: LevelFormat.BULLET, text: "●", style: { paragraph: { indent: { left: 720, hanging: 360 } } } }, { level: 1, format: LevelFormat.BULLET, text: "○", style: { paragraph: { indent: { left: 1440, hanging: 360 } } } }, ], }, { reference: "romans", levels: [ { level: 0, format: LevelFormat.LOWER_ROMAN, text: "%1.", style: { paragraph: { indent: { left: 720, hanging: 360 } } } }, { level: 1, format: LevelFormat.LOWER_LETTER, text: "%2.", style: { paragraph: { indent: { left: 1440, hanging: 360 } } } }, ], }, ], }, footnotes: { 1: { children: [new Paragraph({ children: [new TextRun("A footnote the editor never renders.")] })] }, }, sections: [{ properties: { page: { size: { width: 11906, height: 16838, orientation: "portrait" }, // A4 margin: { top: 1134, right: 1134, bottom: 1134, left: 1701 }, // 2cm / 3cm left }, }, headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun("Fixture header")] })] }), }, footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("Fixture footer")] })] }), }, children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Heading one")] }), new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun("Heading two")] }), new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun("Heading three")] }), new Paragraph({ children: [ new TextRun({ text: "plain " }), new TextRun({ text: "bold", bold: true }), new TextRun({ text: " italic", italics: true }), new TextRun({ text: " underline", underline: {} }), new TextRun({ text: " strike", strike: true }), new TextRun({ text: " sup", superScript: true }), new TextRun({ text: " sub", subScript: true }), new TextRun({ text: " smallcaps", smallCaps: true }), new TextRun({ text: " allcaps", allCaps: true }), ], }), new Paragraph({ children: [ new TextRun({ text: "Georgia 16pt teal", font: "Georgia", size: 32, color: "008080" }), new TextRun({ text: " highlighted", highlight: "yellow" }), ], }), new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun("centred")], }), new Paragraph({ alignment: AlignmentType.RIGHT, children: [new TextRun("right aligned")], }), new Paragraph({ alignment: AlignmentType.JUSTIFIED, spacing: { line: 360, lineRule: "auto", before: 120, after: 240 }, indent: { left: 720 }, children: [new TextRun("justified, 1.5 line spacing, 6pt before, 12pt after, indented one level")], }), new Paragraph({ children: [ new TextRun("before tab"), new TextRun({ children: [new Tab()] }), new TextRun("after tab"), ], }), new Paragraph({ children: [ new TextRun("a link to "), new ExternalHyperlink({ link: "https://silentmode.st/", children: [new TextRun({ text: "silentmode.st", style: "Hyperlink" })], }), new TextRun(" and a footnote"), new FootnoteReferenceRun(1), ], }), new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [new TextRun("bullet one")] }), new Paragraph({ numbering: { reference: "bullets", level: 1 }, children: [new TextRun("nested bullet")] }), new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [new TextRun("bullet two")] }), new Paragraph({ numbering: { reference: "romans", level: 0 }, children: [new TextRun("roman one")] }), new Paragraph({ numbering: { reference: "romans", level: 1 }, children: [new TextRun("lettered sub-item")] }), new Paragraph({ numbering: { reference: "romans", level: 0 }, children: [new TextRun("roman two")] }), new Paragraph({ style: "Quote", children: [new TextRun("A quotation, styled as Quote.")] }), new Paragraph({ children: [new ImageRun({ data: png, type: "png", transformation: { width: 96, height: 72 }, altText: { name: "red", description: "a red rectangle", title: "red" }, })], }), new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, borders: { top: border, bottom: border, left: border, right: border, insideHorizontal: border, insideVertical: border }, rows: [ new TableRow({ children: [cell("head A"), cell("head B"), cell("head C")] }), new TableRow({ children: [cell("spans two", { columnSpan: 2 }), cell("c2")] }), new TableRow({ children: [cell("tall", { rowSpan: 2 }), cell("b3"), cell("c3")] }), new TableRow({ children: [cell("b4"), cell("c4")] }), ], }), new Paragraph({ children: [new TextRun("after the table")] }), new Paragraph({ border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "808080", space: 1 } }, }), new Paragraph({ children: [new TextRun("after the rule")] }), new Paragraph({ children: [new PageBreak()] }), new Paragraph({ children: [new TextRun("second page")] }), ], }], }); return Packer.toBuffer(doc); } if (process.argv[1] && process.argv[1].endsWith("fixture.mjs")) { const out = process.argv[2] || path.join(path.dirname(fileURLToPath(import.meta.url)), "fixture.docx"); const buf = await buildFixture(); writeFileSync(out, buf); console.log(`wrote ${out} (${(buf.length / 1024).toFixed(1)} KB)`); }