创建支持 Markdown 的 Emoji 内联节点
Beta
本指南展示如何为一个小型原子内联节点添加 Markdown 支持,该节点渲染 emoji 短码(例如 :smile:)。我们将分四个步骤讲解,每个步骤都会附上完整示例,确保你始终拥有完整的上下文:
- 创建基础的
emoji节点(无 Markdown 支持)。 - 步骤 2:添加一个分词器,将
:name:转换为 token。 - 步骤 3:添加解析器,将 token 转为 Tiptap 的 JSON。
- 步骤 4:添加渲染器,将 Tiptap JSON 序列化回 Markdown。
我们将支持的示例简写:
Hello :smile: world!步骤 1:创建基础的 emoji 节点
首先定义一个小型的原子内联节点,存储一个 name 属性,并在 HTML 中渲染对应的 emoji。
import { Node } from '@tiptap/core'
const emojiMap = {
smile: '😊',
heart: '❤️',
thumbsup: '👍',
fire: '🔥',
// 根据需要添加更多映射
}
export const Emoji = Node.create({
name: 'emoji',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
name: { default: 'smile' },
}
},
parseHTML() {
return [{ tag: 'span[data-emoji]' }]
},
renderHTML({ node }) {
const emoji = emojiMap[node.attrs.name] || node.attrs.name || 'smile'
return ['span', { 'data-emoji': node.attrs.name }, emoji]
},
})说明:
- 该节点设置了
atom: true和inline,使其表现为一个不可分割的内联内容片段。 emojiMap用来将短名映射成实际的 emoji 字符用于 HTML 输出。
步骤 2:添加自定义 Markdown 分词器
该分词器在内联 Markdown 中识别 :name: 短码,并返回带有 emoji 名称的 token。下面展示包括分词器在内的完整扩展代码,方便你了解如何将其与基础节点整合。
import { Node } from '@tiptap/core'
const emojiMap = {
smile: '😊',
heart: '❤️',
thumbsup: '👍',
fire: '🔥',
// 根据需要添加更多映射
}
export const Emoji = Node.create({
name: 'emoji',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
name: { default: 'smile' },
}
},
parseHTML() {
return [{ tag: 'span[data-emoji]' }]
},
renderHTML({ node }) {
const emoji = emojiMap[node.attrs.name] || node.attrs.name || 'smile'
return ['span', { 'data-emoji': node.attrs.name }, emoji]
},
// 定义 Markdown 分词器,识别 :name: 短码
markdownTokenizer: {
name: 'emoji',
level: 'inline',
// 提供给词法分析器的快速定位提示
start: (src) => src.indexOf(':'),
tokenize: (src, tokens, lexer) => {
// 匹配格式为 :name:,其中 name 可包含字母、数字、下划线及加号
const match = /^:([a-z0-9_+]+):/i.exec(src)
if (!match) return undefined
return {
type: 'emoji',
raw: match[0], // 完整匹配,如 ":smile:"
emojiName: match[1], // 捕获的名称,如 "smile"
}
},
},
})实现说明:
start是词法分析器用于快速寻找候选位置的优化提示。- 分词器返回一个包含
type、raw和emojiName字段的 token,供解析器使用。 - 保持分词器为内联级别(
level: 'inline'),以便与内联解析集成。
步骤 3:添加解析器
parseMarkdown 函数将分词器生成的 token 转换成 Tiptap 节点对象。对于原子内联节点,应返回包含 type 和 attrs 的节点对象。以下示例为包含分词器与解析器的完整扩展。
import { Node } from '@tiptap/core'
const emojiMap = {
smile: '😊',
heart: '❤️',
thumbsup: '👍',
fire: '🔥',
// 根据需要添加更多映射
}
export const Emoji = Node.create({
name: 'emoji',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
name: { default: 'smile' },
}
},
parseHTML() {
return [{ tag: 'span[data-emoji]' }]
},
renderHTML({ node }) {
const emoji = emojiMap[node.attrs.name] || node.attrs.name || 'smile'
return ['span', { 'data-emoji': node.attrs.name }, emoji]
},
markdownTokenizer: {
name: 'emoji',
level: 'inline',
start: (src) => src.indexOf(':'),
tokenize: (src, tokens, lexer) => {
const match = /^:([a-z0-9_+]+):/i.exec(src)
if (!match) return undefined
return {
type: 'emoji',
raw: match[0],
emojiName: match[1],
}
},
},
// 将分词器 token 解析为 Tiptap 节点
parseMarkdown: (token, helpers) => {
return {
type: 'emoji',
attrs: { name: token.emojiName },
}
},
})说明:
parseMarkdown函数返回的对象中,type应当与节点名保持一致。- 原子节点不包含
content,它们是带有属性的独立节点。
步骤 4:添加渲染器
为支持将编辑器状态序列化回 Markdown 短码,实现 renderMarkdown 函数。该函数接收一个 Tiptap 节点对象,返回表示该节点的 Markdown 字符串。下面是包括分词器、解析器和渲染器的完整扩展。
import { Node } from '@tiptap/core'
const emojiMap = {
smile: '😊',
heart: '❤️',
thumbsup: '👍',
fire: '🔥',
// 根据需要添加更多映射
}
export const Emoji = Node.create({
name: 'emoji',
group: 'inline',
inline: true,
atom: true,
addAttributes() {
return {
name: { default: 'smile' },
}
},
parseHTML() {
return [{ tag: 'span[data-emoji]' }]
},
renderHTML({ node }) {
const emoji = emojiMap[node.attrs.name] || node.attrs.name || 'smile'
return ['span', { 'data-emoji': node.attrs.name }, emoji]
},
markdownTokenizer: {
name: 'emoji',
level: 'inline',
start: (src) => src.indexOf(':'),
tokenize: (src, tokens, lexer) => {
const match = /^:([a-z0-9_+]+):/i.exec(src)
if (!match) return undefined
return {
type: 'emoji',
raw: match[0],
emojiName: match[1],
}
},
},
parseMarkdown: (token, helpers) => {
return {
type: 'emoji',
attrs: { name: token.emojiName },
}
},
// 将 Tiptap 节点渲染回 Markdown
renderMarkdown: (node, helpers) => {
// 序列化为 :name: 短码,使用存储的 name 属性
return `:${node.attrs?.name || 'unknown'}:`
},
})使用方法
从包含 emoji 短码的 Markdown 设置编辑器内容。根据你的 Markdown 集成方式,传入 contentType: 'markdown' 或使用相应的 API:
editor.commands.setContent('Hello :smile: :heart: :thumbsup:', { contentType: 'markdown' })这会生成带有对应 name 属性的内联 emoji 节点,HTML 渲染时将显示映射的 emoji 字符(通过 emojiMap)。
测试及边界情况
- 未知名称:如果短码名不在
emojiMap中,节点当前会在span中渲染原始名称(你可能希望显示替代符号或移除该节点)。如果需要,请在markdownTokenizer或parseMarkdown中校验或规范化名称。 - 大小写敏感:分词器使用了
i标志实现不区分大小写;请确保你的emojiMap键与你的命名规范一致,或者使用.toLowerCase()统一处理。 - 内联解析:因为这是条内联分词器,会在段落和内联上下文中尝试匹配,确保它不会与其它内联分词规则(例如强调)冲突,必要时通过调整正则或使用周围空白检查以避免误匹配。
- 原子行为:该节点为原子节点,不可作为文本编辑。这很适合 emoji 元素,但如果你希望短码可编辑,需要采用不同策略。