You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:27:51