0

我需要从选择中提取文本并将其发送到 TTS 服务。TTS 服务将为每个单词返回一个流 URL 和一组索引,指示它们的开始和结束位置(时间和文本)。

当用户播放流时,我想在读出每个单词时突出显示它们。为此,我不能只使用每个单词的文本索引,因为它们无法让我回到原始的 HTML 节点——因此我不能使用toString()严格的文本。

到目前为止,我正在做的是使用范围对象的开始和结束容器创建一个 TreeWalker,并使用它来提取范围中的所有文本节点。

问题: window.getSelection().toString()本质上会忽略未显示的节点。这包括<script>节点,<style>节点,节点display: none;等。使用 TreeWalker 不会。

我知道我可以手动跳过 TreeWalker 中的所有这些节点(就像在getSelection 中建议的那样,其中没有 alt 属性和脚本?),但它很快就会变得非常复杂(尤其是检查每个节点的可见性)。

在进入这个之前,我想问一下,自从我链接的问题得到回答后,是否有任何新的方法或库可用?

我不打算将代码跨浏览器,我使用的是纯 Javascript(即没有 jQuery)。

4

2 回答 2

2

首先,我现在建议不要使用window.getSelection().toString(). 它的行为因浏览器而异,目前没有规范。HTML5 规范的草稿版本要求它应该返回调用toString()每个选择范围的结果的串联,这是 IE 9 实现的;WebKit 和 Mozilla 都做了一些更复杂的事情。此外,WebKit 和 Mozilla 所做的事情之间存在差异,它们可以随时更改其实现。

冒着宣传我自己的东西的风险,您也许可以使用我的Rangy库的TextRange 模块,它试图提供在用户看到的文本中导航 DOM 和其中的范围的方法。另一种方法是自己做很多类似的工作,或者限制你的代码可以使用的 HTML。

于 2013-05-28T15:31:00.543 回答
0

在等待答案的同时,我开始编写自己的解析器。这有点粗糙,因为没有跨浏览器支持,而且我没有对文本进行任何修改 - 这意味着 HTML 中的任何换行符和其他空格都将被保留。

还有很多我还没有清理的冗余,比如遍历我已经知道隐藏的节点的子节点。

无论如何,代码:

function ParsedRange(range){
  this.text = "";
  this.nodeIndices = [];

  this.highlight = function(startIndex, endIndex){
    var selection = window.getSelection();
    var startNode = this.nodeIndices[startIndex].node;
    var endNode = this.nodeIndices[endIndex].node;
    var startOffset = startIndex - this.nodeIndices[startIndex].startIndex;
    var endOffset = endIndex - this.nodeIndices[endIndex].startIndex + 1;

    // Scroll into view
    startNode.parentNode.scrollIntoViewIfNeeded();

    // Highlight
    range.setStart(startNode, startOffset);
    range.setEnd(endNode, endOffset);
    selection.removeAllRanges();
    selection.addRange(range);
  };

  // Parsing starts here
  var startIndex;
  var rootNode = range.commonAncestorContainer;
  var startNode = range.startContainer;
  var endNode = range.endContainer;
  var treeWalker = document.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, null, false); // Only walk text and element nodes
  var currentNode = treeWalker.currentNode;

  // Move to start node
  while (currentNode && currentNode != startNode) currentNode = treeWalker.nextNode();

  // Extract text
  var nodeText;
  while (currentNode && currentNode != endNode){ // Handle end node separately
    // Continue to next node if current node is hidden
    if (isHidden(currentNode)){
      currentNode = treeWalker.nextNode();
      continue;
    }

    // Extract text if text node
    if (currentNode.nodeType == 3){
      if (currentNode == startNode) nodeText = currentNode.nodeValue.substring(range.startOffset); // Extract from start of selection if first node
      else nodeText = currentNode.nodeValue; // Else extra entire node

      this.text += nodeText;
      if (currentNode == startNode) startIndex = range.startOffset * -1;
      else startIndex = this.nodeIndices.length;
      for (var i=0; i<nodeText.length; i++){
        this.nodeIndices.push({
          startIndex: startIndex,
          node: currentNode
        });
      }
    }

    // Continue to next node
    currentNode = treeWalker.nextNode();
  }

  // Extract text from end node if it's a text node
  if (currentNode == endNode && currentNode.nodeType == 3 && !isHidden(currentNode)){
    if (endNode == startNode) nodeText = currentNode.nodeValue.substring(range.startOffset, range.endOffset); // Extract only selected part if end and start nodes are the same
    else nodeText = currentNode.nodeValue.substring(0, range.endOffset); // Else extract up to where the selection ends in the end node

    this.text += nodeText;
    if (currentNode == startNode) startIndex = range.startOffset*-1;
    else startIndex = this.nodeIndices.length;
    for (var i=0; i<nodeText.length; i++){
      this.nodeIndices.push({
        startIndex: startIndex,
        node: currentNode
      });
    }
  }

  return this;
}
ParsedRange.removeHighlight = function(){
  window.getSelection().removeAllRanges();
};

function isHidden(element){
  // Get parent node if element is a text node
  if (element.nodeType == 3) element = element.parentNode;

  // Only check visibility of the element itself
  if (window.getComputedStyle(element, null).getPropertyValue("visibility") == "hidden") return true;

  // Check display and dimensions for element and its parents
  while (element){
    if (element.nodeType == 9) return false; // Document
    if (element.tagName == "NOSCRIPT") return true;

    if (window.getComputedStyle(element, null).getPropertyValue("display") == "none") return true;
    if (element.offsetWidth == 0 || element.offsetHeight == 0){ // If element does not have overflow:visible it is hidden
      if (window.getComputedStyle(element, null).getPropertyValue("overflow") != "visible"){
        return true;
      }
    }
    element = element.parentNode;
  }
  return false;
}

isHidden()由于它集成到我的项目中的方式,我将它作为一个类(除了辅助函数)。

除此之外,该类通过传递一个有效范围来工作,然后它将提取范围内的文本并保存对所有节点的引用。这些引用在函数中使用,该highlight()函数使用浏览器选择根据开始和结束字符索引突出显示。

关于nodeIndices财产的额外说明(认为这可能没有意义)。nodeIndices是一个包含以下形式的对象的数组:

{
  startIndex: // Int
  node: // Reference to text node
}

对于我提取到结果文本中的每个字符,我将其中一个对象推送到 上nodeIndices,该node属性只是对文本节点的引用,文本来自该节点。startIndex定义节点在整个文本中从哪个字符开始。

使用这个数组,我可以将字符索引ParsedParagraph.text转换为 HTML 节点以及该节点内相应字符的索引。

使用示例:

// Get start/end nodes and offsets for range  
var startNode = // Code to get start node here, can be a text node or an element node
var startOffset = // Offset into the start node
var endNode = // Code to get end node here, can be a text node or an element node
var endOffset = // Offset into the end node

// Create the range
var range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);

// Parse the range using the ParsedRange class
var parsedRange = new ParsedRange(range);


parsedRange.text; // Contains visible text with whitespaces preserved.
parsedRange.highlight(startIndex, endIndex); // Will highlight the corresponding text inside parsedRange.text using browser selection
于 2013-05-31T14:23:23.890 回答