Skip to content

Slate

Slate.js is a customizable rich text editor framework for React. Unlike ProseMirror and TipTap, Slate's document model uses an opaque array of element nodes at the top level (no wrapping doc node), and text formatting is encoded as boolean properties directly on text leaf nodes rather than in a separate marks array.

Package: relational-text/slateNamespace: rocks.slate.facet

Functions

ts
import { from, to } from 'relational-text/registry'

from('slate', input: SlateDocument | string): Document

Parse a Slate.js JSON document into a RelationalText Document.

ts
const doc = from('slate', [
  {
    type: 'paragraph',
    children: [
      { text: 'Hello, ' },
      { text: 'world', bold: true },
    ],
  },
])

Accepts either a SlateDocument array or a JSON string. Supports:

  • Inline marks as boolean properties on text leaf nodes: bold, italic, underline, strikethrough / strike, code, superscript, subscript
  • Inline element nodes: link (with url or href field)
  • Block types: paragraph, heading variants (heading-oneheading-six, h1h6), code / code-block, block-quote / blockquote, bulleted-list / ul / unordered-list, numbered-list / ol / ordered-list, list-item / li, horizontal rule variants (thematic-break, divider, horizontal-rule, hr)
  • List items may contain either inline text leaves or nested block elements
  • Unknown block types fall back to paragraph

The result is normalized through the WASM core.

to('slate', doc: Document): SlateDocument

Render a RelationalText Document to a Slate.js JSON array.

ts
const slateDoc = to('slate', doc)
// [{ type: 'paragraph', children: [...] }, ...]

Automatically applies any registered lenses targeting rocks.slate.facet via lensGraph.autoTransform(). Documents imported from other formats (Markdown, HTML, ProseMirror, TipTap, Quill, etc.) convert automatically through the lens graph.

Output uses the Slate conventional type names: bulleted-list, numbered-list, list-item, block-quote, thematic-break, heading-one through heading-six.

ensureSlateLexicon(): void

Explicitly register the Slate lexicon and the slate-to-relationaltext lens (with autoApply: true). Called automatically by from('slate', ...) / to('slate', ...) on first use. Safe to call multiple times — subsequent calls are no-ops.

Feature Mapping

Inline Marks (boolean leaf properties)

Slate leaf propertyFeature nameRT hub nameExpand
bold: trueboldboldboth sides
italic: trueitalicitalicboth sides
underline: trueunderlineunderlineboth sides
strikethrough: true / strike: truestrikethroughstrikethroughboth sides
code: truecodecodeneither
superscript: truesuperscriptsuperscriptneither
subscript: truesubscriptsubscriptneither

Both strikethrough and strike are accepted on import; if both are present on the same leaf only one feature is emitted. Export always uses strikethrough.

Entities

Slate element / propertyFeature nameRT hub nameAttrs
link elementlinklinkurl (falls back to href)
hard-break (via RT hub)hard-breakline-break

Block Elements

Slate element typeFeature nameRT hub nameAttrs
paragraphparagraphparagraph
heading-one / h1headingheading{ level: 1 }
heading-two / h2headingheading{ level: 2 }
heading-three / h3headingheading{ level: 3 }
heading-four / h4headingheading{ level: 4 }
heading-five / h5headingheading{ level: 5 }
heading-six / h6headingheading{ level: 6 }
code / code-blockcode-blockcode-block
thematic-break / divider / horizontal-rule / hrhorizontal-rulehorizontal-rule
block-quote / blockquoteblockquote-marker (+ blockquote container)same
bulleted-list / ul / unordered-listbullet-list-marker (+ ul container)same
numbered-list / ol / ordered-listordered-list-marker (+ ol container)same
list-item / lilist-item-marker + list-item-textsame

Structural marker blocks (blockquote-marker, bullet-list-marker, ordered-list-marker, list-item-marker) are invisible separators used to encode nesting. They produce no output during export.

Examples

Import

ts
import { from } from 'relational-text/registry'

const slateDoc = [
  {
    type: 'heading-one',
    children: [{ text: 'Hello' }],
  },
  {
    type: 'paragraph',
    children: [
      { text: 'Visit ' },
      { type: 'link', url: 'https://example.com', children: [{ text: 'our site' }] },
      { text: '.' },
    ],
  },
]

const doc = from('slate', slateDoc)
console.log(doc.text)
// "\uFFFCHello\nVisit our site."

Export

ts
import { from, to } from 'relational-text/registry'

const doc = from('markdown', '# Title\n\n**bold** and _italic_ text')
const slateDoc = to('slate', doc)
// [
//   { type: 'heading-one', children: [{ text: 'Title' }] },
//   {
//     type: 'paragraph',
//     children: [
//       { text: 'bold', bold: true },
//       { text: ' and ' },
//       { text: 'italic', italic: true },
//       { text: ' text' },
//     ],
//   },
// ]

Cross-format

ts
import { from, to } from 'relational-text/registry'

// Slate → ProseMirror via the RT hub
const slateDoc = [
  {
    type: 'bulleted-list',
    children: [
      { type: 'list-item', children: [{ text: 'Item one' }] },
      { type: 'list-item', children: [{ text: 'Item two' }] },
    ],
  },
]

const doc = from('slate', slateDoc)
const pmDoc = to('prosemirror', doc)
// { type: 'doc', content: [{ type: 'bullet_list', content: [...] }] }

Notes

  • Slate's document model is a plain array — there is no wrapping doc node. The from('slate', ...) input type is SlateElement[] and to('slate', ...) returns SlateElement[].
  • Text formatting uses boolean properties on leaf nodes (e.g. { text: 'hello', bold: true, italic: true }). There is no marks array. On export, RelationalText marks are translated back to boolean properties on the text leaf.
  • Heading types use verbose names on export: heading-one through heading-six. Import also accepts the shorthand h1h6 forms.
  • List items may contain either inline text leaves directly or nested block elements (paragraphs, nested lists). The importer detects which case applies based on whether the first child is a text leaf or a typed element node.
  • Code blocks do not preserve the language attribute — there is no standard slot for it in the default Slate schema. The feature is stored in the RT document but not emitted on export.
  • Horizontal rule variants (thematic-break, divider, horizontal-rule, hr) are all accepted on import. Export always produces { type: 'thematic-break', children: [{ text: '' }] }.
  • Blockquotes on export use block-quote (hyphenated). The children array will always contain at least one element node to satisfy Slate's invariant that element children are non-empty.
  • Block elements on export always include at least one text leaf ({ text: '' }) to satisfy Slate's constraint that element children must be non-empty.