Markdown
Markdown ↔ Plate
'use client';
import React from 'react';
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 {
MarkdownPlugin,
remarkMdx,
remarkMention,
} from '@udecode/plate-markdown';
import { InlineEquationPlugin } from '@udecode/plate-math/react';
import { ImagePlugin } from '@udecode/plate-media/react';
import { MentionPlugin } from '@udecode/plate-mention/react';
import { NodeIdPlugin } from '@udecode/plate-node-id';
import {
TableCellHeaderPlugin,
TableCellPlugin,
TablePlugin,
TableRowPlugin,
} from '@udecode/plate-table/react';
import {
ParagraphPlugin,
Plate,
PlateLeaf,
usePlateEditor,
} from '@udecode/plate/react';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
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 { useDebounce } from '@/hooks/use-debounce';
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';
import { MentionElement } from '../plate-ui/mention-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 website is using \`plate\`.
## GitHub Flavored Markdown
### Task Lists
- [x] Completed task
- [ ] Incomplete task
- [ ] @mentions , [links](https://platejs.org), **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 [markdownValue, setMarkdownValue] = React.useState(initialMarkdown);
const debouncedMarkdownValue = useDebounce(markdownValue, 300);
const markdownEditor = usePlateEditor({
plugins: [],
value: [{ children: [{ text: markdownValue }], type: 'p' }],
});
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,
[MentionPlugin.key]: MentionElement,
[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,
NodeIdPlugin,
HorizontalRulePlugin,
linkPlugin,
tablePlugin,
...mediaPlugins,
InlineEquationPlugin,
HighlightPlugin,
KbdPlugin,
ImagePlugin,
...indentListPlugins,
autoformatPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
},
}),
MentionPlugin.configure({
options: { triggerPreviousCharPattern: /^$|^[\s"']$/ },
}),
],
value: (editor) =>
editor.getApi(MarkdownPlugin).markdown.deserialize(initialMarkdown, {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkEmoji as any],
}),
},
[]
);
React.useEffect(() => {
if (debouncedMarkdownValue !== initialMarkdown) {
editor.tf.reset();
editor.tf.setValue(
editor.api.markdown.deserialize(debouncedMarkdownValue, {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkEmoji as any],
})
);
}
}, [debouncedMarkdownValue, editor]);
return (
<div className="grid h-full grid-cols-2 overflow-y-auto">
<Plate
onValueChange={() => {
const value = markdownEditor.children
.map((node: any) => markdownEditor.api.string(node))
.join('\n');
setMarkdownValue(value);
}}
editor={markdownEditor}
>
<EditorContainer>
<Editor
variant="none"
className="bg-muted/50 p-2 font-mono text-sm"
/>
</EditorContainer>
</Plate>
<Plate editor={editor}>
<EditorContainer>
<Editor variant="none" className="px-4 py-2" />
</EditorContainer>
</Plate>
</div>
);
}
'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>
);
}
Features
- Convert Markdown strings to Slate JSON (
deserialize
). - Convert Slate JSON to Markdown strings (
serialize
). - Safe by default: Handles Markdown and converts it to a Slate-compatible structure without relying on
dangerouslySetInnerHTML
. - Customizable: Use
rules
to define how specific Markdown syntax or custom Slate elements are converted. Supports MDX elements. - Pluggable: Extend functionality with remark plugins via the
remarkPlugins
option. - Compliant: Supports CommonMark. GFM (GitHub Flavored Markdown) support via
remark-gfm
. - Round-trip Serialization: Designed to handle conversion back and forth between Slate and Markdown, preserving custom elements via MDX syntax.
When should I use this?
While other libraries like react-markdown
render Markdown directly to React elements, @udecode/plate-markdown
integrates deeply with the Plate ecosystem. Here's why you might choose it:
- Rich Text Editing: Plate is a full-fledged rich text editor framework.
@udecode/plate-markdown
allows seamless conversion between Markdown and Plate's structured content format (Slate JSON), enabling advanced editing features beyond simple Markdown rendering. - WYSIWYG Experience: Convert Markdown to a rich text view for editing, and serialize it back to Markdown for storage or display.
- Custom Elements & Data: Plate excels at handling complex, custom elements (mentions, embeds, etc.).
@udecode/plate-markdown
provides the mechanism (rules
, MDX) to serialize and deserialize these custom structures, which is often difficult with standard Markdown renderers. - Extensibility: Leverages Plate's plugin system and the unified/remark ecosystem for powerful customization of both the editor behavior and the Markdown conversion process.
If you only need to display Markdown as HTML without editing capabilities or complex custom elements, react-markdown
might be sufficient. If you need a rich text editor that can import/export Markdown and handle custom structured content, @udecode/plate-markdown
is the integrated solution within the Plate framework.
Installation
pnpm add @udecode/plate @udecode/plate-markdown
Usage
Configure the Plugin (Optional but Recommended)
Configuring MarkdownPlugin
is recommended to enable paste handling (converting pasted Markdown to Plate content) and to set default conversion rules
for the API.
import { createPlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin, remarkMention, remarkMdx } from '@udecode/plate-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
const editor = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure({
options: {
// Add remark plugins for syntax extensions (GFM, Math, MDX)
// import remarkMdx from `@udecode/plate-markdown` instead of `remark-mdx`
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
// Define custom rules for serialization/deserialization if needed
rules: {
// Example: custom rule for a 'date' element
// date: { /* ... rule implementation ... */ },
},
},
}),
],
});
// To disable Markdown paste handling:
const editorWithoutPaste = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure(() => ({ parser: null })),
],
});
If you don't use the MarkdownPlugin
, you can still use deserializeMd
and serializeMd
without built-in default rules and Markdown paste handling.
Markdown to Plate (Deserialization)
Use editor.api.markdown.deserialize
to convert a Markdown string into a Slate value (an array of nodes). This is often used to set the initial content of the editor.
import { createPlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
const markdownString = '# Hello, *Plate*!';
const editor = createPlateEditor({
plugins: [
// ... other plugins needed to render the content
MarkdownPlugin,
],
// Use deserialize in the value factory for initial content
value: (editor) => editor.getApi(MarkdownPlugin).markdown.deserialize(markdownString),
});
Initial Value: You can directly use editor.getApi(MarkdownPlugin).markdown.deserialize
when defining the initial value
for createPlateEditor
/ usePlateEditor
. Ensure all necessary Plate plugins to render the resulting Slate nodes are included in the plugins
array.
Plate to Markdown (Serialization)
Use editor.api.markdown.serialize
to convert the current editor's Slate value (or a specific array of nodes) into a Markdown string.
Serializing Current Editor Content:
// Assuming `editor` has content
// Serialize the current editor content to a Markdown string
// Uses rules from MarkdownPlugin config.
const markdownOutput = editor.api.markdown.serialize();
console.log(markdownOutput);
Serializing Specific Nodes:
const specificNodes = [
{ type: 'p', children: [{ text: 'Serialize just this paragraph.' }] },
{ type: 'h1', children: [{ text: 'And this heading.' }] }
];
const partialMarkdownOutput = editor.api.markdown.serialize({
value: specificNodes,
});
console.log(partialMarkdownOutput);
Round-trip Serialization with Custom Elements (MDX)
A key feature is handling custom Slate elements that don't have a standard Markdown representation (e.g., underline, mentions, custom embeds). @udecode/plate-markdown
converts these to MDX elements during serialization and parses them back during deserialization, preserving data integrity.
Example: Handling a custom date
element:
Slate Node Structure:
{
type: 'p',
children: [
{ type: 'text', text: 'Today is ' },
{ type: 'date', date: '2025-03-31', children: [{ type: 'text', text: '' }] } // Note: Leaf elements need a text child
],
}
Configuration with rules
:
import type { MdMdxJsxTextElement } from '@udecode/plate-markdown'; // Import type if needed
MarkdownPlugin.configure({
options: {
rules: {
// Key matches:
// 1. the plugin 'key' or 'type' of the Slate element.
// 2. the mdast(https://github.com/syntax-tree/mdast) node type.
// 3. the mdx tag name.
date: {
// Rule for Markdown -> Slate
deserialize(mdastNode: MdMdxJsxTextElement, deco, options) {
// Extract data from the MDX node attributes or children
// In this simple case, we assume the date is the first child's value
const dateValue = (mdastNode.children?.[0] as any)?.value || '';
return {
type: 'date',
date: dateValue,
children: [{ text: '' }], // Ensure valid Slate structure
};
},
// Rule for Slate -> Markdown (MDX)
serialize: (slateNode): MdMdxJsxTextElement => {
// Create an MDX text element node
return {
type: 'mdxJsxTextElement',
name: 'date', // Tag name for the MDX element
attributes: [], // Add attributes if needed: [{ type: 'mdxJsxAttribute', name: 'date', value: slateNode.date }]
children: [{ type: 'text', value: slateNode.date || '1999-01-01' }], // Content inside the tag
};
},
},
// Add rules for other custom elements (mentions, etc.) here
},
remarkPlugins: [remarkMdx, /* other plugins like remarkGfm */], // Ensure remarkMdx is included
},
});
Conversion Process:
- Serialization (Slate → Markdown):
The Slate
date
node is converted to the MDX tag:<date>2025-03-31</date>
. - Deserialization (Markdown → Slate):
The MDX tag
<date>2025-03-31</date>
is parsed back into the original Slatedate
node structure.
API
MarkdownPlugin
The core plugin configuration object. Use MarkdownPlugin.configure({ options: {} })
to set options.
deserialize?: (mdastNode: any) => boolean
: Filter function during Markdown → Slate conversion. Returntrue
to keep,false
to discard.serialize?: (slateNode: any) => boolean
: Filter function during Slate → Markdown conversion. Returntrue
to keep,false
to discard. Default:null
.
Whitelist specific node types (both Slate types and Markdown AST types like strong
, emphasis
). Cannot be used with disallowedNodes
simultaneously. If specified, only listed types are processed. Default: null
(all allowed).
Blacklist specific node types. Cannot be used with allowedNodes
. Listed types are filtered out during conversion. Default: null
.
Provides fine-grained control over node filtering using custom functions, applied after allowedNodes
/disallowedNodes
.
Defines custom conversion rules between Markdown AST elements and Slate elements. Essential for handling custom Slate nodes and overriding default behavior. Keys correspond to plugin key or default Slate types (for serialize
) or Markdown AST types (for deserialize
). Set to null
to use only default rules. See Round-trip Serialization and Customizing Conversion Rules for details.
Note: When defining rules for marks / leaves, ensure the rule object includes mark: true
.
Default: null
(uses internal defaultRules
).
An array of remark plugins to extend Markdown parsing and serialization. Add plugins like remark-gfm
, remark-math
, remark-mdx
here. These plugins operate on the Markdown AST (mdast
). Default: []
.
Configuration for handling pasted content. Set to null
to disable Markdown paste handling. Default enables pasting text/plain
as Markdown.
See PlatePlugin API > parser for details.
editor.api.markdown.deserialize
Converts a Markdown string into a Slate Value
( Descendant[]
).
Override allowedNodes
from plugin config for this call.
Override disallowedNodes
from plugin config for this call.
Override allowNode
from plugin config for this call.
Enable block-level memoization by adding an _memo
property containing the raw Markdown source to each top-level block. Useful for integrations like PlateStatic
that benefit from memoization. Default: false
.
Override rules
from plugin config for this call.
Options passed to the underlying Markdown block parser (parseMarkdownBlocks
). See its API below.
Override remarkPlugins
from plugin config for this call.
If true
, treat single line breaks (\n
) within paragraphs as paragraph breaks, splitting the text into separate paragraph nodes. Default: false
(follows standard Markdown paragraph behavior).
editor.api.markdown.serialize
Converts a Slate Value
( Descendant[]
) into a Markdown string.
The Slate nodes to serialize. If not provided, the entire current editor value (editor.children
) will be used.
Override allowedNodes
from plugin config for this call.
Override disallowedNodes
from plugin config for this call.
Override allowNode
from plugin config for this call.
Override rules
from plugin config for this call.
Override remarkPlugins
from plugin config for this call. Affects the final stringification process.
parseMarkdownBlocks
Utility function (used internally by deserialize
) to parse a Markdown string into block-level tokens using the marked
lexer. Primarily useful when memoize
is enabled.
Examples
Using a Remark Plugin (GFM)
Add support for GitHub Flavored Markdown (tables, strikethrough, task lists, autolinks).
Plugin Configuration:
import { MarkdownPlugin } from '@udecode/plate-markdown';
import remarkGfm from 'remark-gfm';
// Import Plate plugins for GFM elements
import { TablePlugin } from '@udecode/plate-table/react';
import { TodoListPlugin } from '@udecode/plate-list/react';
import { StrikethroughPlugin } from '@udecode/plate-basic-marks/react';
import { LinkPlugin } from '@udecode/plate-link/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
TablePlugin,
TodoListPlugin,
StrikethroughPlugin,
LinkPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm],
},
}),
],
});
Usage:
const markdown = `
A table:
| a | b |
| - | - |
~~Strikethrough~~
- [x] Task list item
Visit https://platejs.org
`;
const slateValue = editor.api.markdown.deserialize(markdown);
// editor.tf.setValue(slateValue);
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain the GFM syntax
Customizing Rendering (Syntax Highlighting)
This example shows two approaches: customizing the conversion using rules
(advanced) and customizing the rendering using override.components
(common).
Background:
@udecode/plate-markdown
converts Markdown fenced code blocks (```js ... ```) into Slatecode_block
elements containingcode_line
children.- The Plate
CodeBlockElement
component (often from@udecode/plate-code-block/react
) is responsible for rendering this structure. - Syntax highlighting is typically applied within
CodeBlockElement
using a library likelowlight
(viaCodeBlockPlugin
). See the Basic Elements > Code Block documentation for details on configuring syntax highlighting withlowlight
and customizing the code block components.
Approach 1: Customizing Rendering Component (Recommended for UI changes)
If you just want to change how code blocks look, override the component for the code_block
plugin key.
import { createPlateEditor } from '@udecode/plate/react';
import { CodeBlockPlugin, CodeLinePlugin, CodeSyntaxPlugin } from '@udecode/plate-code-block/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
import { MyCustomCodeBlockElement, MyCustomCodeLineElement, MyCustomCodeSyntaxElement } from './MyCustomCodeBlockElement'; // Your custom component
const editor = createPlateEditor({
plugins: [
CodeBlockPlugin, // Include the base plugin for structure/logic
MarkdownPlugin, // For Markdown conversion
// ... other plugins
],
override: {
components: {
[CodeBlockPlugin.key]: MyCustomCodeBlockElement,
[CodeLinePlugin.key]: MyCustomCodeLineElement,
[CodeSyntaxPlugin.key]: MyCustomCodeSyntaxElement,
},
},
});
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// using react-syntax-highlighter or another library, consuming the props
// provided by PlateElement (like element.lang and element.children).
See the Basic Elements > Code Block documentation for complete examples of code block components and syntax highlighting configuration.
Approach 2: Customizing Conversion Rule (Advanced - Changing Slate Structure)
If you need to fundamentally change the Slate JSON structure generated from a Markdown code block (e.g., store the code as a single string prop instead of code_line
children), you would override the deserialize
rule.
import { MarkdownPlugin } from '@udecode/plate-markdown';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
MarkdownPlugin.configure({
options: {
rules: {
// Override deserialization for 'code' type from mdast
code: { // Corresponds to mdast type 'code'
deserialize: (mdastNode, deco, options) => {
// Convert to a structure different from the default
return {
type: CodeBlockPlugin.key, // Use Plate's type
lang: mdastNode.lang ?? undefined,
rawCode: mdastNode.value || '', // Store raw code directly
children: [{ text: '' }], // Still need dummy text child for Slate Element
};
},
},
// You would likely also need a custom `serialize` rule for `code_block`
// to convert `rawCode` back to an mdast 'code' node.
code_block: {
serialize: (slateNode, options) => {
return {
type: 'code',
lang: slateNode.lang,
value: slateNode.rawCode
}
}
}
},
// remarkPlugins: [...]
},
});
// Your custom rendering component (MyCustomCodeBlockElement) would then need
// to be adjusted to read the code from the `rawCode` property instead of children.
Choose the approach based on whether you need to change the UI/rendering library (Approach 1) or the underlying Slate data structure (Approach 2).
Using Remark Plugins for Math (remark-math)
Enable TeX math syntax ($inline$
, $$block$$
).
Plugin Configuration:
import { MarkdownPlugin } from '@udecode/plate-markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import { EquationPlugin, InlineEquationPlugin } from '@udecode/plate-math/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
EquationPlugin, // Renders block equations
InlineEquationPlugin, // Renders inline equations
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath],
// Default rules in plate-markdown handle 'math' and 'inlineMath' mdast types
// generated by remark-math, converting them to 'equation' and 'inline_equation' Slate types.
},
}),
],
});
Usage:
const markdown = `
Inline math: $E=mc^2$
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
const slateValue = editor.api.markdown.deserialize(markdown);
// The value will contain 'inline_equation' and 'equation' nodes.
const markdownOutput = editor.api.markdown.serialize();
// The output will contain $...$ and $$...$$ syntax.
Plugins (remark)
@udecode/plate-markdown
leverages the powerful unified ecosystem, specifically remark for Markdown processing. You can extend its capabilities by adding remark plugins via the remarkPlugins
option in the MarkdownPlugin
configuration.
These plugins operate on the mdast (Markdown Abstract Syntax Tree) during both serialization and deserialization.
Finding Plugins:
- List of remark plugins: Comprehensive list on the official remark repository.
remark-plugin
topic on GitHub: Discover plugins by topic.- Awesome Remark: Curated list of notable remark plugins and resources.
Common Use Cases:
- Syntax Extensions: Add support for syntaxes not in CommonMark (e.g.,
remark-gfm
for tables, task lists;remark-math
for TeX math;remark-frontmatter
for YAML frontmatter;remark-mdx
for custom component syntax). - Linting/Formatting: Integrate linters like
remark-lint
(though often done in separate tooling). - Custom Transformations: Create your own plugins to modify the mdast before it's converted to/from Slate.
Important Note: Unlike some other Markdown renderers, you generally do not need rehype
plugins for rendering when using Plate. Plate components (like TableElement
, CodeBlockElement
, EquationElement
) handle the rendering based on the Slate JSON structure. rehype
plugins are typically used when the target output is HTML, whereas Plate targets its own component system based on Slate's structure. You might use rehype
plugins if you need to manipulate the intermediate HTML AST (hast
) during complex transformations (like handling raw HTML with rehype-raw
), but it's less common for typical Plate usage.
Syntax
@udecode/plate-markdown
uses remark-parse
which adheres to the CommonMark specification by default.
Enable additional syntax support (like GFM) by adding the corresponding remark plugins (e.g., remark-gfm
) to the remarkPlugins
option.
- Learn Markdown: CommonMark Help
- GFM Spec: GitHub Flavored Markdown Spec
Architecture
@udecode/plate-markdown
acts as a bridge between Markdown strings and Plate's editor value format, using the unified/remark ecosystem for robust Markdown processing.
@udecode/plate-markdown
+--------------------------------------------------------------------------------------------+
| |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| | | | | | | | | |
markdown-+->+ remark +-mdast->+ remark plugins +-mdast->+ mdast-to-slate+----->+ nodes +-plate-+->react elements
| | | | | | | | | |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| ^ | |
| | v |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| | | | | | | | | |
| | stringify |<-mdast-+ remark plugins |<-mdast-+ slate-to-mdast+<-----+ serialize | |
| | | | | | | | | |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| |
+--------------------------------------------------------------------------------------------+
The processor goes through these steps:
-
Parse (Deserialization):
- Markdown string is parsed into mdast (Markdown Abstract Syntax Tree) using
remark-parse
- Registered
remarkPlugins
transform the mdast (e.g.,remark-gfm
for tables) mdast-to-slate
converts the mdast into Plate's node structure using configuredrules
- Plate renders the nodes using its component system
- Markdown string is parsed into mdast (Markdown Abstract Syntax Tree) using
-
Stringify (Serialization):
- Plate nodes are converted to mdast using
slate-to-mdast
and configuredrules
remarkPlugins
transform the mdast (e.g., formatting)remark-stringify
converts the mdast back to a Markdown string
- Plate nodes are converted to mdast using
Key differences from react-markdown
:
- Direct Node Rendering: While
react-markdown
uses rehype to convert markdown to HTML then to React, Plate directly renders its nodes using its component system. - Bidirectional: Plate's markdown processor is bidirectional, supporting both deserialization (markdown → nodes) and serialization (nodes → markdown).
- Rich Text Integration: The nodes are fully integrated with Plate's rich text editing capabilities, not just for display.
- Plugin System: Components are registered through Plate's plugin system rather than passed directly to the markdown processor.
Note: Unlike react-markdown
, Plate doesn't use rehype or need HTML as an intermediate step. Plate components directly render the editor's nodes, which are converted to/from markdown using remark.
Migrating from react-markdown
Migrating from react-markdown
to Plate with @udecode/plate-markdown
involves understanding the architectural differences and mapping concepts.
Key Differences:
- Rendering Pipeline:
react-markdown
: Markdown String → Remark (mdast) → Rehype (hast) → React Elements.@udecode/plate-markdown
: Markdown String ↔ Remark (mdast) ↔ Slate JSON. Rendering is handled separately by Plate Components based on the Slate JSON.
- Component Customization:
react-markdown
: Thecomponents
prop directly replaces the React component used for rendering specific HTML tags (e.g.,h1
,code
).- Plate: Component customization happens in two places:
MarkdownPlugin
rules
: Customize the conversion between mdast and Slate JSON.createPlateEditor
override.components
: Customize the React component used by Plate to render a specific Slate node type (e.g.,p
,code_block
). See Appendix C: Components.
- Plugin Ecosystem:
react-markdown
: Uses bothremarkPlugins
(operating on mdast) andrehypePlugins
(operating on hast, often for rendering aspects like raw HTML or KaTeX).@udecode/plate-markdown
: Primarily usesremarkPlugins
.rehype
plugins are generally not needed for rendering, as Plate components handle that. They might be used in advanced scenarios within the remark pipeline (likerehype-raw
for parsing HTML initially).
Mapping Options:
react-markdown Prop | @udecode/plate-markdown Equivalent / Concept | Notes |
---|---|---|
children (string) | Pass string to editor.api.markdown.deserialize(string) | Input for deserialization. Often used in createPlateEditor 's value option. |
remarkPlugins | MarkdownPlugin.configure({ options: { remarkPlugins: [...] }}) | Direct mapping. Operates on mdast. |
rehypePlugins | Usually not needed. Use remarkPlugins for syntax. | Rendering is handled by Plate components. For raw HTML, use rehype-raw via remarkPlugins . |
components={{ h1: MyH1 }} | createPlateEditor({ override: { components: { h1: MyH1 } } }) | Overrides Plate rendering component. See Appendix C. |
components={{ code: MyCode }} | 1. Conversion: MarkdownPlugin > rules > code . 2. Rendering: override: { components: { code_block: MyCode } } | rules handles Markdown AST (code ) to Slate (code_block ). override.components handles Slate (code_block ) rendering. See Syntax Highlighting example. |
allowedElements | MarkdownPlugin.configure({ options: { allowedNodes: [...] }}) | Filters nodes during conversion (based on mdast/Slate types). |
disallowedElements | MarkdownPlugin.configure({ options: { disallowedNodes: [...] }}) | Filters nodes during conversion. |
unwrapDisallowed | No direct equivalent. Filtering removes nodes. | Custom rules could potentially implement unwrapping logic during conversion. |
skipHtml | Default behavior generally ignores/strips HTML tags. | Use rehype-raw via remarkPlugins if HTML processing is required. |
urlTransform | Customize via rules for link (deserialize) or a (serialize). | Handle URL transformations within the conversion rules. |
allowElement | MarkdownPlugin.configure({ options: { allowNode: { ... } } }) | Provides function-based filtering during conversion. |
Appendix A: HTML in Markdown
By default, for security reasons, @udecode/plate-markdown
does not process raw HTML tags within Markdown. Standard Markdown syntax that generates HTML (like *emphasis*
becoming <em>emphasis</em>
) is handled correctly, but literal <div>
, <script>
, etc., tags are typically ignored or stripped during the conversion process.
If you are in a trusted environment where you control the Markdown source and need to render raw HTML:
- Include
remark-mdx
: Addremark-mdx
to yourremarkPlugins
. - Use
rehype-raw
: Addrehype-raw
to yourremarkPlugins
.rehype-raw
parses the raw HTML snippets into a standardhast
structure. - Configure Rules: You might need to add specific
rules
to handle the conversion of the parsed HTMLhast
nodes (e.g.,element
nodes with various tag names) into appropriate Slate structures, or configure Plate components to render them.
import { MarkdownPlugin } from '@udecode/plate-markdown';
import remarkMdx from 'remark-mdx';
import rehypeRaw from 'rehype-raw';
// You might need VFile for rehype-raw
// import { VFile } from 'vfile';
MarkdownPlugin.configure({
options: {
remarkPlugins: [
remarkMdx,
// Rehype plugins can sometimes be used *within* remark pipeline
// Be cautious, as this can be complex.
[rehypeRaw, { /* pass options if needed, e.g., pass vfile */ }],
],
rules: {
// Example: Rule to handle a specific HTML tag parsed by rehype-raw
// Note: mdastNode structure depends on rehype-raw output
element: { // Generic rule for element nodes from rehype-raw
deserialize: (mdastNode, deco, options) => {
// VERY simplified - needs proper handling based on tag name (mdastNode.tagName)
// and attributes. You'll likely need specific rules per tag.
if (mdastNode.tagName === 'div') {
return {
type: 'div', // You'd need a 'div' Plate element
children: convertChildrenDeserialize(mdastNode.children, deco, options),
};
}
// Fallback or handle other tags
return convertChildrenDeserialize(mdastNode.children, deco, options);
}
},
// Add serialization rules if you need to output raw HTML from Slate
},
},
});
Security Warning: Enabling raw HTML rendering significantly increases the risk of XSS attacks if the Markdown source is not fully trusted. Use rehype-sanitize
in your plugin chain after rehype-raw
to configure exactly which HTML elements and attributes are allowed.
Appendix B: Customizing Conversion Rules (rules
)
The rules
option provides granular control over how elements are converted between Markdown AST (mdast
) and Slate JSON. It's an object where keys match node types.
- For Deserialization (Markdown → Slate): Keys are
mdast
node types (e.g.,paragraph
,heading
,strong
,link
,image
,code
,list
,listItem
,table
,tableRow
,tableCell
, or custom types from plugins likemath
,inlineMath
,footnoteReference
, or MDX types likemdxJsxTextElement
). Thedeserialize
function receives themdastNode
,deco
(current decorations), andoptions
, and should return a SlateDescendant
or an array ofDescendant
s. - For Serialization (Slate → Markdown): Keys are Slate element/text types (e.g.,
p
,h1
,a
,img
,code_block
,bold
,italic
, or custom types likedate
,mention
). Theserialize
function receives theslateNode
andoptions
, and should return anmdast
node.
Example: Overriding Link Deserialization
MarkdownPlugin.configure({
options: {
rules: {
// Rule for mdast 'link' type
link: {
deserialize: (mdastNode, deco, options) => {
// Default rule creates { type: 'a', url: ..., children: [...] }
// Let's add a custom property
return {
type: 'a', // Plate link element type
url: mdastNode.url,
title: mdastNode.title, // Add title if present
customProp: 'added-during-deserialize',
children: convertChildrenDeserialize(mdastNode.children, deco, options),
};
},
// Optionally add a serialize rule if the default doesn't handle 'customProp'
serialize: (slateNode, options) => {
return {
type: 'link', // mdast type
url: slateNode.url,
title: slateNode.title,
children: convertNodesSerialize(slateNode.children, options)
}
}
},
// Rule for Plate 'a' type (if serialization needs override)
a: {
serialize: (slateNode, options) => {
return {
type: 'link', // mdast type
url: slateNode.url,
title: slateNode.title,
children: convertNodesSerialize(slateNode.children, options)
}
}
}
},
// ... remarkPlugins ...
}
})
Default Rules Summary:
Here's a summary of some key default conversions. See defaultRules.ts
for the complete list and implementation details.
Markdown (mdast) | Plate Type | Notes |
---|---|---|
paragraph | p | |
heading (depth) | h1 - h6 | Converts based on depth. |
blockquote | blockquote | |
list (ordered) | ol / p * | Converts to ol /li /lic or p with list props (indent). |
list (unordered) | ul / p * | Converts to ul /li /lic or p with list props (indent). |
listItem | li / p * | Handled by list rule. |
code (fenced) | code_block | Contains code_line children. |
inlineCode | code (mark) | Applied to text nodes. |
strong | bold (mark) | Applied to text nodes. |
emphasis | italic (mark) | Applied to text nodes. |
delete | strikethrough (mark) | Applied to text nodes. |
link | a | |
image | img | Wraps in a paragraph during serialization. |
thematicBreak | hr | |
table | table | Contains tr children. |
tableRow | tr | Contains th /td children. |
tableCell | th / td | Contains p children. |
math (block) | equation | Requires remark-math . |
inlineMath | inline_equation | Requires remark-math . |
html (<br> ) | \n (in text) | Breaks within paragraphs. |
mdxJsxFlowElement | Custom | Requires remark-mdx and custom rules . |
mdxJsxTextElement | Custom | Requires remark-mdx and custom rules . |
- The conversion target for lists (
ol
/ul
vs.p
with indent props) depends on whetherIndentListPlugin
is detected in the editor configuration.
Appendix C: Components
While rules
customize the conversion between Markdown and Slate, Plate uses React components to render the Slate nodes in the editor. You can override these rendering components using the override.components
option in createPlateEditor
.
This is similar in concept to react-markdown
's components
prop, but it operates within the Plate editor context.
Keys in override.components
typically correspond to the key
of the Plate plugin responsible for that node type (e.g., ParagraphPlugin.key
, HeadingPlugin.keys.h1
, CodeBlockPlugin.key
) or the default Slate type (p
, h1
, code_block
).
Example:
import { createPlateEditor, PlateElement, PlateLeaf, withProps } from '@udecode/plate/react';
// Import necessary plugins
import { ParagraphPlugin } from '@udecode/plate-paragraph/react';
import { BoldPlugin } from '@udecode/plate-basic-marks/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
// Import your custom components or Plate UI components
import { ParagraphElement } from '@/registry/default/plate-ui/paragraph-element';
import { CodeBlockElement } from '@/registry/default/plate-ui/code-block-element';
import { CodeLeaf } from '@/registry/default/plate-ui/code-leaf';
const editor = createPlateEditor({
plugins: [
ParagraphPlugin,
BoldPlugin,
CodeBlockPlugin,
// ... other plugins
],
override: {
components: {
// Map Slate type 'p' to ParagraphElement component
[ParagraphPlugin.key]: ParagraphElement,
// Map Slate type 'code_block' to CodeBlockElement component
[CodeBlockPlugin.key]: CodeBlockElement,
// Map Slate mark 'bold' to render as <strong>
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
// Map Slate mark 'code' to CodeLeaf component
// Note: CodeLeaf applies syntax highlighting styles
[CodeBlockPlugin.keys.syntax]: CodeLeaf, // Assuming CodeBlockPlugin defines keys.syntax for the leaf
},
},
});
// Now, when Plate renders a node with type: 'p', it will use ParagraphElement.
// When it renders text with { bold: true }, it will use PlateLeaf rendered as <strong>.
Refer to the Plugin Components documentation for more details on creating and registering components.
Appendix D: PlateMarkdown Component
While Plate's architecture differs from react-markdown
, you can create a component that provides a similar API for rendering Markdown content using a self-contained Plate instance:
import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
// Import necessary Plate plugins for rendering common Markdown features
import { ParagraphPlugin } from '@udecode/plate-paragraph/react';
import { HeadingPlugin } from '@udecode/plate-heading/react';
import { BlockquotePlugin } from '@udecode/plate-block-quote/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
import { ListPlugin } from '@udecode/plate-list/react';
import { TablePlugin } from '@udecode/plate-table/react';
import { LinkPlugin } from '@udecode/plate-link/react';
import { ImagePlugin } from '@udecode/plate-media/react';
import {
BoldPlugin,
ItalicPlugin,
StrikethroughPlugin,
} from '@udecode/plate-basic-marks/react';
export interface PlateMarkdownProps {
/**
* Markdown content to render
*/
children: string;
/**
* Array of remark plugins to transform the markdown
*/
remarkPlugins?: any[];
/**
* Override components for specific node types
* Keys should match Plate plugin keys or default types
*/
components?: Record<string, React.ComponentType<any>>;
/**
* Additional className for the editor container
*/
className?: string;
}
export function PlateMarkdown({
children,
remarkPlugins = [],
components = {},
className,
}: PlateMarkdownProps) {
const editor = usePlateEditor({
// Include plugins for all markdown features you want to support
plugins: [
ParagraphPlugin,
HeadingPlugin,
BlockquotePlugin,
CodeBlockPlugin,
ListPlugin,
TablePlugin,
LinkPlugin,
ImagePlugin,
BoldPlugin,
ItalicPlugin,
StrikethroughPlugin,
MarkdownPlugin.configure({
options: { remarkPlugins },
}),
],
// Apply component overrides
override: { components },
});
// Update content when markdown changes
useEffect(() => {
editor.tf.reset();
editor.tf.setValue(
editor.getApi(MarkdownPlugin).markdown.deserialize(children)
);
}, [children, editor]);
return (
<Plate editor={editor}>
<PlateContent
readOnly
className={className}
/>
</Plate>
);
}
// Usage Example:
// const markdown = `# Hello
// This is *Markdown* rendered by Plate.
// `
//
// <PlateMarkdown
// remarkPlugins={[remarkGfm]}
// components={{
// h1: MyStyledH1,
// code_block: MyCodeBlock,
// }}
// className="prose dark:prose-invert"
// >
// {markdown}
// </PlateMarkdown>
Note: This component provides a read-only view of Markdown content. For editing capabilities, see Getting Started.
Security
@udecode/plate-markdown
aims to be safe by default by converting Markdown to a structured Slate format rather than directly rendering HTML. However, security depends on configuration and usage:
- Custom
rules
: Malformeddeserialize
rules could potentially introduce unsafe data into the Slate state if not handled carefully, although rendering is typically controlled by Plate components. remarkPlugins
: Plugins added viaremarkPlugins
could modify the content in insecure ways. Vet any third-party plugins.- Raw HTML: Explicitly enabling raw HTML processing (e.g., using
rehype-raw
) is inherently risky if the Markdown source is not trusted. Always use sanitization (likerehype-sanitize
) in such cases. - Plugin Responsibility: Some security aspects depend on the Plate plugins themselves, such as URL validation logic within
LinkPlugin
(isUrl
) orMediaEmbedPlugin
(parseMediaUrl
). Ensure these plugins are configured securely.
Recommendation: Treat any Markdown input from untrusted sources with caution. If allowing complex features or raw HTML, implement robust sanitization.
Related
- remark: Markdown processor.
- unified: Core processing engine used by remark.
- MDX: Allows using JSX within Markdown (relevant for custom element serialization).
- react-markdown: A popular React component for rendering Markdown (different architecture).
- remark-slate-transformer: Credits to inokawa for the initial work on converting between remark (mdast) and Slate structures.