亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

利用Javascript獲取選擇文本所在的句子詳解

發布時間:2020-08-28 00:44:30 來源:腳本之家 閱讀:197 作者:CRIMX 欄目:web開發

前言

最近收到一個 issue 期望能在劃詞的時候同時保存單詞的上下文和來源網址。這個功能其實很久之前就想過,但感覺不好實現一直拖延沒做。真做完發現其實并不復雜,完整代碼在這里,或者繼續往下閱讀分析。話不多說了,來一起看看詳細的介紹吧。

原理分析

獲取選擇文本

通過 window.getSelection() 即可獲得一個 Selection 對象,再利用 .toString() 即可獲得選擇的文本。

錨節點與焦節點

在 Selection 對象中還保存了兩個重要信息,anchorNode 和 focusNode,分別代表選擇產生那一刻的節點和選擇結束時的節點,而 anchorOffset 和 focusOffset 則保存了選擇在這兩個節點里的偏移值。

這時你可能馬上就想到第一個方案:這不就好辦了么,有了首尾節點和偏移,就可以獲取句子的頭部和尾部,再把選擇文本作為中間,整個句子不就出來了么。

當然不會這么簡單哈stuck_out_tongue。

強調一下

一般情況下,anchorNode 和 focusNode 都是 Text 節點(而且因為這里處理的是文本,所以其它情況也會直接忽略),可以考慮這種情況:

<strong>Saladict</strong> is awesome!

如果選擇的是“awesome”,那么 anchorNode 和 focusNode 都是 is awesome!,所以取不到前面的 “Saladict”。

另外還有嵌套的情況,也是同樣的問題。

Saladict is <strong><a href="#" rel="external nofollow" >awesome</a></strong>!

所以我們還需要遍歷兄弟和父節點來獲取完整的句子。

遍歷到哪?

于是接下就是解決遍歷邊界的問題了。遍歷到什么地方為止呢?我的判斷標準是:跳過 inline-level 元素,遇到 block-level 元素為止。而判斷一個元素是 inline-level 還是 block-level 最準確的方式應該是用 window.getComputedStyle() 。但我認為這么做太重了,也不需要嚴格的準確性,所以用了常見的 inline 標簽來判斷。

const INLINE_TAGS = new Set([
 // Inline text semantics
 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i',
 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small',
 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr'
])

原理總結

句子由三塊組成,選擇文本作為中間,然后遍歷兄弟和父節點獲取首尾補上。

實現

選擇文本

先獲取文本,如果沒有則退出

const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText.trim()) { return '' }

獲取首部

對于 anchorNode 只考慮 Text 節點,通過 anchorOffset 獲取選擇在 anchorNode 的前半段內容。

然后開始補全在 anchorNode 之前的兄弟節點,最后補全在 anchorNode 父元素之前的兄弟元素。注意后面是元素,這樣可以減少遍歷的次數,而且考慮到一些被隱藏的內容不需要獲取,用 innerText 而不是 textContent 屬性。

let sentenceHead = ''
const anchorNode = selection.anchorNode
if (anchorNode.nodeType === Node.TEXT_NODE) {
 let leadingText = anchorNode.textContent.slice(0, selection.anchorOffset)
 for (let node = anchorNode.previousSibling; node; node = node.previousSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 leadingText = node.textContent + leadingText
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 leadingText = node.innerText + leadingText
 }
 }

 for (
 let element = anchorNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.previousElementSibling; el; el = el.previousElementSibling) {
 leadingText = el.innerText + leadingText
 }
 }

 sentenceHead = (leadingText.match(sentenceHeadTester) || [''])[0]
}

最后從提取句子首部用的正則是這個

// match head   a.b is ok chars that ends a sentence
const sentenceHeadTester = /((\.(?![ .]))|[^.?!。?!…\r\n])+$/

前面的 ((\.(?![ .])) 主要是為了跳過 a.b 這樣的特別是在技術文章中常見的寫法。

獲取尾部

跟首部同理,換成往后遍歷。最后的正則保留了標點符號

// match tail       for "..."
const sentenceTailTester = /^((\.(?![ .]))|[^.?!。?!…\r\n])+(.)\3{0,2}/

壓縮換行

拼湊完句子之后壓縮多個換行為一個空白行,以及刪除每行開頭結尾的空白符

return (sentenceHead + selectedText + sentenceTail)
 .replace(/(^\s+)|(\s+$)/gm, '\n') // allow one empty line & trim each line
 .replace(/(^\s+)|(\s+$)/g, '') // remove heading or tailing \n

完整代碼

const INLINE_TAGS = new Set([
 // Inline text semantics
 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'dfn', 'em', 'i',
 'kbd', 'mark', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'small',
 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr'
])

/**
* @returns {string}
*/
export function getSelectionSentence () {
 const selection = window.getSelection()
 const selectedText = selection.toString()
 if (!selectedText.trim()) { return '' }

 var sentenceHead = ''
 var sentenceTail = ''

 const anchorNode = selection.anchorNode
 if (anchorNode.nodeType === Node.TEXT_NODE) {
 let leadingText = anchorNode.textContent.slice(0, selection.anchorOffset)
 for (let node = anchorNode.previousSibling; node; node = node.previousSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 leadingText = node.textContent + leadingText
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 leadingText = node.innerText + leadingText
 }
 }

 for (
 let element = anchorNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.previousElementSibling; el; el = el.previousElementSibling) {
 leadingText = el.innerText + leadingText
 }
 }

 sentenceHead = (leadingText.match(sentenceHeadTester) || [''])[0]
 }

 const focusNode = selection.focusNode
 if (selection.focusNode.nodeType === Node.TEXT_NODE) {
 let tailingText = selection.focusNode.textContent.slice(selection.focusOffset)
 for (let node = focusNode.nextSibling; node; node = node.nextSibling) {
 if (node.nodeType === Node.TEXT_NODE) {
 tailingText += node.textContent
 } else if (node.nodeType === Node.ELEMENT_NODE) {
 tailingText += node.innerText
 }
 }

 for (
 let element = focusNode.parentElement;
 element && INLINE_TAGS.has(element.tagName.toLowerCase()) && element !== document.body;
 element = element.parentElement
 ) {
 for (let el = element.nextElementSibling; el; el = el.nextElementSibling) {
 tailingText += el.innerText
 }
 }

 sentenceTail = (tailingText.match(sentenceTailTester) || [''])[0]
 }

 return (sentenceHead + selectedText + sentenceTail)
 .replace(/(^\s+)|(\s+$)/gm, '\n') // allow one empty line & trim each line
 .replace(/(^\s+)|(\s+$)/g, '') // remove heading or tailing \n
}

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

闻喜县| 凌云县| 陵水| 永靖县| 宁强县| 交城县| 怀宁县| 邳州市| 漾濞| 合江县| 浑源县| 平乡县| 吴旗县| 阳曲县| 荥阳市| 武威市| 武强县| 婺源县| 梓潼县| 尉犁县| 湘西| 台湾省| 杭州市| 都江堰市| 航空| 离岛区| 马边| 甘泉县| 澄迈县| 张家口市| 太仓市| 博野县| 土默特左旗| 砀山县| 墨江| 达孜县| 连江县| 南涧| 鸡西市| 楚雄市| 肥东县|