Rich text / foldkit-richtext

Application node blocks

A Node block is an application's own kind — a Callout, an Image, an embed. Its props are JSON at the codec level, because the document codec cannot know an application's schemas; a Kit's node definition validates them at that boundary, and it also declares what content the kind holds:

ts
const ArticleKit = RichText.kit({
  nodes: [
    RichText.block('Paragraph'),
    RichText.node('Callout', { Props: Schema.Struct({ tone: Schema.Literals(['info', 'warning']) }) }),
    RichText.node('Image'),
    RichText.node('List', { children: RichText.blockContent }),
  ],
  marks: [RichText.Bold],
})

RichText.validate(document, ArticleKit)
// → [] | UnsupportedNode (kind not declared) | InvalidProps (schema refused them)
//   | MismatchedDefinition (declaration and document disagree)

children defaults to RichText.textContent, so a node holds runs; blockContent says it holds nested blocks. RichText.block(name) declares one of the built-in blocks and RichText.atom(name) declares a kind holding nothing, so an atom is exactly an application node with no content. validate walks nested blocks and reports MismatchedDefinition when a declaration and the document disagree about the shape or the content — an atom holding runs, a blockContent kind held as a run holder, or a textContent kind held as a container. An empty application node is the one case it accepts either way, because the document cannot say whether it is an atom or a run holder with no runs.

A node block's children are text runs, so positions, operations, selection, clipboard slices, history, and the interpreters all work on it unchanged — a split keeps its kind and props on both halves. data-node="Kind" is the default rendering in HTML and in the view until a Kit renderer replaces it.

A node block may also accept nested blocks in blocks, which is how a List holds ListItems. The HTML serializer renders them inside the node's element and toText gives one line per text block; the read-only view and the editable adapter do the same, so a list keeps its items in every interpreter. HTML import reads data-node back, and maps <ul>/<ol> to a List and <li> to a ListItem when the Kit declares those names: an element holding block children becomes a container, an element holding inline content becomes a run holder, and a kind the Kit does not declare degrades to its content with a diagnostic. A blockContent declaration settles the one ambiguous case — a container whose element holds only inline content — because the declaration says what the content is. Props are not carried by HTML; the slice format keeps them, so an imported node starts with {} props.