Syntaxes & file types

A ScriptPub script can be stored in any supported syntax. An adapter is the bridge for one syntax — { parse, serialize } against the canonical model: parse turns a document in that syntax into the model, serialize writes it back. Adapters are additive — registering a new syntax id needs no format-version bump. The adapters are the text tier of the format registry, which also describes the binary formats (EPUB, DOCX, PDF) the tooling lists and reads without a text adapter.

Adapter = {
  id: string,
  parse(text: string, config?: FormatConfig): ParsedDocument,   // structural errors throw
  serialize(doc: ParsedDocument): string,
}

The six adapters

Syntax File Config Notes
html .spub.html Embedded <script> The common self-contained default.
md .md Frontmatter or companion CommonMark subset.
fountain .fountain Title-page keys or companion Screenwriting plain text.
fdx .fdx XML container or companion Final Draft interchange.
txt .txt Companion (default) or frontmatter Pure recognition — makes a Gutenberg book editable.
json / jsonl / json5 .spub.json is the config The canonical model, serialized.

File names. SPub files use the *.spub.* double extension — Title-by-Author.spub.html, notes.spub.json. Content-derived syntaxes keep their own plain extensions; the point of those adapters is not renaming anyone’s files.

Two families:

  • Embedded-config (json, html) carry their config in the file. An embedded config always wins over a companion; an html file without a config script parses as a config-less fragment under the companion — a multi-file work’s part.
  • Content-derived (md, txt, fountain, fdx) infer kind from the text and take their config from a companion (Markdown also reads JSON frontmatter).

html — the common default

One self-contained HTML file, readable and styleable by a plain browser with no special editor.

  • Config embedded as pretty JSON in <script type="application/json" id="scriptpub"> in the <head>.
  • The stylesheet is the <style id="scriptpub-style"> element — the document’s visual truth as real CSS (spec §The stylesheet): the machine @layer statement + sp-base prelude are regenerated on save and skipped on parse; everything else round-trips as the document’s own sheet.

The poetry preset is a complete, minimal example of a .spub.html file.

Head-spelled metadata

Every meta key with a standard HTML head expression is spelled as that HTML and omitted from the embedded config JSON; on parse both are read and the head wins. The canonical model is untouched — config.meta remains the one metadata home; this is purely the html syntax’s spelling of it. Keys with no standard expression (year, credit, custom keys, …) stay in the JSON.

meta key Head spelling
title <title>
language <html lang>
author <meta name="author"> (one per author)
description / keywords / copyright / cover <meta name="…">
coverAlt <meta name="cover-alt"> (kebab in HTML, camel in the model)
license <link rel="license"> when a URL, else <meta name="license">
uri <link rel="canonical">

Legacy files carrying these keys only in the JSON parse identically forever; they migrate to the head spelling on next save. parseHtmlHead reads a document’s head alone — config + merged meta, no body work — the cheap per-file scan a library Reindex runs.

A file named index.html whose config says kind: "library" is a library’s index (Libraries) — the plain index.html name (not the double extension) is what static hosting serves by default, and the embedded config is what marks it as ScriptPub.

Supported HTML tags

A curated subset — not the whole HTML spec — chosen so a document still degrades meaningfully with its stylesheet stripped. A tag may be legal for more than one use (blockquote as a text block or a grid; div anywhere).

Use Tags
Text blocks p h1 h2 h3 h4 h5 h6 blockquote pre address figcaption summary li dt dd div — plus no tag (bare text: the default Text block)
Void blocks hr img audio video (media blockssrc and the media attrs; audio / video close-tagged with empty content)
Grid blocks table (a semantic table) · div figure details (first child = its summary) blockquote aside dl (cells: dt / dd) hgroup (wrapper grids)
Section wrappers header main section footer div
Marks a em strong b i u s mark small sub sup cite q abbr dfn code kbd samp var time bdi bdo del ins ruby wbr span
Inline objects img in inline position — an inline image object run, amid a block’s run markup, wrappable by mark tags
Adapter-managed (never a kind’s tag) ul ol (synthesized around list blocks) · caption thead tbody tr th td + div class="cell" (inside a grid) · rt rp (inside ruby) · br (newline encoding) · source track picture (resolved on parse — see the media bullet above) · main (the content root)

md

  • Config in frontmatter, or a companion.
  • Constructs: headings, paragraphs, blockquotes, fenced code, --- rules, GFM pipe tables (↔ a grid block: display: table, headerRows: 1, a :---: column → cell align), images, links, ordered / unordered lists (→ list metadata), footnotes. Kind inference may combine a Markdown construct with text predicates (a scene heading = paragraph and an INT. prefix).
  • Images: ![alt](src "title") amid text ↔ an inline image object run; a paragraph that is nothing but one image ↔ a void image block (the "image" construct). Reference-style links and images (![alt][ref] + [ref]: url "title") parse — definitions are resolved and consumed — but serialization is always inline-style. The other media attributes have no Markdown spelling and drop (tiered).
  • Media-element passthrough: a block-level line opening with <img, <audio, <video, or <figure parses that HTML block through the html adapter’s element machinery into the corresponding block(s), and such blocks serialize back as the same one-element HTML — CommonMark-legal, GFM-renderable. This is the only raw-HTML form the adapter reads; everything else stays unsupported.
  • Fenced code: ``` fences ↔ a pre-tagged code kind; the info string rides as data.language (the escape hatch) and re-emits on the fence.
  • Autolinks (<https://…>) parse to links; GFM task-list markers (- [ ] ) parse with the checkbox state carried as data.task ("todo" / "done") and re-emit.
  • Block refs use the Obsidian-style trailing ^id.

fountain

  • Config in frontmatter / title-page keys, or a companion.
  • Constructs: scene heading, action, character, parenthetical, dialogue, transition, centered, lyrics, page break, section / synopsis, dual dialogue (^), notes [[…]] and boneyard /* … */ (→ printing: false kinds).
  • The context predicates prev / next are essential here — a dialogue line is defined by what precedes it.

fdx

  • Config in an XML container element or comments, or a companion.
  • Constructs: FDX paragraph Type names, dual dialogue, the FDX title page.
  • Imposed preset: exactly two sections — a title page and a body — non-editable.
  • Imposed requirement: exactly one block kind mapped to the FDX Character construct, and that kind must carry an index — the character list and cue mapping derive from it. The requirement is id-agnostic (the kind may be named anything); the format itself stays free of speaker semantics.

txt

  • Config in a companion (the default posture — don’t touch the file) or, if the user opts in, frontmatter.
  • Pure recognition: kinds inferred entirely from text predicates — regex, caps, prefixes, indentation, blank lines, prev / next. The hairiest adapter, and the one that makes a Project Gutenberg book editable in place.

json / jsonl / json5

  • The canonical model, serialized. Kind is explicit per block — always lossless, no inference.
  • jsonl is a line-delimited variant (an interchange / streaming form more than an authoring one); json5 is a relaxed authoring variant of the same shape.

Config placement

Per script, the config lives in one or both of:

1. Embedded — via the syntax’s native metadata mechanism:

Syntax Mechanism
html <script type="application/json" id="scriptpub"> in <head>.
md Frontmatter.
txt Frontmatter — only if the user opts to touch the file.
fountain Frontmatter / title-page keys.
fdx XML container element or comments.
json It is the config (plus content).

2. Companion file<file>.spub.json beside the content file (screenplay.fdxscreenplay.fdx.spub.json), holding the config alone. The natural choice when the content file must stay untouched — someone else’s FDX, a Gutenberg .txt.

If both exist, the companion extends the embedded config: deep merge, arrays merged by id, companion wins on conflict. For the order-significant blocks array, merge-by-id preserves the original order; overlay-only blocks append at the end (lowest inference precedence).

Full-script JSON vs companion config. <name>.spub.json (a full script, has a content key) is distinguished from <name>.<ext>.spub.json (a config-only companion) by both naming convention and content — the parser checks for the content key regardless of the name.