关注 前端瓶子君,回复“交流”

加入我们一起学习,天天进步

来源:木马啊

转载自:https://wintc.top/article/59

很久之前写过一个Vue组件,可以匹配文本内容中的关键词高亮,类似浏览器ctrl+f搜索结果。实现方案是,将文本字符串中的关键字搜索出来,然后使用特殊的标签(比如font标签)包裹关键词替换匹配内容,最后得到一个HTML字符串,渲染该字符串并在font标签上使用CSS样式即可实现高亮的效果。

当时的实现过于简单,没有支持接收HTML字符串作为内容进行关键词匹配。这两天有同学问到,就又思考了这个问题,发现并不是那么麻烦,写了几行代码解决一下。

一、匹配关键字:HTML字符串与文本字符串对比

1. 纯文本字符串的处理

对于纯文本字符串,如:“江畔何人初见月?江月何年初照人?”,假如我们想匹配“江月”这个关键字,则匹配结果可处理为:

江畔何人初见月?<font style="background: #ff9632">江月</font>何年初照人?

这样“江月”两个字被font标签包裹,在font标签上应用特殊的背景样式以达到关键字高亮的效果。

2. 对HTML字符串的处理

对于上述例子,如果内容字符串是一个HTML文本:

江畔何人初见<b>月</b>?江<b>月</b>何年初照人?

对于同样的关键词“江月”,怎样处理它呢?因为关键词中的字在不同的标签内,所以只能分别用font标签进行替换:

江畔何人初见<b>月</b>?<font style="background: #ff9632">江</font><b><font style="background: #ff9632">月</font></b>何年初照人?

这是比较简单的情况,实际情况下关键字则可能跨多级、多层标签。

二、跨标签匹配关键词

跨标签解析关键词,其实就是对于匹配到的关键词,提取出各标签中对应的子片段,然后用font之类的标签包裹,再将高亮样式用于font标签即可。

对于整个HTML内容而言,渲染出来的文本由各类标签内的文本节点组成。因为关键词匹配的内容会跨标签,所以需要将各文本节点有序取出,并将节点内容拼接起来进行匹配。拼接时记下节点文本在拼接串中的起止位置,以便关键词匹配到拼接串的某位置时截取文本片段并使用font标签包裹。

1. 深度优先遍历DOM树取出文本节点

深度优先可以采用循环或者递归的方式遍历,这里采用循环实现,按取出某个元素下所有文本节点(利用nodeType判断文本节点):

function getTextNodeList (dom) {const nodeList = [...dom.childNodes]const textNodes = []while (nodeList.length) {const node = nodeList.shift()if (node.nodeType === node.TEXT_NODE) {textNodes.push(node)} else {nodeList.unshift(...node.childNodes)}}return textNodes
}

2. 取出所有文本内容进行拼接

获取到了文本节点列表,可以取出所有文本内容并记录每个文本片段在拼接结果中的开始、结束索引:

getTextInfoList (textNodes) {let length = 0const textList = textNodes.map(text => {let start = length, end = length + text.wholeText.lengthlength = endreturn [text.wholeText, start, end]})return textList
}

拼接文本:

const content = textList.map(([text]) => text).join('')

3. 匹配关键词

获得了拼接文本,可以利用拼接文本获取所有的拼接结果了。这里偷个懒直接用正则匹配吧,得把正则用到的一些特殊符号进行转义一下:

getMatchList (content, keyword) {const characters = [...'[]()?.+*^${}:'].reduce((r, c) => (r[c] = true, r), {})keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*')const reg = new RegExp(keyword, 'gmi')return [...content.matchAll(reg)] // matchAll结果是个迭代器,用扩展符展开得到数组
}

关键词字符转义处理后,字符与字符之间中间插入了正则中的空白符和换行符(\s\n),以在匹配时忽略一些看不见的字符。上述代码使用了matchAll函数,匹配结果展开后得到的结果是一个数组,数组中的每一项都包含了匹配文本、匹配索引等。matchAll的一个简单例子:

img

4. 关键词使用font标签替换

根据关键词匹配结果索引,以及每个文本节点的起止索引,可以计算出每个关键词匹配了哪几个文本节点,其中对于开始和结束的文本节点,可能只是部分匹配到,而中间的文本节点的所有内容都是匹配到的。

比如对于HTML文本:

<span>江畔何人初见<b>月</b>?江月何年初照人?</span>

其DOM树对应的的文本节点有3个:

img

假如关键字是“何人初见月?”,那此时,对于第一个文本节点匹配了后半部分,第二个文本节点完全匹配,第三个文本节点匹配了第一个字符。三个节点中匹配的部分需要分别用font标签替换:

<span>江畔<font>何人初见</font><b><font>月</font></b><font>?</font>江月何年初照人?</span>

默认情况下,连续的文字会在同一个文本节点中,而对于匹配了部分内容的文本节点,就需要将它一分为二,可以利用Text.splitText()")API来分割文本节点,API接收一个索引值,从索引位置将文本节点后半部分切割并返回包含后半部分内容的新文本节点。上述例子中匹配的是3个节点,拆分后就会得到5个文本节点:

img

中间三个文本节点即是需要被替换的节点,使用replaceChild就可以直接将文本节点替换为font标签。

对于整个HTML字符串,同一个关键词可能同时有多处匹配结果,因此要对所有匹配结果进行上述处理。使用前几步获取的textNodes、textList、matchList,代码实现如下:

function replaceMatchResult (textNodes, textList, matchList) {// 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出for (let i = matchList.length - 1; i >= 0; i--) {const match = matchList[i]const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配结果在拼接字符串中的起止索引// 遍历文本信息列表,查找匹配的文本节点for (let textIdx = 0; textIdx < textList.length; textIdx++) {const { text, startIdx, endIdx } = textList[textIdx] // 文本内容、文本在拼接串中开始、结束索引if (endIdx < matchStart) continue // 匹配的文本节点还在后面if (startIdx >= matchEnd) break // 匹配文本节点已经处理完了let textNode = textNodes[textIdx] // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配内容在文本节点内容中的开始索引const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本节点内容匹配关键词的长度if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx) // textNode取后半部分if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)const font = document.createElement('font')font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength)textNode.parentNode.replaceChild(font, textNode)}}
}

代码里对匹配结果遍历时,采用的是倒序遍历,原因是遍历过程对textNodes存在副作用:在遍历中会对textNodes中的文本节点进行切割。假设同一个文本节点中有多处匹配,会进行多次分割,而textNodes里引用的是原文本节点即前半部分,因此从后往前遍历会确保未处理的匹配文本节点的完整。

同时代码中省去了font节点的样式设置,这个可以根据自己的逻辑来设置。

三、完整代码调用

上述步骤描述了HTML字符串跨标签匹配关键词的所有流程实现,下面是完整的代码调用示例:

function replaceKeywords (htmlString, keyword) {if (!keyword) return htmlStringconst div = document.createElement('div')div.innerHTML = htmlStringconst textNodes = getTextNodeList(div)const textList = getTextInfoList(textNodes)const content = textList.map(({ text }) => text).join('')const matchList = getMatchList(content, keyword)replaceMatchResult(textNodes, textList, matchList)return div.innerHTML
}

输入一个HTML字符串和关键词,将HTML串中的关键词用font标签包裹后返回。

四、总结

上述实现方案中有一些简单的细节省去了,比如设置font标签的样式、隐藏的dom匹配时忽略等。

font标签样式设置看使用场景吧,如果是长HTML字符串匹配建议是不要直接设置style属性,而是操作样式表来达到目的。可以给font标签设置特殊的属性,然后使用属性选择器来设置样式。比如可以给font设置highlight="${i}"属性,来针对匹配的关键词应用不同的样式。操作样式表可以给style标签设置innerText或者调用CSSStyleSheet.insertRule()")和CSSStyleSheet.deleteRule()")。

demo: https://wintc.top/laboratory/#/search-highlight

github查看源码:https://github.com/Lushenggang/vue-search-highlight

最后

欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!

回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!

另外,每周还有手写源码题,瓶子君也会解答哟!

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

关键词高亮:HTML字符串中匹配跨标签关键词相关推荐

  1. 从字符串中删除HTML标签

    是否有从Java字符串中删除HTML的好方法? 一个简单的正则表达式 replaceAll("\\<.*?>","") 可以使用,但& 不会 ...

  2. R语言使用str_replace函数和str_replace_all函数替换字符串中匹配到的模式:str_replace函数替换第一个匹配到的字符串、str_replace_all函数替换所有匹配到的

    R语言使用str_replace函数和str_replace_all函数替换字符串中匹配到的模式:str_replace函数替换第一个匹配到的字符串.str_replace_all函数替换所有匹配到的 ...

  3. 【HTML】处理<br>换行符追加到前端换行无效的问题 --- html中渲染的字符串中包含HTML标签无效的处理方法,字符串中包含HTML标签被转义的问题 解决

    [HTML]处理 换行符追加到前端换行无效的问题 --- html中渲染的字符串中包含HTML标签无效的处理方法,字符串中包含HTML标签被转义的问题 解决 参考文章: (1)[HTML]处理 换行符 ...

  4. 将字符串中的html标签编译,将字符串中的HTML标签包含的内容移除

    public static string DeleteHTML(string Htmlstring)//将字符串中的HTML标签包含的内容移除 { #region //删除js脚本 Htmlstrin ...

  5. 正则匹配不包含某字符串_如何替换JS字符串中匹配到多处中某一指定节点?

    来源 | https://www.cnblogs.com/class1/p/14273231.html 问题先行,要求搜索关键字,匹配到四处,那我鼠标放在第二处,我想把它变个颜色,该怎么实现呢?截图如 ...

  6. 输出字符串中匹配最多的括号数

    [java]输出字符串中匹配最多的括号数 例如: 1: (()) 输出2: 2:((()))()(()) 输出3: public class Demo { public static void mai ...

  7. php去除字符串样式,php去除字符串中的HTML标签方法总结

    php去除字符串中的HTML标签方法有很多的今天在做一个采集小功能时发现了有N种方法,下面我为各位整理一下有原创的也有整理的,希望对大家有帮助. 先来看自己的写法  代码如下 复制代码 str_rep ...

  8. 字符串中匹配中文标点符号

    字符串中匹配中文标点符号 //匹配中文字符以及这些中文标点符号 . ? ! , . : : " " ' ' ( ) < > 〈 〉 [ ] 『 』 「 」 ﹃ ﹄ [ ...

  9. html 标签 r语言,从R中的字符串中删除html标签

    我正在尝试将网页源代码读入R并将其作为字符串处理.我正在尝试删除段落并从段落文本中删除html标签.我遇到了以下问题: 我尝试实现一个功能来删除html标签: cleanFun=function(fu ...

最新文章

  1. 为计算机编程序英语作文,计算机编程员英文简历范文
  2. python通过端口和协议查出服务名
  3. oracle外部表kup-04040,【故障处理】19c PDB中创建外部表时,出现KUP-04040报错
  4. 使用nginx cache缓存网站数据实践
  5. post怎么用php,$_POST[''];怎么用
  6. 网页标题设计原则与一般规律
  7. Kali linux 2016.2(Rolling)中的Exploits模块详解
  8. Excel 动态透视表
  9. 清华大学计算机考研信息汇总
  10. linux 磁盘隔离,Linux 磁盘坏道故障修复
  11. 方差为什么用平方不用绝对值,为什么要对差值求平方而不是取标准偏差的绝对值?...
  12. Ubuntu11.04下安装QQ2011
  13. 短语匹配-match_phrase以及slop参数
  14. 义隆单片机学习笔记之(四) 编程及烧录
  15. 腾讯云香港轻量新IP段简单测评
  16. mdp文件-Chapter2-NVT.mdp
  17. 【莹伙丛】手把手教你:Gradle 安装及配置
  18. 2003系统服务器防域名报毒,【系统之家】木马病毒无孔不入 win 2003系统也要防木马...
  19. ArcGIS如何获取地理要素的几何边界
  20. Deepin XP V5系列完美精简版合集

热门文章

  1. 前端Q面试类文章整理(文末送现金红包等礼品)
  2. 解析视频分辨率和时长
  3. 谈一谈,Web3D!
  4. 最好用的Redis Desktop Manager 0.9.3 版本下载 以及源码编译教程
  5. SequoiaDB 技术简介
  6. mysql executebatch_Mysql批量插入executeBatch测试
  7. 【k8s错误解决系列】kubelet报错too many open files
  8. meshgrid函数说明
  9. 懒汉模式和饿汉模式的区别
  10. 2000年互联网泡沫