【Web技术】791- HTML字符串中匹配关键词高亮

2024-02-28 16:40

本文主要是介绍【Web技术】791- HTML字符串中匹配关键词高亮,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

来源:木马啊

转载自: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

1. JavaScript 重温系列(22篇全)

2. ECMAScript 重温系列(10篇全)

3. JavaScript设计模式 重温系列(9篇全)

4. 正则 / 框架 / 算法等 重温系列(16篇全)

5. Webpack4 入门(上)|| Webpack4 入门(下)

6. MobX 入门(上) ||  MobX 入门(下)

7. 80+篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 80+ 篇原创文章

这篇关于【Web技术】791- HTML字符串中匹配关键词高亮的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:https://blog.csdn.net/qq_36380426/article/details/110297692
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/756033

相关文章

python web 开发之Flask中间件与请求处理钩子的最佳实践

《pythonweb开发之Flask中间件与请求处理钩子的最佳实践》Flask作为轻量级Web框架,提供了灵活的请求处理机制,中间件和请求钩子允许开发者在请求处理的不同阶段插入自定义逻辑,实现诸如... 目录Flask中间件与请求处理钩子完全指南1. 引言2. 请求处理生命周期概述3. 请求钩子详解3.1

golang float和科学计数法转字符串的实现方式

《golangfloat和科学计数法转字符串的实现方式》:本文主要介绍golangfloat和科学计数法转字符串的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望... 目录golang float和科学计数法转字符串需要对float转字符串做处理总结golang float

SpringBoot项目Web拦截器使用的多种方式

《SpringBoot项目Web拦截器使用的多种方式》在SpringBoot应用中,Web拦截器(Interceptor)是一种用于在请求处理的不同阶段执行自定义逻辑的机制,下面给大家介绍Sprin... 目录一、实现 HandlerInterceptor 接口1、创建HandlerInterceptor实

使用Python和Tkinter实现html标签去除工具

《使用Python和Tkinter实现html标签去除工具》本文介绍用Python和Tkinter开发的HTML标签去除工具,支持去除HTML标签、转义实体并输出纯文本,提供图形界面操作及复制功能,需... 目录html 标签去除工具功能介绍创作过程1. 技术选型2. 核心实现逻辑3. 用户体验增强如何运行

Python如何判断字符串中是否包含特殊字符并替换

《Python如何判断字符串中是否包含特殊字符并替换》这篇文章主要为大家详细介绍了如何使用Python实现判断字符串中是否包含特殊字符并使用空字符串替换掉,文中的示例代码讲解详细,感兴趣的小伙伴可以了... 目录python判断字符串中是否包含特殊字符方法一:使用正则表达式方法二:手动检查特定字符Pytho

CSS 样式表的四种应用方式及css注释的应用小结

《CSS样式表的四种应用方式及css注释的应用小结》:本文主要介绍了CSS样式表的四种应用方式及css注释的应用小结,本文通过实例代码给大家介绍的非常详细,详细内容请阅读本文,希望能对你有所帮助... 一、外部 css(推荐方式)定义:将 CSS 代码保存为独立的 .css 文件,通过 <link> 标签

Web技术与Nginx网站环境部署教程

《Web技术与Nginx网站环境部署教程》:本文主要介绍Web技术与Nginx网站环境部署教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、Web基础1.域名系统DNS2.Hosts文件3.DNS4.域名注册二.网页与html1.网页概述2.HTML概述3.

MySQL 字符串截取函数及用法详解

《MySQL字符串截取函数及用法详解》在MySQL中,字符串截取是常见的操作,主要用于从字符串中提取特定部分,MySQL提供了多种函数来实现这一功能,包括LEFT()、RIGHT()、SUBST... 目录mysql 字符串截取函数详解RIGHT(str, length):从右侧截取指定长度的字符SUBST

Python使用Reflex构建现代Web应用的完全指南

《Python使用Reflex构建现代Web应用的完全指南》这篇文章为大家深入介绍了Reflex框架的设计理念,技术特性,项目结构,核心API,实际开发流程以及与其他框架的对比和部署建议,感兴趣的小伙... 目录什么是 ReFlex?为什么选择 Reflex?安装与环境配置构建你的第一个应用核心概念解析组件

Python将字符串转换为小写字母的几种常用方法

《Python将字符串转换为小写字母的几种常用方法》:本文主要介绍Python中将字符串大写字母转小写的四种方法:lower()方法简洁高效,手动ASCII转换灵活可控,str.translate... 目录一、使用内置方法 lower()(最简单)二、手动遍历 + ASCII 码转换三、使用 str.tr