views:

82

answers:

4

It seems elements selected using :contains(sub) with sub containing < or > cannot get to their parents.

The following example should illustrate the problem I'm having in both Safari and Camino (Gecko on Mac):

<html>
 <head>
  <script type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"&gt;&lt;/script&gt;
 </head>
 <body>
  <p><strong>bar</strong></p>
  <p><strong>&lt;foo&gt;</strong></p>
  <script type="text/javascript">
alert($('body strong:contains("bar")').length);
alert($('body strong:contains("bar")').parent().length);
alert($('body strong:contains("<foo>")').length);
alert($('body strong:contains("<foo>")').parent().length); // this fails
alert($('body strong').length);
alert($('body strong').parent().length); // two p elements
alert($('body strong').parent().parent().length); // one body
  </script>
 </body>
</html>

Output is:

1
1
1
0
2
2
1

Any ideas why the fourth one is 0 instead of 1, or how I can circumvent this?

This page mentions escaping names in selectors, but that didn't work either (also, I'm not sure if it's applicable).

+2  A: 

No idea what's causing contains() to fail, but you can use .filter() as an alternative:

alert($('body strong').filter(function() {
    return /<foo>/.test( $(this).text() );
}).parent().length);

That returns 1 as expected. It's ugly, but works.

Tatu Ulmanen
A workaround is fine. Thanks for that!
Daniel Beck
+1  A: 

The <foo> is being evaluated as a tag, thus the parent of it contains nothing except an empty tag, thus the 0.

EDIT: To fully understand look at this: $('body strong:contains("foo")').text().length which yeilds 5. The 1 from $('body strong:contains("<foo>")').length says there is one text node within the strong so length of that is 1 and the length of the text is 5.

The thing that gets interesting is the trailing &gt; which cannot be selected correctly as the > nor the &gt; seem to work due to the > which is used as a css selector. So, <foo works but not <foo>

Here is a fiddle page to play with it: http://jsfiddle.net/2XEUg/1/

Mark Schultheiss
The `:contains()` is a filter on the `:strong()` so this really doesn't cover it, they both have parents in the document...
Nick Craver
probably some weird part of the "text inside the parentheses of :contains() can be written as bare words or surrounded by quotation marks." evaluation for the `>`.
Mark Schultheiss
@Nick Craver - note that :contains() looks in any decendant (I know you knew this)
Mark Schultheiss
@Mark - Yes, that's true, but the match isn't *on* the descendant, it's a filter on the parent, e.g. `something:contains('somethingElse')` the selector should match *nothing* but a `<strong>` element, no matter how deep the contained text is.
Nick Craver
+1  A: 

This is clearly an issue with jQuery's selector parsing. If < and > are present in the selector, jQuery identifies the argument as a document fragment instead of a selector. The result is an element with a tagName of "FOO", and the same selectors would have the same issue:

$('body <foo>')
$('body strong:not(<foo>')

The only difference in your case is that you've used a valid selector and jQuery is identifying it incorrectly.

I made several attempts at a selector-based workaround, but Tatu Ulmanen's was the only one that worked.

EDIT: it seems you can also use .find():

$(document.body).find('strong:contains("<foo>")')
Andy E
Thank you for the analysis. Do you think it's useful to file a bug with jQuery?
Daniel Beck
@Daniel: yes, they might be able to fix this or provide a better workaround in the next version.
Andy E
+3  A: 

Disclaimer: This isn't a solution/workaround use Tatu's answer for that, this is just a description of the problem and what's going on to cause the weird behavior for those who are curious.

The root of the problem is here, the regex that jQuery uses to identify an HTML Fragment:

/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/

Which does match your selector:

body strong:contains("<foo>")

You can try it out:

alert(/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/.test('body strong:contains("<foo>")​​​'));​

So it thinks it's an HTML fragment..so overall these are currently equivalent:

$('body strong:contains("<foo>")');
$('<foo>');

Seeing the second is a clearer illustration that it's a document fragment...which has no parent. It takes the 2nd position in the match array, which you can see is just <foo>, again try it out:

alert(/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/.exec('body strong:contains("<foo>")')[1]);​

This results in you ultimately ending up at this code path in jQuery's $(), building a fragment.

So in short yes, I'd consider this a jQuery bug.

Nick Craver