Markdown

Markdown ↔ Plate

Loading...
Files
components/markdown-to-slate-demo.tsx
'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

![This is an alt text.](https://images.unsplash.com/photo-1506619216599-9d16d0903dfd?q=80&w=2669&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D "This is a sample image.")

## 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>
  );
}
Loading...
Files
components/demo.tsx
'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

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),
});

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:

  1. Serialization (Slate → Markdown): The Slate date node is converted to the MDX tag: <date>2025-03-31</date>.
  2. Deserialization (Markdown → Slate): The MDX tag <date>2025-03-31</date> is parsed back into the original Slate date node structure.

API

MarkdownPlugin

The core plugin configuration object. Use MarkdownPlugin.configure({ options: {} }) to set options.

Options

Collapse all

    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.

    • deserialize?: (mdastNode: any) => boolean: Filter function during Markdown → Slate conversion. Return true to keep, false to discard.
    • serialize?: (slateNode: any) => boolean: Filter function during Slate → Markdown conversion. Return true to keep, false to discard. Default: null.

    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: [].

Attributes

Collapse all

    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[] ).

Parameters

Collapse all

    The Markdown string to deserialize.

    Options for this specific deserialization call, potentially overriding plugin defaults.

OptionsDeserializeMdOptions

Collapse all

    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).

ReturnsDescendant[]

    An array of Slate nodes representing the deserialized Markdown content.


editor.api.markdown.serialize

Converts a Slate Value ( Descendant[] ) into a Markdown string.

Parameters

Collapse all

    Options for this specific serialization call, potentially overriding plugin defaults.

OptionsSerializeMdOptions

Collapse all

    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.

Returnsstring

    A Markdown string representing the serialized Slate content.


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.

Parameters

Collapse all

    The Markdown string to parse.

    Options for parsing.

OptionsParseMarkdownBlocksOptions

Collapse all

    Array of marked token types (e.g., 'space', 'hr') to exclude from the output. Default: ['space'].

    Whether to trim whitespace from the end of the input Markdown string before parsing. Default: true.

ReturnsToken[]

    An array of marked Token objects, each representing a block-level element with its raw Markdown source.

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 Slate code_block elements containing code_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 like lowlight (via CodeBlockPlugin). See the Basic Elements > Code Block documentation for details on configuring syntax highlighting with lowlight 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:

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.

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:

  1. 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 configured rules
    • Plate renders the nodes using its component system
  2. Stringify (Serialization):

    • Plate nodes are converted to mdast using slate-to-mdast and configured rules
    • remarkPlugins transform the mdast (e.g., formatting)
    • remark-stringify converts the mdast back to a Markdown string

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.

Migrating from react-markdown

Migrating from react-markdown to Plate with @udecode/plate-markdown involves understanding the architectural differences and mapping concepts.

Key Differences:

  1. 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.
  2. Component Customization:
    • react-markdown: The components 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.
  3. Plugin Ecosystem:
    • react-markdown: Uses both remarkPlugins (operating on mdast) and rehypePlugins (operating on hast, often for rendering aspects like raw HTML or KaTeX).
    • @udecode/plate-markdown: Primarily uses remarkPlugins. rehype plugins are generally not needed for rendering, as Plate components handle that. They might be used in advanced scenarios within the remark pipeline (like rehype-raw for parsing HTML initially).

Mapping Options:

react-markdown Prop@udecode/plate-markdown Equivalent / ConceptNotes
children (string)Pass string to editor.api.markdown.deserialize(string)Input for deserialization. Often used in createPlateEditor's value option.
remarkPluginsMarkdownPlugin.configure({ options: { remarkPlugins: [...] }})Direct mapping. Operates on mdast.
rehypePluginsUsually 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.
allowedElementsMarkdownPlugin.configure({ options: { allowedNodes: [...] }})Filters nodes during conversion (based on mdast/Slate types).
disallowedElementsMarkdownPlugin.configure({ options: { disallowedNodes: [...] }})Filters nodes during conversion.
unwrapDisallowedNo direct equivalent. Filtering removes nodes.Custom rules could potentially implement unwrapping logic during conversion.
skipHtmlDefault behavior generally ignores/strips HTML tags.Use rehype-raw via remarkPlugins if HTML processing is required.
urlTransformCustomize via rules for link (deserialize) or a (serialize).Handle URL transformations within the conversion rules.
allowElementMarkdownPlugin.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:

  1. Include remark-mdx: Add remark-mdx to your remarkPlugins.
  2. Use rehype-raw: Add rehype-raw to your remarkPlugins. rehype-raw parses the raw HTML snippets into a standard hast structure.
  3. Configure Rules: You might need to add specific rules to handle the conversion of the parsed HTML hast 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 like math, inlineMath, footnoteReference, or MDX types like mdxJsxTextElement). The deserialize function receives the mdastNode, deco (current decorations), and options, and should return a Slate Descendant or an array of Descendants.
  • For Serialization (Slate → Markdown): Keys are Slate element/text types (e.g., p, h1, a, img, code_block, bold, italic, or custom types like date, mention). The serialize function receives the slateNode and options, and should return an mdast 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 TypeNotes
paragraphp
heading (depth)h1 - h6Converts based on depth.
blockquoteblockquote
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).
listItemli / p*Handled by list rule.
code (fenced)code_blockContains code_line children.
inlineCodecode (mark)Applied to text nodes.
strongbold (mark)Applied to text nodes.
emphasisitalic (mark)Applied to text nodes.
deletestrikethrough (mark)Applied to text nodes.
linka
imageimgWraps in a paragraph during serialization.
thematicBreakhr
tabletableContains tr children.
tableRowtrContains th/td children.
tableCellth / tdContains p children.
math (block)equationRequires remark-math.
inlineMathinline_equationRequires remark-math.
html (<br>)\n (in text)Breaks within paragraphs.
mdxJsxFlowElementCustomRequires remark-mdx and custom rules.
mdxJsxTextElementCustomRequires remark-mdx and custom rules.
  • The conversion target for lists (ol/ul vs. p with indent props) depends on whether IndentListPlugin 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>

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: Malformed deserialize 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 via remarkPlugins 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 (like rehype-sanitize) in such cases.
  • Plugin Responsibility: Some security aspects depend on the Plate plugins themselves, such as URL validation logic within LinkPlugin (isUrl) or MediaEmbedPlugin (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.

  • 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.