Formatting
Marks, ranges, and how inline formatting is applied.
Bold is not a wrapper node. If it were, every edit that changed the bold range would have to rebuild tree structure, and the parser would have to decide what to do about overlapping bold and italic runs. SpineEditor represents inline formatting as flat marks on text leaves. A bold run is a text leaf whose marks include a strong mark.
Worked example
Toggle strong across the current selection:
engine.dispatch({
kind: "toggleMark",
markType: "strong" as MarkTypeName,
});
If every character already carries strong, the planner emits RemoveMarkOps. Otherwise it emits AddMarkOps to fill the gaps. On a collapsed caret, toggleMark updates storedMarks — the next typed character will carry the mark.
Applying a link with attributes:
engine.dispatch({
kind: "setLink",
href: "https://example.com",
});
setLink is attr-aware: it replaces existing link attrs across the selection, then (on a non-collapsed application) collapses the caret and clears the link continuation so subsequent typing is unlinked.
Intents
toggleMark— add if any character lacks it, remove if every character carries it. On a caret, updates stored marks.applyMark— unconditional add, with explicitattrs. Used for attr-bearing marks where "toggle" is ambiguous.removeMark— unconditional remove of a mark type across the selection.setLink— attr-aware link application.
All four lower to AddMarkStep / RemoveMarkStep in the step layer.
Mark types
Eleven are shipped:
strong, em, underline, strikethrough, subscript, superscript,
link, fontFamily, fontSize, textColor, highlightColor
The first six are attribute-free toggles. subscript and superscript are mutually exclusive on the supported semantic path. link carries href. fontFamily, fontSize, textColor, and highlightColor carry the relevant presentation attr.
Range semantics
Marks apply to half-open Ranges. Ranges that cross block boundaries split per-block at plan time — each affected block gets its own step. After apply, the normalizer merges adjacent text leaves with identical marks so the document does not accumulate spurious leaf boundaries.
Stored marks
A collapsed selection has no range to paint marks onto. Instead, the engine keeps storedMarks on the state — the marks the next typed character will carry. toggleMark with a caret updates stored marks; typing consumes them. A committed selection-only move clears explicit stored marks unless the transaction set stored marks itself.
Custom marks
A plugin registers a new mark type by contributing a MarkSpec to the schema. The spec declares the mark type name, its MarkPolicy, and its attribute shape. Once in the schema, the existing applyMark / toggleMark / removeMark intents work against it — there is no per-mark API to add.