从HTML中提取文本,同时保留块级元素换行符

时间:2013-12-04 02:04:34

标签: javascript jquery html text strip

背景

关于从HTML中提取文字的大多数questions(即stripping标签)都使用:

jQuery( htmlString ).text();

虽然这会抽象浏览器不一致(例如innerTexttextContent),但函数调用也会忽略块级元素的语义含义(例如li)。

问题

在各种浏览器中保留块级元素的换行符(即语义意图)需要不费力气,如Mike Wilcox describes

一个看似更简单的解决方案是模拟将HTML内容粘贴到<textarea>中,它会删除HTML,同时保留块级元素换行符。但是,当用户将内容粘贴到<textarea>时,基于JavaScript的插入不会触发浏览器使用的相同HTML到文本例程。

我也尝试整合Mike Wilcox的JavaScript code。该代码适用于Chromium,但不适用于Firefox。

问题

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

实施例

考虑:

  1. 选择并复制整个问题。
  2. 打开textarea example page
  3. 将内容粘贴到textarea。
  4. textarea保留有序列表,标题,预格式文本等的换行符。这是我想要实现的结果。

    进一步澄清,给定任何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>
    

    你会如何生产:

      Header
      Paragraph
    
        First
        Second
    
      Term
        Definition
    
      Div with span.
      After the break.
    

    注意:缩进和非规范化的空格都不相关。

5 个答案:

答案 0 :(得分:7)

考虑:

/**
 * 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/

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

答案 1 :(得分: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 :(得分:2)

基于https://stackoverflow.com/a/20384452/3338098 并修复为支持TEXT1<div>TEXT2</div> =&gt; 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';
        }
      }

检查相邻块以确定添加换行符的适当性。

答案 3 :(得分: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"))

答案 4 :(得分: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>