醉美水芙蓉 发表于 2026-7-10 21:25:21

CSS马黑黑 Custom Highlight API+Range+正则实现查找关键字高亮

<style>
    .artBox { margin: 20px auto; font: normal 18px/26px sans-serif; }
    .artBox mark { padding: 2px 6px; background: #ccc; }
    .artBox blockquote { padding: 14px; border: solid gray; border-width: 0 4px; border-radius: 10px; background: #ddd; }
    .artBox p { margin: 10px 0; }
</style>

<div class="artBox">
    <p>实现 Web 页查找关键字高亮,传统的方法都会改变原始 HTML 结构,通常的做法是对匹配的字符串用 span 标签包裹。CSS Custom Highlight API(CSS 自定义高亮接口,下称 CCHA)则完全可以避开对 DOM 树的写操作,直接通过在 DOM 中创建 Range 对象(范围)实现高亮,自2025年起已经得到现代浏览器的广泛支持。</p>
    <h3>一、CCHA 实现高亮步骤拆解</h3>
    <p>CCHA 首先需要在 CSS 中创建一个 ::highlight() 伪元素用以定义高亮样式:</p>

    <div class="hEdiv"><pre class="hEpre">
&lt;style&gt;
    /* 高亮伪元素(::highlight 是CSS内置伪类,hl-keyword 是自定义名称)*/
    ::highlight(hl-keyword) {
      background: tan;
    }
&lt;/style&gt;
    </pre></div>
    <p>::highlight(参数) 伪元素的参数是一个自定义名称,该名称是待引用的依据,习惯上分为两部分用以简单描述自身,中间使用短连接符将它们连接起来成为一体,例如 <mark>hl-keyword</mark> 表示“高亮关键字”。命名随意,起到辅助理解其含义方便后续使用即可。这是第一步。</p>
    <p>接下来是创建 Range 范围。创建 Range 范围应该针对指定元素,例如下例的 id="p" 的段落标签,假设我们想高亮前边的四个字:</p>
   
    <div class="hEdiv"><pre class="hEpre">
&lt;p id="p"&gt;花朝论坛使用 Discuz! X3.4 创建&lt;/p&gt;

&lt;script&gt;
    const p = document.getElementById('p'); // 获取要操作的元素
    const range = new Range(); // 创建 Range 范围
    range.setStart(p.firstChild, 0); // 范围开始位置
    range.setEnd(p.firstChild, 4); // 范围结束位置
&lt;/script&gt;
    </pre></div>
    <p>这样创建出来的 Range 范围就可以使用 CCHA 进行高亮,但还需要重要的两步:为 Range 范围创建高亮对象、注册高亮样式并对 Range 范围应用该样式:</p>
   
    <div class="hEdiv"><pre class="hEpre">
const hl = new Highlight(range); // 创建高亮对象(参数 range 为前面创建的范围)
CSS.highlights.set('hl-keyword', hl); // 为高亮对象注册CSS定义的伪元素(使用该高亮样式)
    </pre></div>
   
    <p>下面将上述所有步骤的代码整合成一个可运行的演示,该演示示例通过按钮的点击事件创建高亮效果并可取消高亮:</p>
   
    <div class="hEdiv" data-prev="1"><pre class="hEpre">
&lt;style&gt;
    ::highlight(hl-keyword) { background: tan; }
&lt;/style&gt;

&lt;p id="p"&gt;花朝论坛使用 Discuz! X3.4 创建&lt;/p&gt;
&lt;p&gt;
    &lt;button onclick="hLight()"&gt;创建高亮&lt;/button&gt;
    &lt;button onclick="clearHLight()"&gt;取消高亮&lt;/button&gt;
&lt;/p&gt;

&lt;script&gt;
    // 高亮函数(设定范围并高亮显示)
    function hLight() {
      const p = document.getElementById('p'); // 获取要操作的元素
      const range = new Range(); // 创建 Range 范围
      range.setStart(p.firstChild, 0); // 范围开始位置
      range.setEnd(p.firstChild, 4); // 范围结束位置
   
      const hl = new Highlight(range); // 创建高亮对象(参数 range 为前面创建的范围)
      CSS.highlights.set('hl-keyword', hl); // 注册CSS定义的伪元素(应用高亮样式)
    }

    // 注销高亮函数
    function clearHLight() {
      CSS.highlights.clear(); // 使用CSS.highlights.clear() 方法清除高亮样式注册
    }
&lt;/script&gt;
    </pre></div>
    <blockquote>【创建高亮小结】
      <ol>
            <li>在CSS中使用 ::highlight() 伪元素定义高亮样式</li>
            <li>创建 Range(范围)对象</li>
            <li>为 Range 范围创建高亮对象(new Highlight)</li>
            <li>将CSS高亮样式注册给高亮对象(即高亮对象应用 CSS 高亮伪元素样式)</li>
      </ol>
    </blockquote>
    <h3>二、使用 TreeWalker 遍历节点</h3>
    <p>前面的示例待操作元素的HTML结构是单纯文本节点所构造,但真正的 Web 页节点构成可能是非常复杂的,文本可能存在于深层嵌套的元素。为此,创建 Range 范围会跟着变成复杂化,我们需要对之进行遍历、找出最里层的文本节点(#text)。遍历指定元素内的子节点有很多方法,其中借助 JS 内置的 TreeWalker API 加以实现最为简洁高效。以下函数演示了如何找出指定元素 pa 包含的所有文本,不论这些文本如何被各种子元素所嵌套:</p>
    <div class="hEdiv"><pre class="hEpre">
function findText(pa) {
    // 创建 TreeWalker 对象,节点过滤器仅接收#text
    const walker = document.createTreeWalker(pa, NodeFilter.SHOW_TEXT);
    // 遍历所有 walker 找到的节点
    while(walker.nextNode()) {
      const textNode = walker.currentNode; // 处理当前文本节点
      const text = textNode.textContent;   // 拿到文本内容
      //console.log(text); // 打印节点文本内容(或处理其它业务逻辑)
    }
}
    </pre></div>
    <p>应该认为 TreeWalker 处理节点的能力相当强大,至少,它轻而易举就能拿到所有被各种嵌套元素分割、包裹的具体文本内容。</p>
    <h3>高亮指定元素内所有匹配的字符</h3>
    <p>接下来的工作是在指定元素内找到匹配查找关键字的所有字符,然后高亮它们。以下示例可以在线预览:</p>

    <div class="hEdiv" data-prev="1"><pre class="hEpre">
&lt;style&gt;
    #papa { margin: 20px auto; width: 600px; border: 1px solid gray; padding: 10px; }
    .tMid { text-align: center; }
    ::highlight(hl-keyword) { background: tan; }
&lt;/style&gt;

&lt;h3 class="tMid"&gt;请在下面方框选择文本(不支持跨行选择)&lt;/h3&gt;
&lt;div id="papa" class="mdiv"&gt;
    9876543210123456789
    &lt;p&gt;abcABCEFG&lt;b&gt;aaa&lt;/b&gt;&lt;/p&gt;
    &lt;p&gt;efgAAA&lt;i&gt;ABC&lt;/i&gt;abc&lt;/p&gt;
    &lt;div&gt;
      &lt;p&gt;啦啦啦……啊啊啊……&lt;/p&gt;
      &lt;p&gt;0123456789876543210&lt;/p&gt;
      &lt;p&gt;&lt;span style="color: blue"&gt;啊啊啊……啦啦啦……&lt;/span&gt;&lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
    const papa = document.getElementById('papa'); // 获取待操作元素

    // 高亮函数
    function hlight(pa, keyword) {
      const ranges = []; // Range 范围数组(存储所有范围)
      // 创建 TreeWalker 对象(指定 pa 元素、仅处理 #text 节点)
      const walker = document.createTreeWalker(pa, NodeFilter.SHOW_TEXT);
      // 遍历拿到的节点并逐一匹配
      while(walker.nextNode()) {
            const textNode = walker.currentNode; // 拿到当前节点
            const reg = new RegExp(keyword, 'gi'); // 创建关键字正则表达式
            let matches = textNode.textContent.matchAll(reg); // 使用 matchAll 匹配正则
            if (!matches) return; // 若无匹配跳过
            // 遍历匹配返回的 matches 数组
            matches.forEach(match =&gt; {
                const range = new Range(); // 创建 Range 范围
                range.setStart(textNode, match['index']); // Range 开始位置
                range.setEnd(textNode, match['index'] + match.length); // Range 结束位置
                ranges.push(range); // 范围存入数组
            });
            const hl = new Highlight(...ranges); // 为所有匹配的范围创建高亮对象
            CSS.highlights.set('hl-keyword', hl); // 注册高亮样式并应用到高亮对象
      }
    }
   
    // 指定元素选择文本处理(鼠标松开事件)
    papa.onmouseup = (e) =&gt; {
      CSS.highlights.clear(); // 清空此前的高亮注册
      const findText = window.getSelection().toString(); // 获取选中的文本
      if (!findText || !e.target.closest('#papa')); // 无选中文本或操作对象元素不是指定元素忽略
      hlight(papa, findText); // 调用高亮函数
    };
&lt;/script&gt;
    </pre></div>
    <p>以上示例综合运用了 CCHA 自定义高亮接口、TreeWalker 遍历接口、Selection 和 Range 对象、正则构造以及 CSS ::highlight() 伪元素实现了文章命题即高亮显示指定元素内所有关键词。其中的正则可能需要扩展,如果不满足于仅查找普通关键词的话。</p>
</div>

<script type="module">
    import hl from 'https://638183.freep.cn/638183/web/js/hl.js';
    const divs = document.querySelectorAll('.hEdiv');
    const pres = document.querySelectorAll('.hEpre');
    divs.forEach((div,idx) => hl.hl(div, pres));
</script>

醉美水芙蓉 发表于 2026-7-10 21:26:59

<div class="t_fsz">
<table cellspacing="0" cellpadding="0"><tr><td class="t_f" id="postmessage_2610537">
【附】匹配HTML特殊字符的正则构造函数<br />
<br />
<div class="blockcode"><div id="code_ZP0"><ol><li>function buildEntityRegex(keyword) {<br />
<li>&nbsp; &nbsp; const entityMap = {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&amp;': '&amp;amp;|&amp;#38;|&amp;#x26;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&lt;': '&amp;lt;|&amp;#60;|&amp;#x3C;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&gt;': '&amp;gt;|&amp;#62;|&amp;#x3E;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&quot;': '&amp;quot;|&amp;#34;|&amp;#x22;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&quot;'&quot;: '&amp;#39;|&amp;#x27;|&amp;apos;'<br />
<li>&nbsp; &nbsp; };<br />
<li>&nbsp; &nbsp; <br />
<li>&nbsp; &nbsp; const regexStr = keyword.split('').map(char =&gt; {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;// 如果字符在实体映射中,允许匹配原字符或实体<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (entityMap) {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;return `(?:${char}|${entityMap})`;<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;// 转义正则特殊字符<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&amp;');<br />
<li>&nbsp; &nbsp; }).join('');<br />
<li><br />
<li>&nbsp; &nbsp; return new RegExp(regexStr, 'gi');<br />
<li>}</ol></div><em onclick="copycode($('code_ZP0'));">复制代码</em></div>加入此函数后,在一楼最后的示例中的 hlight() 函数中,将<br />
<div class="blockcode"><div id="code_iiM"><ol><li>const reg = new RegExp(keyword, 'gi');</ol></div><em onclick="copycode($('code_iiM'));">复制代码</em></div>改为<br />
<div class="blockcode"><div id="code_pSG"><ol><li>const reg = buildEntityRegex(keyword);</ol></div><em onclick="copycode($('code_pSG'));">复制代码</em></div>即可匹配诸如 &gt;、&amp;gt; 之类的HTML字符。<br />
</td></tr></table>

醉美水芙蓉 发表于 2026-7-10 21:26:59

<div class="t_fsz">
<table cellspacing="0" cellpadding="0"><tr><td class="t_f" id="postmessage_2610537">
【附】匹配HTML特殊字符的正则构造函数<br />
<br />
<div class="blockcode"><div id="code_ZP0"><ol><li>function buildEntityRegex(keyword) {<br />
<li>&nbsp; &nbsp; const entityMap = {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&amp;': '&amp;amp;|&amp;#38;|&amp;#x26;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&lt;': '&amp;lt;|&amp;#60;|&amp;#x3C;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&gt;': '&amp;gt;|&amp;#62;|&amp;#x3E;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;'&quot;': '&amp;quot;|&amp;#34;|&amp;#x22;',<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&quot;'&quot;: '&amp;#39;|&amp;#x27;|&amp;apos;'<br />
<li>&nbsp; &nbsp; };<br />
<li>&nbsp; &nbsp; <br />
<li>&nbsp; &nbsp; const regexStr = keyword.split('').map(char =&gt; {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;// 如果字符在实体映射中,允许匹配原字符或实体<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;if (entityMap) {<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;return `(?:${char}|${entityMap})`;<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;}<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;// 转义正则特殊字符<br />
<li>&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp;return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&amp;');<br />
<li>&nbsp; &nbsp; }).join('');<br />
<li><br />
<li>&nbsp; &nbsp; return new RegExp(regexStr, 'gi');<br />
<li>}</ol></div><em onclick="copycode($('code_ZP0'));">复制代码</em></div>加入此函数后,在一楼最后的示例中的 hlight() 函数中,将<br />
<div class="blockcode"><div id="code_iiM"><ol><li>const reg = new RegExp(keyword, 'gi');</ol></div><em onclick="copycode($('code_iiM'));">复制代码</em></div>改为<br />
<div class="blockcode"><div id="code_pSG"><ol><li>const reg = buildEntityRegex(keyword);</ol></div><em onclick="copycode($('code_pSG'));">复制代码</em></div>即可匹配诸如 &gt;、&amp;gt; 之类的HTML字符。<br />
</td></tr></table>

醉美水芙蓉 发表于 2026-7-10 21:27:26


RegExp(keyword, 'gi') 中的 gi 参数,g 表示全局匹配,i 表示不区分大小写。如果希望区分大小写,可将 i 去掉。

klxf 发表于 2026-7-11 13:20:55

多途径实现,谢谢分享
页: [1]
查看完整版本: CSS马黑黑 Custom Highlight API+Range+正则实现查找关键字高亮