Rich Text#

One document model for everything that is more than a string: parse markdown or HTML into it, stream AI output through it without flicker, render it on the web or on Lynx, and edit it in a block editor — all against the same tree.

v0.4.0 ESM-only MIT

Terminal
pnpm add @sigx/richtext @sigx/richtext-markdown

The packages#

The family ships from one repository (signalxjs/richtext) on one version line. The core knows no syntax; a format is a package on top.

Package / entryWhat it gives you
@sigx/richtextThe foundation: the mdast-shaped AST, the schema (NodeSpec — the one table that says what every node type is), the DocumentFormat contract with a generic incremental engine, plainTextFormat, toJSON / fromJSON, createTextStream, the renderer-neutral render engine and the RichTextPlugin contract
@sigx/richtext/domRichTextView for the web, the default DOM components (styled through data-scope / data-part), the CodeBlock chrome and the CodeHighlighter contract
@sigx/richtext/editorThe platform-neutral block-tree editor core: state, steps, history, commands, keymap, input rules, trigger sessions, toolbar items and the surface contracts a platform implements
@sigx/richtext/editor/domRichTextEditor for the web: contenteditable block surfaces, toolbar, block handles and menu, slash commands, mentions
@sigx/richtext/testingstrip(), feed(), fake surfaces and the surface conformance suite
@sigx/richtext-markdownMarkdown as a format: the CommonMark + GFM parser, toMarkdown, markdownFormat, the markdown slice plugins fill; ./editor has markdownPreset
@sigx/richtext-htmlHTML as a format: a platform-free parser, toHtml, htmlFormat, the HTML slice plugins fill; ./editor has htmlPreset
@sigx/richtext-shikiShiki highlighting as a plugin: shikiPlugin() and createShikiHighlighter()

Quick start#

Stream a model's answer into a view. createTextStream coalesces tokens into one signal write per frame, and RichTextView parses with the format you give it:

TSX
import { component } from 'sigx';
import { createTextStream } from '@sigx/richtext';
import { RichTextView } from '@sigx/richtext/dom';
import { markdownFormat } from '@sigx/richtext-markdown';

const stream = createTextStream({ flushIntervalMs: 16 });

async function ask(prompt: string) {
    stream.reset();
    await stream.pipe(completion(prompt));   // any AsyncIterable<string>
}

export const Answer = component(() => () => (
    <RichTextView value={stream.value.value} format={markdownFormat} />
));

Every finalized block keeps its identity as tokens arrive, so only the block still being written re-renders.

Why it exists#

  • One tree, every surface. The same document renders on the web, on Lynx and in a terminal renderer; a plugin adds its syntax, node types, components, commands and toolbar items once.
  • Streaming-stable. Finalized blocks are the same objects between parses and their keys never change, so the UI never remounts completed content.
  • mdast-shaped. The document is a standard mdast Root, so it round-trips through remark tooling and saves as plain JSON.
  • Formats are codecs. Markdown, HTML and plain text all parse into the one tree and serialize out of it — converting between them is parse, then serialize.
  • A real editor. A block-tree editor core with platform surfaces, not a textarea with a preview.

On Lynx#

@sigx/lynx-markdown renders and edits the same trees with native views: MarkdownView is a Lynx component map over this render engine, and MarkdownEditor is the Lynx host of the editor core. Plugins written against RichTextPlugin work on both.

Where next#

  • Installation — packages, entries and peers.
  • Documents & formats — the tree, the schema and the format contract.
  • Streaming — createTextStream and the incremental engine.
  • Rendering — RichTextView, component maps and styling.
  • Editor — the block-tree editor and RichTextEditor.