Skip to main content
WebEditor supports markdown syntax for quick formatting and JSX-like tags for inserting components. This guide explains the parsing pipeline and how content is transformed.

Parsing pipeline

Markdown content flows through several stages before becoming editable content:
1

Preprocessing

The preprocessMarkdownToHTML function handles frontmatter removal and code block attributes
2

Markdown parsing

marked.js converts markdown syntax to HTML
3

DOM parsing

ProseMirror’s DOMParser converts HTML to document nodes based on the schema
4

Component resolution

Custom input rules detect JSX-like tags and create corresponding component nodes

Preprocessing stage

The preprocessing step prepares markdown for parsing:
~/workspace/source/packages/webeditor/src/editor/preprocessMarkdown.ts:29-36
export function preprocessMarkdownToHTML(markdown: string): string {
  markdown = markdown.trim();

  // Remove gray matter
  markdown = markdown.replace(/^---[\s\S]*?---\n?/m, "");

  return marked.parse(markdown, { renderer }) as string;
}

Code block handling

Code blocks receive special treatment to preserve language and title attributes:
~/workspace/source/packages/webeditor/src/editor/preprocessMarkdown.ts:16-27
renderer.code = (code: string, lang?: string) => {
  if (lang?.includes("#")) {
    const [language, title] = lang.split("#");
    return `<pre><code title="${escapeHtml(title)}" language="${escapeHtml(language)}">${code}</code></pre>`;
  }

  if (lang) {
    return `<pre><code language="${escapeHtml(lang)}">${code}</code></pre>`;
  }

  return `<pre><code>${code}</code></pre>`;
};
You can specify a code block title using the language#title syntax:
```javascript#Example.js
console.log('Hello world');
```

Markdown syntax support

WebEditor recognizes standard markdown patterns through input rules:

Text formatting

**bold text**
Triggers the addStrongRule to apply the strong mark

Block elements

Headings

# Heading 1
## Heading 2
### Heading 3

Lists

- Bullet item
* Also works

1. Numbered item
2. Second item

Blockquotes

> This is a quote
> Continues here

Code blocks

```javascript
const x = 42;
```

Input rules implementation

Input rules detect patterns as you type and transform them into appropriate nodes or marks:
~/workspace/source/packages/webeditor/src/editor/markdown/input.tsx:37-51
export const addStrongRule = new InputRule(/\*\*([^*]+)\*\*/, (state, match, start, end) => {
  const tr = state.tr;

  if (match) {
    const textContent = match[1];

    // Replace the entire match with just the text content
    tr.replaceWith(start, end, state.schema.text(textContent, [state.schema.marks.strong.create()]));

    // Don't continue with the mark
    tr.removeStoredMark(schema.marks.strong);
  }

  return tr;
});

JSX-like component syntax

WebEditor supports JSX-like tags for inserting rich components. These are detected by input rules and transformed into component nodes.

Basic component syntax

Components use XML-like tags with attributes:
<card title="My Card" icon="FileText" horizontal>
  Content goes here
</card>

Self-closing tags

Some components can be self-closing:
<break height="2" />
<icon name="Star" size="24" />
<badge text="New" variant="primary" />

Attribute parsing

Component attributes are extracted using regex patterns:
Example from card component
const addCardRule = new InputRule(/^<card(?:\s+([^>]+))?(?:\s*\/>|\s*>)/, (state, match, start, end) => {
  const { tr, schema } = state;

  // Default attributes
  const attrs = {
    title: "Card Title",
    icon: null,
    showIcon: true,
    horizontal: false,
    href: null,
  };

  // Parse attributes if they exist
  if (match[1]) {
    const attrString = match[1];

    // Parse title="..." attribute
    const titleMatch = attrString.match(/title="([^"]*)"/); 
    if (titleMatch) attrs.title = titleMatch[1];

    // Parse boolean attributes
    attrs.horizontal = /horizontal(?:=true|\s|$)/.test(attrString);
  }

  const card = schema.nodes.card.create(attrs, schema.nodes.paragraph.create());
  tr.replaceRangeWith(start, end, card);
  
  return tr;
});

Available component tags

WebEditor includes input rules for these component tags:
  • <card> - Card with title and icon
  • <callout> - Info/warning/error callouts
  • <tabs> - Tabbed interface
  • <accordion> - Collapsible section
  • <frame> - Content frame with caption
  • <columns> - Multi-column layout
  • <break> - Spacing between content
  • <code_snippet> - Syntax-highlighted code
  • <mermaid> - Mermaid diagram
  • <badge> - Inline badge
  • <icon> - Lucide icon
  • <field> - Parameter field

Document conversion

WebEditor provides utilities to convert between formats:

Markdown to ProseMirror

~/workspace/source/packages/webeditor/src/editor/index.tsx:64-67
function mdxLikeToProseMirror(markdown: string) {
  const html = preprocessMarkdownToHTML(markdown);
  return htmlToProseMirror(html);
}

HTML to ProseMirror

~/workspace/source/packages/webeditor/src/editor/index.tsx:53-62
function htmlToProseMirror(html: string) {
  const tempDiv = document.createElement("div");
  tempDiv.innerHTML = html;

  // Use ProseMirror's DOMParser to convert back
  const parser = DOMParser.fromSchema(schema);
  const fragment = parser.parseSlice(tempDiv).content;

  return fragment;
}

ProseMirror to HTML

~/workspace/source/packages/webeditor/src/editor/index.tsx:43-51
function proseMirrorToHTML(doc: Node) {
  const serializer = DOMSerializer.fromSchema(schema);
  const domNode = serializer.serializeFragment(doc.content);

  const container = document.createElement("div");
  container.appendChild(domNode);

  return container.innerHTML;
}

Paste handling

WebEditor includes custom paste handling to preserve formatting:
Custom paste plugin
function customPastePlugin() {
  return new Plugin({
    props: {
      handlePaste(view, event) {
        if (event.clipboardData) {
          // Prefer HTML if available
          const html = event.clipboardData.getData("text/html");
          if (html) {
            const fragment = htmlToProseMirror(html);
            if (fragment) {
              const topNode = view.state.schema.topNodeType.create(null, fragment);
              view.dispatch(view.state.tr.replaceSelectionWith(topNode));
              return true;
            }
          }

          // Fallback to plain text
          const text = event.clipboardData.getData("text/plain");
          if (text) {
            const fragment = mdxLikeToProseMirror(text);
            if (fragment) {
              const topNode = view.state.schema.topNodeType.create(null, fragment);
              view.dispatch(view.state.tr.replaceSelectionWith(topNode));
              return true;
            }
          }
        }
        return false;
      },
    },
  });
}
When pasting content, WebEditor first attempts to preserve rich HTML formatting, then falls back to parsing as markdown if only plain text is available.

Input rule registration

All input rules are registered in the input plugin:
~/workspace/source/packages/webeditor/src/editor/markdown/input.tsx:115-142
export function inputPlugin() {
  return inputRules({
    rules: [
      addHeadersRule,
      addUnorderedListRule,
      addOrderedListRule,
      addQuoteRule,
      addStrongRule,
      addEmRule,
      addStrikethroughRule,
      addCardRule,
      addTabsRule,
      addCalloutRule,
      addBadgeRule,
      addCodeSnippetRule(schema),
      addBreakRule(schema),
      addStepRule,
      addAccordionRule,
      addColumnsRule,
      addFooRule(schema),
      addIconRule,
      addMermaidRule,
      addFieldRule,
      addFrameRule,
    ],
  });
}

Next steps

Components

Explore the component system and how to insert components

Marks

Learn about text formatting and inline marks

Build docs developers (and LLMs) love