web-dev-qa-db-ja.com

ブロックレベルの要素の改行を保持しながら、HTMLからテキストを抽出します

バックグラウンド

ほとんどの 質問 HTMLからテキストを抽出することについて(つまり、 ストリッピング タグ)使用:

jQuery( htmlString ).text();

これはブラウザの不整合(innerTexttextContentなど)を抽象化しますが、関数呼び出しはブロックレベルの要素(liなど)のセマンティックな意味も無視します。

問題

Mike Wilcoxが説明している のように、さまざまなブラウザ間でブロックレベル要素の改行(つまり、セマンティックインテント)を保持することは、小さな労力を必要としません。

一見簡単な解決策は、HTMLコンテンツを<textarea>に貼り付けることをエミュレートすることです。これにより、ブロックレベルの要素の改行を保持しながらHTMLが削除されます。ただし、JavaScriptベースの挿入は、ユーザーがコンテンツを<textarea>に貼り付けるときにブラウザーが使用するのと同じHTMLからテキストへのルーチンをトリガーしません。

また、Mike Wilcoxの JavaScriptコード を統合してみました。コードはChromiumで機能しますが、Firefoxでは機能しません。

質問

JQuery(または Vanilla JavaScript )を使用して ブロックレベルの要素 のセマンティック改行を保持しながらHTMLからテキストを抽出する最も簡単なクロスブラウザーの方法は何ですか?

考えてみましょう:

  1. この質問全体を選択してコピーします。
  2. textareaサンプルページ を開きます。
  3. コンテンツをtextareaに貼り付けます。

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>

どのように生産しますか:

ヘッダー
段落
 
最初
 2番目
 
用語
定義
 
スパンで分割します。
休憩後。

注:インデントも正規化されていない空白も関係ありません。

28
Dave Jarvis

考えてみましょう:

/**
 * 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つか2つの例外を除いて、各ノードを反復処理してその内容を出力し、ブラウザの計算されたスタイルで改行を挿入するタイミングを通知します。

7
svidgen

これは(ほぼ)あなたが望むことをしているようです:

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('');
}

[〜#〜]デモ[〜#〜]

すべてのテキストノードの値を再帰的に連結し、<br>要素を改行に置き換えます。

しかし、これにはセマンティクスはありません。完全に元のHTMLフォーマットに依存しています(先頭と末尾の空白は、jsFiddleがHTMLを埋め込む方法に由来しているようです。しかし、それらは簡単にトリミングできます)。たとえば、定義用語がどのようにインデントされているかに注目してください。

セマンティックレベルでこれを本当に実行したい場合は、ブロックレベルの要素のリストが必要です。要素を再帰的に繰り返し、それに応じてインデントします。インデントとその周囲の改行に関して、異なるブロック要素を異なる方法で処理します。これはそれほど難しいことではありません。

3
Felix Kling

https://stackoverflow.com/a/20384452/3338098 に基づき、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';
        }
      }

隣接するブロックをチェックして、改行を追加することの適切性を判断します。

2
user3338098

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"))
1
Antonio Romano