# 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:` 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 `` 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: `.{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.