# Typst export The tool does not write Typst documents. It loads a Typst file you own, finds the markers in it, and injects data. Layout is yours; the payload is the tool's. ```console $ coursebank template dump # get the built-in templates as files $ coursebank template list # see which template each document uses $ coursebank template config # write templates/typst.yaml $ coursebank export typst exam-2 # render paper, key, and answer sheet ``` ## Markers Injection points are Typst line comments, so a template is a valid `.typ` file that compiles on its own: ```typst // coursebank:begin questions #render-question((number: 1, stem: [Sample.], options: ())) // coursebank:end questions ``` Everything *between* the markers is replaced. The marker lines survive. Two consequences: - The bundled templates ship with sample data inside their regions, so `typst watch templates/exam.typ` works before you have exported anything. Restyle against the sample, then export. - **An exported document is itself a valid template.** Exporting into a file you have since restyled replaces the questions and leaves your edits alone. This is the difference between a generator you can use twice and one you copy out of once. A bare `// coursebank:questions` also works. It is rewritten into a region on output, so the second export behaves like every one after it. Malformed markers are reported all at once, with line numbers, and the export stops: an unknown slot name, an unclosed region, a stray `end`, a nested region, or the same slot claimed twice. ## Slots | Slot | Injected | |:--|:--| | `meta` | `#let cb-meta = (...)`: course, assessment, form, totals, objectives | | `questions` | one `#render-question((...))` call per printed item | | `data` | `#let cb-data = (...)`: the metadata and the questions together | `questions` unrolls the loop using the record's own numbering. `data` hands you the array and gets out of the way; the bundled key and answer sheet use it because a table suits a loop better than a sequence of calls. Use either, both, or neither. Only slots your template actually contains are built, so nothing costs anything until it is asked for. Each question arrives as a **single positional dictionary**, not named arguments, so turning a field on or off in the config never changes your function's signature. Read optional fields with `q.at("level", default: none)`. ## Configuration `templates/typst.yaml`, three layers, each overriding the last: built-in defaults for the variant, then `defaults:`, then `variants:`. ```yaml defaults: question_fn: render-question # the function the `questions` slot calls content: content # `content` -> [...] | `str` -> "..." for eval() letters: upper # upper | lower | numeric | roman | nothing extra: accent: "#017ab9" font: Roboto variants: key: reveal: everything extra: show-solutions: true ``` Keys are snake_case, matching every other coursebank YAML file. Keys *under* `extra` are yours and reach Typst verbatim, so they conventionally use hyphens. `coursebank template config --resolved exam` prints what a variant actually ends up with, which is the quickest way to find out which layer won. ### `extra` is the escape hatch Anything under `extra` is carried through untouched and arrives as `extra` in the payload. Tier colours, a font stack, a `show-solutions` flag, a watermark, column counts. Put it there and read it in the template. Nothing about appearance needs to be added to this crate. ### `reveal` is not cosmetic `reveal` controls whether the payload contains the answer **at all**. The exam variant uses `nothing`, and that means an option dictionary on the paper has no `correct` field, not `correct: false`. A template cannot leak a field it was never given, and that stays true after someone edits the template without reading this page. Do not set `reveal: key` on the exam variant to build a solutions copy. Export the `key` variant. The failure mode of the other approach is one forgotten `if` away, and it is discovered by the whole room at once. ## Template lookup 1. `--template `, or `template:` in the render config 2. `templates/-.typ`, a one-off layout for one exam 3. `templates/.typ`, the course's own default 4. the template compiled into the binary `coursebank template list` prints this chain with the resolved entry marked, plus the slots each template declares. `coursebank template dump` writes step 4 into step 3. ## JSON `coursebank export typst exam-2 --json` also writes the payload as JSON, for a template that reads `json("exam-2-A.json")` instead of taking an injected region. Key spellings are identical between the two paths (`level-name`, not `level_name`), so a template can move between them without edits. JSON has no content type, so set `content: str` and `eval(q.stem, mode: "markup")` if you use this path. ## What is still guaranteed **The key matches its paper.** Option order comes from the form's recorded seed via `select::option_order`, never from anything stored, and the paper, key, and answer sheet are built from one payload. Every export of form B agrees with every other. **Question numbers are the recorded ones.** Not positions on the page. The recorded number is the join key to every grading export and response row; renumbering after a drop breaks that join silently, and the symptom is item statistics attributed to the wrong question. `number-from-record: false` exists but you almost certainly want it left alone. ## Advisory warnings `export typst` exits `2` and prints warnings, without refusing to write, when: - a stem or option has unbalanced `[` `]`, which would otherwise surface as a Typst parse error somewhere downstream of the item that caused it, with no way for the compiler to name the question - a template declares no markers at all, so nothing was injected