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
partsgroup present,kindstill"work"(multi-file-ness is storage, the waylibraryis 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.htmlpasted 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 assembly — assembleWork 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 formanualre-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
catalogsection 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.