Rich text / foldkit-richtext
Kits
A Kit declares the vocabulary one editor accepts — node kinds and marks — as
plain data, with no renderers or executable code:
const ArticleKit = RichText.kit({
nodes: [RichText.block('Paragraph'), RichText.block('Heading'), RichText.atom('Image')],
marks: [RichText.Bold, RichText.Italic],
})
RichText.validate(document, ArticleKit)
// → [] when the document fits; otherwise a diagnostic the caller decides onfoldkit-richtext also ships a standard vocabulary — the kinds and marks a document
uses to mean what Markdown and HTML also mean:
RichText.kit({
nodes: [...RichText.standardNodes, Callout],
marks: [...RichText.standardMarks, Highlight],
})standardNodes is Paragraph, Heading, Quote, List, ListItem, TaskItem,
CodeBlock, ThematicBreak, Image, Table, TableRow, TableCell; standardMarks
is the shipped three plus Strikethrough and Link. standardRendering gives each kind
its element — a List an ol or ul, an Image an img with its src, a TaskItem its
data-task — and renderingOver(base, extra) builds a registry over another, so an
application adds its own kinds beside the standard ones. A TableRow that is the header
renders data-header, and a Table's rows go in a tbody (the entry's inner), because an
HTML parser inserts one around bare rows, and markup sent from a server has to parse to the tree
that was rendered. An entry's adornments are elements drawn inside the block before its content
that are not content: a task's checkbox, a figure's picture and caption, a callout's icon. They
carry data-adornment, hold no position, and an HTML import drops them; one with toggles: 'open'
flips that boolean prop when clicked, through the editor's ChangedProps, and one with an action
('copy') is reported to the application with its block, for something that is not an edit:
RichText.renderingOver(RichText.standardRendering, {
nodes: {
TaskItem: block => ({
tag: 'li',
attributes: {},
adornments: [
{
tag: 'input',
attributes: { type: 'checkbox', ...(block.props.checked === true ? { checked: '' } : {}) },
toggles: 'checked',
},
],
}),
},
})A standard Image also carries an optional caption, a width in CSS
pixels, and an align (start, center, end); the standard rendering draws the img
alone, and a rendering that draws a figure reads the rest. A link's href and an image's src pass safeUrl again as they are
rendered, and one it refuses is left out, so a javascript: URL that reached the document
by decoding, sync, or SetMark rather than import is drawn inert.
A kind can state rules stricter than its content mode. blocksOf(...kinds) accepts only
those block kinds, marks: 'none' forbids marks on the kind's own runs, isolating: true
makes the kind a boundary a lift never crosses, as a table cell is, splitProps names
props an item split off one of this kind starts with (typed from the kind's Props), lineBreaks:
true makes Enter a newline in the kind's text rather than a split (Enter on an empty last line
leaves it for a paragraph after; the standard CodeBlock declares it), and within names the kinds it may stand
in, so a ListItem stands only in a List and never at the top level:
RichText.node('List', { children: RichText.blocksOf('ListItem', 'TaskItem') })
RichText.node('CodeBlock', { Props: Language, children: RichText.textContent, marks: 'none' })
RichText.atom('Image', { Props: Schema.Struct({ src: Schema.String }) })
RichText.node('TaskItem', {
Props: Schema.Struct({ checked: Schema.Boolean }),
children: RichText.blockContent,
splitProps: { checked: false },
within: ['List'],
})validate reports those violations as UnexpectedChild, ForbiddenMark, and
MisplacedNode (a kind outside what its within names), and checks
an atom's props the same way it checks a node's. run refuses the edits that would create
an unexpected child, a forbidden mark, or a misplaced kind (a move, a wrap, or a paste), when it
is given the vocabulary — nodes: RichText.nodeRegistry(kit.nodes), the node
counterpart of the marks option:
RichText.run(state, { type: 'ToggleMark', mark: 'Bold' }, ids, {
nodes: RichText.nodeRegistry(kit.nodes),
})
// → { ok: false, error: 'ForbiddenMark' } when the caret is inside a CodeBlockThe codec keeps a document a Kit would reject, and apply takes no vocabulary: a durable
transaction must not depend on a declaration that may have moved. Removing a mark is
always allowed, so a preserved document that already carries one has a way back.
validate reads the document and never repairs it: callers decide whether a
diagnostic blocks publishing or shows a placeholder. The Kit does not drive parsing
or apply.