HTML實(shí)現(xiàn)關(guān)鍵詞高亮字符串中匹配“跨標(biāo)簽關(guān)鍵詞”

來源 |?https://wintc.top/article/59
一、匹配關(guān)鍵字:HTML字符串與文本字符串對(duì)比
1. 純文本字符串的處理
江畔何人初見月?<font style="background:?#ff9632">江月font>何年初照人?2. 對(duì)HTML字符串的處理
江畔何人初見<b>月b>?江<b>月b>何年初照人?江畔何人初見<b>月b>?<font style="background: #ff9632">江font><b><font style="background: #ff9632">月font>b>何年初照人?二、跨標(biāo)簽匹配關(guān)鍵詞
1. 深度優(yōu)先遍歷DOM樹取出文本節(jié)點(diǎn)
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. 取出所有文本內(nèi)容進(jìn)行拼接
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. 匹配關(guān)鍵詞
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結(jié)果是個(gè)迭代器,用擴(kuò)展符展開得到數(shù)組}

4. 關(guān)鍵詞使用font標(biāo)簽替換
<span>江畔何人初見<b>月b>?江月何年初照人?span>
<span>江畔<font>何人初見font><b><font>月font>b><font>?font>江月何年初照人?span>
function replaceMatchResult (textNodes, textList, matchList) {// 對(duì)于每一個(gè)匹配結(jié)果,可能分散在多個(gè)標(biāo)簽中,找出這些標(biāo)簽,截取匹配片段并用font標(biāo)簽替換出for (let i = matchList.length - 1; i >= 0; i--) {const match = matchList[i]const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配結(jié)果在拼接字符串中的起止索引// 遍歷文本信息列表,查找匹配的文本節(jié)點(diǎn)for (let textIdx = 0; textIdx < textList.length; textIdx++) {const { text, startIdx, endIdx } = textList[textIdx] // 文本內(nèi)容、文本在拼接串中開始、結(jié)束索引if (endIdx < matchStart) continue // 匹配的文本節(jié)點(diǎn)還在后面if (startIdx >= matchEnd) break // 匹配文本節(jié)點(diǎn)已經(jīng)處理完了let textNode = textNodes[textIdx] // 這個(gè)節(jié)點(diǎn)中的部分或全部?jī)?nèi)容匹配到了關(guān)鍵詞,將匹配部分截取出來進(jìn)行替換const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配內(nèi)容在文本節(jié)點(diǎn)內(nèi)容中的開始索引const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本節(jié)點(diǎn)內(nèi)容匹配關(guān)鍵詞的長(zhǎng)度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)}}}
代碼里對(duì)匹配結(jié)果遍歷時(shí),采用的是倒序遍歷,原因是遍歷過程對(duì)textNodes存在副作用:在遍歷中會(huì)對(duì)textNodes中的文本節(jié)點(diǎn)進(jìn)行切割。假設(shè)同一個(gè)文本節(jié)點(diǎn)中有多處匹配,會(huì)進(jìn)行多次分割,而textNodes里引用的是原文本節(jié)點(diǎn)即前半部分,因此從后往前遍歷會(huì)確保未處理的匹配文本節(jié)點(diǎn)的完整。
同時(shí)代碼中省去了font節(jié)點(diǎn)的樣式設(shè)置,這個(gè)可以根據(jù)自己的邏輯來設(shè)置。
三、完整代碼調(diào)用
上述步驟描述了HTML字符串跨標(biāo)簽匹配關(guān)鍵詞的所有流程實(shí)現(xiàn),下面是完整的代碼調(diào)用示例:
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}
四、總結(jié)
github查看源碼:https://github.com/Lushenggang/vue-search-highlight

評(píng)論
圖片
表情
