The three models

A caller describes a cheatsheet in three objects, each with a schema of its own: a document of a title and ordered sections; a theme of palette roles, a type stack and scale, the spacing step, the curve and shape tokens and the stroke weights; and a layout of a named composition, the grid it sits on, and the slots a section may occupy. The document says nothing about how any of it is drawn, which is what lets one document be set to two layouts.

A section asks for a kind of slot, not for a slot

A section asks the layout for a kind of slot rather than for a slot by name - entries or listing - and the layout answers with the one it keeps for that content. A composition sets one stream of rows, so a layout keeps one such slot, and the rows are placed from that slot's own box: a layout file that names no slot for a section, or that keeps two of them, is refused by path on the way in rather than drawn in a panel nobody asked for.

Entries, lines, and the compact shape

A section carries entries or lines: entries are a term, a signature and a description each, and lines is a worked code listing, whose syntax role per line and whose indentation both carry meaning an entry has nowhere to put. A document may also be written in the compact shape - a title and one flat code array, which is what every sheet.json in this repository is in and what the page reads. Both shapes are permanent: the compact one is exactly a formal document of one untitled listing, so it is the shorthand for the common case rather than a draft of anything.

The whole public input surface

The three are the whole public input surface, alongside a seed and the two brand assets. Nothing the renderer reads for itself is accepted: the design language record and the two width tables used to be passable as options.language, options.face and options.codeFace, and each is now refused by name pointing at the model that replaced it. So a caller composes a sheet without naming anything a renderer owns.

Derived from the design language, not written out again

The theme and the layout are both derived from DESIGN_LANGUAGE.md rather than written out again, so a figure corrected in the record is corrected in both. Pass neither and a sheet composes to what the eleven images were measured at; pass one and only what it carries moves - a theme changes every paint and not one coordinate.

A theme may be partial

It is laid over the recorded theme and then validated, so one colour moves without the palette being restated, and a misspelt field is still named back by path. It is also held to being readable before anything is drawn - every pairing of ink and ground the theme and the layout imply is measured against the text floor, after the one substitution the design language nominates, and a theme that fails is refused rather than composed and annotated.

The type stack, and which family drew it

The type stack is a list of families rather than a CSS string, and a render records which of them actually drew it: the SVG names a stack it cannot guarantee, so the browser route measures what the engine resolved and reports it beside the bytes.

The way out

The way out is the layer's too, as of 2026.10.31. renderSVG is the canonical output and every other format is derived from it, renderFragment is that markup made embeddable in a page, and renderSheet names any of the five formats and takes the raster route from the call:

const render = await renderSheet(doc, { format: "webp", route: "browser" });
render.data        // the bytes, on every format
render.route       // which renderer answered
render.publishable // whether it draws the face the design language measured

Every render comes back as data, so a sheet is embedded without a path being named. Writing one is a second import, api/cheatsheet/files.mjs, which is the only module in the folder that reads node:fs - the page loads the surface with a bare import, so a Node builtin in its static graph would fail in a browser at load. That module reads a document as well as writing a render: readDocumentFile takes a path and holds the JSON to whichever of the two document shapes it is written in, while readDocument beside it is the plain-object form. Those two are the whole input model, and a path naming any other format is refused by name rather than handed to JSON.parse.

Each of the five formats has a worked example of its own in examples/render-formats.mjs, which composes one document and renders it five times - and reports which route answered, which family drew, and whether the result may be shown to a reader.

When a model is wrong

A model that is wrong is refused with the path to the field, in the notation a caller would reach it by:

cheatsheet: there is one problem with the sheet document.
  document.sections[2].entries[1].term: must be a string, and is absent

Part of the cheatsheets documentation. The repository is on GitHub, and the example page it documents is here.

Contents