求助:如何用Remark插件为指定字符串包裹span标签?
实现思路与代码示例
可以实现,核心是通过操作MDAST节点拆分文本并插入JSX元素,既不影响URL/图片,又能给目标字符串添加tooltip。
关键步骤
- 用
unist-util-visit遍历AST,仅处理纯文本节点,跳过链接URL、图片地址等非显示文本属性 - 按术语长度降序生成正则,避免短术语优先匹配导致长术语被拆分
- 将包含术语的文本节点拆分为「普通文本」+「带tooltip的span JSX节点」+「剩余文本」
- 给span节点添加自定义属性存储术语解释,前端通过CSS实现tooltip交互
插件代码实现
import { visit } from 'unist-util-visit'; // 自定义术语表:键为目标字符串,值为tooltip内容 const glossary = { "MDX": "结合Markdown与JSX的交互式内容格式", "Next.js": "基于React的服务端渲染框架", "Remark": "Markdown处理的插件系统" }; // 生成匹配正则:长术语优先,自动转义特殊字符 function getTermRegex() { const sortedTerms = Object.keys(glossary).sort((a, b) => b.length - a.length); const escapedTerms = sortedTerms.map(term => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); return new RegExp(`\\b(${escapedTerms.join('|')})\\b`, 'g'); } export default function remarkGlossaryTooltip() { const termRegex = getTermRegex(); return (tree) => { visit(tree, (node, index, parent) => { // 仅处理纯文本节点,跳过已被JSX包裹的文本避免重复处理 if (node.type !== 'text' || parent.type === 'mdxJsxTextElement') return; const matches = [...node.value.matchAll(termRegex)]; if (matches.length === 0) return; const newChildren = []; let lastPos = 0; matches.forEach((match) => { const term = match[0]; const start = match.index; const end = start + term.length; // 添加匹配前的普通文本 if (start > lastPos) { newChildren.push({ type: 'text', value: node.value.slice(lastPos, start) }); } // 创建MDX兼容的span节点,携带tooltip属性 newChildren.push({ type: 'mdxJsxTextElement', name: 'span', attributes: [ { type: 'mdxJsxAttribute', name: 'data-tooltip', value: glossary[term] }, { type: 'mdxJsxAttribute', name: 'class', value: 'tooltip-term' } ], children: [{ type: 'text', value: term }] }); lastPos = end; }); // 添加剩余未匹配的文本 if (lastPos < node.value.length) { newChildren.push({ type: 'text', value: node.value.slice(lastPos) }); } // 替换原文本节点为拆分后的节点数组 parent.children.splice(index, 1, ...newChildren); }); }; }
在next-mdx-remote中使用插件
import { serialize } from 'next-mdx-remote/serialize'; import remarkGlossaryTooltip from './path/to/remark-glossary-tooltip'; export async function serializeMdxContent(content) { return serialize(content, { mdxOptions: { remarkPlugins: [remarkGlossaryTooltip], // 可添加其他remark/rehype插件 } }); }
前端Tooltip样式实现(CSS)
.tooltip-term { position: relative; text-decoration: underline dotted; color: #165DFF; cursor: help; } .tooltip-term::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: #2D3748; color: #FFFFFF; padding: 4px 8px; border-radius: 4px; font-size: 0.875rem; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; margin-bottom: 6px; } .tooltip-term:hover::after { opacity: 1; }
注意事项
- 正则中的
\b是单词边界,若需匹配包含术语的长字符串,可移除该标识 - 术语包含特殊字符时,正则转义逻辑已自动处理,无需额外操作
- 插件仅处理页面显示文本,不会修改链接
href、图片src等属性,完全符合需求
内容的提问来源于stack exchange,提问作者Sam
相关产品推荐
相关产品推荐

