Blocks

A block is one paragraph-level unit of the content stream. Its kind is recognized by matching its element against the block types’ CSS selectors (see Kind inference), and its shape determines what content it holds (see the three shapes).

The block types available to a work are two species under one merged vocabulary:

  • Built-in kinds — the supported HTML elements, present in EVERY work without being declared: p (the default), h1h6, blockquote, pre, dt, dd, address, hr, table, figure, figcaption, img, audio, video. Their ids are the lowercase tag names and their labels are lowercase words (p → “paragraph”, h2 → “heading 2”, hr → “rule”, dt → “term”). Each recognizes its bare tag. They live in the engine, never in the file. The list wrappers <ul>/<ol> and <dl> are NOT kinds — they are grouping elements that dissolve on parse and regroup on write (lists via list metadata, definition lists from consecutive dt/dd blocks).
  • Declared kinds — what config.blocks stores, as an array of BlockDef: the work’s custom kinds, whose id and label both begin with a capital letter ("ChapterTitle" / “Chapter Title”), plus any overrides of built-ins — same lowercase id as the built-in tag, and a label that begins with a capital ({ "id": "p", "label": "Paragraph" }), which is how a reader tells an override from the stock kind. An override replaces the built-in def wholly; a custom kind is recognized through its own selector like any declared kind, and outranks a built-in’s bare-tag claim by specificity (declared kinds also win bare-tag ties, coming first in the merged order).

Even media — image, audio, video — is just void-shaped built-in kinds (or classed overrides of them); no field below is media-specific. The same two-species rule applies to marks.

BlockDef

Field Type Required Description
id string Unique kind id. One class-safe token.
label string Display label; default: the id.
shape "text" | "void" | "grid" Content shape; default "text".
grid { cols? } Default column count when authoring a new instance of a grid-shaped kind.
default boolean true on at most one declared block. When none claims it, the built-in p is the default.
selector string ✓* The kind’s CSS selector — its recognition rule AND the source of its write spelling. Required unless default is true.
behavior BlockBehavior Structural flags (below).
indexes IndexDef[] Navigator index groups this kind feeds.
editor BlockEditor Editing behavior — shortcut, placeholder, Enter/Tab flow, allowed sections.
syntax Record<string, BlockSyntax> Foreign-syntax spellings (md / fountain / fdx / txt), keyed by syntax id — used by import/export converters, never by the file itself. See Kind inference.

A kind’s visual styling is not a BlockDef field: it is a rule in the document stylesheet whose selector is naturally the same selector — h3.scene { … } both recognizes and styles the kind.

Recognition — the selector

An element’s kind is resolved by testing it against every block’s selector with CSS semantics:

  • Matching is Element.matches semantics. The engine implements the compound subset — type selector, .class (all class tokens), [attr], [attr="v"], *, and comma-separated alternatives — and the spec permits anything modern CSS can express; engines with a live DOM may delegate to Element.matches directly.
  • Precedence is CSS specificity: among all matching kinds, the highest selector specificity wins (p.parenthetical beats a kind claiming bare p, regardless of order); ties resolve by config.blocks definition order.
  • The default block is the fallback: an element (or bare text) matching no selector is the default Paragraph kind.

Write spelling — derived from the selector

A kind is written back as its selector’s first comma-alternative: that alternative’s subject compound supplies the tag plus class list — "h3.scene" writes <h3 class="scene">. Consequently the first alternative must be a plain tag(.class)* compound (attribute tests are not writable — a validation error); when it states no tag, the shape default applies: p for text, hr for void, div for grid. The selector classes are the kind’s identity and are consumed on parse — additional authored classes stay in attrs.class.

The default Paragraph block

Exactly one block type in the merged vocabulary carries default: true — the built-in p unless a declared block (an override of p, or a custom kind) claims it instead. The default is the fallback for any content that matches no selector — and it declares no selector of its own. It writes as an unclassed <p> — real paragraph semantics in a bare browser — and an unclassed <p> (or bare text) falls through to the default kind again on re-parse. A config declaring more than one default block is invalid; a config declaring none simply inherits p.

BlockBehavior

Structural flags — all optional. Together they express a block type’s role in the document’s outline, numbering, and print continuation.

Field Type Default Description
outline boolean false Show blocks of this kind in the outline / navigator. A kind with outline is a division.
rank number Outline nesting rank (1 = top). Only meaningful with outline. Division structure lives here.
numbered boolean false Automatic running number (e.g. scene numbers).
restartNumbering boolean false On a numbered division, restart the count at 1 whenever a coarser division (lower rank) appears — a play’s scenes reset each act. Off = the count runs continuously (screenplay style).
summary boolean false Summarizes the division it opens (the navigator shows an auto-blurb).
printing boolean true false = the block exists in the file but never prints or renders for reading — writer-only notes (Fountain [[…]], boneyard /* … */).
more string | true Text printed at the foot of a page when this block breaks across pages; true = the default "(MORE)". Localizable.
resumed string | true Text appended to a continued cue — after a page break, and on a consecutive same-index block of this kind; true = the default "(CONT'D)". Localizable.

Breaks and keeps are real CSS, not behavior flags. A kind that starts a new page declares break-before: page in the sheet; a cue never orphaned from its dialogue declares break-after: avoid; an atomic block declares break-inside: avoid. The full CSS Fragmentation vocabulary applies (page, left, right, recto, verso, avoid, avoid-page, column, …), in the sheet or on an instance’s style attribute. Only printing stays a behavior flag — it is semantic (the block is writer-only), not visual.

The page break

The built-in vocabulary carries ONE kind that is not an HTML element: the page break — id PageBreak, a void marker whose block IS the end of the physical page. It spells as hr.page-break, real HTML, but it is hidden structure, not a visible rule: the machine base sheet hides the element (visibility: hidden, no border, no height) while keeping its forced break, so a bare .spub.html shows nothing on screen and breaks the page in print. Paginating renderers give it an intrinsic break-after: page — no sheet rule required — and the machine base sheet repeats the same declaration so a bare file prints identically; either CSS channel may still override or refine it (a format could make it break-after: left, or re-show the marker as a visible ornament). Being Capitalized like a custom kind (it is not an element), a work may override it to attach foreign-syntax spellings or editor flow — the screenplay preset maps it to Fountain’s ===.

There is no speaker / cue flag. Speaker identity is derived from indexes — the character list is just an index group — and same-speaker (CONT'D) falls out of resumed combined with index-match equality on consecutive cue blocks.

Divisions (blocks with outline + rank) are the repeating, ranked headings — Act, Sequence, Scene, Chapter — that form the outline. They are a distinct concept from sections; see that page for the full distinction.

Example — a screenplay scene heading

{
  "id": "scene_heading",
  "label": "Scene Heading",
  "selector": "h3.scene",
  "behavior": { "outline": true, "rank": 3, "numbered": true, "summary": true },
  "indexes": [
    { "label": "Locations", "pattern": "^(?:INT\\.?|EXT\\.?|I/E)\\.?\\s+(.+?)(?:\\s+[-–—]\\s+.*)?$" }
  ],
  "editor": { "shortcut": "Mod-1", "enter": "action", "tab": "action", "shiftTab": "transition", "sections": ["body"] },
  "syntax": {
    "fountain": { "match": [{ "prefix": "INT." }, { "prefix": "EXT." }], "write": { "transform": ["upper"] } }
  }
}

Its styling is a sheet rule with the same selector:

h3.scene { text-transform: uppercase; font-weight: bold; margin-top: 1.6rem; break-after: avoid; }

IndexDef

An index is a navigator sidebar group — a list of entries collected from the text of blocks of a kind. A character list, a location list, a props list: each is an index.

Field Type Required Description
label string Group label in the sidebar (e.g. "Characters").
pattern string (regex) Regex source run against the block’s text; matches collect into the group.

A block type may declare zero or more indexes. Multiple block types can feed the same label (they merge under one group). This is how a screenplay’s character block populates a Characters index — and, combined with behavior.resumed, how same-speaker (CONT'D) is derived.

BlockEditor

The editing behavior of a block type — its keyboard shortcut, its placeholder, its Enter/Tab flow, and where it’s allowed.

Field Type Description
shortcut string Keyboard shortcut, e.g. "Mod-1" (Mod = ⌘ on macOS, Ctrl elsewhere).
placeholder string Ghost text shown in an empty block.
enter string Kind of the next block created on Enter (a block id).
tab string Kind this block cycles to on Tab.
shiftTab string Kind this block cycles to on Shift-Tab.
sections string[] Allowed section ids (sectioned documents only) — standard or custom. Omitted = anywhere. Edge blocks are exempt.

enter / tab / shiftTab are what make a screenplay editor feel like a typewriter: press Enter after a Character cue and you’re in Dialogue; press Tab and Scene Heading cycles to Action. The flow is entirely format data.

Two model-level editing rules follow from the flat stream: when a deletion merges or removes a block boundary, the caret collapses to the resulting position in the nearest surviving block; and deleting all content of the last remaining block retains one empty block rather than producing an empty document. Page-turn and pagination-surface behavior is application UI, outside this specification.