创建支持 Markdown 的高亮标记
Beta
本指南介绍如何为一个使用 ==text== 简写(某些 Markdown 语法中常见)生成 HTML 中 mark 元素的小型内联 highlight 标记添加 Markdown 支持。
我们将按四个清晰步骤进行讲解,每个步骤都会附上完整示例,确保你始终掌握完整上下文:
- 创建基本的 Tiptap
Mark扩展(不带 Markdown 支持)。 - 添加自定义 Markdown 分词器,将原始 Markdown 转成 token。
- 添加解析器,将 token 转换成 Tiptap JSON(并应用标记)。
- 添加渲染器(序列化器),将 Tiptap 内容转换回 Markdown。
我们支持的示例简写:
This is ==highlighted== text.第 1 步:创建基本的 highlight Mark
从最简 Mark 定义开始,描述标记名称、HTML 解析/渲染行为及任何选项。暂时跳过 Markdown 集成,先专注于 schema 和 HTML 输入输出。
import { Mark } from '@tiptap/core'
export const Highlight = Mark.create({
name: 'highlight',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [{ tag: 'mark' }]
},
renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0]
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name)
},
}
},
})说明:
- 该标记简单映射为 HTML 中的
<mark>标签。 toggleHighlight命令可用于程序化切换该标记。
第 2 步:添加自定义 Markdown 分词器
分词器负责识别原始 Markdown 中的 ==text==,并返回包含内部文本及任何嵌套内联 token 的标记。此步重点先实现分词器,方便单独测试识别功能。
import { Mark } from '@tiptap/core'
export const Highlight = Mark.create({
name: 'highlight',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [{ tag: 'mark' }]
},
renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0]
},
// 定义自定义 Markdown 分词器以识别 ==text==
markdownTokenizer: {
name: 'highlight',
level: 'inline', // 内联元素
// lexer 的快速提示,用于寻找候选位置
start: (src) => src.indexOf('=='),
tokenize: (src, tokens, lexer) => {
// 在剩余文本开头匹配 ==text==
const match = /^==([^=]+)==/.exec(src)
if (!match) return undefined
return {
type: 'highlight', // token 类型(必须与 name 一致)
raw: match[0], // 完整匹配字符串:==text==
text: match[1], // 内部内容:text
// 让 Markdown lexer 解析嵌套的内联格式
tokens: lexer.inlineTokens(match[1]),
}
},
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name)
},
}
},
})实现要点:
start用于优化,帮助 lexer 快速定位潜在匹配位置。- 分词器返回
tokens: lexer.inlineTokens(match[1]),以保留内部的加粗、斜体、链接等嵌套格式。
第 3 步:添加解析器
parseMarkdown 函数将分词器产生的 token 转换成 Tiptap 表示。对于标记,通常将内部 tokens 解析为内联节点,再对内容应用标记。
import { Mark } from '@tiptap/core'
export const Highlight = Mark.create({
name: 'highlight',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [{ tag: 'mark' }]
},
renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0]
},
// 定义自定义 Markdown 分词器以识别 ==text==
markdownTokenizer: {
name: 'highlight',
level: 'inline', // 内联元素
// lexer 的快速提示,用于寻找候选位置
start: (src) => src.indexOf('=='),
tokenize: (src, tokens, lexer) => {
// 在剩余文本开头匹配 ==text==
const match = /^==([^=]+)==/.exec(src)
if (!match) return undefined
return {
type: 'highlight', // token 类型(必须与 name 一致)
raw: match[0], // 完整匹配字符串:==text==
text: match[1], // 内部内容:text
// 让 Markdown lexer 解析嵌套的内联格式
tokens: lexer.inlineTokens(match[1]),
}
},
},
// 将 Markdown token 解析为 Tiptap JSON
parseMarkdown: (token, helpers) => {
// 将嵌套的内联 tokens 解析为 Tiptap 内联内容
const content = helpers.parseInline(token.tokens || [])
// 对解析出的内容应用 'highlight' 标记
return helpers.applyMark('highlight', content)
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name)
},
}
},
})说明:
helpers.parseInline将嵌套内联 token 转为 tiptap 兼容的content数组。helpers.applyMark('highlight', content)对解析出的内联内容应用标记,并返回 Markdown 解析器预期的结构。
第 4 步:添加渲染器
为了支持将编辑器状态序列化回 Markdown,需实现 renderMarkdown 函数。它接收 Tiptap 节点(或标记节点结构),应返回 Markdown 字符串。使用 helpers.renderChildren 序列化嵌套内容。
import { Mark } from '@tiptap/core'
export const Highlight = Mark.create({
name: 'highlight',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [{ tag: 'mark' }]
},
renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0]
},
// 定义自定义 Markdown 分词器以识别 ==text==
markdownTokenizer: {
name: 'highlight',
level: 'inline', // 内联元素
// lexer 的快速提示,用于寻找候选位置
start: (src) => src.indexOf('=='),
tokenize: (src, tokens, lexer) => {
// 在剩余文本开头匹配 ==text==
const match = /^==([^=]+)==/.exec(src)
if (!match) return undefined
return {
type: 'highlight', // token 类型(必须与 name 一致)
raw: match[0], // 完整匹配字符串:==text==
text: match[1], // 内部内容:text
// 让 Markdown lexer 解析嵌套的内联格式
tokens: lexer.inlineTokens(match[1]),
}
},
},
// 将 Markdown token 解析为 Tiptap JSON
parseMarkdown: (token, helpers) => {
// 将嵌套的内联 tokens 解析为 Tiptap 内联内容
const content = helpers.parseInline(token.tokens || [])
// 对解析出的内容应用 'highlight' 标记
return helpers.applyMark('highlight', content)
},
// 将 Tiptap 节点渲染回 Markdown
renderMarkdown: (node, helpers) => {
const content = helpers.renderChildren(node.content || [])
// 用 == 包裹序列化后的子内容
return `==${content}==`
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) => {
return commands.toggleMark(this.name)
},
}
},
})说明:
helpers.renderChildren用于序列化节点/标记子节点回 Markdown,支持嵌套格式。- 请确保
renderMarkdown输出和分词器匹配(空格、换行等),这种内联标记使用简单的==content==形式即可。
使用方法
将扩展添加到编辑器,并从 Markdown 设置内容:
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Markdown from 'some-markdown-integration' // 替换为你的 Markdown 集成
import { Highlight } from './highlight'
const editor = new Editor({
extensions: [StarterKit, Markdown, Highlight],
})
// 设置 Markdown 内容(Markdown 集成必须支持 'contentType' 或类似选项)
editor.commands.setContent('This is ==highlighted== text', { contentType: 'markdown' })
// 程序化切换高亮标记
editor.commands.toggleHighlight()测试与边界情况
- 嵌套内联格式:分词器使用了
lexer.inlineTokens(match[1]),因此嵌套内联 token(加粗、斜体、链接)应能正确解析和渲染。 - 贪婪匹配:分词器使用简单正则
^==([^=]+)==,不能匹配包含==的内容。如果需要支持嵌套==或多行高亮,请相应扩展正则表达式。 - 空白处理:部分 Markdown 语法允许定界符内含空格(如
== text ==)。如需支持,请更新分词器和renderMarkdown的输出,保持该格式一致。 - 与其他分词器冲突:
start优化帮助 lexer 寻找候选位置,但请确保正则不与 Markdown 工具链中其他内联分词器冲突。