Подтвердить что ты не робот

Как обернуть часть текста в node с помощью JavaScript

У меня есть сложная проблема. Я работаю над script, который принимает регулярное выражение в качестве ввода. Этот script затем находит все совпадения для этого регулярного выражения в документе и обертывает каждое соответствие в своем собственном <span> элемент. Жесткая часть заключается в том, что текст является форматированным html-документом, поэтому мой script должен перемещаться по DOM и применять регулярное выражение на нескольких текстовых узлах одновременно, выясняя, где он должен разделить текстовые узлы, если это необходимо.

Например, с регулярным выражением, которое фиксирует полные предложения, начинающиеся с заглавной буквы и заканчивающиеся на период, этот документ:

<p>
  <b>HTML</b> is a language used to make <b>websites.</b>
  It was developed by <i>CERN</i> employees in the early 90s.
<p>

Будет превращено в это:

<p>
  <span><b>HTML</b> is a language used to make <b>websites.</b></span>
  <span>It was developed by <i>CERN</i> employees in the early 90s.</span>
<p>

script затем возвращает список всех созданных интервалов.

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

function SmartNode(node, depth, start) {
  this.node = node;
  this.depth = depth;
  this.start = start;
}


function findTextNodes(node, depth, start) {
  var list = [];
  var start = start || 0;
  depth = (typeof depth !== "undefined" ? depth : -1);

  if(node.nodeType === Node.TEXT_NODE) {
    list.push(new SmartNode(node, depth, start));
  } else {
    for(var i=0; i < node.childNodes.length; ++i) {
      list = list.concat(findTextNodes(node.childNodes[i], depth+1, start));
      if(list.length) start += list[list.length-1].node.nodeValue.length;
    }
  }

  return list;
}

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

Но проблема возникает, когда у меня есть такой документ:

<p>
  This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
</p>

Там предложение, которое начинается за пределами тега <a>, но заканчивается внутри него. Теперь я не хочу, чтобы script разделил эту ссылку на два тега. В более сложном документе это может испортить страницу, если это произойдет. Код может либо обернуть два предложения вместе:

<p>
  <span>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></span>
</p>

Или просто оберните каждую часть в свой собственный элемент:

<p>
  <span>This program is </span>
  <a href="beta.html">
    <span>not stable yet.</span>
    <span>Do not use this in production yet.</span>
  </a>
</p>

Может быть параметр, чтобы указать, что он должен делать. Я просто не уверен, что как выяснить, когда произойдет невозможное сокращение, и как его восстановить.

Другая проблема возникает, когда у меня есть пробел внутри дочернего элемента вроде:

<p>This is a <b>sentence. </b></p>

Технически совпадение регулярных выражений заканчивается сразу после периода, до конца тега <b>. Однако было бы гораздо лучше рассмотреть пространство как часть матча и обернуть его следующим образом:

<p><span>This is a <b>sentence. </b></span></p>

Чем это:

<p><span>This is a </span><b><span>sentence.</span> </b></p>

Но это второстепенная проблема. В конце концов, я мог бы просто добавить лишнее белое пространство в регулярное выражение.

Я знаю, что это может звучать как вопрос "сделай это для меня", и это не тот быстрый вопрос, который мы видим на SO на ежедневной основе, но я застрял на нем какое-то время, и это для открытого -source library, над которой я работаю. Решение этой проблемы является последним препятствием. Если вы считаете, что другой сайт SE лучше всего подходит для этого вопроса, перенаправите меня, пожалуйста.

4b9b3361

Ответ 1

Вот два способа справиться с этим.

Я не знаю, соответствует ли вам следующее. Это достаточно простое решение проблемы, но по крайней мере не использует RegEx для управления тегами HTML. Он выполняет сопоставление образцов с сырым текстом, а затем использует DOM для управления содержимым.


Первый подход

Этот подход создает только один тег <span> для каждого совпадения, используя некоторые менее распространенные API-интерфейсы браузера.
(См. Основную проблему этого подхода ниже демонстрации, и если не уверен, используйте второй подход).

Класс Range представляет фрагмент текста. Он имеет функцию surroundContents, которая позволяет обернуть диапазон в элементе. Кроме того, он имеет оговорку:

Этот метод почти эквивалентен newNode.appendChild(range.extractContents()); range.insertNode(newNode). После окружения граничные точки диапазона включают newNode.

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

Ну, обходной путь предоставляется в MDN, так что все хорошо.

Итак, вот алгоритм:

  • Составьте список узлов Text и сохраните их начальные индексы в тексте
  • Объедините значения этих узлов, чтобы получить Text
  • Найти совпадения по тексту и для каждого соответствия:

    • Найдите начальный и конечный узлы совпадения, сравнивая начальные индексы узлов с позицией соответствия
    • Создайте Range по совпадению
    • Пусть браузер выполняет грязную работу, используя трюк выше
    • Перестройте список node, поскольку последнее действие изменило DOM

Здесь моя реализация с демо:

function highlight(element, regex) {
    var document = element.ownerDocument;
    
    var getNodes = function() {
        var nodes = [],
            offset = 0,
            node,
            nodeIterator = document.createNodeIterator(element, NodeFilter.SHOW_TEXT, null, false);
            
        while (node = nodeIterator.nextNode()) {
            nodes.push({
                textNode: node,
                start: offset,
                length: node.nodeValue.length
            });
            offset += node.nodeValue.length
        }
        return nodes;
    }
    
    var nodes = getNodes(nodes);
    if (!nodes.length)
        return;
    
    var text = "";
    for (var i = 0; i < nodes.length; ++i)
        text += nodes[i].textNode.nodeValue;

    var match;
    while (match = regex.exec(text)) {
        // Prevent empty matches causing infinite loops        
        if (!match[0].length)
        {
            regex.lastIndex++;
            continue;
        }
        
        // Find the start and end text node
        var startNode = null, endNode = null;
        for (i = 0; i < nodes.length; ++i) {
            var node = nodes[i];
            
            if (node.start + node.length <= match.index)
                continue;
            
            if (!startNode)
                startNode = node;
            
            if (node.start + node.length >= match.index + match[0].length)
            {
                endNode = node;
                break;
            }
        }
        
        var range = document.createRange();
        range.setStart(startNode.textNode, match.index - startNode.start);
        range.setEnd(endNode.textNode, match.index + match[0].length - endNode.start);
        
        var spanNode = document.createElement("span");
        spanNode.className = "highlight";

        spanNode.appendChild(range.extractContents());
        range.insertNode(spanNode);
        
        nodes = getNodes();
    }
}

// Test code
var testDiv = document.getElementById("test-cases");
var originalHtml = testDiv.innerHTML;
function test() {
    testDiv.innerHTML = originalHtml;
    try {
        var regex = new RegExp(document.getElementById("regex").value, "g");
        highlight(testDiv, regex);
    }
    catch(e) {
        testDiv.innerText = e;
    }
}
document.getElementById("runBtn").onclick = test;
test();
.highlight {
  background-color: yellow;
  border: 1px solid orange;
  border-radius: 5px;
}

.section {
  border: 1px solid gray;
  padding: 10px;
  margin: 10px;
}
<form class="section">
  RegEx: <input id="regex" type="text" value="[A-Z].*?\." /> <button id="runBtn">Highlight</button>
</form>

<div id="test-cases" class="section">
  <div>foo bar baz</div>
  <p>
    <b>HTML</b> is a language used to make <b>websites.</b>
	It was developed by <i>CERN</i> employees in the early 90s.
  <p>
  <p>
    This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a>
  </p>
  <div>foo bar baz</div>
</div>

Ответ 2

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

EDIT: Думаю, теперь я понял суть этого.

function myReplace(str) {
  myRegexp = /((^<[^>*]>)+|([^<>\.]*|(<[^\/>]*>[^<>\.]+<\/[^>]*>)+)*[^<>\.]*\.\s*|<[^>]*>|[^\.<>]+\.*\s*)/g; 
  arr = str.match(myRegexp);
  var out = "";
  for (i in arr) {
var node = arr[i];
if (node.indexOf("<")===0) out += node;
else out += "<span>"+node+"</span>"; // Here is where you would run whichever 
                                     // regex you want to match by
  }
  document.write(out.replace(/</g, "&lt;").replace(/>/g, "&gt;")+"<br>");
  console.log(out);
}

myReplace('<p>This program is <a href="beta.html">not stable yet. Do not use this in production yet.</a></p>');
myReplace('<p>This is a <b>sentence. </b></p>');
myReplace('<p>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</p>');
myReplace('<p>This is a <b>a sentence</b>. Followed <i>by</i> another one.</p>');
myReplace('<p>This is a <b>an even</b> more <i>complex sentence. </i></p>');

/* Will output:
<p><span>This program is </span><a href="beta.html"><span>not stable yet. </span><span>Do not use this in production yet.</span></a></p>
<p><span>This is a </span><b><span>sentence. </span></b></p>
<p><span>This is a <b>another</b> and <i>more complex</i> even <b>super complex</b> sentence.</span></p>
<p><span>This is a <b>a sentence</b>. </span><span>Followed <i>by</i> another one.</span></p>
<p><span>This is a </span><b><span>an even</span></b><span> more </span><i><span>complex sentence. </span></i></p>
*/

Ответ 3

Я бы использовал представление "flat DOM" для такой задачи.

В плоском DOM этот абзац

<p>abc <a href="beta.html">def. ghij.</p>

будет представлен двумя векторами:

chars: "abc def. ghij.",
props:  ....aaaaaaaaaa, 

Вы будете использовать нормальное регулярное выражение для chars для обозначения областей пролета в векторе реквизита:

chars: "abc def. ghij."
props:  ssssaaaaaaaaaa  
            ssss sssss

Я использую схематическое представление здесь, эта реальная структура представляет собой массив массивов:

props: [
  [s],
  [s],
  [s],
  [s],
  [a,s],
  [a,s],
  ...
]

дерево преобразования - DOM ↔ flat-DOM может использовать простые автоматы состояния.

В конце вы преобразуете плоский DOM в DOM дерева, который будет выглядеть следующим образом:

<p><s>abc </s><a href="beta.html"><s>def.</s> <s>ghij.</s></p>

На всякий случай: я использую этот подход в своих редакторах HTML WYSIWYG.

Ответ 4

function parseText( element ){
  var stack = [ element ];
  var group = false;
  var re = /(?!\s|$).*?(\.|$)/;
  while ( stack.length > 0 ){
    var node = stack.shift();
    if ( node.nodeType === Node.TEXT_NODE )
    {
      if ( node.textContent.trim() != "" )
      {
        var match;
        while( node && (match = re.exec( node.textContent )) )
        {
          var start  = group ? 0 : match.index;
          var length = match[0].length + match.index - start;
          if ( start > 0 )
          {
            node = node.splitText( start );
          }
          var wrapper = document.createElement( 'span' );
          var next    = null;
          if ( match[1].length > 0 ){
            if ( node.textContent.length > length )
              next = node.splitText( length );
            group = false;
            wrapper.className = "sentence sentence-end";
          }
          else
          {
            wrapper.className = "sentence";
            group = true;
          }
          var parent  = node.parentNode;
          var sibling = node.nextSibling;
          wrapper.appendChild( node );
          if ( sibling )
            parent.insertBefore( wrapper, sibling );
          else
            parent.appendChild( wrapper );
          node = next;
        }
      }
    }
    else if ( node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE )
    {
      stack.unshift.apply( stack, node.childNodes );
    }
  }
}

parseText( document.body );
.sentence {
  text-decoration: underline wavy red;
}

.sentence-end {
  border-right: 1px solid red;
}
<p>This is a sentence. This is another sentence.</p>
<p>This sentence has <strong>emphasis</strong> inside it.</p>
<p><span>This sentence spans</span><span> two elements.</span></p>