The amd_blocks module
One AMD record body, read into a list of typed blocks.
Overview
amd_core.parse gives a renderer everything about a document's structure — the
record tree, the fence facts, the reference spans — and nothing about its body,
which arrives as raw lines exactly as typed. Every consumer used to re-derive meaning
from those lines itself, which is how the same % and the same - [label](target)
came to be read four slightly different ways.
This reads them once:
[{"type": "cue", "line": 12, "speaker": "vex", "surface": "comms"},
{"type": "speech", "line": 13, "variants": [{"text": "...", "gate": None}]},
{"type": "choice", "line": 15, "label": "Pay", "target": "paid", ...}]
Every block is a plain JSON-able dict with a 1-based line, so blocks cross a process
boundary — the language server, a --format json dump, a golden test — untouched.
This module owns no grammar
Every mark is recognized by the function in procedural.amd that already owned it.
What lives here is the order those recognizers are tried in, and where one block
ends — which is exactly the part that has to be identical between the game and a
printed page.
Deliberately absent: measuring, wrapping, styling, or anything needing the engine. A block says "these lines were a table"; how wide its columns are is the renderer's business, and the in-game one answers differently from a sheet of paper on purpose.
Profiles
profile="player" is a hard filter, not a stylesheet class. An author-only =
synopsis, a choice's guard and its outcomes are absent from the result, because
"print to PDF" and "view source" have to agree about what a player was told.
API
One AMD record body -> a list of typed blocks.
amd_core.parse gives a renderer everything about a document's STRUCTURE - the
record tree, the fence facts, the reference spans - and nothing about its BODY,
which arrives as node.body_lines: raw text, exactly as typed. Every consumer so
far has re-derived meaning from those lines itself, which is why the same % and
the same - [label](target) were being read four slightly different ways.
This module reads them ONCE, into blocks:
[{"type": "cue", "line": 12, "speaker": "vex", "surface": "comms"},
{"type": "speech", "line": 13, "variants": [{"text": "...", "gate": None}]},
{"type": "choice", "line": 15, "label": "Pay", "target": "paid", ...}]
Every block is a plain JSON-able dict with a 1-based line, following the house
style amd_schema set for descriptors - so blocks cross a process boundary (the
LSP, a --format json dump, a golden test) untouched, and a test can pin the
MEANING of a document rather than one renderer's HTML.
This module owns no grammar. Every mark is recognized by the function in
amd that already owned it. What lives here is only the ORDER those recognizers
are tried in and where one block ends and the next begins - which is exactly the
part that has to be identical between the game and a printed page.
Deliberately NOT here: measuring, wrapping, styling, or anything that needs the engine. A block says "these lines were a table"; how wide its columns are is the renderer's business, and the in-game one answers differently from a sheet of paper on purpose.
amd_blocks(node, doc=None, profile='author', resolve=None, depth=0, _seen=None)
The blocks of one amd_core.AmdNode's body.
doc lets ![[key]] transclusions and [[key]] links resolve; resolve is
an optional key -> AmdNode callable for a MISSION-wide lookup, which a
single document cannot do on its own. profile is "author" or "player" -
see amd_blocks_filter.
amd_blocks_filter(blocks, profile='author')
Strip everything profile must not see, recursively.
player is a HARD filter, not a stylesheet class: an author-only note or a
choice's outcome must be ABSENT from the output, because "print to PDF" and
"view source" have to agree about what a player was told. Anything merely
hidden by styling is one right-click from being read.
amd_blocks and amd_blocks_text both run this before returning, so a
caller never has to remember to. It stays public for a caller that built the
author cut once and now wants the player one without re-parsing.
amd_blocks_text(text, profile='author')
The blocks of a raw body string, for callers that have text and no node.
A record read this way has no document around it, so [[links]] are recorded
but transclusions cannot resolve.