Serializing Markdown
Markdown <-> Slate
'use client';
import React from 'react';
import { Plate } from '@udecode/plate/react';
import { editorPlugins } from '@/components/editor/plugins/editor-plugins';
import { useCreateEditor } from '@/components/editor/use-create-editor';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
import { DEMO_VALUES } from './values/demo-values';
export default function Demo({ id }: { id: string }) {
const editor = useCreateEditor({
plugins: [...editorPlugins],
value: DEMO_VALUES[id],
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}
Installation
pnpm add @udecode/plate-markdown
Usage
Markdown to Slate
import { MarkdownPlugin } from '@udecode/plate-markdown';
const editor = createPlateEditor({
plugins: [
// ...other plugins,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
],
});
const value = editor.api.markdown.deserialize('**Hello world!**');
'use client';
import React, { useState } from 'react';
import type { Value } from '@udecode/plate';
import { withProps } from '@udecode/cn';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
SubscriptPlugin,
SuperscriptPlugin,
UnderlinePlugin,
} from '@udecode/plate-basic-marks/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import {
CodeBlockPlugin,
CodeSyntaxPlugin,
} from '@udecode/plate-code-block/react';
import { HEADING_KEYS } from '@udecode/plate-heading';
import { HighlightPlugin } from '@udecode/plate-highlight/react';
import { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';
import { KbdPlugin } from '@udecode/plate-kbd/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import { deserializeMd, MarkdownPlugin } from '@udecode/plate-markdown';
import { InlineEquationPlugin } from '@udecode/plate-math/react';
import { ImagePlugin } from '@udecode/plate-media/react';
import {
TableCellHeaderPlugin,
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from '@udecode/plate-table/react';
import {
type PlateEditor,
ParagraphPlugin,
Plate,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate/react';
import { cloneDeep } from 'lodash';
import remarkEmoji from 'remark-emoji';
import { autoformatPlugin } from '@/components/editor/plugins/autoformat-plugin';
import { basicNodesPlugins } from '@/components/editor/plugins/basic-nodes-plugins';
import { indentListPlugins } from '@/components/editor/plugins/indent-list-plugins';
import { linkPlugin } from '@/components/editor/plugins/link-plugin';
import { mediaPlugins } from '@/components/editor/plugins/media-plugins';
import { tablePlugin } from '@/components/editor/plugins/table-plugin';
import { BlockquoteElement } from '@/components/plate-ui/blockquote-element';
import { CodeBlockElement } from '@/components/plate-ui/code-block-element';
import { CodeLeaf } from '@/components/plate-ui/code-leaf';
import { CodeSyntaxLeaf } from '@/components/plate-ui/code-syntax-leaf';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';
import { HeadingElement } from '@/components/plate-ui/heading-element';
import { HighlightLeaf } from '@/components/plate-ui/highlight-leaf';
import { HrElement } from '@/components/plate-ui/hr-element';
import { ImageElement } from '@/components/plate-ui/image-element';
import { KbdLeaf } from '@/components/plate-ui/kbd-leaf';
import { LinkElement } from '@/components/plate-ui/link-element';
import { ParagraphElement } from '@/components/plate-ui/paragraph-element';
import {
TableCellElement,
TableCellHeaderElement,
} from '@/components/plate-ui/table-cell-element';
import { TableElement } from '@/components/plate-ui/table-element';
import { TableRowElement } from '@/components/plate-ui/table-row-element';
const initialMarkdown = `# Markdown syntax guide
## Headers
# This is a Heading h1
## This is a Heading h2
###### This is a Heading h6
## Emphasis
*This text will be italic*
_This will also be italic_
**This text will be bold**
__This will also be bold__
_You **can** combine them_
## Lists
### Unordered
* Item 1
* Item 2
* Item 2a
* Item 2b
### Ordered
1. Item 1
2. Item 2
3. Item 3
1. Item 3a
2. Item 3b
## Images

## Links
You may be using [Markdown Live Preview](https://markdownlivepreview.com/).
## Blockquotes
> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz.
## Tables
| Left columns | Right columns |
| ------------- |:-------------:|
| left foo | right foo |
| left bar | right bar |
| left baz | right baz |
## Blocks of code
\`\`\`js
let message = 'Hello world';
alert(message);
\`\`\`
## Inline code
This web site is using \`plate\`.
## GitHub Flavored Markdown
### Task Lists
- [x] Completed task
- [ ] Incomplete task
- [x] @mentions, #refs, [links](), **formatting**, and <del>tags</del> supported
- [ ] list syntax required (any unordered or ordered list supported)
### Strikethrough
~~This text is strikethrough~~
### Autolinks
Visit https://github.com automatically converts to a link
Email example@example.com also converts automatically
### Emoji
:smile: :heart:
`;
export default function MarkdownDemo() {
const markdownEditor = usePlateEditor({
plugins: [MarkdownPlugin],
value: [{ children: [{ text: initialMarkdown }], type: 'p' }],
});
const [value, setValue] = useState<Value>([]);
const editor = usePlateEditor(
{
override: {
components: {
[BlockquotePlugin.key]: BlockquoteElement,
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
[CodeBlockPlugin.key]: CodeBlockElement,
[CodePlugin.key]: CodeLeaf,
[CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
[HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),
[HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),
[HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),
[HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),
[HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),
[HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),
[HighlightPlugin.key]: HighlightLeaf,
[HorizontalRulePlugin.key]: HrElement,
[ImagePlugin.key]: ImageElement,
[ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),
[KbdPlugin.key]: KbdLeaf,
[LinkPlugin.key]: LinkElement,
[ParagraphPlugin.key]: ParagraphElement,
[StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),
[SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),
[SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),
[TableCellHeaderPlugin.key]: TableCellHeaderElement,
[TableCellPlugin.key]: TableCellElement,
[TablePlugin.key]: TableElement,
[TableRowPlugin.key]: TableRowElement,
[UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),
},
},
plugins: [
...basicNodesPlugins,
HorizontalRulePlugin,
linkPlugin,
tablePlugin,
...mediaPlugins,
InlineEquationPlugin,
HighlightPlugin,
KbdPlugin,
ImagePlugin,
...indentListPlugins,
autoformatPlugin,
MarkdownPlugin,
],
value: (editor) =>
deserializeMd(editor, initialMarkdown, {
remarkPlugins: [remarkEmoji as any],
}),
},
[]
);
useResetEditorOnChange({ editor, value: value }, [value]);
return (
<div className="grid grid-cols-2 overflow-y-auto">
<Plate
onValueChange={() => {
setValue(
editor.api.markdown.deserialize(
markdownEditor.api.markdown.serialize()
)
);
}}
editor={markdownEditor}
>
<EditorContainer>
<Editor variant="none" className="p-2 font-mono text-sm" />
</EditorContainer>
</Plate>
<Plate editor={editor}>
<EditorContainer className="bg-muted/50">
<Editor variant="none" className="p-2" />
</EditorContainer>
</Plate>
</div>
);
}
function useResetEditorOnChange(
{ editor, value }: { editor: PlateEditor; value: Value },
deps: any[]
) {
React.useEffect(() => {
if (value.length > 0) {
editor.tf.replaceNodes(cloneDeep(value), {
at: [],
children: true,
});
editor.history.undos = [];
editor.history.redos = [];
editor.operations = [];
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps]);
}
Slate to Markdown
const editor = createPlateEditor({
value,
plugins: [
// ...other plugins,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
,
],
});
const content = editor.api.markdown.serialize();
Round-trip Serialization
When implementing round-trip serialization, the key is to handle custom nodes that exist in the Plate editor but don't exist in standard Markdown syntax. These nodes (such as date
, mention
, etc.) need to be converted to MDX syntax (similar to JSX) to maintain data integrity.
Let's illustrate how to implement round-trip serialization with a date picker node example:
Assume we have the following Slate node structure:
{
type: 'p',
children: [{ type: 'text', text: '现在是' }, { type: 'date', date: '2025-03-31',children:[{type:'text',text:'2025-03-31'}] }],
}
To implement complete round-trip serialization, you need to configure conversion rules for both directions:
- Serialization (Slate → Markdown): Convert Slate nodes to MDX tags
- Deserialization (Markdown → Slate): Parse MDX tags back to Slate nodes
Here's a complete configuration example:
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
nodes: {
date: {
deserialize(mdastNode: MdMdxJsxTextElement, deco, options) {
if (mdastNode.children?.[0] && 'value' in mdastNode.children[0]) {
return {
children: [{ text: '', type: 'text' }],
date: mdastNode.children[0].value,
type: 'date',
};
}
// Fallback
return {
children: [{ text: '', type: 'text' }],
date: '',
type: 'date',
};
},
serialize: (slateNode): MdMdxJsxTextElement => {
return {
attributes: [],
children: [{ type: 'text', value: slateNode.date || '1999-01-01' }],
name: 'date',
type: 'mdxJsxTextElement',
};
},
},
},
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
});
After using the above configuration, content will be converted as follows:
- Slate node → Markdown:
Convert date slate node to <date>2025-03-31</date>
- Markdown → Slate: Parse
<date>2025-03-31</date>
back to the original Slate node structure
API
MarkdownPlugin
deserialize
: Custom filtering function when parsing Markdownserialize
: Custom filtering function when converting to Markdown Returntrue
to keep the node, returnfalse
to filter it out. This is useful for scenarios requiring complex logic to decide whether to include a node. Default:null
Configure allowed node types. Cannot be used with disallowedNodes simultaneously.
If specified, only node types in the list will be processed, others will be filtered out.
Default: null
Configure disallowed node types. Cannot be used with allowedNodes simultaneously.
Node types in the list will be filtered out and won't appear in the final result.
Default: null
Custom node filtering function. Called after allowedNodes/disallowedNodes checks.
Defines rules for converting Markdown syntax elements to Slate editor elements, or rules for converting Slate editor elements to Markdown syntax elements. Includes conversion rules for paragraphs, headings, lists, links, images, and other elements. When set to null, default conversion rules will be used.
Default: undefined
An array of remark plugins for extending Markdown parsing and serialization functionality. For example, you can add remark-gfm to support GFM syntax, remark-math to support mathematical formulas, etc. These plugins will be used when parsing and generating Markdown text.
Default: []
When text contains \n, split the text into separate paragraphs. Line breaks between paragraphs will also be converted to separate paragraphs.
Default: false
editor.api.markdown.deserialize
Convert a Markdown string to Slate value.
Configure allowed node types. Cannot be used with disallowedNodes simultaneously.
Configure disallowed node types. Cannot be used with allowedNodes simultaneously.
Custom node filtering function. Called after allowedNodes/disallowedNodes checks.
Enable block-level memoization with _memo
property, making it compatible with PlateStatic
memoization.
Defines rules for converting Markdown syntax elements to Slate editor elements.
Options for the token parser. Can filter out specific Markdown token types (e.g., 'space').
An array of remark plugins for extending Markdown parsing functionality.
When text contains \n, split the text into separate paragraphs.
editor.api.markdown.serialize
Convert the current Slate value to a Markdown string.
Configure allowed node types. Cannot be used with disallowedNodes simultaneously.
Configure disallowed node types. Cannot be used with allowedNodes simultaneously.
Custom node filtering function. Called after allowedNodes/disallowedNodes checks.
Defines rules for converting Slate editor elements to Markdown syntax elements.
An array of remark plugins for extending Markdown serialization functionality.
The Slate nodes to serialize. If not provided, the entire editor value will be used.
parseMarkdownBlocks
Extract and filter Markdown tokens using the marked lexer.