Markdown Utilities
Block Utilities
createBlockMarkdownSpec 
Creates a complete Markdown specification for block-level nodes using Pandoc-style syntax (:::blockName).
This utility can be be imported from @tiptap/core.
Syntax
:::blockName {attributes}
Content goes here
Can be **multiple** paragraphs
:::Usage
import { Node } from '@tiptap/core'
import { createBlockMarkdownSpec } from '@tiptap/core'
const Callout = Node.create({
  name: 'callout',
  group: 'block',
  content: 'block+',
  addAttributes() {
    return {
      type: { default: 'info' },
      title: { default: null },
    }
  },
  parseHTML() {
    return [{ tag: 'div[data-callout]' }]
  },
  renderHTML({ node }) {
    return ['div', { 'data-callout': node.attrs.type }, 0]
  },
  // Use the utility to generate Markdown support
  ...createBlockMarkdownSpec({
    nodeName: 'callout',
    defaultAttributes: { type: 'info' },
    allowedAttributes: ['type', 'title'],
    content: 'block', // Allow nested block content
  }),
})Options
| Option | Type | Default | Description | 
|---|---|---|---|
nodeName | string | required | Tiptap node name | 
name | string | nodeName | Markdown syntax name | 
content | 'block' | 'inline' | 'block' | Content type | 
defaultAttributes | Object | {} | Default attrs when parsing | 
allowedAttributes | string[] | all | Whitelist for rendering | 
getContent | (token) => string | auto | Custom content extraction | 
parseAttributes | (str) => Object | auto | Custom attribute parser | 
serializeAttributes | (attrs) => string | auto | Custom serializer | 
Example Markdown
:::callout {type="warning" title="Important"}
This is a warning callout with a title.
It can contain multiple paragraphs and **formatting**.
:::
:::note
Simple note without attributes.
:::createAtomBlockMarkdownSpec 
Creates a Markdown specification for atomic (self-closing) block nodes using Pandoc syntax.
This utility can be be imported from @tiptap/core.
Syntax
:::nodeName {attributes} :::No closing tag, no content. Perfect for embeds, images, horizontal rules, etc.
Usage
import { Node } from '@tiptap/core'
import { createAtomBlockMarkdownSpec } from '@tiptap/core'
const Youtube = Node.create({
  name: 'youtube',
  group: 'block',
  atom: true,
  addAttributes() {
    return {
      src: { default: null },
      start: { default: 0 },
      width: { default: 640 },
      height: { default: 480 },
    }
  },
  parseHTML() {
    return [
      {
        tag: 'iframe[src*="youtube.com"]',
        getAttrs: dom => ({
          src: dom.getAttribute('src'),
        }),
      },
    ]
  },
  renderHTML({ node }) {
    return ['iframe', { src: node.attrs.src }, 0]
  },
  // Use the utility for atomic block Markdown
  ...createAtomBlockMarkdownSpec({
    nodeName: 'youtube',
    requiredAttributes: ['src'], // Must have src attribute
    defaultAttributes: { start: 0 },
    allowedAttributes: ['src', 'start', 'width', 'height'],
  }),
})Options
| Option | Type | Default | Description | 
|---|---|---|---|
nodeName | string | required | Tiptap node name | 
name | string | nodeName | Markdown syntax name | 
requiredAttributes | string[] | [] | Required attrs for valid parse | 
defaultAttributes | Object | {} | Default attrs when parsing | 
allowedAttributes | string[] | all | Whitelist for rendering | 
parseAttributes | (str) => Object | auto | Custom attribute parser | 
serializeAttributes | (attrs) => string | auto | Custom serializer | 
Example Markdown
:::youtube {src="https://youtube.com/watch?v=dQw4w9WgXcQ" start="30"}
:::image {src="photo.jpg" alt="A beautiful photo" width="800"}
:::hrInline Utilities
createInlineMarkdownSpec 
Creates a Markdown specification for inline nodes using shortcode syntax ([nodeName]).
This utility can be be imported from @tiptap/core.
Syntax
<!-- Self-closing -->
[nodeName attribute="value" other="data"]
<!-- With content -->
[nodeName attribute="value"]content[/nodeName]Usage - Self-Closing
import { Node } from '@tiptap/core'
import { createInlineMarkdownSpec } from '@tiptap/core'
const Mention = Node.create({
  name: 'mention',
  group: 'inline',
  inline: true,
  atom: true,
  addAttributes() {
    return {
      id: { default: null },
      label: { default: null },
    }
  },
  parseHTML() {
    return [{ tag: 'span[data-mention]' }]
  },
  renderHTML({ node }) {
    return ['span', { 'data-mention': node.attrs.id }, `@${node.attrs.label}`]
  },
  // Use the utility for self-closing inline Markdown
 ...createInlineMarkdownSpec({
    nodeName: 'mention',
    selfClosing: true,
    allowedAttributes: ['id', 'label'],
  }),
})Usage - With Content
const Highlight = Node.create({
  name: 'highlight',
  group: 'inline',
  content: 'inline*',
  addAttributes() {
    return {
      color: { default: 'yellow' },
    }
  },
  parseHTML() {
    return [{ tag: 'mark' }]
  },
  renderHTML({ node }) {
    return ['mark', { 'data-color': node.attrs.color }, 0]
  },
  // Use the utility for inline Markdown with content
  ...createInlineMarkdownSpec({
    nodeName: 'highlight',
    selfClosing: false, // Has content
    allowedAttributes: ['color'],
  }),
})Options
| Option | Type | Default | Description | 
|---|---|---|---|
nodeName | string | required | Tiptap node name | 
name | string | nodeName | Shortcode name | 
selfClosing | boolean | false | Has no content | 
defaultAttributes | Object | {} | Default attrs when parsing | 
allowedAttributes | string[] | all | Whitelist for rendering | 
getContent | (node) => string | auto | Custom content extraction | 
parseAttributes | (str) => Object | auto | Custom attribute parser | 
serializeAttributes | (attrs) => string | auto | Custom serializer | 
Example Markdown
<!-- Mentions -->
Hey [mention id="user123" label="John"]!
<!-- Emoji -->
Party time [emoji name="party_popper"]!
<!-- Highlight with content -->
This is [highlight color="yellow"]important text[/highlight] to read.Parse Helpers
Helpers provided to extension parse handlers.
parseInline(tokens) 
Parse inline tokens (bold, italic, links, etc.).
helpers.parseInline(tokens: MarkdownToken[]): JSONContent[]Parameters:
tokens: Array of inline Markdown tokens
Returns:
JSONContent[]- Array of Tiptap JSON nodes
Example:
parse: (token, helpers) => {
  return {
    type: 'paragraph',
    content: helpers.parseInline(token.tokens || []),
  }
}parseChildren(tokens) 
Parse block-level child tokens.
helpers.parseChildren(tokens: MarkdownToken[]): JSONContent[]Parameters:
tokens: Array of block-level Markdown tokens
Returns:
JSONContent[]- Array of Tiptap JSON nodes
Example:
parse: (token, helpers) => {
  return {
    type: 'blockquote',
    content: helpers.parseChildren(token.tokens || []),
  }
}createTextNode(text, marks) 
Create a text node with optional marks.
helpers.createTextNode(
  text: string,
  marks?: Array<{ type: string; attrs?: any }>
): JSONContentParameters:
text: The text contentmarks: Optional array of marks to apply
Returns:
JSONContent- Text node
Example:
parse: (token, helpers) => {
  return helpers.createTextNode('Hello', [{ type: 'bold' }, { type: 'italic' }])
}createNode(type, attrs, content) 
Create a node with type, attributes, and content.
helpers.createNode(
  type: string,
  attrs?: Record<string, any>,
  content?: JSONContent[]
): JSONContentParameters:
type: Node type nameattrs: Optional node attributescontent: Optional node content
Returns:
JSONContent- The created node
Example:
parse: (token, helpers) => {
  return helpers.createNode('heading', { level: 2 }, [helpers.createTextNode('Title')])
}applyMark(markType, content, attrs) 
Apply a mark to content (for inline formatting).
helpers.applyMark(
  markType: string,
  content: JSONContent[],
  attrs?: Record<string, any>
): MarkdownParseResultParameters:
markType: Mark type namecontent: Content to apply mark toattrs: Optional mark attributes
Returns:
MarkdownParseResult- Mark result object
Example:
parse: (token, helpers) => {
  const content = helpers.parseInline(token.tokens || [])
  return helpers.applyMark('bold', content)
}Render Helpers
Helpers provided to extension render handlers.
renderChildren(nodes, separator) 
Render child nodes to Markdown.
helpers.renderChildren(
  nodes: JSONContent | JSONContent[],
  separator?: string
): stringParameters:
nodes: Node or array of nodes to renderseparator: Optional separator between nodes (default:'')
Returns:
string- Rendered Markdown
Example:
render: (node, helpers) => {
  const content = helpers.renderChildren(node.content || [])
  return `> ${content}\n\n`
}indent(content) 
Add indentation to content.
helpers.indent(content: string): stringParameters:
content: Content to indent
Returns:
string- Indented content
Example:
render: (node, helpers) => {
  const content = helpers.renderChildren(node.content || [])
  return helpers.indent(content)
}wrapInBlock(prefix, content) 
Wrap content with a prefix on each line.
helpers.wrapInBlock(
  prefix: string,
  content: string
): stringParameters:
prefix: Prefix to add to each linecontent: Content to wrap
Returns:
string- Wrapped content
Example:
render: (node, helpers) => {
  const content = helpers.renderChildren(node.content || [])
  return helpers.wrapInBlock('> ', content) + '\n\n'
}Miscellaneous Utilities
parseAttributes 
The parseAttributes utility is mostly used internally for building attribute objects for Pandoc-style strings. You most likely won't use it except
you want to build a custom syntax that requires similar syntax to the Pandoc attribute style.
This utility can be be imported from @tiptap/core.
Supported Formats
import { parseAttributes } from '@tiptap/core'
// Classes (prefix with .)
parseAttributes('.btn .primary')
// → { class: 'btn primary' }
// IDs (prefix with #)
parseAttributes('#submit')
// → { id: 'submit' }
// Key-value pairs (quoted values)
parseAttributes('type="button" disabled')
// → { type: 'button', disabled: true }
// Combined
parseAttributes('.btn #submit type="button" disabled')
// → { class: 'btn', id: 'submit', type: 'button', disabled: true }
// Complex example
parseAttributes('.card .elevated #main-card title="My Card" data-id="123" visible')
// → {
//   class: 'card elevated',
//   id: 'main-card',
//   title: 'My Card',
//   'data-id': '123',
//   visible: true
// }Usage
import { parseAttributes } from '@tiptap/core'
const attrString = '.highlight #section-1 color="yellow" bold'
const attrs = parseAttributes(attrString)
console.log(attrs)
// {
//   class: 'highlight',
//   id: 'section-1',
//   color: 'yellow',
//   bold: true
// }serializeAttributes 
The serializeAttributes utility is mostly used internally for converting attribute objects back to Pandoc-style strings. You most likely won't use it except
you want to build a custom syntax that requires similar syntax to the Pandoc attribute style.
This utility can be be imported from @tiptap/core.
Usage
import { serializeAttributes } from '@tiptap/core'
const attrs = {
  class: 'btn primary',
  id: 'submit',
  type: 'button',
  disabled: true,
  'data-value': '123',
}
const attrString = serializeAttributes(attrs)
console.log(attrString)
// .btn.primary #submit disabled type="button" data-value="123"Rules
- Classes are prefixed with 
.and space-separated - IDs are prefixed with 
# - Boolean 
truevalues become standalone attributes - String values are quoted with 
" - Null/undefined values are omitted
 
parseIndentedBlocks 
Advanced utility for parsing hierarchical indented blocks (lists, task lists, etc.).
This utility can be be imported from @tiptap/core.
When to Use
Use this when you need to parse Markdown with:
- Nested items based on indentation
 - Hierarchical structure (not flat)
 - Custom block patterns
 
Usage Example - Task List
import { parseIndentedBlocks } from '@tiptap/core'
const src = `
- [ ] Task 1
  - [x] Subtask 1.1
  - [ ] Subtask 1.2
- [x] Task 2
`
const result = parseIndentedBlocks(
  src,
  {
    // Pattern to match task items
    itemPattern: /^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,
    // Extract data from matched line
    extractItemData: match => ({
      indentLevel: match[1].length,
      mainContent: match[4],
      checked: match[3].toLowerCase() === 'x',
    }),
    // Create the final token
    createToken: (data, nestedTokens) => ({
      type: 'taskItem',
      checked: data.checked,
      text: data.mainContent,
      nestedTokens, // Nested items
    }),
  },
  lexer,
)
console.log(result)
// {
//   items: [
//     {
//       type: 'taskItem',
//       checked: false,
//       text: 'Task 1',
//       nestedTokens: [
//         { type: 'taskItem', checked: true, text: 'Subtask 1.1' },
//         { type: 'taskItem', checked: false, text: 'Subtask 1.2' }
//       ]
//     },
//     {
//       type: 'taskItem',
//       checked: true,
//       text: 'Task 2'
//     }
//   ]
// }Options
interface BlockParserConfig {
  itemPattern: RegExp // Pattern to match items
  extractItemData: (match) => {
    mainContent: string
    indentLevel: number
    [key: string]: any
  }
  createToken: (data, nestedTokens?) => ParsedBlock
  baseIndentSize?: number // Base indent (default: 2)
  customNestedParser?: (src) => any[] // Custom nested parser
}renderNestedMarkdownContent 
Utility for rendering nodes with nested content, properly indenting child elements.
This utility can be be imported from @tiptap/core.
When to Use
Use this when rendering:
- List items with nested content
 - Blockquotes with nested elements
 - Task items with subtasks
 - Any node with a prefix and nested children
 
Usage Example - List Item
import { renderNestedMarkdownContent } from '@tiptap/core'
const ListItem = Node.create({
  name: 'listItem',
  renderMarkdown: (node, h) => {
    // Static prefix
    return renderNestedMarkdownContent(node, h, '- ')
  },
})Usage Example - Task Item
const TaskItem = Node.create({
  name: 'taskItem',
  renderMarkdown: (node, h) => {
    // Dynamic prefix based on checked state
    const prefix = `- [${node.attrs?.checked ? 'x' : ' '}] `
    return renderNestedMarkdownContent(node, h, prefix)
  },
})Usage Example - Context-Based Prefix
const ListItem = Node.create({
  name: 'listItem',
  renderMarkdown: (node, h, ctx) => {
    // Prefix changes based on parent type
    return renderNestedMarkdownContent(
      node,
      h,
      ctx => {
        if (ctx.parentType === 'orderedList') {
          return `${ctx.index + 1}. `
        }
        return '- '
      },
      ctx,
    )
  },
})Signature
function renderNestedMarkdownContent(
  node: JSONContent,
  helpers: {
    renderChildren: (nodes: JSONContent[]) => string
    indent: (text: string) => string
  },
  prefixOrGenerator: string | ((ctx: any) => string),
  ctx?: any,
): stringComplete Examples
Example 1: Callout Block
import { Node } from '@tiptap/core'
import { createBlockMarkdownSpec } from '@tiptap/core'
export const Callout = Node.create({
  name: 'callout',
  group: 'block',
  content: 'block+',
  addAttributes() {
    return {
      type: { default: 'info' },
      title: { default: null },
    }
  },
  parseHTML() {
    return [{ tag: 'div[data-callout]' }]
  },
  renderHTML({ node }) {
    return [
      'div',
      {
        'data-callout': '',
        'data-type': node.attrs.type,
        'data-title': node.attrs.title,
      },
      0,
    ]
  },
  ...createBlockMarkdownSpec({
    nodeName: 'callout',
    defaultAttributes: { type: 'info' },
    allowedAttributes: ['type', 'title'],
  }),
})Markdown:
:::callout {type="warning" title="Watch out!"}
This is important information that needs attention.
:::Example 2: YouTube Embed
import { Node } from '@tiptap/core'
import { createAtomBlockMarkdownSpec } from '@tiptap/core'
export const Youtube = Node.create({
  name: 'youtube',
  group: 'block',
  atom: true,
  addAttributes() {
    return {
      src: { default: null },
      start: { default: 0 },
    }
  },
  parseHTML() {
    return [{ tag: 'iframe[src*="youtube.com"]' }]
  },
  renderHTML({ node }) {
    return ['iframe', { src: node.attrs.src }]
  },
  ...createAtomBlockMarkdownSpec({
    nodeName: 'youtube',
    requiredAttributes: ['src'],
    allowedAttributes: ['src', 'start'],
  }),
})Markdown:
:::youtube {src="https://youtube.com/watch?v=dQw4w9WgXcQ" start="30"}Example 3: Mention (Inline)
import { Node } from '@tiptap/core'
import { createInlineMarkdownSpec } from '@tiptap/core'
export const Mention = Node.create({
  name: 'mention',
  group: 'inline',
  inline: true,
  atom: true,
  addAttributes() {
    return {
      id: { default: null },
      label: { default: null },
    }
  },
  parseHTML() {
    return [{ tag: 'span[data-mention]' }]
  },
  renderHTML({ node }) {
    return ['span', { 'data-mention': node.attrs.id }, `@${node.attrs.label}`]
  },
  ...createInlineMarkdownSpec({
    nodeName: 'mention',
    selfClosing: true,
    allowedAttributes: ['id', 'label'],
  }),
})Markdown:
Hey [mention id="user123" label="John"], check this out!When to Use What
Use Utilities When:
- ✅ Following standard Markdown conventions (Pandoc, shortcodes)
 - ✅ Standard attribute parsing is sufficient
 - ✅ Block/inline distinction is clear
 - ✅ Quick implementation is desired
 - ✅ Maintaining consistency across extensions
 
Use Custom Implementation When:
- ✅ Non-standard Markdown syntax required
 - ✅ Complex parsing logic needed
 - ✅ Fine-grained control over tokens
 - ✅ Custom attribute formats
 
Related Documentation
- Custom Tokenizers - For custom syntax that doesn't follow conventions
 - Extension Integration - General guide to adding Markdown to extensions
 - Advanced Customization - Custom parse and render handlers
 - API Reference - Complete API documentation
 
Pro Tip: Start with utilities for standard patterns, then move to custom implementations only when you need specific behavior that utilities don't provide.