HTML和JS:使用<span>标记</span>包围文档中的每个单词

时间:2014-09-25 03:54:24

标签: javascript html regex

我正在尝试使用Javascript来修改现有的HTML文档,这样我就可以使用span标记来包围网页中的每个文字。这是一个非常具体的问题,所以我将提供一个示例案例:

<body><p>hello, <br>
change this</p> 
<img src="lorempixel.com/200/200> <br></body></html>

这应该改为:

  <body><p><span id="1">hello,</span>
  <br> <span id="2"> change</span><span id="3"> this</span> </p>
  <br> <img src="lorempixel.com/200/200> <br></body></html>

我正在思考或regex解决方案,但它们变得非常复杂,我不确定如何忽略标记并更改文本而不会完全破坏页面。

任何想法都赞赏!

2 个答案:

答案 0 :(得分:7)

不要在原始HTML上使用正则表达式。仅在文本上使用它。这是因为正则表达式是一个无上下文解析器,但HTML是一种递归语言。您需要一个递归下降解析器来正确处理HTML。

首先是DOM的一些有用功能:

  1. document.body是DOM的根
  2. DOM的每个节点都有一个childNodes数组(甚至是注释,文本和属性)
  3. <span><h>等元素节点不包含文本,而是包含包含文本的文本节点。
  4. 所有节点都有nodeType属性,文本节点类型为3
  5. 所有节点都有nodeValue属性,根据节点的类型,它具有不同的含义。对于文本节点nodeValue包含实际文本。
  6. 因此,使用上面的信息,我们可以用跨度包围所有单词。

    首先是一个简单的实用函数,它允许我们处理DOM:

    // First a simple implementation of recursive descent,
    // visit all nodes in the DOM and process it with a callback:
    function walkDOM (node,callback) {
        if (node.nodeName != 'SCRIPT') { // ignore javascript
            callback(node);
            for (var i=0; i<node.childNodes.length; i++) {
                walkDOM(node.childNodes[i],callback);
            }
        }
    }
    

    现在我们可以遍历DOM并找到文本节点:

    var textNodes = [];
    walkDOM(document.body,function(n){
        if (n.nodeType == 3) {
            textNodes.push(n);
        }
    });
    

    请注意,我这两步都是为了避免包含两次单词。

    现在我们可以处理文本节点:

    // simple utility functions to avoid a lot of typing:
    function insertBefore (new_element, element) {
        element.parentNode.insertBefore(new_element,element);
    }
    function removeElement (element) {
        element.parentNode.removeChild(element);
    }
    function makeSpan (txt, attrs) {
        var s = document.createElement('span');
        for (var i in attrs) {
            if (attrs.hasOwnProperty(i)) s[i] = attrs[i];
        }
        s.appendChild(makeText(txt));
        return s;
    }
    function makeText (txt) {return document.createTextNode(txt)}
    
    var id_count = 1;
    for (var i=0; i<textNodes.length; i++) {
        var n = textNodes[i];
        var txt = n.nodeValue;
        var words = txt.split(' ');
    
        // Insert span surrounded words:
        insertBefore(makeSpan(words[0],{id:id_count++}),n);
        for (var j=1; j<words.length; j++) {
            insertBefore(makeText(' '),n); // join the words with spaces
            insertBefore(makeSpan(words[j],{id:id_count++}),n);
        }
        // Now remove the original text node:
        removeElement(n);
    }
    

    你有它。它很麻烦但是100%安全 - 它永远不会破坏你网页中其他javascript标签。我上面的很多实用功能都可以用你选择的库替换。但是,不要采取将整个文档视为巨型innerHTML字符串的捷径。除非你愿意用纯JavaScript编写HTML解析器。

答案 1 :(得分:1)

这种处理总是比你想象的要复杂得多。以下将包装与\S+匹配的字符序列(非空白序列),而不包装与\s+(空格)匹配的序列。

它还允许跳过某些元素的内容,例如脚本,输入,按钮,选择等。请注意, childNodes 返回的实时集合必须转换为静态数组,否则会受到添加的新节点的影响。另一种方法是使用element.querySelectorAll(),但 childNodes 有更广泛的支持。

// Copy numeric properties of Obj from 0 to length
// to an array
function toArray(obj) {
  var arr = [];
  for (var i=0, iLen=obj.length; i<iLen; i++) {
    arr.push(obj[i]);
  }
  return arr;
}


// Wrap the words of an element and child elements in a span
// Recurs over child elements, add an ID and class to the wrapping span
// Does not affect elements with no content, or those to be excluded
var wrapContent = (function() {
  var count = 0;

  return function(el) {

    // If element provided, start there, otherwise use the body
    el = el && el.parentNode? el : document.body;

    // Get all child nodes as a static array
    var node, nodes = toArray(el.childNodes);
    var frag, parent, text;
    var re = /\S+/;
    var sp, span = document.createElement('span');

    // Tag names of elements to skip, there are more to add
    var skip = {'script':'', 'button':'', 'input':'', 'select':'',
                'textarea':'', 'option':''};

    // For each child node...
    for (var i=0, iLen=nodes.length; i<iLen; i++) {
      node = nodes[i];

      // If it's an element, call wrapContent
      if (node.nodeType == 1 && !(node.tagName.toLowerCase() in skip)) {
        wrapContent(node);

      // If it's a text node, wrap words
      } else if (node.nodeType == 3) {

        // Match sequences of whitespace and non-whitespace
        text = node.data.match(/\s+|\S+/g);

        if (text) {

          // Create a fragment, handy suckers these
          frag = document.createDocumentFragment();

          for (var j=0, jLen=text.length; j<jLen; j++) {

            // If not whitespace, wrap it and append to the fragment
            if (re.test(text[j])) {
              sp = span.cloneNode(false);
              sp.id = count++;
              sp.className = 'foo';
              sp.appendChild(document.createTextNode(text[j]));
              frag.appendChild(sp);

            // Otherwise, just append it to the fragment
            } else {
              frag.appendChild(document.createTextNode(text[j]));
            }
          }
        }

        // Replace the original node with the fragment
        node.parentNode.replaceChild(frag, node);
      }
    }
  }
}());

window.onload = wrapContent;

以上仅针对最常见的情况,需要更多的工作和彻底的测试。