# ocda-write, in Gleam Sample writers over the abstraction: ocda-write --text|--md|--html|--epub|--typst [lang] ocda-write --md [lang] [--image-prefix ] writes a document out, from any serialisation of its abstraction. ## Modules | | | |---|---| | `write/text` | the model -> spine's plain text, byte for byte | | `write/html` | the model -> one self-contained, valid html5 file | | `write/epub` | the model -> an epub3, one file per segment | | `write/markdown` | the model -> CommonMark, object numbers and all | | `write/typst` | the model -> typst markup, for a pdf | | `write/zip` | writing a zip, stored only, with no timestamps | | `write/rgx` | the internal markup, as compiled patterns | | `ocda_write` | the program: which format, which file, which language | `write.text.write` takes an `ocda/model.Document` and returns a `String`. It is given no file, no path and no option: a writer is a function from the model to bytes. ## Build and run ```sh nix build ./result/bin/ocda-write --text .ssp ./result/bin/ocda-write --text .ocda.db de ./result/bin/ocda-write --md .ssp ./result/bin/ocda-write --epub .ocda.db en --out .epub ./result/bin/ocda-write --typst .ocda.db en --out out/.typ ``` `flake.lock` pins the library at the commit this was checked against. In a development shell `gleam` reads it through the `./ocda-lib` symlink (below): ```sh nix develop -c gleam build ``` ## The check ```sh ./result/bin/ocda-write --text .ssp | cmp - ``` **Every document-language, from both serialisations, byte identical to the D writer** - which is the check that matters now, since byte identity with `spine --text` is no longer the target. And for `--md`, `--html`, `--epub` and `--typst`, **byte identical to the D writer on every document**, which is the agreement that says the model is enough. None of them is held to spine's bytes: the epub's judge is epubcheck, which read the specification so that the writer need not be trusted to have, and the pdf's is typst, where a link to a label that does not exist is a compile error. See the repository README for what they do differently and why. ## The library, as a path dependency inside the package `gleam.toml` names it `./ocda-lib`, and that is a **symlink** to `../../sisudoc-ocda-lib/gleam`, kept out of git. It has to be inside the package: a path dependency reaching outside cannot be seen by a sandboxed build, and a store path in `gleam.toml` would change with every change to the library. So the path is stable, and what sits at the end of it is arranged by whoever is building: a symlink in a working tree, a copy from the store in the nix build, where nix-gleam puts the library in place as a local package. Create it after cloning: ```sh ln -s ../../sisudoc-ocda-lib/gleam ocda-lib ``` ## Places this is not idiomatic Gleam, on purpose - **A column's width is the widest cell in bytes, and the padding is counted in codepoints.** Not a typo. Spine takes the width from `cell.length`, which for a D string is a byte count, and pads with `format("%-*s")`, which counts codepoints. So a cell holding `©` is padded one place further than one holding `c`. Measured rather than guessed, and reproduced rather than corrected, because the output is compared byte for byte. - **The escape in `write/typst` walks codepoints and folds a list**, where everything else here is a regular expression. It has to: the escaping skips protected runs, the protection nests, and the D writer walks codepoints, so a grapheme walk would part company with it over a combining mark. `label_of` walks codepoints for the same reason, and once did not: it used graphemes while the D one used bytes, and five documents with typographic quotation marks in a book index entry disagreed. - **`gleam/regexp` has no replace-with-function**, so `replace_all` here is `scan` plus a walk over the subject. It is the one piece of scaffolding this writer needs that the D one gets from Phobos. - **The claimed-id set is threaded across segments, not restarted in each.** An id has to be unique in a *file* to be valid, and unique in the *publication* to be useful: a link carries a file and a fragment, and an anchor with two destinations has none. Restarting the set per segment gave twelve documents that epubcheck passed and the D writer disagreed with. - **Every replacement goes through the function form, never the string form.** Erlang's `re:replace` reads `&` in a replacement *string* as the whole match, so replacing `&` with `"&"` yields `"quot;` where the match was a quotation mark. Byte identity with the D writer is what caught it, on one document's copyright notice, and nothing else would have: the file still parsed and still read correctly everywhere else. ## Shipping An Erlang shipment rather than an escript, because the library reaches sqlite through a NIF and a NIF cannot be loaded from inside an escript archive.