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 = 0
const textList = textNodes.map(text => {
let start = length, end = length + text.wholeText.length
length = end
return [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 htmlString
const div = document.createElement('div')
div.innerHTML = htmlString
const 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
评论