国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

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

很久之前(好像剛好是一年前)寫過一個(gè)Vue組件,匹配文本內(nèi)容中的關(guān)鍵詞高亮,類似瀏覽器ctrl+f搜索結(jié)果。實(shí)現(xiàn)方案是,將文本字符串中的關(guān)鍵字搜索出來,然后使用特殊的標(biāo)簽(比如font標(biāo)簽)包裹關(guān)鍵詞替換匹配內(nèi)容,最后得到一個(gè)HTML字符串,渲染該字符串并在font標(biāo)簽上使用CSS樣式即可實(shí)現(xiàn)高亮的效果。

當(dāng)時(shí)的實(shí)現(xiàn)過于簡單,沒有支持接收HTML字符串作為內(nèi)容進(jìn)行關(guān)鍵詞匹配。這兩天有同學(xué)問到,就又思考了這個(gè)問題,發(fā)現(xiàn)并不是那么麻煩,寫了幾行代碼解決了一下。

一、匹配關(guān)鍵字:HTML字符串與文本字符串對比

1. 純文本字符串的處理

對于純文本字符串,如:“江畔何人初見月?江月何年初照人? ”,假如我們想匹配“江月”這個(gè)關(guān)鍵字,則匹配結(jié)果可處理為:

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

這樣“江月”兩個(gè)字被font標(biāo)簽包裹,在font標(biāo)簽上應(yīng)用特殊的背景樣式以達(dá)到關(guān)鍵字高亮的效果。

2. 對HTML字符串的處理

對于上述例子,如果內(nèi)容字符串是一個(gè)HTML文本:

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

對于同樣的關(guān)鍵詞“江月”,怎樣處理它呢?因?yàn)殛P(guān)鍵詞中的字在不同的標(biāo)簽內(nèi),所以只能分別用font標(biāo)簽進(jìn)行替換:

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

這是比較簡單的情況,實(shí)際情況下關(guān)鍵字則可能跨多級、多層標(biāo)簽。

二、跨標(biāo)簽匹配關(guān)鍵詞

跨標(biāo)簽解析關(guān)鍵詞,其實(shí)就是對于匹配到的關(guān)鍵詞,提取出各標(biāo)簽中對應(yīng)的子片段,然后用font之類的標(biāo)簽包裹,再將高亮樣式用于font標(biāo)簽即可。

對于整個(gè)HTML內(nèi)容而言,渲染出來的文本由各類標(biāo)簽內(nèi)的文本節(jié)點(diǎn)組成。因?yàn)殛P(guān)鍵詞匹配的內(nèi)容會跨標(biāo)簽,所以需要將各文本節(jié)點(diǎn)有序取出,并將節(jié)點(diǎn)內(nèi)容拼接起來進(jìn)行匹配。拼接時(shí)記下節(jié)點(diǎn)文本在拼接串中的起止位置,以便關(guān)鍵詞匹配到拼接串的某位置時(shí)截取文本片段并使用font標(biāo)簽包裹。

1. 深度優(yōu)先遍歷DOM樹取出文本節(jié)點(diǎn)

深度優(yōu)先可以采用循環(huán)或者遞歸的方式遍歷,這里采用循環(huán)實(shí)現(xiàn),按取出某個(gè)元素下所有文本節(jié)點(diǎn)(利用nodeType判斷文本節(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)行拼接

獲取到了文本節(jié)點(diǎn)列表,可以取出所有文本內(nèi)容并記錄每個(gè)文本片段在拼接結(jié)果中的開始、結(jié)束索引:

function getTextInfoList (textNodes) {
  let length = 0
  const textList = textNodes.map(node => {
    let startIdx = length, endIdx = length + node.wholeText.length
    length = endIdx
    return {
      text: node.wholeText,
      startIdx,
      endIdx
    }
  })
  return textList
},

拼接文本:

const content = textList.map(({ text }) => text).join('')

3. 匹配關(guān)鍵詞

獲得了拼接文本,可以利用拼接文本獲取所有的拼接結(jié)果了。這里偷個(gè)懶直接用正則匹配吧,得把正則用到的一些特殊符號進(jìn)行轉(zhuǎ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')
  const matchList = []
  let match = reg.exec(content)
  while (match) {
    matchList.push(match)
    match = reg.exec(content)
  }
  return matchList
},

關(guān)鍵詞字符轉(zhuǎn)義處理后,字符與字符之間中間插入了正則中的空白符和換行符(\s\n),以在匹配時(shí)忽略一些看不見的字符。上述代碼循環(huán)使用使用RegExp.prototype.exec匹配拼接字符串中的所有關(guān)鍵詞,每一次匹配的結(jié)果中都包含了本次匹配到的文本、匹配索引等,一個(gè)簡單的例子:

注:String.prototype.matchAll可以一次查出所有匹配結(jié)果,但是瀏覽器兼容性不好,matchAll的匹配示例:

4. 關(guān)鍵詞使用font標(biāo)簽替換

根據(jù)關(guān)鍵詞匹配結(jié)果索引,以及每個(gè)文本節(jié)點(diǎn)的起止索引,可以計(jì)算出每個(gè)關(guān)鍵詞匹配了哪幾個(gè)文本節(jié)點(diǎn),其中對于開始和結(jié)束的文本節(jié)點(diǎn),可能只是部分匹配到,而中間的文本節(jié)點(diǎn)的所有內(nèi)容都是匹配到的。

比如對于HTML文本:

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

其DOM樹對應(yīng)的的文本節(jié)點(diǎn)有3個(gè):

假如關(guān)鍵字是“何人初見月?”,那此時(shí),對于第一個(gè)文本節(jié)點(diǎn)匹配了后半部分,第二個(gè)文本節(jié)點(diǎn)完全匹配,第三個(gè)文本節(jié)點(diǎn)匹配了第一個(gè)字符。三個(gè)節(jié)點(diǎn)中匹配的部分需要分別用font標(biāo)簽替換:

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

默認(rèn)情況下,連續(xù)的文字會在同一個(gè)文本節(jié)點(diǎn)中,而對于匹配了部分內(nèi)容的文本節(jié)點(diǎn),就需要將它一分為二,可以利用Text.splitText()API來分割文本節(jié)點(diǎn),API接收一個(gè)索引值,從索引位置將文本節(jié)點(diǎn)后半部分切割并返回包含后半部分內(nèi)容的新文本節(jié)點(diǎn)。上述例子中匹配的是3個(gè)節(jié)點(diǎn),拆分后就會得到5個(gè)文本節(jié)點(diǎn):

中間三個(gè)文本節(jié)點(diǎn)即是需要被替換的節(jié)點(diǎn),使用replaceChild就可以直接將文本節(jié)點(diǎn)替換為font標(biāo)簽。

對于整個(gè)HTML字符串,同一個(gè)關(guān)鍵詞可能同時(shí)有多處匹配結(jié)果,因此要對所有匹配結(jié)果進(jìn)行上述處理。使用前幾步獲取的textNodes、textList、matchList,代碼實(shí)現(xiàn)如下:

function replaceMatchResult (textNodes, textList, matchList) {
  // 對于每一個(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)中的部分或全部內(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)鍵詞的長度
      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)
    }
  }
}

代碼里對匹配結(jié)果遍歷時(shí),采用的是倒序遍歷,原因是遍歷過程對textNodes存在副作用:在遍歷中會對textNodes中的文本節(jié)點(diǎn)進(jìn)行切割。假設(shè)同一個(gè)文本節(jié)點(diǎn)中有多處匹配,會進(jìn)行多次分割,而textNodes里引用的是原文本節(jié)點(diǎn)即前半部分,因此從后往前遍歷會確保未處理的匹配文本節(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 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
}

輸入一個(gè)HTML字符串和關(guān)鍵詞,將HTML串中的關(guān)鍵詞用font標(biāo)簽包裹后返回。

四、總結(jié)

上述實(shí)現(xiàn)方案中有一些簡單的細(xì)節(jié)省去了,比如設(shè)置font標(biāo)簽的樣式、隱藏的dom匹配時(shí)忽略等。

font標(biāo)簽樣式設(shè)置看使用場景吧,如果是長HTML字符串匹配建議是不要直接設(shè)置style屬性,而是操作樣式表來達(dá)到目的??梢越ofont標(biāo)簽設(shè)置特殊的屬性,然后使用屬性選擇器來設(shè)置樣式。比如可以給font設(shè)置highlight="${i}"屬性,來針對匹配的關(guān)鍵詞應(yīng)用不同的樣式。操作樣式表可以給style標(biāo)簽設(shè)置innerText或者調(diào)用CSSStyleSheet.insertRule()CSSStyleSheet.deleteRule()。

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

github查看源碼:https://github.com/wintc23/vue-search-highlight


本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
HTML+CSS+JS詳解 | w3c筆記
比正則表達(dá)式好用,用BeautifulSoup來煲美味的湯
解析庫BeautifulSoup4基本使用[python]
什么是H1標(biāo)簽,網(wǎng)站優(yōu)化時(shí)怎么使用H1標(biāo)簽?
FastReport"Text"對象中的HTML標(biāo)簽介紹以及使用
CSS標(biāo)簽選擇器之樣式選擇器(1)
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服