Javascriptを利用して選択テキストのある文の詳細を取得します。


前言
最近issueを受け取りました。単語のコンテキストとソースアドレスを一緒に保存したいです。この機能はずっと前から考えていましたが、実現がよくないと思います。ずっと遅延しています。実際に完了したら、複雑ではないです。完全コードはここですにあります。あるいは続けて読んで分析します。話が多くないので、詳しく紹介してみましょう。
原理分析
選択テキストを取得window.getSelection()でSelectionオブジェクトが得られ、.toString() で選択テキストが得られます。
アンカーノードとフォーカスノード
Selectionオブジェクトには、2つの重要な情報が保存されています。anchorrNodeとfocus Nodeは、その時点で発生したノードと選択終了時のノードをそれぞれ表し、anchorrOffsetとfocusOffsetは、この2つのノードに選択されたオフセット値を保存しています。
この時、すぐに第一案を思いつくかもしれません。これでいいですか?先頭ノードとオフセットがあれば、文の頭と尾を取って、選択テキストを中間として、文全体が出てきます。
もちろんこんなに簡単にはできません。out_トング。
強調してください
一般的には、anchorrNodeとfocusNodeはTextノードである(しかもここで処理されているのはテキストなので、他の状況も直接無視される)。

<strong>Saladict</strong> is awesome!
もし選択したのが「awesome」なら、anchorrNodeとfocus Nodeはすべてis awesomeです。前の「Saladict」が取れません。
また入れ子がある場合も同じ問題です。

Saladict is <strong><a href="#" rel="external nofollow" >awesome</a></strong>!
ですから、私たちは兄弟と両親のノードを通して、完璧な文章を取得する必要があります。
どこまで遍歴しますか?
そして次は境界を巡る問題を解決することです。どこまで遍歴しますか?私の判断基準は、inline-level要素をスキップし、block-level要素に出会うまでです。要素がinline-levelかそれともblock-levelかを判断する最も正確な方法はwindow.getComputedStyle()を使うべきです。しかし、このようにするのは重すぎて、厳密な正確性も必要ないと思います。

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'
])
原理のまとめ
文は3つのブロックから構成され、テキストを中間として選択し、兄弟と親ノードを経て最初の最後の部分を取得します。
実現する
テキストを選択
テキストを先に取得し、なければ終了します。

const selection = window.getSelection()
const selectedText = selection.toString()
if (!selectedText.trim()) { return '' }
ヘッダを取得
anchorrNodeについてはTextノードのみを考慮し、anchorrOffsetを通じてanchorrNodeで選択された前半のコンテンツを取得する。
その後、アンチョーノードの前の兄弟ノードを補完し始め、最後にアンチョーNodeの父要素の前の兄弟要素を補完します。後に要素があることに注意してください。このようにして遍歴の回数を減らすことができます。また、隠しコンテンツのいくつかを考慮して、textContent属性ではなくinnerTextを使用します。

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
])+$/
前の(\.(?!))主にa.bのような、特に技術的な文章でよく見られる書き方をスキップするためです。
末尾を取得
最初と同じ理屈で、後になって巡回します。最後の正則は句読点を残しています。

// match tail       for "..."
const sentenceTailTester = /^((\.(?![ .]))|[^.?!。?!…\r
])+(.)\3{0,2}/
行を詰める
文をまとめた後、複数の行を空白行に圧縮し、各行の先頭の空白文字を削除します。

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

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, '
') // allow one empty line & trim each line .replace(/(^\s+)|(\s+$)/g, '') // remove heading or tailing
}
締め括りをつける
以上はこの文章の全部の内容です。本文の内容は皆さんの学習や仕事に対して一定の参考となる学習価値を持っています。質問があれば、メッセージを書いて交流してください。ありがとうございます。