Multi-file works

A multi-file work is one work whose content lives in several files in a folder — the way an EPUB is one book across many spine documents, or a repo’s docs/ folder is one manual across many Markdown files. The folder is still one work: one title, one format, one outline, one entry in a library’s index. Splitting is a storage arrangement — for very long works, for hand-managed folders of Markdown, for memory and view-management — and it never leaks past the storage boundary: assembled, a multi-file work is the same { config, content, sections } every surface already renders.

The file set

Decline-and-Fall/
  Decline-and-Fall.spub.html      ← the WORK INDEX: config + the spine
  01-Augustus.md                  ← a part: config-less fragment
  02-Tiberius.md
  03-Caligula.spub.html           ← parts may mix syntaxes
  Decline-and-Fall.de.spub.html   ← a translation: its own sibling, not a part
  notes/…                         ← excluded by parts.scan: inert cargo
  • The index is an ordinary SPub document identified by its config — a parts group present, kind still "work" (multi-file-ness is storage, the way library is for stores). Detection is head-based, never filename-based; the convention is the folder’s <slug>.spub.html — the same file the work would be as a single file, so conversion is “move the content into parts and put the spine in the body”. One parts-declaring index per folder; a second is a scan error (the first by sorted path wins).
  • Parts are config-less fragments in any supported syntax (below).
  • Translations are <slug>.<lang> siblings of the index — a single file or a whole work folder of their own — exactly as for single-file works.

The index is a catalog document

The index’s body is the spine: the ordered list of part references, in the exact catalog shape a collection or a library index uses — list blocks whose runs carry an href mark, derived by the same docCatalog. An ordered list, because a spine is a sequence. Everything else in the index body — a foreword, notes between entries — is manifest annotation: preserved and rendered when you open the index itself, but never part of the assembled work’s content (a title page is just the first part).

{
  "scriptpub": 2,
  "meta": { "title": "The Decline and Fall", "author": "…" },
  "parts": {
    "split": "division",                 // division (default) · manual
    "scan": { "include": ["*.md", "*.spub.html"], "exclude": ["notes/**"] }
  },
  "blocks": [ … ], "marks": [ … ]
}

The parts group

  • split — what defines part boundaries when the work is re-split (a future write-back; assembly never consults it):
    • "division" (default) — parts break at top-rank divisions, the same rule as the EPUB spine. Boundaries are derivable from content, so a re-split self-heals: a new chapter naturally becomes a new file.
    • "manual" — boundaries are wherever the files are. For hand-curated folders (a repo docs set); a re-split preserves the recorded part spans and never moves content between files unasked.
  • scan — which sibling paths are part files, for the spine generator only (include/exclude globs, the library’s mini-glob, relative to the index’s folder; default include: every known-syntax extension). The body’s spine is always the authoritative membership and order for assembly — the scan feeds regeneration, never reading.

Parts are config-less fragments

A part is a fragment of the work’s content in any supported syntax — Markdown, .spub.html, Fountain, plain text. It carries no config of its own: every part parses under the work’s config as its companion, so one format rules the whole work — kinds, styling, views, everything. A folder of plain .md files plus one index just works.

  • In the embedding syntaxes, a config-less file is a fragment by construction: Markdown without frontmatter, HTML without a <script id="scriptpub"> config (and without a <main>, an HTML fragment’s whole body is the content root).
  • A part that does carry its own config (stray frontmatter, a full .spub.html pasted in) degrades, loudly: its content is kept, its config discarded, and the assembler warns — a part’s config never silently forks the work’s format.
  • Pinned block ids must be unique across the whole work, not per file — cross-part links depend on it; duplicates warn.

Assembly

Reading a multi-file work is assemblyassembleWork in @script-pub/format, pure and IO-free like reindex (the caller resolves the spine’s hrefs to text and hands the parts in):

assembleWork(index document, resolved parts) → { doc, part spans, warnings }
  • Parts concatenate in spine order into one flat content — the assembled work is an ordinary document; outline, indexes, block coordinates, and every renderer work unchanged.
  • A spine entry whose part is missing fails loudly (a stale spine is an error, not a guess); an unreferenced part file warns and is ignored.
  • Part section spans re-base onto the assembled stream (adjacent same-section spans merge).
  • Part media references resolve against the part’s folder; when a part lives in a subfolder, assembly rewrites its relative references index-relative, so the assembled document’s references resolve from the work folder like any single-file work’s. (data: URIs and absolute URLs pass through.)
  • The returned part spans — which [start, end) of the stream came from which file — are derived, never authoritative: they exist for manual re-splitting and for tooling, and any edit of the document invalidates them.

The spine generator

The spine is maintained the way a library’s index is — reindexWork(config, part heads, previous index?), the Reindex pattern one level down. The caller scans the folder per parts.scan and hands in the part heads; the generator emits one ordered, linked entry per part. So a folder of Markdown needs zero ceremony: point the generator at it and the spine writes itself.

  • No previous index: entries in path order — the 01-…, 02-… filename convention reads naturally.
  • With a previous index, its entry order is preserved (the hand-arranged spine wins); new parts append, removed parts drop.
  • The catalog section rule matches Reindex: declared and present, only that span regenerates — annotation around the spine survives.

In a library — folder claiming

A library’s scan would see each part file as a work of its own (**/*.md matches them all). So a parts-declaring head claims its folder: every other scanned path under the index’s directory is that work’s cargo — a part, a note — and never lists in the library, except translations (translationOf set), which stay listed as <slug>.<lang> siblings under the base work. One head, one entry: a multi-file work lists in a library exactly like a single-file one.

Translations

A translation of a multi-file work is its own sibling document<slug>.<lang>.spub.html beside the index, either a single file or a work folder of its own — the ordinary translation convention. Part-by-part, spine-aligned translation (each part paired with a translated part) is deliberately reserved for a later revision.