5

I have to convert user entered unordered lists in a content management system into a bootstrap menu (navbar) using jquery.

80% there apart from one challenge that I can't figure out a nice solution for - ie one that uses selectors rather than string manipulation or regex. After all, we all know that we never parse html with regex :)

So, using the limited UI tools at the user's disposal, they generate a list, usually a two level nested list something like this

<ul>
 <li>Blah1
  <ul>
   <li><a href='http://xxxx'>Blah1a</a></li>
   <li><a href='http://yyyy'>Blah1b</a></li>
   <li>Blah1c</li>
   <li><a href='http://zzzz'>Blah1d</a></li>
  </ul>
 </li>
 <li><a href='http://aaaa'>Blah2</a></li>
 <li>Blah3
  <ul>
   <li><a href='http://xxxx'>Blah2a</a></li>
   <li><a href='http://yyyy'>Blah2b</a></li>
  </ul>
 </li> 
</ul>

And so on... The important thing is that some of their list items are links, some are just text.

I need to select each block of text contained in an <li> that is not already wrapped in an <a> and wrap it in an <a href="#"> so that the above would be transformed into:

<ul>
 <li><a href='#'>Blah1</a>
  <ul>
   <li><a href='http://xxxx'>Blah1a</a></li>
   <li><a href='http://yyyy'>Blah1b</a></li>
   <li><a href='#'>Blah1c</a></li>
   <li><a href='http://zzzz'>Blah1d</a></li>
  </ul>
 </li>
 <li><a href='http://aaaa'>Blah2</a></li>
 <li><a href='#'>Blah3</a>
  <ul>
   <li><a href='http://xxxx'>Blah2a</a></li>
   <li><a href='http://yyyy'>Blah2b</a></li>
  </ul>
 </li> 
</ul>

Shouldn't be that difficult I'm sure, but after an hour of playing I'm getting nowhere.

Arun Bertil
  • 4,598
  • 4
  • 33
  • 59
PerryW
  • 1,426
  • 1
  • 15
  • 25
  • After the bit of tongue-in-cheek about never using regex to parse HTML I am left wondering whether you have tried it for this problem and if not, why not? How about a selection across all "li" elements and iterating through the children to see if there is a direct "a" element descendant (slow, but easy to check for correctness). – abiessu Sep 09 '13 at 05:04
  • Indeed I probably could - but I think my soul would be damned for all of eternity http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags – PerryW Sep 09 '13 at 05:10
  • I actually have a t-shirt of that classic StackOverflow answer - how sad is that? – PerryW Sep 09 '13 at 05:11
  • I am very sad that it is a closed post and I can't up vote the Chuck Norris can parse HTML with regex comment. – abiessu Sep 09 '13 at 05:20

2 Answers2

3

Try

$('li').contents().filter(function(){
    return this.nodeType == 3 && $.trim($(this).text()).length > 0
}).wrap('<a href="#" />')

Demo: Fiddle

Arun P Johny
  • 384,651
  • 66
  • 527
  • 531
  • Deleting mine, yours is better – Starx Sep 09 '13 at 05:06
  • 1
    You should use `Node.TEXT_NODE` rather than just 3; it's more descriptive of what you're doing. – Dan Hlavenka Sep 09 '13 at 05:10
  • Like that - short, elegant and working in my test examples. Thanks +1 now and accepted later (just in case someone aces it) – PerryW Sep 09 '13 at 05:14
  • What's wrong with my answer? Uses jQuery selectors and should perform faster. – Solomon Closson Sep 09 '13 at 05:15
  • Not sure what's happening - have a look at the generated html on your fiddle - you'll see it has created a number of empty a's. Look after each sub-list for an example – PerryW Sep 09 '13 at 06:07
  • Two great answers - difficult to pick between them so I'm taking some advice from a discussion over on Meta and going for the one that suits me the best. This one works consistently across all of my existing test cases and will be easiest to adapt when users come up with a new variation. As this is what I'll be using, both get +1's but this is the one that gets the tick. – PerryW Sep 09 '13 at 23:30
3

One line of code here:

$("li").not(":has(>a)").wrapInner('<a href="#" />');

jsFiddle

So, if you want to just get the text nodes than, yeah, use .contents(), which will get all nodes, including text nodes. Here are 2 methods you can use below for this:

$("li").not(":has(>a)").contents().filter(function(){
    return this.nodeType == 3 && $.trim($(this).text()).length > 0
}).wrap('<a href="#" />');

jsFiddle

And here is another approach, just for the sake of something different:

$("li").not(":has(>a)").each(function(){
      var $contents = $(this).contents();
      if ($contents.length)
          $contents.eq(0).wrap('<a href="#" />');
});

jsFiddle

Solomon Closson
  • 6,111
  • 14
  • 73
  • 115