205 lines
8.3 KiB
JavaScript
205 lines
8.3 KiB
JavaScript
|
|
// 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)`);
|
||
|
|
}
|