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
+67
View File
@@ -0,0 +1,67 @@
// Defaults for the roster transforms. These started life as module constants
// in the Python scripts this was ported from; the values are the same, but the app
// passes them in so they can be edited without touching the transform code.
export const defaultConfig = {
// --- mergeDuplicateUnits -------------------------------------------------
// Two force-level selections are the same unit when all of these attributes
// agree. ``entryId`` identifies the catalogue entry plus the option path
// that produced it, so it is the real key; ``name`` and ``type`` are there
// so a unit the user renamed by hand stays a unit of its own.
unitMatchAttrs: ["entryId", "name", "type"],
// Selections carrying one of these categories are roster bookkeeping
// (battle size, detachment, ...), not datasheets. Leave them alone.
skipCategories: ["Configuration"],
// --- removeLeaderAbilities ----------------------------------------------
// Abilities to strip, matched against the ``name`` attribute (exactly,
// case-sensitively). Both the Abilities profile and the same-named rule go.
abilitiesToStrip: ["Leader", "Support"],
// --- convertChoiceAbilities ---------------------------------------------
// Abilities to split into per-option profiles, matched against the
// profile's ``name`` attribute.
abilitiesToConvert: ["Canticles of the Omnissiah", "Battle Protocols"],
// Title of the generated profile group, per ability. Anything not listed
// here uses the ability's own name, which is what the datasheets do.
groupTitleOverrides: {
"Canticles of the Omnissiah": "CANTICLES OF THE OMNISSIAH",
"Battle Protocols": "BATTLE PROTOCOLS",
},
};
// Which transforms run by default when a roster is loaded.
export const defaultToggles = {
mergeDuplicateUnits: true,
removeLeaderAbilities: true,
convertChoiceAbilities: true,
};
// typeName BattleScribe files datasheet abilities under. Only profiles of
// this type are matched, so a *weapon* that happened to be called "Support"
// stays, and a rule that happens to share an ability's name is still removed
// deliberately (the strip wants both halves).
export const ABILITY_TYPE_NAME = "Abilities";
// Name of the characteristic holding the rules text.
export const DESCRIPTION_CHARACTERISTIC = "Description";
// Child elements of a <selection> that get merged, in the order BattleScribe
// writes them. The order only matters when a container is missing from the
// unit we keep and has to be created.
export const CONTAINER_ORDER = [
"rules",
"profiles",
"selections",
"costs",
"categories",
];
// Containers that are removed once a strip empties them. A container that was
// *already* empty is left as it was.
export const PRUNABLE_CONTAINERS = ["profiles", "rules"];