Извлечение текста из HTML при сохранении новых строк уровня блока - программирование
Подтвердить что ты не робот

Извлечение текста из HTML при сохранении новых строк уровня блока

Фон

В большинстве вопросов об извлечении текста из HTML (т.е. удаление тегов) используйте:

jQuery( htmlString ).text();

Хотя это абстрагирует несоответствия браузера (например, innerText vs. textContent), вызов функции также игнорирует семантическое значение элементов уровня блока (например, li).

Проблема

Сохранение новых строк элементов уровня блока (т.е. семантического намерения) в разных браузерах влечет за собой небольшие усилия, поскольку описывает Майк Уилкокс.

По-видимому, более простым решением было бы эмулировать вставку HTML-содержимого в <textarea>, который удаляет HTML, сохраняя при этом элементы новой строки уровня блока. Однако вставки на основе JavaScript не запускают те же подпрограммы HTML-to-text, которые используются браузерами, когда пользователи вставляют контент в <textarea>.

Я также попытался интегрировать Mike Wilcox код JavaScript. Код работает в Chromium, но не в Firefox.

Вопрос

Каков самый простой кросс-браузерный способ извлечения текста из HTML, сохраняя при этом семантические символы новой строки для элементы уровня блока, используя jQuery (или ванильный JavaScript)?

Пример

Рассмотрим:

  • Выберите и скопируйте весь этот вопрос.
  • Откройте страницу примера 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.

Примечание. Не имеет значения ни отступы, ни ненормированные пробелы.

4b9b3361

Ответ 1

Рассмотрим:

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

То есть, с исключением или двумя, выполните итерацию через каждый node и распечатайте его содержимое, позволяя вычисленному стилю браузера указать, когда нужно вставлять новые строки.

Ответ 2

Кажется, что (почти) делает то, что вы хотите:

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-код, но вы можете легко обрезать их). Например, обратите внимание на то, как он отбрасывает термин определения.

Если вы действительно хотите сделать это на семантическом уровне, вам нужен список элементов уровня блока, рекурсивно перебирать элементы и соответственно отступать их. Вы обрабатываете разные элементы блока по-разному в отношении отступов и разрывов строк вокруг них. Это не должно быть слишком сложно.

Ответ 3

на основе fooobar.com/questions/63534/... и фиксируется для поддержки 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';
        }
      }

чтобы проверить соседние блоки, чтобы определить целесообразность добавления новой строки.

Ответ 4

Я хотел бы предложить небольшое редактирование из кода 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;
 };