# 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.
