dorfteich/apps/api/scripts/gen-classified-reference-docs.mjs
Claude Fable 5 74a9e495e4
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 6m34s
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Has been skipped
CI / Auth e2e pack (pull_request) Has been skipped
#209: pandoc reference documents carry the VS-NfD marking for DOCX/ODT
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>
2026-07-31 07:09:47 +02:00

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/** 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})`);