---
title: "使用支持 Markdown 的 Admonition 块"
description: "学习如何在 Tiptap 中创建支持 Markdown 语法的自定义 Admonition 块。"
canonical_url: "https://tiptap.zhcndoc.com/editor/markdown/guides/create-a-admonition-block"
---

# 使用支持 Markdown 的 Admonition 块

学习如何在 Tiptap 中创建支持 Markdown 语法的自定义 Admonition 块。

本指南将引导你在 Tiptap 中为自定义的 “Admonition” 块添加 Markdown 支持。我们将过程拆分为四个明确的步骤，并在每个步骤中包含完整的示例代码，以便你始终拥有完整上下文。

**步骤**：

1. 创建基础的 Tiptap `Node` 扩展（不含 Markdown 支持）。
2. 添加自定义 Markdown 分词器，将原始 Markdown 转换为标记（tokens）。
3. 添加解析器，将标记转换成 Tiptap 的 JSON 格式。
4. 添加渲染器（序列化器），将 Tiptap 节点转换回 Markdown。

我们将采用 `:::type` 的风格定义 admonition，例如：

```javascript
:::warning
这是带有 **加粗** 文本的警告。
:::
```

---

## 第 1 步：创建基础扩展

从一个最简的 `Node` 定义开始，该定义描述了结构、HTML 解析/渲染和属性。暂时不涉及 Markdown 集成，以便你专注于模式和 HTML 的输入输出。

```javascript
import { Node } from '@tiptap/core'

export const Admonition = Node.create({
  name: 'admonition',

  group: 'block',
  content: 'block+',

  addAttributes() {
    return {
      type: {
        default: 'note',
        parseHTML: (element) => element.getAttribute('data-type'),
        renderHTML: (attributes) => ({
          'data-type': attributes.type,
        }),
      },
    }
  },

  parseHTML() {
    return [{ tag: 'div[data-admonition]' }]
  },

  renderHTML({ node, HTMLAttributes }) {
    return ['div', { 'data-admonition': '', ...HTMLAttributes }, 0]
  },
})
```

**说明：**

- `content: 'block+'` 允许 admonition 内嵌套多个块内容。
- admonition 的 `type` 存储为节点属性（HTML 中为 `data-type`）。

---

## 第 2 步：添加自定义 Markdown 分词器

Tiptap 的 Markdown 集成允许你添加一个分词器，将 Markdown 源码转换成 Markdown 解析器能理解的标记。分词器负责识别 `:::type` ... `:::` 块，并返回一个包含相关元数据及嵌套标记（内容）的标记对象。

下面是一个完整示例，包含基础 Node 及添加的 `markdownTokenizer`，以便你了解分词器如何与 Node 集成。

```javascript
import { Node } from '@tiptap/core'

export const Admonition = Node.create({
  name: 'admonition',

  group: 'block',
  content: 'block+',

  addAttributes() {
    return {
      type: {
        default: 'note',
        parseHTML: (element) => element.getAttribute('data-type'),
        renderHTML: (attributes) => ({
          'data-type': attributes.type,
        }),
      },
    }
  },

  parseHTML() {
    return [{ tag: 'div[data-admonition]' }]
  },

  renderHTML({ node, HTMLAttributes }) {
    return ['div', { 'data-admonition': '', ...HTMLAttributes }, 0]
  },

  markdownTokenizer: {
    name: 'admonition',
    level: 'block', // 块级元素

    // 快速起始检测：未找到时返回 -1。
    // 用于词法分析器优化扫描。
    start: (src) => src.indexOf(':::'),

    // 实际的分词函数，负责构建标记
    tokenize: (src, tokens, lexer) => {
      // 匹配格式：
      // :::type\n
      // （任意内容包括换行）\n
      // :::
      const match = /^:::(\w+)\n([\s\S]*?)\n:::\n?/.exec(src)
      if (!match) return undefined

      return {
        type: 'admonition',
        raw: match[0], // 完整匹配的 Markdown
        admonitionType: match[1], // 例如 'warning'
        text: match[2], // 内部 Markdown 内容

        // 让 Markdown 词法分析器将内部内容解析成块级标记。
        tokens: lexer.blockTokens(match[2]),
      }
    },
  },
})
```

**实现细节：**

- `start` 用于词法分析器提前定位可能的起点。
- `markdownTokenizer.tokenize` 不匹配时返回 `undefined`，否则必须返回包含 `raw` 字符串和解析时需要的字段的标记对象。
- 使用 `lexer.blockTokens()`（或你的 Markdown 工具链中的类似方法）将内部内容解析成子块标记，方便解析器复用已有的块级解析逻辑。

---

## 第 3 步：添加解析器

`parseMarkdown` 函数接收分词器产生的标记，必须返回 Tiptap 兼容的节点 JSON。使用提供的 `helpers` 解析子标记为子内容。

以下是包含基础 Node、分词器及 `parseMarkdown` 函数的完整示例，展示了各部分如何协同工作。

```javascript
import { Node } from '@tiptap/core'

export const Admonition = Node.create({
  name: 'admonition',

  group: 'block',
  content: 'block+',

  addAttributes() {
    return {
      type: {
        default: 'note',
        parseHTML: (element) => element.getAttribute('data-type'),
        renderHTML: (attributes) => ({
          'data-type': attributes.type,
        }),
      },
    }
  },

  parseHTML() {
    return [{ tag: 'div[data-admonition]' }]
  },

  renderHTML({ node, HTMLAttributes }) {
    return ['div', { 'data-admonition': '', ...HTMLAttributes }, 0]
  },

  markdownTokenizer: {
    name: 'admonition',
    level: 'block',

    start: (src) => src.indexOf(':::'),

    tokenize: (src, tokens, lexer) => {
      const match = /^:::(\w+)\n([\s\S]*?)\n:::\n?/.exec(src)
      if (!match) return undefined

      return {
        type: 'admonition',
        raw: match[0],
        admonitionType: match[1],
        text: match[2],
        tokens: lexer.blockTokens(match[2]),
      }
    },
  },

  // 将 Markdown 标记解析成 Tiptap JSON
  parseMarkdown: (token, helpers) => {
    return {
      type: 'admonition',
      attrs: { type: token.admonitionType || 'note' },
      // 使用 helpers 解析子标记为 Tiptap 内容
      content: helpers.parseChildren(token.tokens || []),
    }
  },
})
```

**说明：**

- `helpers.parseChildren` 会将内部标记转换为 Tiptap 期望的节点内容数组。
- 确保这里的 `type` 与你节点的 `name` 一致。

---

## 第 4 步：添加渲染器

为了将内容序列化回 Markdown，实现 `renderMarkdown` 函数。该函数接收一个 Tiptap 节点，应返回对应的 Markdown 字符串。使用 `helpers.renderChildren` 序列化节点内容。

下面是包含分词器、解析器和渲染器的完整示例，这样你就拥有了一个支持 Markdown 输入输出和 HTML 渲染的完整扩展。

```javascript
import { Node } from '@tiptap/core'

export const Admonition = Node.create({
  name: 'admonition',
  group: 'block',
  content: 'block+',

  addAttributes() {
    return {
      type: {
        default: 'note',
        parseHTML: (element) => element.getAttribute('data-type'),
        renderHTML: (attributes) => ({
          'data-type': attributes.type,
        }),
      },
    }
  },

  parseHTML() {
    return [{ tag: 'div[data-admonition]' }]
  },

  renderHTML({ node, HTMLAttributes }) {
    return ['div', { 'data-admonition': '', ...HTMLAttributes }, 0]
  },

  markdownTokenizer: {
    name: 'admonition',
    level: 'block',

    start: (src) => src.indexOf(':::'),

    tokenize: (src, tokens, lexer) => {
      const match = /^:::(\w+)\n([\s\S]*?)\n:::\n?/.exec(src)
      if (!match) return undefined

      return {
        type: 'admonition',
        raw: match[0],
        admonitionType: match[1],
        text: match[2],
        tokens: lexer.blockTokens(match[2]),
      }
    },
  },

  parseMarkdown: (token, helpers) => {
    return {
      type: 'admonition',
      attrs: { type: token.admonitionType || 'note' },
      content: helpers.parseChildren(token.tokens || []),
    }
  },

  renderMarkdown: (node, helpers) => {
    const type = node.attrs?.type || 'note'
    const content = helpers.renderChildren(node.content || [])
    // 重新构造 :::type ... ::: 块。确保空格和换行符符合你的 Markdown 解析器要求。
    return `:::${type}\n${content}:::\n\n`
  },
})
```

---

## 使用方法

要从使用 admonition 语法的 Markdown 设置编辑器内容，传入 Markdown 字符串并确保设置了 `contentType: 'markdown'`（根据你的编辑器集成而定）：

```javascript
const markdown = `
:::warning
这是带有 **加粗** 文本的警告消息。
:::
`

editor.commands.setContent(markdown, { contentType: 'markdown' })
```

这将创建一个 `admonition` 节点，属性为 `type: 'warning'`，嵌套内容将作为 Markdown 解析。

---

## 测试和边界情况

- 嵌套块：分词器调用 `lexer.blockTokens()` 解析内部内容，内部 Markdown（列表、段落、标题等）会被解析为常规块标记并转换为 Tiptap 内容。
- 行内格式：admonition 内的加粗、斜体、链接等格式应由你的 Markdown 解析器处理，前提是 `helpers.renderChildren` 和 `helpers.parseChildren` 使用相同的标记集。
- 尾部换行：注意分词器正则消耗的尾部换行符。可调整正则表达式或 `renderMarkdown` 输出以匹配你的 Markdown 工具链预期。
- 类型校验：如需限制特定类型（例如 `note | warning | tip | danger`），可在 `markdownTokenizer.tokenize` 或 `parseMarkdown` 中验证 `admonitionType`，并在必要时回退为默认值。
