HTML实现关键词高亮字符串中匹配“跨标签关键词”

来源 | https://wintc.top/article/59
一、匹配关键字:HTML字符串与文本字符串对比
1. 纯文本字符串的处理
江畔何人初见月?<font style="background: #ff9632">江月font>何年初照人?2. 对HTML字符串的处理
江畔何人初见<b>月b>?江<b>月b>何年初照人?江畔何人初见<b>月b>?<font style="background: #ff9632">江font><b><font style="background: #ff9632">月font>b>何年初照人?二、跨标签匹配关键词
1. 深度优先遍历DOM树取出文本节点
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结果是个迭代器,用扩展符展开得到数组}

4. 关键词使用font标签替换
<span>江畔何人初见<b>月b>?江月何年初照人?span>
<span>江畔<font>何人初见font><b><font>月font>b><font>?font>江月何年初照人?span>
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}
四、总结
github查看源码:https://github.com/Lushenggang/vue-search-highlight

评论
