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
相关产品推荐
相关产品推荐

