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),h1–h6,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 consecutivedt/ddblocks). - Declared kinds — what
config.blocksstores, as an array ofBlockDef: 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.matchessemantics. 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 toElement.matchesdirectly. - Precedence is CSS specificity: among all matching kinds, the highest
selector specificity wins (
p.parentheticalbeats a kind claiming barep, regardless of order); ties resolve byconfig.blocksdefinition 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: pagein the sheet; a cue never orphaned from its dialogue declaresbreak-after: avoid; an atomic block declaresbreak-inside: avoid. The full CSS Fragmentation vocabulary applies (page,left,right,recto,verso,avoid,avoid-page,column, …), in the sheet or on an instance’sstyleattribute. Onlyprintingstays 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 ofresumedcombined 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.