Fork FancyScribe as BrevyScribe with the datasheet transforms built in
CI / check (push) Has been cancelled

Print generic datasheets - every option a unit could take, the way the
official cards read - instead of a record of one particular list.

The three Python scripts that used to rewrite the .ros file before upload
are now transforms in src/transforms/, running in the browser between
DOMParser and the roster parser. Most of each script was machinery for
preserving the file byte for byte on the way back to disk; in the browser
the document is never serialised, so only the domain logic came across.

Because they run on the parsed document rather than the uploaded file,
flipping a transform off rebuilds from the original XML with no
re-upload. Saved rosters therefore hold the raw roster XML rather than
the parsed object, under new localStorage keys.

The scripts' output over four 11th-edition rosters is checked in as test
fixtures, so the port is measured against an independent implementation
that was verified by printing the cards; scripts/update-fixtures.mjs
refuses to overwrite those without --force.

Two parser call sites now use :scope> rather than repeating the scope
element's own name, which means the same thing in a browser and also
works under jsdom, where the old form matched nothing.

Also: served at the root rather than a GitHub Pages subpath, PostHog
analytics removed, and deploy/ carries an nginx site for
scribe.luxick.de plus an rsync deploy script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 15:11:10 +02:00
parent 3a0fdc2c26
commit 62d6bd211b
45 changed files with 3317 additions and 191 deletions
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
//
// Write the expected-output fixtures for a roster added to
// src/transforms/__fixtures__/.
//
// node scripts/update-fixtures.mjs # only writes what is missing
// node scripts/update-fixtures.mjs --force # re-baselines everything
//
// Drop a new `<name>.ros` in that directory and run this to generate the four
// `<name>.{merge,strip,convert,all}.ros` files the test suite compares against.
// Read the diff before committing: this records whatever the transforms do
// today, so it cannot tell a fix from a regression.
//
// Existing fixtures are left alone unless --force is given, and that guard is
// the point rather than a convenience. The fixtures that came with this repo
// were produced by a separate Python implementation and verified by printing
// the cards; the test suite is worth something because it checks the transforms
// against that independent output. Re-baselining a fixture from the JS replaces
// it with the very thing under test, and the comparison becomes a tautology
// that passes no matter what the code does.
import { readFileSync, readdirSync, existsSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { JSDOM } from "jsdom";
import {
convertChoiceAbilities,
defaultConfig,
mergeDuplicateUnits,
removeLeaderAbilities,
} from "../src/transforms/index.js";
const FIXTURES = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"src",
"transforms",
"__fixtures__",
);
const STEPS = {
merge: (doc) => mergeDuplicateUnits(doc, defaultConfig),
strip: (doc) => removeLeaderAbilities(doc, defaultConfig),
convert: (doc) => convertChoiceAbilities(doc, defaultConfig),
};
// Chained, in the order the app runs them; see src/transforms/index.js.
const PIPELINE = ["merge", "strip", "convert"];
const force = process.argv.includes("--force");
const { window } = new JSDOM();
const inputs = readdirSync(FIXTURES)
.filter((name) => name.endsWith(".ros") && name.split(".").length === 2)
.map((name) => name.replace(/\.ros$/, ""));
let written = 0;
let kept = 0;
for (const input of inputs) {
const xml = readFileSync(join(FIXTURES, `${input}.ros`), "utf8");
const outputs = { ...STEPS, all: null };
for (const label of Object.keys(outputs)) {
const path = join(FIXTURES, `${input}.${label}.ros`);
if (existsSync(path) && !force) {
kept++;
continue;
}
const doc = new window.DOMParser().parseFromString(xml, "text/xml");
if (label === "all") {
for (const step of PIPELINE) STEPS[step](doc);
} else {
STEPS[label](doc);
}
writeFileSync(path, new window.XMLSerializer().serializeToString(doc), "utf8");
console.log(`wrote ${input}.${label}.ros`);
written++;
}
}
console.log(`\n${written} written, ${kept} left alone.`);
if (kept > 0 && !force) {
console.log("Pass --force to re-baseline the rest, but read the header first.");
}