从HTML中提取文本并保留块级元素的换行符

32

背景

大多数与从HTML中提取文本(即去除标签)有关的 问题 采用以下方法:

jQuery( htmlString ).text();

虽然这种方法摆脱了浏览器不一致性(例如 innerTexttextContent),但该函数调用也忽略了块级元素(例如 li)的语义意义。

问题

在各种浏览器中保留块级元素(即语义意图)的换行符需要付出很大的努力,正如 Mike Wilcox 描述 的那样。

一个看似更简单的解决方案是模拟将 HTML 内容粘贴到 <textarea> 中,这样可以剥离 HTML 并保留块级元素的换行符。然而,基于 JavaScript 的插入不会触发用户将内容粘贴到 <textarea> 时浏览器使用的相同 HTML-to-text 程序。

我还尝试集成 Mike Wilcox 的 JavaScript 代码。该代码在 Chromium 中有效,但在 Firefox 中无效。

问题

什么是使用jQuery(或vanilla JavaScript)从HTML中提取文本的最简单的跨浏览器方法,同时保留块级元素的语义换行符?

示例

考虑以下内容:

  1. 选择并复制整个问题。
  2. 打开textarea示例页面
  3. 将内容粘贴到文本区域中。

文本区域会保留有序列表、标题、预格式化文本等的换行符。这就是我想要实现的结果。

进一步说明,对于任何HTML内容,例如:

   <h1>Header</h1>
   <p>Paragraph</p>
   <ul>
     <li>First</li>
     <li>Second</li>
   </ul>
   <dl>
     <dt>Term</dt>
       <dd>Definition</dd>
   </dl>
   <div>Div with <span>span</span>.<br />After the <a href="...">break</a>.</div>

我该如何创建以下内容:
标题 段落
第一项 第二项
术语 定义
带有 span 的 div。 分隔符后。
注:缩进和非规范化空格均不相关。

听起来是一个有趣的问题。我期望一种方法,可以迭代元素和节点,在进入或离开具有块计算样式(但如果没有中间文本节点则不要双重)或导致新行的元素时插入一个新行(也许是BR、HR、TR)。处理表格是另一个问题。 - RobG
1
我已经有了一个可行的方案,但它依赖于HTML格式:http://jsfiddle.net/jLpCT/。如果你真的想要基于块级事件获取缩进文本,你需要检查每个元素并根据元素是否为块级元素做出决策(也许还要对不同的块级元素进行不同的处理)。正如你所看到的,我发布了一个答案,但由于我不确定这是否是你想要的方向,所以将其删除了。 - Felix Kling
5个回答

9

请考虑:

/**
 * Returns the style for a node.
 *
 * @param n The node to check.
 * @param p The property to retrieve (usually 'display').
 * @link http://www.quirksmode.org/dom/getstyles.html
 */
this.getStyle = function( n, p ) {
  return n.currentStyle ?
    n.currentStyle[p] :
    document.defaultView.getComputedStyle(n, null).getPropertyValue(p);
}

/**
 * Converts HTML to text, preserving semantic newlines for block-level
 * elements.
 *
 * @param node - The HTML node to perform text extraction.
 */
this.toText = function( node ) {
  var result = '';

  if( node.nodeType == document.TEXT_NODE ) {
    // Replace repeated spaces, newlines, and tabs with a single space.
    result = node.nodeValue.replace( /\s+/g, ' ' );
  }
  else {
    for( var i = 0, j = node.childNodes.length; i < j; i++ ) {
      result += _this.toText( node.childNodes[i] );
    }

    var d = _this.getStyle( node, 'display' );

    if( d.match( /^block/ ) || d.match( /list/ ) || d.match( /row/ ) ||
        node.tagName == 'BR' || node.tagName == 'HR' ) {
      result += '\n';
    }
  }

  return result;
}

http://jsfiddle.net/3mzrV/2/

换句话说,除了一两个例外,遍历每个节点并打印其内容,让浏览器的计算样式告诉您何时插入换行符。


getStyle 总是返回 '',所以我假设这意味着 "node" 必须在 DOM 中处于活动状态才能工作? - user3338098
改为支持 TEXT1<div>TEXT2</div> => TEXT1\nTEXT2 https://dev59.com/JWIj5IYBdhLWcg3wWT2c#29706729 - user3338098
这个fiddle使用了答案中展示的简化版本的代码。最终我使用了这个fiddle版本,并通过运行结果.replace(/\n{3,}/g,'\n\n')来去除多余的换行符。 - apostl3pol
创建了这个分支(http://jsfiddle.net/14rpmyn3/2/),使用`<p>包装块代替\n`,这对于插入到DOM中更加合适。 - Edward D'Souza

3

这似乎(几乎)可以做到你想要的:

function getText($node) {
    return $node.contents().map(function () {
        if (this.nodeName === 'BR') {
            return '\n';
        } else if (this.nodeType === 3) {
            return this.nodeValue;
        } else {
            return getText($(this));
        }
    }).get().join('');
}

DEMO

这个程序会递归地连接所有文本节点的值,并使用换行符替换<br>元素。

但是它没有任何语义,它完全依赖于原始HTML格式(前导和尾随的空白似乎来自于jsFiddle嵌入HTML的方式,但是你可以轻松地修剪它们)。例如,请注意它如何缩进定义术语。

如果你真的想在语义层次上完成这个任务,你需要一个块级元素列表,对元素进行递归迭代,并相应地进行缩进。你需要根据块级元素的不同而不同地处理它们与缩进以及其中间的换行符。这不应该太困难。


2

基于 https://dev59.com/JWIj5IYBdhLWcg3wWT2c#20384452 的实现,修复了对于 TEXT1<div>TEXT2</div>=>TEXT1\nTEXT2 的支持,并允许非DOM节点。

/**
 * Returns the style for a node.
 *
 * @param n The node to check.
 * @param p The property to retrieve (usually 'display').
 * @link http://www.quirksmode.org/dom/getstyles.html
 */
function getNodeStyle( n, p ) {
  return n.currentStyle ?
    n.currentStyle[p] :
    document.defaultView.getComputedStyle(n, null).getPropertyValue(p);
}

//IF THE NODE IS NOT ACTUALLY IN THE DOM then this won't take into account <div style="display: inline;">text</div>
//however for simple things like `contenteditable` this is sufficient, however for arbitrary html this will not work
function isNodeBlock(node) {
  if (node.nodeType == document.TEXT_NODE) {return false;}
  var d = getNodeStyle( node, 'display' );//this is irrelevant if the node isn't currently in the current DOM.
  if (d.match( /^block/ ) || d.match( /list/ ) || d.match( /row/ ) ||
      node.tagName == 'BR' || node.tagName == 'HR' ||
      node.tagName == 'DIV' // div,p,... add as needed to support non-DOM nodes
     ) {
    return true;
  }
  return false;
}

/**
 * Converts HTML to text, preserving semantic newlines for block-level
 * elements.
 *
 * @param node - The HTML node to perform text extraction.
 */
function htmlToText( htmlOrNode, isNode ) {
  var node = htmlOrNode;
  if (!isNode) {node = jQuery("<span>"+htmlOrNode+"</span>")[0];}
  //TODO: inject "unsafe" HTML into current DOM while guaranteeing that it won't
  //      change the visible DOM so that `isNodeBlock` will work reliably
  var result = '';
  if( node.nodeType == document.TEXT_NODE ) {
    // Replace repeated spaces, newlines, and tabs with a single space.
    result = node.nodeValue.replace( /\s+/g, ' ' );
  } else {
    for( var i = 0, j = node.childNodes.length; i < j; i++ ) {
      result += htmlToText( node.childNodes[i], true );
      if (i < j-1) {
        if (isNodeBlock(node.childNodes[i])) {
          result += '\n';
        } else if (isNodeBlock(node.childNodes[i+1]) &&
                   node.childNodes[i+1].tagName != 'BR' &&
                   node.childNodes[i+1].tagName != 'HR') {
          result += '\n';
        }
      }
    }
  }
  return result;
}

主要变化是:
      if (i < j-1) {
        if (isNodeBlock(node.childNodes[i])) {
          result += '\n';
        } else if (isNodeBlock(node.childNodes[i+1]) &&
                   node.childNodes[i+1].tagName != 'BR' &&
                   node.childNodes[i+1].tagName != 'HR') {
          result += '\n';
        }
      }

检查相邻块以确定是否适合添加换行符。


1
我想建议对svidgen的代码进行一些小修改:

function getText(n, isInnerNode) {
  var rv = '';
  if (n.nodeType == 3) {
      rv = n.nodeValue;
  } else {
      var partial = "";
      var d = getComputedStyle(n).getPropertyValue('display');
      if (isInnerNode && d.match(/^block/) || d.match(/list/) || n.tagName == 'BR') {
          partial += "\n";
      }

      for (var i = 0; i < n.childNodes.length; i++) {
          partial += getText(n.childNodes[i], true);
      }
      rv = partial;
  }
  return rv;
 };

我刚刚在for循环前添加了换行符,这样我们就可以在块之前有一个新行,并且还有一个变量来避免根元素的新行。

应该调用代码:

getText(document.getElementById("divElement"))


0
使用 element.innerText 来获取元素文本内容,这样就不会返回从 contenteditable 元素中添加的额外节点。 如果你使用了 element.innerHTML,则文本将包含额外的标记,但 innerText 将返回元素内容中所看到的文本。
<div id="txt" contenteditable="true"></div>

<script>
  var txt=document.getElementById("txt");
  var withMarkup=txt.innerHTML;
  var textOnly=txt.innerText;
  console.log(withMarkup);
  console.log(textOnly);
</script>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接