Fork FancyScribe as BrevyScribe with the datasheet transforms built in
CI / check (push) Has been cancelled
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:
@@ -0,0 +1,152 @@
|
||||
# BrevyScribe
|
||||
|
||||
A fork of [FancyScribe](https://github.com/NilsUeter/fancyscribe) that prints
|
||||
**generic datasheets** rather than a record of one particular army list.
|
||||
|
||||
FancyScribe renders a BattleScribe or New Recruit roster as 10th-edition-style
|
||||
datacards, showing exactly the wargear you picked. That is the right thing for a
|
||||
list you are about to play, but the wrong thing for a reference card you want to
|
||||
keep: the official cards show *every* option a unit could take. BrevyScribe
|
||||
rewrites the roster on the way in so the printed cards read like the official
|
||||
ones.
|
||||
|
||||
Three transforms do the work, all of them toggleable in the UI:
|
||||
|
||||
| Transform | What it does |
|
||||
| --- | --- |
|
||||
| **Merge duplicates** | Mutually exclusive wargear forces you to take a datasheet twice - one Skatros with a radium jezzail, another with a transuranic arquebus. Copies of the same unit are folded into one card carrying every option. Points are not summed; the card keeps the highest cost of the copies it absorbed. |
|
||||
| **Drop Leader/Support** | Removes the attachment rules. Once the army is built they say nothing you need mid-game, and they are long enough to push the rules you *do* need off the card. |
|
||||
| **Split choice abilities** | An ability like *Canticles of the Omnissiah* arrives as one blob of text. This splits it into the intro rule plus one titled row per option, which is how the datasheets print it. |
|
||||
|
||||
Everything still runs in the browser. There is no server component, no upload,
|
||||
no account, and no analytics; rosters are held in `localStorage` and never leave
|
||||
the machine.
|
||||
|
||||
## Running it
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
npm test # the parity suite, see below
|
||||
npm run build # static files into dist/
|
||||
```
|
||||
|
||||
`npm run lint` reports pre-existing findings inherited from upstream (mostly
|
||||
`a11y/useButtonType`), so it is deliberately not part of CI. Compare against
|
||||
`git show upstream/main:<file>` before treating any of them as new.
|
||||
|
||||
## Deploying to scribe.luxick.de
|
||||
|
||||
The build output is **static files** - no application server, no socket, nothing
|
||||
for nginx to proxy to. The app does all its work in the browser, which is why
|
||||
upstream can live on GitHub Pages.
|
||||
|
||||
One-time setup on the server:
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /var/www/brevyscribe
|
||||
sudo chown "$USER" /var/www/brevyscribe
|
||||
sudo cp deploy/nginx-scribe.luxick.de.conf /etc/nginx/sites-available/scribe.luxick.de
|
||||
sudo ln -s /etc/nginx/sites-available/scribe.luxick.de /etc/nginx/sites-enabled/
|
||||
sudo certbot --nginx -d scribe.luxick.de
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Then every deploy is one command from a checkout on your own machine - it runs
|
||||
the tests, builds, and rsyncs `dist/` over. The server needs no Node.
|
||||
|
||||
```sh
|
||||
./deploy/deploy.sh
|
||||
```
|
||||
|
||||
`--delete` is deliberate: asset filenames are content-hashed, so without it old
|
||||
bundles would pile up forever. There is no state on the server and nothing to
|
||||
back up; every roster lives in the browser's `localStorage`.
|
||||
|
||||
`vite.config.js` sets `base: "/"`, because this is served at a domain root -
|
||||
upstream sets `/fancyscribe` for GitHub Pages. If you ever serve it from a
|
||||
subpath, that is the setting to change.
|
||||
|
||||
Note that `index.html` loads Noto Sans from Google Fonts, so a page load reaches
|
||||
out to `fonts.googleapis.com`. If you would rather it did not, drop the
|
||||
`<link>` tags and self-host the font next to the ones already in `public/fonts/`.
|
||||
|
||||
## How the transforms fit in
|
||||
|
||||
```
|
||||
upload/.rosz -> unzip -> DOMParser -> applyTransforms -> Create40kRoster* -> render
|
||||
^
|
||||
src/transforms/index.js
|
||||
```
|
||||
|
||||
`src/App.jsx` runs them between parsing the XML and building the roster, so
|
||||
flipping a toggle rebuilds from the original XML with no re-upload. That is also
|
||||
why saved rosters hold the raw roster XML rather than the parsed object.
|
||||
|
||||
Which abilities get stripped and split is configured in
|
||||
[`src/transforms/config.js`](src/transforms/config.js) - add to
|
||||
`abilitiesToStrip` and `abilitiesToConvert` as you meet more of them.
|
||||
|
||||
## The test suite
|
||||
|
||||
The transforms began as three standalone Python scripts (`merge_duplicate_units`,
|
||||
`remove_leader_abilities`, `convert_choice_abilities`) that rewrote the `.ros`
|
||||
file before it was uploaded to FancyScribe. They are gone now, but they are what
|
||||
the tests measure against.
|
||||
|
||||
`src/transforms/__fixtures__/` holds four 11th-edition rosters plus, for each,
|
||||
the output those scripts produced: `<name>.{merge,strip,convert,all}.ros`. The
|
||||
suite runs each transform over the input and asserts it produces the same roster.
|
||||
That is worth more than a snapshot of the current code, because the expected
|
||||
output came from a **different implementation** that was verified by actually
|
||||
printing the cards.
|
||||
|
||||
That property is easy to destroy and hard to notice: regenerate a fixture from
|
||||
the JS and the test compares the code against itself, passing no matter what it
|
||||
does. So `scripts/update-fixtures.mjs` will not overwrite an existing fixture
|
||||
without `--force`. Use it when you add a new example roster, and read the diff:
|
||||
|
||||
```sh
|
||||
# drop a new roster in src/transforms/__fixtures__/, then
|
||||
npm run fixtures
|
||||
```
|
||||
|
||||
Comparison is structural rather than textual. The scripts edited the file as text
|
||||
to keep its formatting byte for byte; these transforms build DOM nodes, so
|
||||
attribute order and whitespace legitimately differ. Generated `id`/`typeId`
|
||||
values are compared as *tokens*, so what has to match is that ids are shared and
|
||||
distinct in the same pattern, not that both implementations hash alike.
|
||||
|
||||
`integration.test.js` goes further and pushes the transformed document through
|
||||
the real roster parser, checking what lands on the card: one Skitarii card
|
||||
carrying both weapons, the data-tether ability travelling with the model that
|
||||
brings it, no Support ability left, and each Canticle its own row.
|
||||
|
||||
One wrinkle: the tests run under jsdom, which does not implement scoped selectors
|
||||
the way browsers do. `force.querySelectorAll("force>selections>…")` finds nothing
|
||||
there, where a browser matches the selector against the whole tree and then keeps
|
||||
the descendants of `force` - so under jsdom the parser found no units at all. The
|
||||
two call sites that relied on it now say `:scope>…`, which means the same thing in
|
||||
a browser and works in both.
|
||||
|
||||
## Staying current with upstream
|
||||
|
||||
FancyScribe is actively developed - 11th edition support landed recently - and
|
||||
this fork touches little of it. `upstream` is wired up:
|
||||
|
||||
```sh
|
||||
git fetch upstream
|
||||
git merge upstream/main
|
||||
```
|
||||
|
||||
Conflicts should be confined to `src/App.jsx`, `index.html`, `vite.config.js` and
|
||||
`package.json`. `src/transforms/` is entirely new and will never conflict.
|
||||
|
||||
## Credit
|
||||
|
||||
All the hard parts - the parsing, the card layout, the print CSS - are
|
||||
[Nils Ueter's](https://github.com/NilsUeter/fancyscribe), with parsing logic
|
||||
descended in turn from
|
||||
[PrettyScribe](https://github.com/rweyrauch/PrettyScribe). Upstream ships no
|
||||
licence file, so treat this fork as a private, personal deployment rather than
|
||||
something to redistribute.
|
||||
Reference in New Issue
Block a user