Rich text / foldkit-richtext-dom

Sixty seconds: an editor on a page

ts
import { Bundle } from 'foldkit-bundle'
import * as RichText from 'foldkit-richtext'
import * as Runtime from 'foldkit/runtime'
import { editorAt, Model, type ParentMessage } from 'foldkit-richtext-dom/editor-bundle'

// One editor, placed at the host id its view renders, with what it draws and allows.
const body = editorAt('body', {
  rendering: RichText.standardRendering,
  vocabulary: {
    marks: RichText.markRegistry(RichText.standardMarks),
    nodes: RichText.nodeRegistry(RichText.standardNodes),
  },
  placeholder: 'Write something…',
})
const app = Bundle.assemble<Model, ParentMessage>()([body])

const document = RichText.decodeDocument({
  version: 1,
  children: [
    { type: 'Paragraph', id: 'p', children: [{ type: 'Text', id: 't', text: 'Hello', marks: [] }] },
  ],
})

Runtime.embed(
  Runtime.makeElement({
    Model,
    container: window.document.getElementById('app')!,
    init: () => app.initial({ document }),
    update: app.update(),
    view: (model, h) => h.main([], [body.view(model, h)]),
  }),
)

What each part does, and does not do:

  • editorAt(hostId, placement) records, under hostId, how this editor renders, which marks and kinds its edits may use, and what a blank document shows. It performs no I/O and renders nothing yet. The placement is recorded rather than passed as an arg because it holds functions and schemas, which a Bundle's schema-decoded args cannot carry.
  • Model is { document, editor }: the document, and the editor's interaction state beside it (selection, stored marks, undo history, the slash menu's highlight). The document is the parent's; the editor reads it through its Link and never keeps a copy.
  • body.view renders one element, <foldkit-richtext id="body">, and mounts the interpreter into it once. Everything inside belongs to this package.
  • An edit arrives as a Message, update runs the command against the vocabulary, the new document lands in model.document in the same transition, and a patch Command then updates the DOM. An edit the vocabulary forbids is refused and changes nothing.