Document model
Every ScriptPub work parses into one canonical model. This is the data structure the file reads into and serializes from, and everything else — an editor, a reader, diff/merge, printing — operates on it.
ParsedDocument = {
config: FormatConfig, // the format — see /format/config
content: ContentBlock[], // one flat, ordered stream of blocks
sections?: SectionSpan[], // resolved section membership (see below)
css?: string, // the document's own stylesheet text — see /format/styling
}
contentis one flat stream of blocks, in document order. A block never contains a section — sections are body-level spans over the stream, not containers inside blocks.sectionscarries the resolved section membership as spans. Absent or empty means an unsectioned document (the whole stream).cssis the document’s own sheet — the text of<style id="scriptpub-style">minus the machine-regeneratedspub-baselayer. See The stylesheet.
An engine may populate additional in-memory projections on a parsed
document — ordered publication-file spans (files) and resolved navigation
(toc) — when an app-level assembler or a package codec produces the
document. They are never serialized into a .spub.html: an authored table of
contents is real content (the toc section, a <nav> of real links), and a
single file has no file spans.
ContentBlock
A block is one paragraph-level unit of the stream. Which of runs / rows it
carries follows the block kind’s declared shape; a void
block carries neither.
| Field | Type | Description |
|---|---|---|
kind* |
string |
The block kind — a config.blocks[].id, or the default Paragraph kind. |
runs |
Run[] |
Inline content — text shape. |
rows |
Cell[][] |
Rows of cells of blocks — grid shape (tables, side-by-side, wrappers). |
attrs |
BlockAttrs |
Fixed-vocabulary attributes, including authored classes, EPUB semantics, accessibility, language, and custom data. |
The three shapes
A block type declares one shape. There are exactly three:
| Shape | Content field | Covers |
|---|---|---|
text |
runs |
Paragraphs, headings, cues, dialogue — the default. |
void |
(none — attrs only) | Rules, media — images, audio, video (src and the other media attrs). |
grid |
rows |
Tables, side-by-side / dual dialogue, card grids, <figure> / <details> / <blockquote>. |
A grid is the one construct that covers tables, side-by-side dialogue,
card grids, and single-cell wrappers alike. Its presentation — table vs
css-grid vs masonry vs flow — is styling (display,
grid-template-columns, …), not a distinct shape: a table is a grid;
side-by-side dialogue is a one-row grid; a <figure> is a single-cell grid.
A grid nests exactly one level deep: a grid’s cells hold leaf blocks —
text / void (plus lists via attrs) — never another grid. Cells carry the
shared element attributes in addition to spans, header
state, and alignment. This bounded nesting is deliberate — it keeps the model
flat enough to diff, paginate, and reason about, while still expressing
side-by-side dialogue and grids.
In a reflowable paged view, a semantic HTML table grid fragments between rows
when it is taller than the available page area. Rows connected by rowspan
form one indivisible row group, and leading grid.headerRows repeat on each
continuation page. An authored break-inside: avoid (real CSS, in the sheet
or on the element) still makes the complete table atomic. Other grid
presentations remain atomic unless their format defines a
presentation-specific fragmentation rule.
Lists are flat blocks, not a grid
A list item is a plain text block carrying list metadata — not a nested
structure:
{ "kind": "paragraph", "runs": [ … ], "attrs": { "list": { "type": "ordered", "depth": 2 } } }
type is "ordered" or "unordered"; depth (default 1) expresses
nesting as data. Ordered lists may additionally carry start on their first
block and an explicit item value, preserving HTML/EPUB numbering without
turning the list into a container. The file spelling
synthesizes <ul>/<ol>/<li> on save and dissolves them on parse — the
model never stores a <ul> as a block. Ordered lists number automatically,
dovetailing with behavior.numbered.
Definition lists follow the same flatness: <dt> and <dd> are ordinary
built-in text kinds (“term” / “definition”), and the <dl> wrapper is
structural, never a kind — it dissolves on parse and one <dl> regroups each
consecutive run of term/definition blocks on write (HTML’s optional
name-value <div> wrappers inside a <dl> are transparent and not
preserved).
Run
A run is a stretch of text — or one atomic inline object — under one mark
stack. Exactly one of text / object is present.
| Field | Type | Description |
|---|---|---|
text |
string |
The text (a text run). May contain literal newlines; the file encodes them per syntax.html.newline (<br /> by default). |
object |
InlineObject |
An atomic inline object (an object run) — an inline image or mathematical expression. One caret position; never split; carries its own shared element attributes. |
marks* |
MarkUse[] |
The mark stack, outermost first. |
A MarkUse is either a bare mark id ("em") or, when the mark carries
attributes, an object { id, attrs } — a link’s href, ruby’s rt, a
revision rev, or a publisher class / EPUB semantic / ARIA attribute. Marks
nest freely, like HTML, and apply to object runs exactly as to text (a
linked inline image or emphasized formula remains one object run under those
marks). See Marks & links, Media, and
Mathematics.
{
"kind": "line",
"runs": [
{ "text": "And every light that says ", "marks": [] },
{ "text": "come home", "marks": ["em"] },
{ "text": " says ", "marks": [] },
{ "text": "stay away", "marks": ["strong"] },
{ "text": " the same.", "marks": [] }
]
}
Cell
A grid cell is a list of leaf blocks, optionally spanning, marked as a header, or aligned:
Cell = {
blocks: ContentBlock[],
attrs?: {
colspan?: number,
rowspan?: number,
header?: boolean,
align?: "start" | "center" | "end",
},
}
A grid block’s rows is a Cell[][] — an array of rows, each an array of
cells. A cell’s blocks are leaf blocks (text / void, plus lists via
attrs) — a grid never nests inside a grid.
SectionSpan
Sections are body-level elements in the file, but the
resolved membership is stored as half-open spans over content:
SectionSpan = {
section: string, // a section id — standard or custom
start: number, // first block index (inclusive)
end: number, // one-past-last block index (exclusive)
matter?: "front" | "body" | "back", // CUSTOM sections only (data-spub-matter)
}
sectionis a section id — one of the standard vocabulary or a custom id.matteroverrides the publication partition for a custom section (standard sections derive matter from the vocabulary table); it serializes as thedata-spub-matterattribute.- Any block index covered by no span is an edge block — a direct
<body>child outside every section element.
Spans are derived at parse and serialized as the body’s element structure, never stored as data. The grouped, section-by-section view a reader uses is likewise derived from these spans, never stored as nesting.
What is deliberately absent
- No pagination directive. Page breaks and keeps are real CSS
fragmentation properties (
break-before/break-after/break-inside) in the sheet or on an element’sstyleattribute. - No serialized
files/toc. They exist only as in-memory projections of app-level assembly or package codecs (above). - No section nesting, no block-level section field. Membership is the span list; blocks stay exactly as specified.