packages/lexical-mdast/README.md
@lexical/mdast⚠️ Experimental: everything in this package is marked
@experimentaland may change between any two Lexical releases — including breaking renames, signature changes, or behavior changes — until the API stabilizes.@lexical/markdownremains the supported default for production apps that don't want to track an experimental API.
An alternative to @lexical/markdown that is built on the
micromark /
mdast ecosystem.
Where @lexical/markdown ships its own regular-expression based parser, this
package delegates Markdown parsing and serialization to micromark and
mdast-util-*. That means CommonMark + GFM compliance comes from the same
parser used by remark, and Markdown shortcuts are recognized by feeding
keystrokes back through that same parser — there is no second grammar to keep
in sync.
Like @lexical/markdown, the original syntax of a construct is preserved on
the Lexical nodes (the bullet character of a list, a code block's fence, and a
hard line break's style), so re-serializing produces minimally different
Markdown — * a/+ b bullets and ~~~ fences round-trip unchanged.
@lexical/mdast is set up exclusively through the Lexical extension
system, modeled on @lexical/html's DOMImportExtension. Each feature
extension ships the nodes it needs and contributes its import/export rules (and
the micromark/mdast extensions that tokenize them) to the core
MdastImportExtension registry:
CommonMark features:
| Extension | Ships | Adds |
|---|---|---|
MdastHeadingExtension | HeadingNode | ATX & setext headings |
MdastBlockquoteExtension | QuoteNode | block quotes |
MdastListExtension | ListNode, ListItemNode | ordered/unordered lists |
MdastCodeExtension | CodeNode | fenced & indented code |
MdastLinkExtension | LinkNode | links, <autolinks>, reference links |
MdastHorizontalRuleExtension | HorizontalRuleNode | thematic breaks (---) |
GFM features:
| Extension | Ships | Adds |
|---|---|---|
MdastStrikethroughExtension | – | ~~strikethrough~~ |
MdastTaskListExtension | – | task lists (- [x] …) |
MdastAutolinkLiteralExtension | – | literal autolinks (bare https://… in prose) |
MdastTableExtension | TableNode, … | tables |
Behavior and convenience bundles:
| Extension | Adds |
|---|---|
MdastCommonMarkExtension | bundle of the six CommonMark extensions |
MdastGfmExtension | bundle of the four GFM extensions |
MdastRichTextExtension | bundle of heading + blockquote |
MdastExportExtension | serialization back to Markdown ($convertToMarkdownString) |
MdastExtension | bundle of MdastImportExtension + MdastExportExtension |
MdastShadowRootQuoteExtension | opt-in: blockquotes as block containers (full-fidelity nested content) |
MdastShortcutsExtension | streaming keyboard shortcuts |
Everything composes granularly and degrades gracefully: an editor with only
the extensions it wants imports unsupported constructs as their content
(a table becomes its cell text), and the typing shortcuts — driven by the
same registry — only fire for constructs the editor can represent (>
stays literal without MdastBlockquoteExtension).
Import and export are separate extensions: MdastImportExtension (and the
feature extensions that contribute to it) only parse, and
MdastExportExtension compiles the same registry into a serializer. An
editor that never converts back to Markdown simply omits
MdastExportExtension and doesn't bundle mdast-util-to-markdown. When you
want both directions without thinking about it, depend on MdastExtension,
which bundles the two.
import {
$convertFromMarkdownString,
$convertToMarkdownString,
MdastCommonMarkExtension,
MdastExtension,
MdastGfmExtension,
MdastShortcutsExtension,
} from '@lexical/mdast';
import {buildEditorFromExtensions} from '@lexical/extension';
import {defineExtension} from 'lexical';
const editor = buildEditorFromExtensions(
defineExtension({
// CommonMark + GFM grammar, import + export (MdastExtension), and
// typing shortcuts. Swap bundles for individual feature extensions
// to trim what you don't need.
dependencies: [
MdastCommonMarkExtension,
MdastGfmExtension,
MdastExtension,
MdastShortcutsExtension,
],
name: '[root]',
}),
);
// Import / export run inside the editor; both are `$`-functions.
editor.update(() => {
$convertFromMarkdownString('# Hello *world*');
});
const markdown = editor.read(() => $convertToMarkdownString());
The same API is available from the editor as
$getExtensionOutput(MdastImportExtension).$convertFromMarkdownString(...)
and
$getExtensionOutput(MdastExportExtension).$convertToMarkdownString(...).
$convertSelectionToMarkdownString(selection?) serializes only the
selected content (defaulting to the current selection): unselected
blocks and list items are skipped and partially selected text is
sliced to the selected range.
The mdast tree itself is part of the API, so editor content can flow
through the wider unified ecosystem — remark
plugins, remark-rehype for HTML rendering, tree diffing:
import {$convertFromMdast, $convertToMdast} from '@lexical/mdast';
// Editor -> mdast tree (before serialization).
const tree = editor.read(() => $convertToMdast());
// ... run remark plugins / transform the tree ...
// mdast tree -> editor.
editor.update(() => $convertFromMdast(tree));
The *FromMarkdownString functions parse the source text themselves
(which is also what enables source-based syntax preservation, e.g.
keeping * vs - bullets); the *FromMdast functions take an
already-parsed tree, where no source text exists so syntax
preservation is skipped.
To convert Markdown into nodes without replacing the document —
e.g. to insert at the current selection —
$generateNodesFromMarkdownString(markdown) (and its tree-taking
sibling $generateNodesFromMdast(tree)) returns a detached array of
block-level nodes and leaves the document and selection untouched:
import {$generateNodesFromMarkdownString} from '@lexical/mdast';
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertNodes($generateNodesFromMarkdownString('# Inserted'));
}
});
Document-level mdast-util-to-markdown options (bullet, emphasis
marker, fence, ...) can be contributed like any other configuration —
scalar options in a toMarkdownExtensions entry apply document-wide
and override the package defaults:
import {MdastImportExtension} from '@lexical/mdast';
import {configExtension} from 'lexical';
// Serialize bullets as `+` and emphasis as `_`. Per-node syntax
// recorded on import (a list's bullet, a code block's fence, ...)
// still wins for those nodes' own output.
configExtension(MdastImportExtension, {
toMarkdownExtensions: [{bullet: '+', emphasis: '_'}],
});
Because extensions are the unit of configuration, you add or override behavior
by contributing rules to MdastImportExtension from your own extension:
import {MdastImportExtension} from '@lexical/mdast';
import {configExtension, defineExtension} from 'lexical';
export const MyMdastExtension = defineExtension({
name: 'my-mdast',
nodes: [MyNode],
dependencies: [
configExtension(MdastImportExtension, {
importRules: [{type: 'myMdastType', $import: $importMyNode}],
exportRules: [{type: 'my-node', $export: $exportMyNode}],
micromarkExtensions: [myMicromarkExtension()],
mdastExtensions: [myMdastExtension()],
toMarkdownExtensions: [myToMarkdownExtension()],
}),
],
});