Как искать и выделять совпадающие тексты/фразы в текстовом содержимом всего тела документа?

Я хочу выполнить регулярное выражение и заменить только текстовое содержимое (innerText) HTML и, в конце концов, сохранить все элементы HTML (или восстановить их в том виде, в котором они были).

Регулярное выражение не должно проверять элементы HTML, а только текстовое содержимое внутри HTML (innerText, textContent и т. д.).

Выдуманный пример, «подсветка диалогов»

нить:

<html>
<body>
    <h1>Hello, World!</h1>
    <p id = "aaaa";>"Omae wa moshindeiru."</p>
    <p id = "aaaa";>"Naani!"</p>
</body>
</html>

Javascript:

element = document.querySelector('body');
element.innerText = element.innerText
.replace(/[\"“”]([^\"”“\n]+)[\"”“]/g, '\"€€$1××\"');
element.innerHTML = element.innerHTML
.replace(/€€/g, '<span style = "color: red">')
.replace(/××/g, '</span>')

ожидаемый результат:

<html>
<body>
    <h1>Hello, World!</h1>
    <p id = "aaaa";>"<span style = "color: red;">Omae wa mo shindeiru</span>"</p>
    <p id = "aaaa";>"<span style = "color: red;">Naani!</span>"</p>
</body>
</html>

Фактический результат:

<html>
<body>
Hello, World!<br><br>"<span style = "color: red;">Omae wa moshindeiru.</span>"<br><br>"<span style = "color: red;">Naani!</span>"
</body>
</html>

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

🤔 А знаете ли вы, что...
JavaScript поддерживает модульную структуру, что способствует организации кода на больших проектах.


71
1

Ответ:

Решено

Цитирую себя из комментария выше...

Решение, основанное исключительно на тексте и регулярных выражениях/заменах, не является подходящим инструментом для таких задач. Что вам нужно, так это комбинация обхода по дереву (в DOM) и тестов на основе регулярных выражений, чтобы захватить все соответствующие текстовые узлы. Отсюда вам потребуются методы узлов, чтобы создать и вставить правильное сочетание соответствующего текстового контента и каждого из его новых и включающих <span/> элементов.

Приведенный пример кода реализует ровно две функции:...

  • collectEveryTextNode, который называется «путешественник по деревьям», который рекурсивно собирает каждый текстовый узел, начиная с предоставленного элемента или текстового узла или списка узлов,

  • и replaceWithMatchingMarkerFragment, которая представляет собой this контекстно-зависимую функцию, которая создает текстовые узлы или узлы-элементы из предоставленного текстового узла, где последний содержит соответствующую фразу/подстроку хотя бы один раз. Функция заменяет переданный текстовый узел фрагментом документа, к которому добавлен хотя бы один маркерный узел, заключающий соответствующий текст.

const regXQuotedPhrase = /(?<quote>["“”])(?<phrase>[^"“”]+)\k<quote>/;

const matchingTextNodes = collectEveryTextNode(document.body)
 .filter(({ nodeValue }) => regXQuotedPhrase.test(nodeValue));

console.info({ 
  matchingTextContents: matchingTextNodes
    .map(({ nodeValue }) => nodeValue)
});

matchingTextNodes
  .forEach(replaceWithMatchingMarkerFragment, {
    /**
     *  - `forEach`'s 2nd `thisArg` parameter gets 
     *    used as config, where one can provide the
     *    matching criteria as regular expression and
     *    a custom element node too which wraps itself
     *    as marker around each matching text-fragment.
     */
    regX: regXQuotedPhrase,
    node: document.createElement('mark'),
  });
mark {
  color: #006;
  background-color: #ff0;
}
.as-console-wrapper {
  left: auto!important;
  width: 55%;
  min-height: 100%;
}
<h1>Hello, World!</h1>

<p id = "aaaa">   Foo ... "Omae wa moshindeiru." ... bar.   </p>
<p id = "aaaa">bar ... "Naani!" ... baz ... "Naani!" ... biz.</p>


<script>
/**
 *  - The **treewalker** which recursively collects
 *    every text-node, starting from either a provided
 *    (elemen/text) node or a node-list/collection.
 */
function collectEveryTextNode(nodeOrCollection) {
  const { ELEMENT_NODE, TEXT_NODE } = Node;

  nodeOrCollection = nodeOrCollection || {};

  return (nodeOrCollection.nodeType === TEXT_NODE)

    ? [nodeOrCollection]    
    : Array
        .from(
          nodeOrCollection.childNodes ?? nodeOrCollection
        )
        .reduce((result, childNode) => {
          const { nodeType } = childNode;

          if (nodeType === TEXT_NODE) {

            result
              .push(childNode);

          } else if (nodeType === ELEMENT_NODE) {

            result = result
              .concat(
                // self recursive call.
                collectEveryTextNode(childNode)
              );
          }
          return result;
        }, []);
}

/**
 *  - The `this` context-aware function which creates
 *    either text- or element-nodes from a provided
 *    text-node, where the latter contains the matching
 *    phrase/substring at least once.
 *  - The passed text-node then gets replaced by a
 *    document-fragment which has got appended at least
 *    one marker node that encloses a matching text.
 */
function replaceWithMatchingMarkerFragment(textNode) {

  const { regX, node: markerNode } = this;
  const { parentNode, nodeValue } = textNode;
 
  const fragment = document.createDocumentFragment();
  const nodeList = [];

  let text = nodeValue;
  let regXResult;

  while (regXResult = regX.exec(text)) {

    const { index, input } = regXResult;

    const quotedPhrase = regXResult[0];
    const prePhrase = input.slice(0, index);

    if (prePhrase) {

      nodeList.push(
        document.createTextNode(prePhrase),
      );
    }
    const elmNode = markerNode.cloneNode(true);

    elmNode.appendChild(
      document.createTextNode(quotedPhrase),
    );
    nodeList.push(elmNode);

    text = input.slice(index + quotedPhrase.length);
  }
  if (text) {
    // equals a `postPhrase`.

    nodeList.push(
      document.createTextNode(text),
    );
  }
  nodeList.forEach(node => fragment.appendChild(node));

  textNode.replaceWith(fragment);
}
</script>