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. |
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, sortable, borders, hasHeaderRow, processed (internal: column widths fitted). |
table_row | table_cell* | — | Attributes: height, heightRule, isConditional, isValid, errorMessage, codeTs. |
table_cell | block+ | — | Attributes: colspan, rowspan, colwidth, verticalAlign, bgColor, borders, isHeader (derived from the table’s hasHeaderRow; header cells 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. |
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 ((block | table)+): section_content,
header_footer_content, and wrapper nodes that opt in. Plain block+
containers (table_cell, footnote_body, endnote_body) therefore exclude
tables structurally: nested tables and tables in footnotes/endnotes are not
supported.
Consequences:
insertTableis refused inside a table, a note body or a code block.- Pasting a table into a cell never nests: ProseMirror’s fitting relocates or flattens the content instead.
- Legacy documents containing nested tables are healed on load
(
createNormalizeTransaction): the inner table is flattened into its cell’s blocks, with steps that converge across collab clients. - DOCX import flattens nested tables (and tables inside footnotes/endnotes) with an import warning.
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. |
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, wrap, side, offsetX, offsetY, crop, transform. |
math_inline | inline math | Inline equation (atom, LaTeX as text content). Attribute: mathMarks. |
footnote_anchor | inline | Footnote reference. Attributes: footnoteRef, index, isFirstOccurrence. |
endnote_anchor | inline | Endnote reference. Attributes: endnoteRef, index, isFirstOccurrence. |
page_number | inline | Page number field. Attribute: pageType (currentPage | totalPages). |
cross_reference | inline | Reference to another node. Attributes: targetId, text, refTo, separator, missingReference. |
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. Attributes: _text, _path, _fullText (derived by the numbering plugin, never round-tripped) and prevText. |
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, lineHeightRule, 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 and table to those.
Spacing Roles
The spacing plugin replaces CSS margin collapse with computed _marginTop attributes (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.
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.