使用支持 Markdown 的 Admonition 块
本指南将引导你在 Tiptap 中为自定义的 “Admonition” 块添加 Markdown 支持。我们将过程拆分为四个明确的步骤,并在每个步骤中包含完整的示例代码,以便你始终拥有完整上下文。
步骤:
- 创建基础的 Tiptap
Node扩展(不含 Markdown 支持)。 - 添加自定义 Markdown 分词器,将原始 Markdown 转换为标记(tokens)。
- 添加解析器,将标记转换成 Tiptap 的 JSON 格式。
- 添加渲染器(序列化器),将 Tiptap 节点转换回 Markdown。
我们将采用 :::type 的风格定义 admonition,例如:
:::warning
这是带有 **加粗** 文本的警告。
:::第 1 步:创建基础扩展
从一个最简的 Node 定义开始,该定义描述了结构、HTML 解析/渲染和属性。暂时不涉及 Markdown 集成,以便你专注于模式和 HTML 的输入输出。
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 集成。
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 函数的完整示例,展示了各部分如何协同工作。
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 渲染的完整扩展。
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'(根据你的编辑器集成而定):
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,并在必要时回退为默认值。