Rich text / foldkit-richtext
Decorations
A decoration is derived, ephemeral presentation over a document range — a search match, a lint warning, a remote cursor, a syntax token — and never document content:
const matches: RichText.DecorationSet = [{ from: at(0), to: at(7), kind: 'search' }]
RichText.decorationsIn(document, matches)
// → Map<runId, Array<{ from, to, decoration }>>, each cut at its run's edgedecoration.data is the renderer's own payload and the core never reads it. A decoration
is not in the codec, not a Transaction, not undo, and not replicated: a caller computes a
set for one render and discards it. decorationsIn is the projection every interpreter
shares — a decoration crossing runs is cut at each run's edge, an endpoint that does not
resolve skips the decoration, a backwards range is honoured, and one run's spans come back
in text order.
A producer comes with it. findMatches(document, query, { caseSensitive }) lists every
occurrence (its block, its offset there, and its ends as positions), case not counting unless
asked: a match across two runs kept whole, and no match across two blocks, because what a
document has between blocks is structure rather than text. searchDecorations(document, query,
{ caseSensitive, current }) draws them as decorations of kind search (SEARCH_DECORATION),
the one at index current as search-current, changing nothing. Replace ({ query,
replacement, caseSensitive, at }) replaces the match at at, or every match without it, as one
transaction, reading each match again where the replacements after it left the text. positionInBlock(block,
offset) is the read it shares with textRangeBefore — the position a block's own text offset
addresses — and the one any producer of ranges needs.
The outline is a read of the same kind. outlineOf(document) lists the headings that have text,
in document order and inside containers too, as { node, level, text }; sectionAt(document,
selection) is the heading of the section the selection starts in, the last such heading at or
before its block, and undefined before the first. A table of contents draws the one and marks the
other.
Footnotes are opt-in vocabulary: add FootnoteRef to the kit's marks and FootnoteDefinition to
its nodes. A reference is the FootnoteRef mark ({ label }) on the run that shows the label, so
it moves and undoes with the text; a definition is a block of blocks with the same label, so its
note is ordinary editable text. The label is all that ties them. footnotesOf(document) lists
every footnote by first reference, as { label, definition, references, text }, then the
definitions nothing refers to; footnoteAt(document, selection) is the label of the reference the
caret is in. InsertFootnote ({ text? }) puts a reference showing nextFootnoteLabel at the
caret, its definition after the last block, and the caret in the note; RenameFootnote ({ from,
to }) relabels the definition and every reference, refusing a label another footnote has.
Media beside Image is opt-in the same way: mediaNodes declares Video and Audio ({ src,
title? }, a video also width?) and Attachment ({ src, name, size?, type? }), and
mediaRendering, laid under a rendering with renderingOver, draws them as video and audio
with the browser's controls and an attachment as a download link showing its name and size.
mediaBlock(file) is the block for a file a reader added, by its type. Where the file lives is
the application's. A source must pass the URL policy, or be a data: URL of the kind's own
family (video/…, audio/…); an attachment's may be any data: type that cannot run as a page
(not HTML, SVG or XML), since it is only downloaded.
Code highlighting is the second producer. A CodeBlock holds its language and plain text,
and a CodeTokenizer — a pure (text) => ReadonlyArray<{ from, to, kind }> over that text —
says what is in it; codeDecorations(document, tokenizers) runs the tokenizer registered for
each block's language and returns the tokens as decorations, so the document never holds a
token. The registry is a Map keyed by language, and a block whose language nothing is
registered for yields nothing. A token outside its block's text, covering nothing, or at a
fractional offset throws, naming the language: a tokenizer's bug should not become a
highlight over the wrong text. The grammars themselves are not here — they are
format-specific, and belong to foldkit-richtext-code.
const numbers: RichText.CodeTokenizer = text =>
Array.from(text.matchAll(/\d+/g), match => ({
from: match.index,
to: match.index + match[0].length,
kind: 'syntax-number',
}))
RichText.codeDecorations(document, new Map([['json', numbers]]))