reference-vs-nfd.docx/.odt ship as derived binaries: the pinned pandoc's
default reference documents plus a header and footer with the marking —
part of the document's page setup, so it repeats on every page in Word
and LibreOffice and is not deletable body text. Source of truth is
scripts/gen-classified-reference-docs.mjs (wording from shared
classificationMarking(); maintenance documented in assets/README.md).
The converter passes reference docs to pandoc-server via in-request
files + reference-doc; the worker attaches them for marked docx/odt jobs
(job option {marking}, as in #208). Unclassified exports pass nothing
and are unchanged (pinned by fake-converter test). Fidelity suite
asserts against real pandoc 3.6 that marked outputs carry the
header/footer parts and unmarked ones do not; per-page repetition
verified via LibreOffice 25.8 headless PDF (5/5 pages, 2 markings each,
both formats). Word: quick manual look pending (sample files in the
workspace), procedure documented in assets/README.md.
Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
119 lines
5.1 KiB
JavaScript
119 lines
5.1 KiB
JavaScript
/**
|
|
* Regenerate the classified reference documents (issue #209, ADR 0022):
|
|
* `apps/api/assets/reference-vs-nfd.docx` / `.odt`.
|
|
*
|
|
* The DOCX/ODT export of a classified page passes these to pandoc via
|
|
* `--reference-doc`; pandoc copies the reference's page setup — including
|
|
* headers and footers — into its output, which is how the VS-NfD marking
|
|
* repeats on every page in Word and LibreOffice without being deletable
|
|
* body text.
|
|
*
|
|
* The binaries are DERIVED files: base = the default reference documents of
|
|
* the PINNED pandoc (`pandoc/core:3.6`, the exact sidecar the stages run),
|
|
* plus a header and footer carrying the marking. Never edit the binaries by
|
|
* hand — edit this script and re-run it (Docker required):
|
|
*
|
|
* node apps/api/scripts/gen-classified-reference-docs.mjs
|
|
*
|
|
* The marking wording comes from @dorfteich/shared (single source, ADR
|
|
* 0022); the shared package must be built (`pnpm --filter @dorfteich/shared
|
|
* build`).
|
|
*/
|
|
import { execFileSync } from 'node:child_process';
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { classificationMarking } from '@dorfteich/shared';
|
|
import { strToU8, strFromU8, unzipSync, zipSync } from 'fflate';
|
|
|
|
const PANDOC_IMAGE = 'pandoc/core:3.6';
|
|
const MARKING = classificationMarking('vs_nfd');
|
|
const outDir = join(dirname(fileURLToPath(import.meta.url)), '../assets');
|
|
|
|
function defaultReference(name) {
|
|
return execFileSync('docker', ['run', '--rm', PANDOC_IMAGE, '--print-default-data-file', name], {
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
}
|
|
|
|
function escapeXml(value) {
|
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
/** DOCX: add word/header1.xml + word/footer1.xml, register them in the
|
|
* content types and document relationships, and reference them from the
|
|
* document's sectPr — Word repeats them on every page. */
|
|
function patchDocx(bytes) {
|
|
const zip = unzipSync(new Uint8Array(bytes));
|
|
const marking = escapeXml(MARKING);
|
|
|
|
const partXml = (root) =>
|
|
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n` +
|
|
`<w:${root} xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">` +
|
|
`<w:p><w:pPr><w:jc w:val="center"/></w:pPr>` +
|
|
`<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">${marking}</w:t></w:r>` +
|
|
`</w:p></w:${root}>`;
|
|
zip['word/header1.xml'] = strToU8(partXml('hdr'));
|
|
zip['word/footer1.xml'] = strToU8(partXml('ftr'));
|
|
|
|
const types = strFromU8(zip['[Content_Types].xml']);
|
|
zip['[Content_Types].xml'] = strToU8(
|
|
types.replace(
|
|
'</Types>',
|
|
'<Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" />' +
|
|
'<Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" />' +
|
|
'</Types>',
|
|
),
|
|
);
|
|
|
|
const rels = strFromU8(zip['word/_rels/document.xml.rels']);
|
|
zip['word/_rels/document.xml.rels'] = strToU8(
|
|
rels.replace(
|
|
'</Relationships>',
|
|
'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Id="rIdVsNfdHeader" Target="header1.xml" />' +
|
|
'<Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Id="rIdVsNfdFooter" Target="footer1.xml" />' +
|
|
'</Relationships>',
|
|
),
|
|
);
|
|
|
|
const doc = strFromU8(zip['word/document.xml']);
|
|
if (!doc.includes('<w:sectPr>')) throw new Error('reference.docx has no sectPr');
|
|
zip['word/document.xml'] = strToU8(
|
|
doc.replace(
|
|
'<w:sectPr>',
|
|
'<w:sectPr>' +
|
|
'<w:headerReference xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" w:type="default" r:id="rIdVsNfdHeader" />' +
|
|
'<w:footerReference xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" w:type="default" r:id="rIdVsNfdFooter" />',
|
|
),
|
|
);
|
|
|
|
return zipSync(zip);
|
|
}
|
|
|
|
/** ODT: give the Standard master page a header with the marking and put the
|
|
* marking next to the existing page number in its footer — LibreOffice
|
|
* repeats master-page headers/footers on every page. */
|
|
function patchOdt(bytes) {
|
|
const zip = unzipSync(new Uint8Array(bytes));
|
|
const marking = escapeXml(MARKING);
|
|
const styles = strFromU8(zip['styles.xml']);
|
|
if (!styles.includes('<style:footer>')) throw new Error('reference.odt has no footer');
|
|
const patched = styles
|
|
.replace(
|
|
'<style:footer>',
|
|
`<style:header><text:p text:style-name="MP1">${marking}</text:p></style:header><style:footer>`,
|
|
)
|
|
.replace(
|
|
'<style:footer>\n <text:p text:style-name="MP1">',
|
|
`<style:footer>\n <text:p text:style-name="MP1">${marking} · `,
|
|
);
|
|
zip['styles.xml'] = strToU8(patched);
|
|
return zipSync(zip);
|
|
}
|
|
|
|
mkdirSync(outDir, { recursive: true });
|
|
writeFileSync(join(outDir, 'reference-vs-nfd.docx'), patchDocx(defaultReference('reference.docx')));
|
|
writeFileSync(join(outDir, 'reference-vs-nfd.odt'), patchOdt(defaultReference('reference.odt')));
|
|
console.log(`generated reference-vs-nfd.docx/.odt in ${outDir} (marking: ${MARKING})`);
|