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

Safari兼容:不使用正则lookbehind实现HTML关键词高亮

HTML关键词高亮Safari兼容方案

现存问题

  • 待匹配关键词存在长短包含关系,例如storage box、kitchen storage box、kitchen organiser。现有逻辑已经将关键词按长度降序排序,优先匹配更长的关键词,避免长词高亮后内部包含的短词被重复高亮。
  • 原有实现通过正则负向后行断言、负向前行断言排除已被<mark>标签包裹的内容,核心代码如下:
_.each(this.keywords, keyword => {
    keyword.value = this.replaceNbsps(keyword.value)
    // 匹配关键词,排除已被高亮标签包裹的内容
    let regex = new RegExp(`((?<=\\s|^|\\W)(?<!<mark class="keyword_\\d">)${this.escapeRegExp(keyword.value)}(?!<\/mark>)(?=\\s|$|\\W))`, 'guim')
    highlightedContent = highlightedContent.replace(regex, `<mark class="keyword_${keyword.index}">$&</mark>`)
});
  • 由于Safari浏览器不支持正则负向后行断言语法,上述代码在Safari环境下直接报错,无法运行。

兼容实现方案

不要在完整HTML字符串上靠正则硬匹配排除标签内容,改用DOM节点拆分的思路实现,从根源上规避匹配到标签、已高亮内容的问题,全程不需要使用任何前后行断言,全浏览器兼容:

  • 预处理阶段保留原有逻辑:关键词按长度降序排序,提前处理关键词内的非断空格、转义正则特殊字符。
  • 创建离屏DOM容器装载待处理的HTML字符串,递归遍历容器内的所有节点:
    • 遇到元素节点时,如果是已生成的<mark>高亮标签,直接跳过,不处理其内部内容,从根源避免重复高亮;如果是其他普通元素节点,继续递归遍历它的子节点。
    • 遇到纯文本节点时,说明这部分内容没有任何HTML标签,直接对这段纯文本做关键词边界匹配,把匹配到的关键词替换为带对应类名的<mark>标签,将生成的HTML片段替换原文本节点的位置即可。
  • 遍历完成后,离屏容器的innerHTML就是最终处理完成的高亮内容。

参考实现代码

// 预处理关键词:按长度降序排序
const sortedKeywords = [...this.keywords]
  .map(k => ({
    ...k,
    value: this.replaceNbsps(k.value)
  }))
  .sort((a, b) => b.value.length - a.value.length);

// 创建离屏容器装载待处理内容
const container = document.createElement('div');
container.innerHTML = originalContent;

// 递归处理节点的方法
function processNode(node) {
  // 是mark标签直接跳过,不处理内部内容
  if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === 'mark') {
    return;
  }
  // 是元素节点就遍历子节点
  if (node.nodeType === Node.ELEMENT_NODE) {
    Array.from(node.childNodes).forEach(child => processNode(child));
    return;
  }
  // 剩下的是文本节点,做关键词匹配
  if (node.nodeType === Node.TEXT_NODE) {
    const text = node.textContent;
    if (!text.trim()) return;

    let tempHtml = text;
    // 逐个替换关键词,已经按长度降序排列,不会出现短词覆盖长词的问题
    sortedKeywords.forEach(keyword => {
      const kw = keyword.value;
      // 匹配单词边界,无需前后行断言,捕获边界字符替换时拼回即可
      const kwRegex = new RegExp(`(^|\\s|\\W)(${this.escapeRegExp(kw)})(\\s|$|\\W)`, 'guim');
      tempHtml = tempHtml.replace(kwRegex, (match, prefix, content, suffix) => {
        return `${prefix}<mark class="keyword_${keyword.index}">${content}</mark>${suffix}`;
      });
    });

    // 把替换后的HTML替换回原节点位置
    if (tempHtml !== text) {
      const replacement = document.createElement('span');
      replacement.innerHTML = tempHtml;
      node.replaceWith(...replacement.childNodes);
    }
  }
}

// 启动处理
Array.from(container.childNodes).forEach(child => processNode(child));

// 拿到最终高亮后的内容
const highlightedContent = container.innerHTML;

方案优势

  • 无任何正则高级语法依赖,兼容所有主流浏览器包括低版本Safari
  • 不会出现正则匹配误判HTML标签属性、标签内容的问题,不会破坏原有HTML结构
  • 逻辑上天然跳过已高亮的<mark>标签内部内容,比正则排除的方案稳定性更高

内容的提问来源于stack exchange,提问作者musicvicious

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 11:33:15