Markdown
Convert Plate content to Markdown and vice-versa.
The @udecode/plate-markdown
package provides robust, two-way conversion between Markdown and Plate's content structure.
'use client';
import * as React from 'react';
import {
MarkdownPlugin,
remarkMdx,
remarkMention,
} from '@udecode/plate-markdown';
import { Plate, usePlateEditor } from '@udecode/plate/react';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import { editorPlugins } from '@/components/editor/plugins/editor-plugins';
import { editorComponents } from '@/components/editor/use-create-editor';
import { useDebounce } from '@/hooks/use-debounce';
import { Editor, EditorContainer } from '@/components/ui/editor';
const initialMarkdown = `## Basic Markdown
> The following node and marks is supported by the Markdown standard.
Format text with **bold**, _italic_, _**combined styles**_, ~~strikethrough~~, \`code\` formatting, and [hyperlinks](https://en.wikipedia.org/wiki/Hypertext).
\`\`\`javascript
// Use code blocks to showcase code snippets
function greet() {
console.info("Hello World!")
}
\`\`\`
- Simple lists for organizing content
1. Numbered lists for sequential steps
| **Plugin** | **Element** | **Inline** | **Void** |
| ----------- | ----------- | ---------- | -------- |
| **Heading** | | | No |
| **Image** | Yes | No | Yes |
| **Mention** | Yes | Yes | Yes |

- [x] Completed tasks
- [ ] Pending tasks
---
## Advanced Features
<callout>
The following node and marks are not supported in Markdown but can be serialized and deserialized using MDX or specialized UnifiedJS plugins.
</callout>
Advanced marks: <kbd>⌘ + B</kbd>,<u>underlined</u>, <mark>highlighted</mark> text, <span style="color: #93C47D;">colored text</span> and <span style="background-color: #6C9EEB;">background highlights</span> for visual emphasis.
Superscript like E=mc<sup>2</sup> and subscript like H<sub>2</sub>O demonstrate mathematical and chemical notation capabilities.
Add mentions like @BB-8, dates (<date>2025-05-08</date>), and math formulas ($E=mc^2$).
The table of contents feature automatically generates document structure for easy navigation.
<toc />
Math formula support makes displaying complex mathematical expressions simple.
$$
\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
$$
Multi-column layout features enable richer page designs and content layouts.
<column_group layout="[50,50]">
<column width="50%">
left
</column>
<column width="50%">
right
</column>
</column_group>
PDF embedding makes document referencing simple and intuitive.
<file name="sample.pdf" align="center" src="https://s26.q4cdn.com/900411403/files/doc_downloads/test.pdf" width="80%" isUpload="true" />
Audio players can be embedded directly into documents, supporting online audio resources.
<audio align="center" src="https://samplelib.com/lib/preview/mp3/sample-3s.mp3" width="80%" />
Video playback features support embedding various online video resources, enriching document content.
<video align="center" src="https://videos.pexels.com/video-files/6769791/6769791-uhd_2560_1440_24fps.mp4" width="80%" isUpload="true" />
: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(
{
components: editorComponents,
plugins: editorPlugins,
value: (editor) =>
editor.getApi(MarkdownPlugin).markdown.deserialize(initialMarkdown, {
remarkPlugins: [
remarkMath,
remarkGfm,
remarkMdx,
remarkMention,
remarkEmoji as any,
],
}),
},
[]
);
React.useEffect(() => {
if (debouncedMarkdownValue !== initialMarkdown) {
editor.tf.reset();
editor.tf.setValue(
editor.api.markdown.deserialize(debouncedMarkdownValue, {
remarkPlugins: [
remarkMath,
remarkGfm,
remarkMdx,
remarkMention,
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 * as 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/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
- Markdown to Slate JSON: Convert Markdown strings to Plate's editable format (
deserialize
). - Slate JSON to Markdown: Convert Plate content back to Markdown strings (
serialize
). - Safe by Default: Handles Markdown conversion without
dangerouslySetInnerHTML
. - Customizable Rules: Define how specific Markdown syntax or custom Plate elements are converted using
rules
. Supports MDX. - Extensible: Utilize remark plugins via the
remarkPlugins
option. - Compliant: Supports CommonMark, with GFM (GitHub Flavored Markdown) available via
remark-gfm
. - Round-Trip Serialization: Preserves custom elements through MDX syntax during conversion cycles.
Why Use @udecode/plate-markdown
?
While libraries like react-markdown
render Markdown to React elements, @udecode/plate-markdown
offers deeper integration with the Plate ecosystem:
- Rich Text Editing: Enables advanced editing features by converting Markdown to Plate's structured format.
- WYSIWYG Experience: Edit content in a rich text view and serialize it back to Markdown.
- Custom Elements & Data: Handles complex custom Plate elements (mentions, embeds) by converting them to/from MDX.
- Extensibility: Leverages Plate's plugin system and the unified/remark ecosystem for powerful customization.
If you only need to display Markdown as HTML without editing or custom elements, react-markdown
might be sufficient. For a rich text editor with Markdown import/export and custom content, @udecode/plate-markdown
is the integrated solution.
Installation
pnpm add @udecode/plate @udecode/plate-markdown
Usage
Configure the Plugin
Configuring MarkdownPlugin
is recommended to enable Markdown paste handling and set default conversion rules
.
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)
remarkPlugins: [remarkMath, remarkGfm, remarkMdx, remarkMention],
// Define custom rules if needed
rules: {
// date: { /* ... rule implementation ... */ },
},
},
}),
],
});
// To disable Markdown paste handling:
const editorWithoutPaste = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure(() => ({ parser: null })),
],
});
If you don't use MarkdownPlugin
with configure
, you can still use editor.api.markdown.deserialize
and editor.api.markdown.serialize
directly, but without plugin-configured default rules or 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 for the editor's initial content.
import { createPlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
// ... import other necessary Plate plugins for rendering elements
const markdownString = '# Hello, *Plate*!';
const editor = createPlateEditor({
plugins: [
// MarkdownPlugin must be included
MarkdownPlugin,
// ... other plugins needed to render the deserialized elements (e.g., HeadingPlugin, ItalicPlugin)
],
// Use deserialize in the value factory for initial content
value: (editor) => editor.getApi(MarkdownPlugin).markdown.deserialize(markdownString),
});
Plugin Requirements
Ensure all Plate plugins required to render the deserialized Markdown (e.g., HeadingPlugin
for #
, TablePlugin
for tables) are included in your editor's plugins
array.
Plate to Markdown (Serialization)
Use editor.api.markdown.serialize
to convert the current editor content (or a specific array of nodes) into a Markdown string.
Serializing Current Editor Content:
// Assuming `editor` is your Plate editor instance with content
const markdownOutput = editor.api.markdown.serialize();
console.info(markdownOutput);
Serializing Specific Nodes:
const specificNodes = [
{ type: 'p', children: [{ text: 'Serialize just this paragraph.' }] },
{ type: 'h1', children: [{ text: 'And this heading.' }] }
];
// Assuming `editor` is your Plate editor instance
const partialMarkdownOutput = editor.api.markdown.serialize({
value: specificNodes,
});
console.info(partialMarkdownOutput);
Round-Trip Serialization with Custom Elements (MDX)
A key feature is handling custom Plate elements that lack standard Markdown representation (e.g., underline, mentions). @udecode/plate-markdown
converts these to MDX elements during serialization and parses them back during deserialization.
Example: Handling a custom date
element.
Slate Node Structure:
{
type: 'p',
children: [
{ text: 'Today is ' },
{ type: 'date', date: '2025-03-31', children: [{ text: '' }] } // Leaf elements need a text child
],
}
Plugin Configuration with rules
:
import type { MdMdxJsxTextElement } from '@udecode/plate-markdown';
import { MarkdownPlugin, remarkMdx } from '@udecode/plate-markdown';
// ... other imports
MarkdownPlugin.configure({
options: {
rules: {
// Key matches:
// 1. Plate element's plugin 'key' or 'type'.
// 2. mdast node type.
// 3. MDX tag name.
date: {
// Markdown -> Slate
deserialize(mdastNode: MdMdxJsxTextElement, deco, options) {
const dateValue = (mdastNode.children?.[0] as any)?.value || '';
return {
type: 'date', // Your Plate element type
date: dateValue,
children: [{ text: '' }], // Valid Slate structure
};
},
// Slate -> Markdown (MDX)
serialize: (slateNode): MdMdxJsxTextElement => {
return {
type: 'mdxJsxTextElement',
name: 'date', // MDX tag name
attributes: [], // Optional: [{ type: 'mdxJsxAttribute', name: 'date', value: slateNode.date }]
children: [{ type: 'text', value: slateNode.date || '1999-01-01' }],
};
},
},
// ... rules for other custom elements
},
remarkPlugins: [remarkMdx /*, ... other remark plugins like remarkGfm */],
},
});
Conversion Process:
- Serialization (Slate → Markdown): The Plate
date
node becomes<date>2025-03-31</date>
. - Deserialization (Markdown → Slate): The MDX tag
<date>2025-03-31</date>
converts back to the Platedate
node.
API Reference
MarkdownPlugin
The core plugin configuration object. Use MarkdownPlugin.configure({ options: {} })
to set global options for Markdown processing.
deserialize?: (mdastNode: any) => boolean
: Filter for Markdown → Slate. Returntrue
to keep.serialize?: (slateNode: any) => boolean
: Filter for Slate → Markdown. Returntrue
to keep. Default:null
.
Whitelist specific node types (Slate types and Markdown AST types like strong
). Cannot be used with disallowedNodes
. If set, only listed types are processed. Default: null
(all allowed).
Blacklist specific node types. Cannot be used with allowedNodes
. Listed types are filtered out. Default: null
.
Fine-grained node filtering with custom functions, applied after allowedNodes
/disallowedNodes
.
Custom conversion rules between Markdown AST and Slate elements. See Round-Trip Serialization and Customizing Conversion Rules. For marks/leaves, ensure the rule object has mark: true
. Default: null
(uses internal defaultRules
).
Array of remark plugins (e.g., remark-gfm
, remark-math
, remark-mdx
). Operates on Markdown AST (mdast
). Default: []
.
Configuration for pasted content. Set to null
to disable Markdown paste handling. Default enables pasting text/plain
as Markdown. See PlatePlugin API > parser.
editor.api.markdown.deserialize
Converts a Markdown string into a Slate Value
(Descendant[]
).
Override plugin allowedNodes
.
Override plugin disallowedNodes
.
Override plugin allowNode
.
Adds _memo
property with raw Markdown to top-level blocks for memoization (e.g., with PlateStatic
). Default: false
.
Override plugin rules
.
Options for the underlying Markdown block parser (parseMarkdownBlocks
). See below.
Override plugin remarkPlugins
.
If true
, single line breaks (\\n
) in paragraphs become paragraph breaks. Default: false
.
editor.api.markdown.serialize
Converts a Slate Value
(Descendant[]
) into a Markdown string.
parseMarkdownBlocks
Utility to parse a Markdown string into block-level tokens (used by deserialize
, useful with memoize
).
Examples
Using a Remark Plugin (GFM)
Add support for GitHub Flavored Markdown (tables, strikethrough, task lists, autolinks).
Plugin Configuration:
import { createPlateEditor } from '@udecode/plate/react';
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'; // Ensure this is the correct List plugin for tasks
import { StrikethroughPlugin } from '@udecode/plate-basic-marks/react';
import { LinkPlugin } from '@udecode/plate-link/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
TablePlugin,
TodoListPlugin, // Or your specific task list plugin
StrikethroughPlugin,
LinkPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm],
},
}),
],
});
Usage:
const markdown = `
A table:
| a | b |
| - | - |
~~Strikethrough~~
- [x] Task list item
Visit https://platejs.org
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// editor.tf.setValue(slateValue); // To set editor content
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain GFM syntax
Customizing Rendering (Syntax Highlighting)
This example shows two approaches: customizing the rendering component (common for UI changes) and customizing the conversion rule (advanced, for changing Slate structure).
Background:
@udecode/plate-markdown
converts Markdown fenced code blocks (e.g., ```js ... ```) to Slatecode_block
elements withcode_line
children.- The Plate
CodeBlockElement
(often from@udecode/plate-code-block/react
) renders this structure. - Syntax highlighting typically occurs within
CodeBlockElement
using a library likelowlight
(viaCodeBlockPlugin
). See Code Block Plugin for details.
Approach 1: Customizing Rendering Component (Recommended for UI)
To change how code blocks appear, customize the component for the code_block
plugin key.
import { createPlateEditor } from '@udecode/plate/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react'; // Main plugin
import { MarkdownPlugin } from '@udecode/plate-markdown';
import { MyCustomCodeBlockElement } from './my-custom-code-block'; // Your custom component
const editor = createPlateEditor({
plugins: [
CodeBlockPlugin, // Base plugin for structure/logic
MarkdownPlugin, // For Markdown conversion
// ... other plugins
],
components: {
[CodeBlockPlugin.key]: MyCustomCodeBlockElement,
// You might also need to customize components for CodeLinePlugin.key and CodeSyntaxPlugin.key
// depending on your MyCustomCodeBlockElement implementation.
},
});
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// (e.g., using react-syntax-highlighter), consuming props from PlateElement.
Refer to the Code Block Plugin documentation for complete examples.
Approach 2: Customizing Conversion Rule (Advanced - Changing Slate Structure)
To fundamentally alter the Slate JSON for code blocks (e.g., storing code as a single string prop), 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 mdast 'code' type
code: {
deserialize: (mdastNode, deco, options) => {
return {
type: CodeBlockPlugin.key, // Use Plate's type
lang: mdastNode.lang ?? undefined,
rawCode: mdastNode.value || '', // Store raw code directly
children: [{ text: '' }], // Slate Element needs a dummy text child
};
},
},
// A custom `serialize` rule for `code_block` would also be needed
// to convert `rawCode` back to an mdast 'code' node.
[CodeBlockPlugin.key]: { // Assuming CodeBlockPlugin.key is 'code_block'
serialize: (slateNode, options) => {
return { // mdast 'code' node
type: 'code',
lang: slateNode.lang,
value: slateNode.rawCode,
};
},
},
},
// remarkPlugins: [...]
},
});
// Your custom rendering component (MyCustomCodeBlockElement) would then
// need to read the code from the `rawCode` property.
Choose based on whether you're changing UI (Approach 1) or data structure (Approach 2).
Using Remark Plugins for Math (remark-math
)
Enable TeX math syntax ($inline$
, $$block$$
).
Plugin Configuration:
import { createPlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import { MathPlugin } from '@udecode/plate-math/react'; // Main Math plugin
const editor = createPlateEditor({
plugins: [
// ...other plugins
MathPlugin, // Renders block and inline equations
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath],
// Default rules handle 'math' and 'inlineMath' mdast types from remark-math,
// converting them to Plate's 'equation' and 'inline_equation' types.
},
}),
],
});
Usage:
const markdown = `
Inline math: $E=mc^2$
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// slateValue will contain 'inline_equation' and 'equation' nodes.
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// markdownOutput will contain $...$ and $$...$$ syntax.
Remark Plugins
@udecode/plate-markdown
leverages the unified / remark ecosystem. Extend its capabilities by adding remark plugins via the remarkPlugins
option in MarkdownPlugin.configure
. These plugins operate on the mdast (Markdown Abstract Syntax Tree).
Finding Plugins:
Common Uses:
- Syntax Extensions:
remark-gfm
(tables, etc.),remark-math
(TeX),remark-frontmatter
,remark-mdx
. - Linting/Formatting:
remark-lint
(often separate tooling). - Custom Transformations: Custom plugins to modify mdast.
Remark vs. Rehype
Plate components (e.g., TableElement
, CodeBlockElement
) render Slate JSON. remarkPlugins
modify the Markdown AST. Unlike some renderers, rehypePlugins
(for HTML AST) are generally not needed for Plate rendering, but can be used within the remark pipeline for complex HTML transformations (e.g., with rehype-raw
).
Syntax Support
@udecode/plate-markdown
uses remark-parse
, adhering to CommonMark. Enable GFM or other syntaxes via remarkPlugins
.
- Learn Markdown: CommonMark Help
- GFM Spec: GitHub Flavored Markdown Spec
Architecture Overview
@udecode/plate-markdown
bridges Markdown strings and Plate's editor format using the unified/remark ecosystem.
@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 | |
| | | | | | | | | |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| |
+--------------------------------------------------------------------------------------------+
Key Steps:
- Parse (Deserialization):
- Markdown string →
remark-parse
→ mdast. remarkPlugins
transform mdast (e.g.,remark-gfm
).mdast-to-slate
converts mdast to Plate nodes usingrules
.- Plate renders nodes via its component system.
- Markdown string →
- Stringify (Serialization):
- Plate nodes →
slate-to-mdast
(usingrules
) → mdast. remarkPlugins
transform mdast.remark-stringify
converts mdast to Markdown string.
- Plate nodes →
Comparison with react-markdown
- Direct Node Rendering: Plate directly renders its nodes via components, unlike
react-markdown
which often uses rehype to convert Markdown to HTML, then to React elements. - Bidirectional: Plate's Markdown processor is fully bidirectional.
- Rich Text Integration: Nodes are integrated with Plate's editing capabilities.
- Plugin System: Components are managed via Plate's plugin system.
Migrating from react-markdown
Migrating involves mapping react-markdown
concepts to Plate's architecture.
Key Differences:
- Rendering Pipeline:
react-markdown
(MD → mdast → hast → React) vs.@udecode/plate-markdown
(MD ↔ mdast ↔ Slate JSON; Plate components render Slate JSON). - Component Customization:
react-markdown
:components
prop replaces HTML tag renderers.- Plate:
MarkdownPlugin
rules
: Customize mdast ↔ Slate JSON conversion.createPlateEditor
components
: Customize React components for Plate node types. See Appendix C.
- Plugin Ecosystem:
@udecode/plate-markdown
primarily usesremarkPlugins
.rehypePlugins
are less common.
Mapping Options:
react-markdown Prop | @udecode/plate-markdown Equivalent/Concept | Notes |
---|---|---|
children (string) | Pass to editor.api.markdown.deserialize(string) | Input for deserialization; often in createPlateEditor value option. |
remarkPlugins | MarkdownPlugin.configure({ options: { remarkPlugins: [...] }}) | Direct mapping; operates on mdast. |
rehypePlugins | Usually not needed. Use remarkPlugins for syntax. | Plate components handle rendering. For raw HTML, use rehype-raw via remarkPlugins . |
components={{ h1: MyH1 }} | createPlateEditor({ components: { h1: MyH1 } }) | Configures Plate rendering component. Key depends on HeadingPlugin config. |
components={{ code: MyCode }} | 1. Conversion: MarkdownPlugin > rules > code . 2. Rendering: components: { [CodeBlockPlugin.key]: MyCode } | rules for mdast (code ) to Slate (code_block ). components for Slate rendering. |
allowedElements | MarkdownPlugin.configure({ options: { allowedNodes: [...] }}) | Filters nodes during conversion (mdast/Slate types). |
disallowedElements | MarkdownPlugin.configure({ options: { disallowedNodes: [...] }}) | Filters nodes during conversion. |
unwrapDisallowed | No direct equivalent. Filtering removes nodes. | Custom rules could implement unwrapping. |
skipHtml | Default behavior strips most HTML. | Use rehype-raw via remarkPlugins for HTML processing. |
urlTransform | Customize via rules for link (deserialize) or plugin type (serialize). | Handle URL transformations in conversion rules. |
allowElement | MarkdownPlugin.configure({ options: { allowNode: { ... } } }) | Function-based filtering during conversion. |
Appendix A: HTML in Markdown
By default, @udecode/plate-markdown
does not process raw HTML tags for security. Standard Markdown generating HTML (e.g., *emphasis*
→ <em>
) is handled.
To process raw HTML in a trusted environment:
- Include
remark-mdx
: Add toremarkPlugins
. - Use
rehype-raw
: Addrehype-raw
toremarkPlugins
. - Configure Rules: May need
rules
for parsed HTMLhast
nodes to Slate structures.
import { MarkdownPlugin, remarkMdx } from '@udecode/plate-markdown';
import rehypeRaw from 'rehype-raw'; // May require VFile, ensure compatibility
// import { VFile } from 'vfile'; // If needed by rehype-raw setup
MarkdownPlugin.configure({
options: {
remarkPlugins: [
remarkMdx,
// Using rehype plugins within remark pipeline can be complex.
[rehypeRaw, { /* options, e.g., pass vfile */ }],
],
rules: {
// Example: Rule for HTML tags parsed by rehype-raw.
// mdastNode structure depends on rehype-raw output.
element: { // Generic rule for 'element' nodes from rehype-raw
deserialize: (mdastNode, deco, options) => {
// Simplified: Needs proper handling based on mdastNode.tagName and attributes.
// You'll likely need specific rules per HTML tag.
if (mdastNode.tagName === 'div') {
return {
type: 'html_div', // Example: Map to a custom 'html_div' Plate element
children: convertChildrenDeserialize(mdastNode.children, deco, options),
};
}
// Fallback or handle other tags
return convertChildrenDeserialize(mdastNode.children, deco, options);
},
},
// Add serialization rules if outputting raw HTML from Slate.
},
},
});
Security Warning
Enabling raw HTML rendering increases XSS risk if the Markdown source isn't trusted. Use rehype-sanitize
after rehype-raw
to whitelist HTML elements/attributes.
Appendix B: Customizing Conversion Rules (rules
)
The rules
option in MarkdownPlugin.configure
offers fine-grained control over mdast ↔ Slate JSON conversion. Keys in the rules
object match node types.
- Deserialization (Markdown → Slate): Keys are
mdast
node types (e.g.,paragraph
,heading
,strong
,link
, MDX types likemdxJsxTextElement
). Thedeserialize
function takes(mdastNode, deco, options)
and returns a SlateDescendant
orDescendant[]
. - Serialization (Slate → Markdown): Keys are Plate element/text types (e.g.,
p
,h1
,a
,code_block
,bold
). Theserialize
function takes(slateNode, options)
and returns anmdast
node.
Example: Overriding Link Deserialization
MarkdownPlugin.configure({
options: {
rules: {
// Rule for mdast 'link' type
link: {
deserialize: (mdastNode, deco, options) => {
// Default creates { type: 'a', url: ..., children: [...] }
// Add a custom property:
return {
type: 'a', // Plate link element type
url: mdastNode.url,
title: mdastNode.title,
customProp: 'added-during-deserialize',
children: convertChildrenDeserialize(mdastNode.children, deco, options),
};
},
},
// Rule for Plate 'a' type (if serialization needs override for customProp)
a: { // Assuming 'a' is the Plate type for links
serialize: (slateNode, options) => {
// Default creates mdast 'link'
// Handle customProp if needed in MDX attributes or similar
return {
type: 'link', // mdast type
url: slateNode.url,
title: slateNode.title,
// customProp: slateNode.customProp, // MDX attribute?
children: convertNodesSerialize(slateNode.children, options),
};
},
},
},
// ... remarkPlugins ...
},
});
Default Rules Summary:
Refer to defaultRules.ts
for the complete list. Key conversions include:
Markdown (mdast) | Plate Type | Notes |
---|---|---|
paragraph | p | |
heading (depth) | h1 - h6 | Based on depth. |
blockquote | blockquote | |
list (ordered) | ol / p * | ol /li /lic or p with list indent props. |
list (unordered) | ul / p * | ul /li /lic or p with list indent props. |
code (fenced) | code_block | Contains code_line children. |
inlineCode | code (mark) | Applied to text. |
strong | bold (mark) | Applied to text. |
emphasis | italic (mark) | Applied to text. |
delete | strikethrough (mark) | Applied to text. |
link | a | |
image | img | Wraps in paragraph during serialization. |
thematicBreak | hr | |
table | table | Contains tr . |
math (block) | equation | Requires remark-math . |
inlineMath | inline_equation | Requires remark-math . |
mdxJsxFlowElement | Custom | Requires remark-mdx and custom rules . |
mdxJsxTextElement | Custom | Requires remark-mdx and custom rules . |
* List conversion depends on IndentListPlugin
detection.
Default MDX Conversions (with remark-mdx
):
MDX (mdast) | Plate Type | Notes |
---|---|---|
<del>...</del> | strikethrough (mark) | Alt for ~~strikethrough~~ |
<sub>...</sub> | subscript (mark) | H2O |
<sup>...</sup> | superscript (mark) | E=mc2 |
<u>...</u> | underline (mark) | Underlined |
<mark>...</mark> | highlight (mark) | Highlighted |
<span style="font-family: ..."> | fontFamily (mark) | |
<span style="font-size: ..."> | fontSize (mark) | |
<span style="font-weight: ..."> | fontWeight (mark) | |
<span style="color: ..."> | color (mark) | |
<span style="background-color: ..."> | backgroundColor (mark) | |
<date>...</date> | date | Custom Date element |
@mention | mention | Custom Mention element |
<file name="..." /> | file | Custom File element |
<audio src="..." /> | audio | Custom Audio element |
<video src="..." /> | video | Custom Video element |
<toc /> | toc | Table of Contents |
<callout>...</callout> | callout | Callout block |
<column_group>... | column_group | Multi-column layout |
<column>... | column | Single column (part of column_group) |
Appendix C: Components for Rendering
While rules
handle MD ↔ Slate conversion, Plate uses React components to render Slate nodes. Configure these in createPlateEditor
via the components
option. This is analogous to react-markdown
's components
prop but within Plate's ecosystem.
Keys in components
usually match Plate plugin keys (e.g., ParagraphPlugin.key
, CodeBlockPlugin.key
) or default Slate types (p
, h1
).
Example:
import { createPlateEditor, PlateLeaf, withProps } from '@udecode/plate/react';
import { BoldPlugin } from '@udecode/plate-basic-marks/react';
import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
import { ParagraphElement } from '@/components/ui/paragraph-element'; // Example UI component
import { CodeBlockElement } from '@/components/ui/code-block-element'; // Example UI component
// For marks, you might use PlateLeaf or a custom leaf component
// import { CodeLeaf } from '@/components/ui/code-leaf';
const editor = createPlateEditor({
plugins: [BoldPlugin, CodeBlockPlugin /* ... */],
components: {
[ParagraphPlugin.key]: ParagraphElement,
[CodeBlockPlugin.key]: CodeBlockElement,
[BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),
// For code syntax highlighting within code blocks:
// [CodeBlockPlugin.keys.syntax]: CodeLeaf, // If CodeBlockPlugin provides such a key
},
});
Refer to Plugin Components for more on creating/registering components.
Appendix D: PlateMarkdown
Component (Read-Only Display)
For a react-markdown
-like component for read-only display:
import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from '@udecode/plate/react';
import { MarkdownPlugin } from '@udecode/plate-markdown';
// Import necessary Plate plugins for common Markdown features
import { HeadingPlugin } from '@udecode/plate-heading/react';
// ... include other plugins like BlockquotePlugin, CodeBlockPlugin, ListPlugin, etc.
// ... and mark plugins like BoldPlugin, ItalicPlugin, etc.
export interface PlateMarkdownProps {
children: string; // Markdown content
remarkPlugins?: any[];
components?: Record<string, React.ComponentType<any>>; // Plate component overrides
className?: string;
}
export function PlateMarkdown({
children,
remarkPlugins = [],
components = {},
className,
}: PlateMarkdownProps) {
const editor = usePlateEditor({
plugins: [ // Include all plugins needed to render your Markdown
HeadingPlugin, /* ... other plugins ... */
MarkdownPlugin.configure({ options: { remarkPlugins } }),
],
components, // Pass through component overrides
});
useEffect(() => {
editor.tf.reset(); // Clear previous content
editor.tf.setValue(
editor.getApi(MarkdownPlugin).markdown.deserialize(children)
);
}, [children, editor, remarkPlugins]); // Re-deserialize if markdown or plugins change
return (
<Plate editor={editor}>
<PlateContent readOnly className={className} />
</Plate>
);
}
// Usage Example:
// const markdownString = "# Hello\nThis is *Markdown*.";
// <PlateMarkdown className="prose dark:prose-invert">
// {markdownString}
// </PlateMarkdown>
Initial Value
This PlateMarkdown
component provides a read-only view. For full editing, see the Installation guides.
Security Considerations
@udecode/plate-markdown
prioritizes safety by converting Markdown to a structured Slate format, avoiding direct HTML rendering. However, security depends on:
- Custom
rules
: Ensuredeserialize
rules don't introduce unsafe data. remarkPlugins
: Vet third-party remark plugins for potential security risks.- Raw HTML Processing: If
rehype-raw
is used, always sanitize withrehype-sanitize
if the source is untrusted. - Plugin Responsibility: URL validation in
LinkPlugin
(isUrl
) orMediaEmbedPlugin
(parseMediaUrl
) is crucial.
Recommendation: Treat untrusted Markdown input cautiously. Sanitize if allowing complex features or raw HTML.
Related Links
- remark: Markdown processor.
- unified: Core processing engine.
- MDX: JSX in Markdown.
- react-markdown: Alternative React Markdown component.
- remark-slate-transformer: Initial mdast ↔ Slate conversion work by inokawa.