Schema
The schema defines every node and mark the editor supports. stesuraSchema() from @stesura/core/schema builds it: the core node specs, plus shared attributes (numbering, formatting, spacing, …) composed onto them by the global-attribute system.
Document Structure
doc → section+ footnotes endnotes?
section → section_header section_content section_footer
section_content → (block | table | page_break | toc | file)+endnotes is created lazily, when the first endnote is inserted.
doc attributes:
| Attribute | Description |
|---|---|
docBgColor | Canvas color behind the pages (default #FFFFFF). Distinct from blockStyling’s bgColor. |
listConfigs | List definitions, keyed by list ref. See Numbering. |
styleSheet | The stylesheet definitions map. A block references it through styleId. |
endnotesOptions | showAt, format, continuous, startAt. |
language | Default proofing language: the bottom of paragraph.proofingLanguage ?? style.language ?? doc.language. |
dir | Default text direction ("ltr"): the bottom of paragraph.dir ?? section.dir ?? doc.dir. |
pageBordersSurroundHeader, pageBordersSurroundFooter | Whether text-measured page borders enclose the header / footer (default true). Document-wide, as in Word’s settings. |
Nodes
Block Nodes
| Node | Content | Group | Description |
|---|---|---|---|
paragraph | counter_node? inline* | block | Standard text block. |
heading | counter_node? inline* | block | Attribute: level (1–9). Levels 7–9 render as <p role="heading" aria-level>. |
table | table_row+ | — | Not in the block group; hosts admit it by name (see Table placement). Attributes: align, width (auto, fixed px or pct), layout (Word’s fixed/autofit, not rendered), sortable, borders, headerRowCount (leading header rows), repeatHeaderRows (repeat them on every page the table continues onto, Word’s “Repeat Header Rows”), marginStart. |
table_row | table_cell* | — | Attributes: height, heightRule, isConditional, isValid, errorMessage, codeTs. |
table_cell | (block | table)* block | — | A table may sit in a cell, but never last. Attributes: colspan, rowspan, colwidth, verticalAlign, bgColor, borders, isHeader (derived: the cell starts in one of the table’s headerRowCount rows; header cells render and serialize as <th scope="col">). |
code_block | text* | block | Syntax-highlighted code. Admits only the track-changes marks. Attributes: codeLanguage, theme, showLineNumbers. |
horizontal_rule | — | block | Divider. Attribute: border (one border side’s options; null = default hairline). |
image | — | block | Attributes: src, alt, decorative, width, height, maxWidth, aspectRatio, align, wrap, crop, transform. |
math_display | text* | block math | Display equation. The LaTeX is the node’s text; edited in a floating editor (atom). Attribute: mathMarks. |
callout | block+ | block | A kind-tagged aside or a quotation (see Callouts). Attributes: kind (note, tip, warning, danger, quote, or any lowercase identifier), title (plain text or null). Kept on one page when it fits. |
upload_block | — | block | Upload placeholder. Attribute: uploadType. |
Table placement
table is deliberately not in the block group. The containers that host
tables admit them by name: section_content, header_footer_content, wrapper
nodes that opt in ((block | table)+), and table_cell
((block | table)* block). Plain block+ containers (footnote_body,
endnote_body) therefore exclude tables structurally.
A table can sit in a table cell, one level deep: a table in a nested
table’s cell is not supported. A content expression can’t state depth, so the
guards enforce it (see
@stesura/core/guards). A cell never ends in a table, so
the caret always has a paragraph after it, as in Word.
Consequences:
insertTablein a table cell sizes the new table to that cell. It is refused inside a nested table, a note body, a code block, or a cell too narrow for the columns.- Pasting a table into a cell nests it. A paste that would put a table three levels deep flattens that table into its cell’s blocks.
- Documents with a table too deep, or in a note body, are healed on load
(
prepareDocForLoadlocally,createNormalizeTransactionin collab): the table is flattened into its cell’s blocks, with steps that converge across collab clients. - DOCX import keeps a table in a table cell and flattens deeper ones (and tables inside footnotes/endnotes) with an import warning.
- Pagination breaks a nested table between its rows, never through one (see Pagination).
A custom container that should host tables must use (block | table)+. See
Extending the Schema.
Section Nodes
| Node | Content | Description |
|---|---|---|
section | section_header section_content section_footer | Page container. Attributes: orientation, margin, pageSize, headerFromTop, footerFromBottom, lineNumbers, dir, pageNumbering, columns, pageBorders, watermark. |
section_header | header_footer_content+ | Header area. |
section_footer | header_footer_content+ | Footer area. |
section_content | (block | table | page_break | toc | file)+ | Main content area. |
header_footer_content | (block | table)+ | One header/footer variant. Attribute: variant. |
section has no parse rule: pasting a section brings its blocks, not its page setup.
Inline Nodes
| Node | Group | Description |
|---|---|---|
text | inline | Plain text. |
hard_break | inline | Manual line break (Shift+Enter). |
image_anchored | inline | Image anchored to a text position (Word-style wrapping). Declares its own id. Attributes: src, alt, decorative, width, height, aspectRatio, crop, transform, plus the floating family: wrap, anchorPosition, wrapDistance, zIndex. |
floating_box | inline | A text box or frame: blocks (block+, no tables) in an atom anchored in a textblock, edited through a sub-editor. Declares its own id. Attributes: origin (textbox |
math_inline | inline math | Inline equation (atom, LaTeX as text content). Attribute: mathMarks. |
footnote_anchor | inline | Footnote reference. Attributes: id (this occurrence’s), footnoteRef (the note, shared by repeated anchors). Its number is derived (noteAnchorStore, @stesura/references). |
endnote_anchor | inline | Endnote reference. Attributes: id, endnoteRef. Its number is derived, as for footnotes. |
page_number | inline | Page number field. Attribute: pageType (currentPage | totalPages). |
cross_reference | inline | Reference to another node. Attributes: id, targetId, refTo, separator, textCase, bookmarkText (a text reference’s content), pageLabel (a page reference’s cached page), missingLabel (the label kept while the target is missing). The label it shows is derived (crossReferenceStore). |
comment_open | inline | Comment range start. Attributes: anchorId, threadId, originEditorId (clipboard only). |
comment_close | inline | Comment range end. Attributes: anchorId, threadId, originEditorId (clipboard only). |
counter_node | — | List number, admitted by name as the first child of paragraph/heading. No attributes: its label is the numbering store’s. |
boundary_open_node / boundary_close_node | — | Conditional-content boundary markers. No core content expression admits them. |
Special Nodes
Not in the block group; section_content admits them by name, so they cannot appear in table cells, note bodies or headers/footers.
| Node | Description |
|---|---|
page_break | Manual page break. |
toc | Table of contents (atom). Attributes: showPageNumbers, alignNumberRight, showLeaderDots, scope (document | section). |
file | Embedded PDF. Serialized as a link; the node view renders a preview when allowed (see Embedding files). Attributes: src, height, name, mimeType, size, rotation, displayPreview. |
Footnote / Endnote Nodes
| Node | Content | Description |
|---|---|---|
footnotes | footnote_body* | Hidden doc-level container for footnote bodies. |
footnote_body | block+ | One footnote. Attributes: footnoteRef, index. |
endnotes | endnote_body* | Hidden doc-level container for endnote bodies. Body order is not meaningful: numbering follows anchor order. |
endnote_body | block+ | One endnote. Attributes: endnoteRef, index. |
Marks
Text Formatting
| Mark | Attributes | Description |
|---|---|---|
strong | — | Bold. |
em | — | Italic. |
underline | variant, color | Variants: single, double, thick, dotted, dottedHeavy, dash, dashedHeavy, wave, wavyHeavy. |
strikethrough | — | Strikethrough. |
superscript | — | Excludes subscript. |
subscript | — | Excludes superscript. |
code | — | Inline code. |
Styling
| Mark | Attributes | Description |
|---|---|---|
textStyle | color, fontSize, fontFamily, caps ("all" | "small" | "none"), bgColor, off (marks this run negates against its paragraph style) | Character styling. Ordered to wrap the other marks, so they inherit its style. |
highlight | color | Highlight. Default #FFFF00. |
border | style, color, size, space | Inline text border. |
link | url | Hyperlink. Non-inclusive: typing at its edge doesn’t extend it. |
Track Changes
| Mark | Attributes | Excludes | Description |
|---|---|---|---|
insertion | id, date, userId, userName | insertion, modification | Suggested insertion. |
deletion | id, date, userId, userName | deletion | Suggested deletion. May stack on another user’s insertion. |
modification | id, type, attrName, previousValue, newValue, date, userId, userName | insertion | Suggested formatting change (added/removed mark). Stacks, so each change resolves independently. |
Utility
| Mark | Attributes | Description |
|---|---|---|
proofReadIgnore | _pri_length | Text skipped by spell checking. |
Global Attributes
Many node attributes come from the global-attribute system rather than the node spec. stesuraSchema applies them at construction, and its globalAttributes option changes which nodes carry each one. See Global Attributes.
Default bindings:
| Attribute | Global attribute | Default nodes | Description |
|---|---|---|---|
id | uniqueId | Every non-inline node | Unique id, maintained by uniqueIdPlugin. |
numbering | numbering | paragraph, heading | List numbering; null inherits from the style. See Numbering. |
indent, lineHeight, lineHeightAtLeast, tabStops | formatting | FORMATTED_NODES | Indentation, line height, tab stops. |
textAlign | textAlign | FORMATTED_NODES | start (null), center, end, justify. |
dir | direction | FORMATTED_NODES | ltr | rtl | null (inherit). |
suppressLineNumbers | suppressLineNumbers | FORMATTED_NODES | Skips the block’s lines in the section’s line numbering. |
spacing | spacing | SPACED_NODES | Before/after spacing. |
styleId | styleId | paragraph, heading | Paragraph style (Normal, Heading1, …). |
bgColor, borders | blockStyling | paragraph, heading | Block fill and borders. |
pagination | pagination | paragraph, heading, code_block | keepWithNext, keepLines, widowOrphan, pageBreakBefore. |
proofingLanguage | proofingLanguage | paragraph, heading | Spell-check language; null inherits. |
trackChanges, trackChangesModification | trackChanges, trackChangesModification | BLOCK_TRACK_CHANGES_NODES | Block-level suggestions. |
nodeComments | nodeComment | none (opt-in) | Comment threads on a whole node. |
FORMATTED_NODES is heading, paragraph, image, toc and horizontal_rule. SPACED_NODES adds code_block, file, table and callout to those.
Spacing Roles
The spacing plugin replaces CSS margin collapse with computed top margins (the spacing store’s, never stored in the document) (see Global Attributes). It groups blocks into collapse chains: margins collapse between consecutive members of a chain. A container’s spacing role sets how it interacts with chains:
- Spaced (nodes carrying
spacing: paragraphs, headings, tables, images, …): the node is one opaque chain member. Its children never collapse with the outside. - Pass-through: the node’s children join the surrounding chain, so a paragraph after the wrapper collapses against the last paragraph inside it.
- Boundary: the node’s children form an isolated chain, as in a table cell.
A callout is spaced (it collapses with its neighbors as one block) and a boundary inside (its first child keeps its own spacing).
The role is inferred: a wrapper-shaped node (block node with block content, not an atom) is pass-through, unless it is spaced or a core flow boundary (section scaffolding, table/row/cell, note containers and bodies). A custom wrapper node therefore works with no registration.
To override the inference, declare spacingRole on the node spec (typed by StesuraNodeSpec from @stesura/core/schema):
import type { StesuraNodeSpec } from "@stesura/core/schema";
const sidebarSpec: StesuraNodeSpec = {
content: "block+",
group: "block",
spacingRole: "boundary", // isolate children from the surrounding flow
// ...
};spacingRole: "passThrough" on a spaced node is ignored with a console error. See Extending the Schema for a full wrapper-node example.
Constants
import {
NODE_NAMES, // node type names; prefer these over string literals
} from "@stesura/core/schema";
import {
DEFAULT_TEXT_STYLE, // { fontSize: 10, fontFamily: "Arimo", color: null, ... }
DEFAULT_LINE_HEIGHT, // 1.5
DEFAULT_SPACING, // { before: 0, after: 8 }
PAGE_FORMATS, // A4 (default), A3, A5, B5, envelopes, Legal, Letter
HIGHLIGHT_COLORS, // yellow, lime, aqua, fuchsia, red
UNDERLINE_TYPES, // CSS for each underline variant
FONT_FAMILIES, // available font families
} from "@stesura/core/constants";Next Steps
- Global Attributes: how shared attributes are composed onto nodes.
- Extending the Schema: adding custom nodes and marks.
- Commands: Core: manipulating nodes and marks programmatically.