Kind inference
In every syntax except JSON, a block’s kind is inferred from its spelling — its caps, prefixes, indentation, or the construct it’s written as — never from a stored annotation. This is the heart of ScriptPub’s losslessness: there is nothing extra to store, so nothing is lost. (In JSON the kind is explicit, so there is no inference.)
Each BlockDef.syntax.<id> is a pair:
{
"match": [ Rule, … ], // how the kind is recognized
"write": Spelling // the one canonical way it is serialized
}
Match rules — recognition
A rule is a conjunction of predicates: all present keys must hold (AND). A
kind may list several rules, and it matches if any of them holds (OR). A
MatchRule is a flat object; these are its predicates:
| Predicate | Applies to | Meaning |
|---|---|---|
construct |
the block | The syntax’s base construct. HTML: the tag name only ("p"). Markdown: a construct name ("heading", "paragraph", "blockquote", "code", "rule", "image", "list", "table", "footnote"). Fountain: an element. FDX: the paragraph Type. |
level |
the block | Markdown heading level (with construct: "heading"). |
class |
the block | HTML class — a predicate separate from construct. A kind claiming a class beats one claiming the bare tag (class-then-tag). |
regex |
the block | Regex source, tested against the block’s plain text. |
prefix / suffix |
the block | Literal affix of the text ("INT.", "TO:"). |
caps |
the block | All-caps: ≥1 letter and no lowercase letter. |
indent |
the block | Leading-whitespace count (tab = 1). A number (exact) or { min?, max? }. |
blankBefore / blankAfter |
the block | Whether a blank line sits before / after the block. |
prev |
the previous block | Its resolved kind — a block id, or any of a set. |
next |
the next block | Its raw text only — a TextRule, never its resolved kind (no circularity). Lookahead distance is 1. |
A TextRule (used only by next) is the text-only subset: regex / prefix /
suffix / caps / indent / empty.
Two media notes: in HTML, img / audio / video are
ordinary tag constructs (an <img> in inline position parses as an
inline image, not a block); in
Markdown, the "image" construct is a paragraph consisting of a single
 — an image amid text is an inline object run, never a block.
Text predicates (regex / prefix / caps / …) test a block’s plain-text
projection, in which an object run reads as empty — a paragraph opening
with a decorative image still matches its text rules.
Precedence — dumb and explicit
The parser resolves kinds in one pass, top to bottom, and precedence is deliberately simple:
- Block types are tried in
config.blocksdefinition order. - Within a kind, rules are tried in listed order.
- First match wins. There is no computed specificity.
The default Text block is the implicit final fallback and lists no rules. Two kinds whose spellings collide are the format editor’s problem to warn about — not the model’s.
In HTML the rule is class-then-tag: <p class="parenthetical"> matches the
kind claiming that class before any kind claiming bare <p>; at most one kind may
claim a bare tag; a class matching no kind falls through to tag inference.
Example — recognizing a Fountain character cue and its dialogue
"character": {
"fountain": { "match": [{ "caps": true, "blankBefore": true, "next": { "empty": false } }], "write": { "transform": ["upper"] } }
},
"dialogue": {
"fountain": { "match": [{ "prev": ["character", "parenthetical", "dialogue"], "blankBefore": false }], "write": {} }
}
A character cue is an all-caps line, preceded by a blank line, followed by a
non-empty line; dialogue is any line whose resolved previous block was a
character, parenthetical, or dialogue. Note how next looks only at raw text
(empty: false) while prev looks at the resolved kind — that asymmetry is what
prevents circular resolution.
Write form — serialization is normalization
write is the kind’s one canonical spelling: a per-syntax structural
descriptor applied to the block’s existing runs. It is not a template that
replaces the text — the writer’s own words are preserved. Its fields, and which
adapters read them:
| Field | Used by | Meaning |
|---|---|---|
tag, class |
html | Element tag + class token. |
construct, level |
md, fountain, fdx | Construct / element / Type name; Markdown heading level. |
transform |
txt, fountain, md | ("upper" | "lower" | "capitalize")[] case transforms. |
indent |
txt, fountain | Leading indent to ensure. |
prefix, suffix |
txt, fountain, md | Fixed affix ensured present — idempotently (not doubled if the text already has it). |
Because kind is inferred from spelling, changing a block’s kind rewrites its
spelling so it parses back the same way. Retype a paragraph as a scene heading
and the editor uppercases it, adds the INT. affix if configured, indents it — the
typewriter tradition, formalized: the layout is the element.
The idempotent affix rule is subtle and important: a scene heading whose text the
writer typed as INT. KITCHEN and a write.prefix of "INT. " coexist — the
fixed prefix is ensured present, not blindly prepended, so a preserved INT.
isn’t doubled into INT. INT. KITCHEN.