views:

148

answers:

2

My current project requires locating an array of strings within an element's text content, then wrapping those matching strings in <a> elements using JavaScript (requirements simplified here for clarity). I need to avoid jQuery if at all possible - at least including the full library.

For example, given this block of HTML:

<div>
  <p>This is a paragraph of text used as an example in this Stack Overflow
     question.</p>
</div>

and this array of strings to match:

['paragraph', 'example']

I would need to arrive at this:

<div>
  <p>This is a <a href="http://www.example.com/"&gt;paragraph&lt;/a&gt; of text used
     as an <a href="http://www.example.com/"&gt;example&lt;/a&gt; in this Stack
     Overflow question.</p>
</div>

I've arrived at a solution to this by using the innerHTML() method and some string manipulation - basically using the offsets (via indexOf()) and lengths of the strings in the array to break the HTML string apart at the appropriate character offsets and insert <a href="http://www.example.com/"&gt; and </a> tags where needed.

However, an additional requirement has me stumped. I'm not allowed to wrap any matched strings in <a> elements if they're already in one, or if they're a descendant of a heading element (<h1> to <h6>).

So, given the same array of strings above and this block of HTML (the term matching has to be case-insensitive, by the way):

<div>
  <h1>Example</a>
  <p>This is a <a href="http://www.example.com/"&gt;paragraph of text</a> used
     as an example in this Stack Overflow question.</p>
</div>

I would need to disregard both the occurrence of "Example" in the <h1> element, and the "paragraph" in <a href="http://www.example.com/"&gt;paragraph of text</a>.

This suggests to me that I have to determine which node each matched string is in, and then traverse its ancestors until I hit <body>, checking to see if I encounter a <a> or <h_> node along the way.

Firstly, does this sound reasonable? Is there a simpler or more obvious approach that I've failed to consider? It doesn't seem like regular expressions or another string-based comparison to find bounding tags would be robust - I'm thinking of issues like self-closing elements, irregularly nested tags, etc. There's also this...

Secondly, is this possible, and if so, how would I approach it?

+4  A: 

You should probably iterate the dom elements. Here's a simple recursive dom iterator, you can fill in the rest:

function iterateDom (node) {
switch (node.nodeType) {
    case 1: // ELEMENT_NODE
        {
        if (node.tagName != "H1") {
            for (var i=0; i<node.childNodes.length; i++)
                    iterateDom(node.childNodes[i]);
            }
        }
        break;
    case 3: //TEXT_NODE
        {
        // node.nodeValue = node.nodeValue.replace(...);
        break;
    }
return true;
}
rob
Thanks - this is useful - but how do I get the original node? All that I have to work with is an HTML string and an offset. How do I go from that to the node that that string position is a part of?
Bungle
+1, @Bungle - this is a better approach than maintaing track of indexes, you can use the above to recurse into non `<a>` and `<h1-6>` nodes, and once you reach a text node (nodeType == 3), use the replace() function in String.
Anurag
If all you have is the html, you can do: var tmp = document.createElement("DIV"); tmp.innerHTML = myHtml; iterateDom(tmp);or the like.
rob
Aaaahhh, I gotcha. I left out one requirement that's probably critical - I can only link the first matching term that's linked in the block of HTML. It looks like your example iterator function preserves the order of the text nodes, though, so I think this may work after all - I'd just need to store the text nodes in an array, loop through it and test each node with a matching string for its parents (weeding out `<a>` and `<h1-6>`), and then set a flag after finding the first valid match so I don't link another. Thanks very much for your help!
Bungle
+1  A: 

Take a look at the jQuery Highlight plugin. It does almost what you need, since you need a link, and only the first occurrence of each word. Its source code is extremely simple, so it shouldn't be too much work to get it working (Even if you aren't using jQuery it can help you a lot - it doesn't use jQuery internally, only to select DOM elements).

Kobi
Thanks, Kobi! I ended up basing my approach on rob's answer, but the jQuery Highlight plugin that you suggested actually gave me some inspiration for the function that wraps the matching terms in `<a>` elements. I appreciate it.
Bungle