Javascript Regex для замены текста NOT в атрибутах html

Я хотел бы, чтобы Javascript Regex обертывал определенный список слов в заданный старт (<span>) и конечный тег (т.е. </span>), но только если слово на самом деле является "видимым текстом" на а не внутри атрибута html (например, тега заголовка ссылки или внутри блока <script></script>.

Я создал скрипт JS с установкой основы: http://jsfiddle.net/4YCR6/1/

Ответ 1

HTML слишком сложный для надежного анализа с регулярным выражением.

Если вы хотите сделать это на стороне клиента, вы можете создать фрагмент документа и/или отключить DOM node (ни один из них нигде не отображается) и инициализировать его своей строкой HTML, а затем пройти через результирующий DOM и обрабатывать текстовые узлы. (Или используйте библиотеку, чтобы помочь вам в этом, хотя на самом деле это довольно просто.)

Здесь пример DOM. Этот пример немного проще, чем ваша проблема, потому что он просто обновляет текст, он не добавляет новые элементы в структуру (обертывание частей текста в span включает в себя обновление структуры), но это должно вас заставить. Заметки о том, что вам нужно будет изменить в конце.

var html =
    "<p>This is a test.</p>" +
    "<form><input type='text' value='test value'></form>" +
    "<p class='testing test'>Testing here too</p>";
var frag = document.createDocumentFragment();
var body = document.createElement('body');
var node, next;

// Turn the HTML string into a DOM tree
body.innerHTML = html;

// Walk the dom looking for the given text in text nodes
walk(body);

// Insert the result into the current document via a fragment
node = body.firstChild;
while (node) {
  next = node.nextSibling;
  frag.appendChild(node);
  node = next;
}
document.body.appendChild(frag);

// Our walker function
function walk(node) {
  var child, next;

  switch (node.nodeType) {
    case 1:  // Element
    case 9:  // Document
    case 11: // Document fragment
      child = node.firstChild;
      while (child) {
        next = child.nextSibling;
        walk(child);
        child = next;
      }
      break;
    case 3: // Text node
      handleText(node);
      break;
  }
}

function handleText(textNode) {
  textNode.nodeValue = textNode.nodeValue.replace(/test/gi, "TEST");
}

Живой пример

Изменения, которые вам нужно сделать, будут находиться в handleText. В частности, вместо обновления nodeValue вам необходимо:

  • Найдите индекс начала каждого слова в строке nodeValue.
  • Используйте Node#splitText, чтобы разделить текст node на три текстовых узла (часть перед вашим соответствующим текстом, часть, которая это ваш соответствующий текст и часть, соответствующая вашему совпадающему тексту).
  • Используйте document.createElement для создания нового span (это буквально просто span = document.createElement('span')).
  • Используйте Node#insertBefore, чтобы вставить новый span перед третьим текстом node (тот, который содержит текст, следующий за вашим согласованный текст); это нормально, если вам не нужно было создавать третий node, потому что ваш сопоставленный текст был в конце текста node, просто перейдите в null как refChild.
  • Используйте Node#appendChild, чтобы переместить второй текст node (тот, у которого соответствующий текст) в span. (Не нужно сначала удалять его из родителя, appendChild делает это для вас.)

Ответ 2

T.J. Правильный ответ. Я пошел немного по-другому: вот полностью сформированный пример, который работает во всех основных браузерах. Я уже опубликовал варианты этого кода в Qaru ( здесь и здесь), и сделало его приятным и универсальным, поэтому мне (или кому-либо еще) не нужно его много менять, чтобы повторно использовать его.

Пример jsFiddle: http://jsfiddle.net/7Vf5J/38/

код:

// Reusable generic function
function surroundInElement(el, regex, surrounderCreateFunc) {
    // script and style elements are left alone
    if (!/^(script|style)$/.test(el.tagName)) {
        var child = el.lastChild;
        while (child) {
            if (child.nodeType == 1) {
                surroundInElement(child, regex, surrounderCreateFunc);
            } else if (child.nodeType == 3) {
                surroundMatchingText(child, regex, surrounderCreateFunc);
            }
            child = child.previousSibling;
        }
    }
}

// Reusable generic function
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
    var parent = textNode.parentNode;
    var result, surroundingNode, matchedTextNode, matchLength, matchedText;
    while ( textNode && (result = regex.exec(textNode.data)) ) {
        matchedTextNode = textNode.splitText(result.index);
        matchedText = result[0];
        matchLength = matchedText.length;
        textNode = (matchedTextNode.length > matchLength) ?
            matchedTextNode.splitText(matchLength) : null;
        // Ensure searching starts at the beginning of the text node
        regex.lastIndex = 0;
        surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
        parent.insertBefore(surroundingNode, matchedTextNode);
        parent.removeChild(matchedTextNode);
    }
}

// This function does the surrounding for every matched piece of text
// and can be customized  to do what you like
function createSpan(matchedTextNode) {
    var el = document.createElement("span");
    el.style.color = "red";
    el.appendChild(matchedTextNode);
    return el;
}

// The main function
function wrapWords(container, words) {
    // Replace the words one at a time to ensure "test2" gets matched
    for (var i = 0, len = words.length; i < len; ++i) {
        surroundInElement(container, new RegExp(words[i]), createSpan);
    }
}

wrapWords(document.getElementById("container"), ["test2", "test"]);