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 ![alt](src) — 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:

  1. Block types are tried in config.blocks definition order.
  2. Within a kind, rules are tried in listed order.
  3. 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.