Skip to content

The amd_render module

AMD → a self-contained HTML document, laid out for print. Drives sbs docs.

Overview

Four lenses, because an .amd file is three documents wearing one syntax. A file with no fence is prose and converts almost directly; a file whose content is the fence is a catalog, where printing Color: #ffcc44 as a line of text would be worthless; and a quest tree's meaning lives in Parent: / Starts when: / Then:, which flattening destroys. So the number of renderings is the number of audiences:

Lens Reader wants
prose sentences — a manual or a story book
catalog to look something up — reference cards, fields rendered as their type
screenplay to perform it — Fountain geometry
bible to see the machine — the quest spine, triggers and graph

Field typing comes from amd_schema, so a color becomes a swatch, a reference a working link, a coordinate a cell chip.

Anchors are path-based and unique per file. Bare keys are not unique — 40 of the corpus's 374 repeat, one file holds three recover records, and five .amd basenames repeat across the missions. A per-key anchor does not dangle; it aims a working link at the wrong record.

The contents list and every cross-reference are built from what the body actually emitted, so a link to a record a lens skipped is demoted to plain text rather than offered as a jump that lands nowhere.

API

AMD -> a self-contained HTML document, laid out for PRINT.

AMD is where a mission's design lives - the quests, the dialogue, the factions, the lore, the help text - and until now the only thing that could read it was the game. A writer reviewing dialogue, a modder looking up faction keys and a playtester reading a mission bible all had to open a code editor and read markup.

The output is one HTML file with its CSS inlined and no external requests, sized for paper: @page rules, controlled breaks, a table of contents. "Print to PDF" in any browser is the PDF path, deliberately - there is no PDF library anywhere in this stack and adding one to sbs.pyz would be a new dependency for a job the browser already does.

Four lenses, because AMD is three documents wearing one syntax. A file with no fence at all (help_docs.amd, lore.amd) is prose and converts almost directly. A file whose content IS the fence (sides, items, drops) is a catalog, and printing Color: #ffcc44 as a line of text would be worthless - it wants a swatch. A quest tree's meaning lives in Parent: / Starts when: / Then:, and flattening it destroys exactly the thing a designer opened the document to see. So the number of renderings is the number of AUDIENCES:

prose       a manual or a story book - the reader wants sentences
catalog     a sourcebook - the reader wants to look something up
screenplay  a script - the reader wants to perform it
bible       a design document - the reader wants to see the machine

Two profiles. player is a HARD filter, not a stylesheet class: an author-only note must be ABSENT from the file, because "print to PDF" and "view source" have to agree about what a player was told.

Stdlib only, like every other module here that ships inside sbs.pyz.

NoAssets

The default asset resolver: resolves nothing, and says so on the page.

A printed page should say "a face goes here, of an Arvonian" rather than silently closing the gap - a missing illustration the reader cannot see is a missing illustration nobody fixes.

amd_render_html(docs, lens='prose', profile='author', title=None, assets=None, resolve=None, show_internal=False, faces='canvas')

[(uri, AmdDocument)] -> one self-contained HTML page.

lens picks the audience (see the module docstring); profile is author or player. resolve is an optional mission-wide key -> AmdNode lookup so a reference that crosses a file still links - something a single document cannot do on its own.

blocks_html(blocks, ctx, depth=0)

Render a block list. ctx carries the document, profile and assets.

doc_tokens(docs)

A short, unique id fragment per document.

slug(basename) alone is not unique: five basenames repeat across the corpus, and OpenUniverse holds captains/ashfang.amd AND dialogue/ashfang.amd. Two files sharing an anchor prefix do not dangle - they do something worse, and aim a working link at the wrong record. So a collision walks back up the path until the token is unique.

facts_html(node, ctx, only=None, drop=None, css='facts')

A record's fence as a definition list, in the schema's field order.

Order comes from amd_schema.template_fields, not from what the author happened to type, so every card of a kind lists its fields the same way and a missing field reads as a gap rather than as a reshuffle. Fields the schema does not know follow, in authored order - which is how a mission's own registered vocabulary shows up.

lens_bible(docs, ctx)

The design document: the quest spine, its triggers, and the causal graph.

Every structural fact here comes from amd_timeline, which already computes the beat ranking, the spine/pool split and the cross-file signal join. That join is the reason the module exists and the reason this lens cannot be done a document at a time: a signal emitted in one file is waited on in another.

lens_catalog(docs, ctx)

Every record that HAS a fence, grouped by archetype, as reference cards.

This is the lens that proves the field registry was worth building: the fence is the content, so each field is rendered as its TYPE - a color as a swatch, a reference as a working link, a coordinate as a cell - rather than as the line of text it was typed as.

lens_prose(docs, ctx)

Every record, in document order, as a book.

The lens the fence-less files were written for - help_docs.amd, library_docs.amd, lore.amd - where the body already IS markdown and the conversion is nearly a straight one.

lens_screenplay(docs, ctx)

Dialogue and beats as a script someone could read aloud.

Selection is by CONTENT as well as by archetype, and that second clause is what makes the lens work on the corpus that exists: not one shipped .amd contains a @cue or a (direction). What they contain is Speaker: in the fence and bare % variant lines, so a record with speech and no cue prints its fence speaker as an implicit one.

slug(text)

A stable id fragment. Anchors are PATH-based, never bare keys: 40 of the corpus's 374 keys repeat, and one file alone holds three recover records.