资讯专栏INFORMATION COLUMN

获取选择文本所在的句子

Eidesen / 2404人阅读

摘要:原理分析获取选择文本通过即可获得一个对象,再利用即可获得选择的文本。所以我们还需要遍历兄弟和父节点来获取完整的句子。实现选择文本先获取文本,如果没有则退出获取首部对于只考虑节点,通过获取选择在的前半段内容。

最近收到一个 issue 期望能在划词的时候同时保存单词的上下文和来源网址。这个功能其实很久之前就想过,但感觉不好实现一直拖延没做。真做完发现其实并不复杂,完整代码在这里,或者继续往下阅读分析。

原理分析 获取选择文本

通过 window.getSelection() 即可获得一个 Selection 对象,再利用 .toString() 即可获得选择的文本。

锚节点与焦节点

Selection 对象中还保存了两个重要信息,anchorNodefocusNode,分别代表选择产生那一刻的节点和选择结束时的节点,而 anchorOffsetfocusOffset 则保存了选择在这两个节点里的偏移值。

这时你可能马上就想到第一个方案:这不就好办了么,有了首尾节点和偏移,就可以获取句子的头部和尾部,再把选择文本作为中间,整个句子不就出来了么。

当然不会这么简单哈。

强调一下

一般情况下,anchorNodefocusNode 都是 Text 节点(而且因为这里处理的是文本,所以其它情况也会直接忽略),可以考虑这种情况:

Saladict is awesome!

如果选择的是“awesome”,那么 anchorNodefocusNode 都是 is awesome!,所以取不到前面的 “Saladict”。

另外还有嵌套的情况,也是同样的问题。

Saladict is awesome!

所以我们还需要遍历兄弟和父节点来获取完整的句子。

遍历到哪?

于是接下就是解决遍历边界的问题了。遍历到什么地方为止呢?我的判断标准是:跳过 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 = /((.(?![ .]))|[^.?!。?!…
])+$/

前面的 ((.(?![ .])) 主要是为了跳过 a.b 这样的特别是在技术文章中常见的写法。

获取尾部

跟首部同理,换成往后遍历。最后的正则保留了标点符号

// match tail                                                    for "..."
const sentenceTailTester = /^((.(?![ .]))|[^.?!。?!…
])+(.)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 

}

【完】

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90147.html

相关文章

  • LSTM分类相关

    摘要:而检验模型用到的原材料,包括薛云老师提供的蒙牛牛奶的评论,以及从网络购买的某款手机的评论数据见附件。不同行业某些词语的词频会有比较大的差别,而这些词有可能是情感分类的关键词之一。这是由于文本情感分类的本质复杂性所致的。 文本情感分类--传统模型(转) showImg(https://segmentfault.com/img/bVKjWF?w=2192&h=534); 传统的基于情感词典...

    MartinHan 评论0 收藏0
  • 编辑器之神-VIM

    摘要:在这天地间,流传这两大神器的故事据说是神的编辑器,而是编辑器之神。正所谓,工欲善其事必先利其器。今天就和大家分享一下关于编辑器之神的传说。主要用于用来编写和查看文本文件。 在这天地间,流传这两大神器的故事:据说Emacs是神的编辑器,而Vim是编辑器之神。正所谓,工欲善其事,必先利其器。今天就和大家分享一下关于编辑器之神Vim的传说。 一、Vim的历史 1.下图是关于几款主流编辑器...

    imingyu 评论0 收藏0
  • 关于深度学习中注意力机制,这篇文章从实例到原理都帮你参透了

    摘要:本文以机器翻译为例,深入浅出地介绍了深度学习中注意力机制的原理及关键计算机制,同时也抽象出其本质思想,并介绍了注意力模型在图像及语音等领域的典型应用场景。 最近两年,注意力模型(Attention Model)被广泛使用在自然语言处理、图像识别及语音识别等各种不同类型的深度学习任务中,是深度学习技术中最值得关注与深入了解的核心技术之一。本文以机器翻译为例,深入浅出地介绍了深度学习中注意力机制...

    iliyaku 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<