Re: [SelectorsAPI] Analysis of Context-Rooted Queries (was: Thoughts on querySelectorAll)

Hi,
   I've taken the time to analyse the situation regarding support for 
context-rooted queries, considering the use cases and possible 
implementation strategies.

This is a rather long email, and I have divided it into sections.

Table of Contents:

* Introduction
* Use cases
   - Descendants
   - Children
   - Siblings
* Summary of Problems
* Summary of Possible Solutions
* Analysis of Solutions
   - Solution 1
   - Solution 2
   - Solution 2
   - Solution 4
   - Solution 5
* Conclusion
* Appendix


*Introduction*

For all cases where authors are only using a simple selector or a 
sequence of simple selectors (i.e. no combinators), then the result will 
be the same in both the current API and existing JS libraries.  So any 
use cases that don't use a combinator have been ignored in this analysis.

There are 3 types of combinators that need to be considered: descendant 
combinators (" "), child combinators (">") and sibling combinators ("+" 
and "~"), and I have separated into those 3 categories.

For simplicity, I'm only comparing with JQuery and Dojo, but similar 
examples could probably be found using any other JS library that 
supports a similar feature.

All real world examples I've linked to are on google code.  I realise 
this is quite a limited sample and may not be totally representative of 
the code elsewhere on the web, but finding code elsewhere and manually 
inspecting the source code of pages is difficult and time consuming. 
So, it's the best I could reasonably do.

For all examples, assume:

   var foo = document.getElementById("foo");


*Use Cases*

*Descendants*

1. Finding descendants of another element that is also a descendant of
    the context node.
    JQuery: foo.find("div p");
    Dojo:   dojo.query("div p", foo);

Those would match the p element in the following markup:

a. <section id="foo">
      <div>
        <p>...</p>
      </div>
    </section>

But would not match in either of these:

b. <div id="foo">
      <p>...</p>
    </div>

c. <div>
      <section id="foo">
        <p>...</p>
      </section>
    </div>

Examples:
http://www.google.com/codesearch?q=file%3Ajs%24+jquery+\.find\([%27%22][^%27%22%3E%2C]%2B[\+]
http://www.google.com/codesearch?q=file%3Ajs%24+dojo\.query\([%27%22][^%27%22%3E%2C]%2B[\+]

This is not entirely possible with the current API.  With JQuery and 
Dojo, it requires all sequences of simple selectors in the chain to 
match elements that are descendants of the context node.  With the 
current API, all but the last sequence of simple selectors in the chain 
could match any ancestor element.

Using the same selector with the current API would match the p element 
in all 3 markup examples above.

   foo.querySelector("div p");

Achieving the same result as the JS libraries is only possible with the 
current API by explicitly prepending a selector that matches the context 
node itself, but doesn't match any other element.  e.g. Using an ID 
selector would suffice, if the ID is known and assuming no duplicate IDs:

   foo.querySelector("#foo div p");

However, this is currently somewhat problematic where there is no such 
ID nor any other method of easily identifying the context node within 
the selector.

*Children*

2. Finding a child of the context node.
    JQuery: foo.find(">div");
    Dojo:   dojo.query(">div", foo);

Those would match the div element in the following markup:

a. <section id="foo">
      <div>...</div>
    </section>

but does not match in this markup:

b. <section id="foo">
      <article>
        <div>...</div>
      </article>
    </section>

Examples:
http://www.google.com/codesearch?q=file%3Ajs%24+jquery+\.find\([%27%22]%3E
http://www.google.com/codesearch?q=file%3Ajs%24+dojo\.query\([%27%22]%3E

Similarly to the workaround for the descendant combinator above, this 
can only be achieved by prepending a selector that only matches the 
context node, and is subject to the same limitations.

foo.querySelector("#foo>div")


3. Finding a child of an element that is itself a descendant of the
    context node.
    JQuery: foo.find("p>span");
    Dojo:   dojo.query("p > span", foo);

(Dojo seems to be buggy and requires spaces around the '>' combinator.)

Those would match the span element in the following markup examples:

a. <div id="foo">
      <p><span>...</span></p>
    </div>

But would not match in the following:

b. <p id="foo"><span>...</span></p>

Examples:
http://www.google.com/codesearch?q=file%3Ajs%24+jquery+\.find\([%27%22][^%27%22%3E]%2B[%3E]
http://www.google.com/codesearch?q=file%3Ajs%24+dojo\.query\([%27%22][^%27%22%3E]%2B[%3E]
(No examples found using dojo)

With JQuery and Dojo, no selector in the chain can match the context 
node at all, whereas it can in the current API.  The span would be 
matched in both markup examples above.

foo.querySelector("p>span");

To only match in example a, but not in b, would again require prepending 
a selector and descendant combinator that only matches the context node.

foo.querySelector("#foo p>span");


*Siblings*


4. Finding a sibling of the context node.
    JQuery: foo.find("+p");
            foo.find("~p");

Those would match the p element in the following markup:

a. <div id="foo">...</div>
    <p>...</p>


http://www.google.com/codesearch?q=file%3Ajs%24+jquery+\.find\([%27%22][\%2B~]
http://www.google.com/codesearch?q=file%3Ajs%24+dojo\.query\([%27%22][\%2B~]
(No examples found using dojo, I don't think it's supported)

This one is not possible with the current API, since it is defined that 
only descendants of the context node are returned, which automatically 
excludes its siblings.

The only way to get the same result would be by using 
document.querySelectorAll() and prepending a selector that only matches 
the desired context node, which is subject to the same limitations 
mentioned earlier.

e.g.

document.querySelectorAll("#foo+div");


5. Finding a sibling of another element that is also a descendant of the
    context node.
    JQuery: foo.find("h1+p");
    Dojo:   dojo.query("h1 + p", foo);

Those would match the p element in the following markup:

a. <div id="foo">
      <h1>...</h1>
      <p>...</p>
    </div>

http://www.google.com/codesearch?q=file%3Ajs%24+jquery+\.find\([%27%22][^%27%22]%2B[\%2B~]
http://www.google.com/codesearch?q=file%3Ajs%24+dojo\.query\([%27%22][^%27%22]%2B[\%2B~]
(No examples found using dojo, though it is supported.)

It's obviously not as common as the others, though, as I mentioned 
earlier, my search is quite limited.

This one is already possible using the current API, since siblings both 
share the same parent, and thus would automatically be descendants of 
the context node.  So that works the same as the following, without any 
changes to the API.

   foo.querySelectorAll("h1+p");
   foo.querySelectorAll("h1~p");


*Summary of Problems*

This leaves us with a few problems to solve for

1. Finding descendants of another element that is also a descendant of
    the context node.
2. Finding a child of the context node.
3. Finding a child of an element that is itself a descendant of the
    context node.
4. Finding a sibling of the context node.


*Summary of Possible Solutions*

Solution 1. Define a simple selector that always matches the context 
node of the query.

This would most likely be a pseudo-class, and could be called :scope, 
:scope-root, :this, :self, :context-node, :context-root, :context, or 
any other name.  I'll use :scope for now only because that's what has 
been used in previous discussion, not because it is necessarily the best 
name.

Solution 2. Define that selectors are only evaluated in the scope of the 
context node, rather than the whole document, but *excluding* the 
context node itself.  Selectors in the chain can only match descendants 
of the context node, but not the context node itself or its ancestors.

Solution 3. Define that selectors are only evaluated in the scope of the 
context node, rather than the whole document, *including* the context 
node itself.

Solution 4. Define that selectors are only evaluated in the scope of the 
context node *including* the context node itself, but only descendants 
of the node are returned even if the context node matches the selector.

Solution 5. Define the methods to behave as if an implicit :scope 
selector and descendant combinator were prepended to each selector in 
the group.

If either of solutions 2 to 5 are possible, they could also be defined 
using additional methods instead of redefining the current methods.


*Analysis of Solutions*

*Solution 1*

   Define a simple selector that always matches the context node of
   the query.

Implementing this does not require significant modification of selector 
implementations in browsers.  In particular, no changes to the parser 
would be required.  We just need to know whether or not this solution 
adequately addresss the use cases and problems.

1. Finding descendants of another element that is also a descendant of
    the context node.

foo.querySelectorAll(":scope div p");

Correctly matches the p element in use case 1, example a, and not in 
examples b or c.

2. Finding a child of the context node.

foo.querySelector(":scope>div");

Correctly matches the div element in use case 2, example a, and not in 
example b.

3. Finding a child of an element that is itself a descendant of the
    context node.

foo.querySelector(":scope p>span");

Correctly matches the div in use case 3, example a, and not in example b.

4. Finding a sibling of the context node.

foo.querySelector(":scope+p");

This does not match anything because the method is defined to only match 
descendants, not siblings.  So, this raises the question of whether it 
is possible to redefine the methods to also check sibling elements?

The answer is no because the elements that are checked cannot be 
dependant upon the combinators used.  e.g.

foo.querySelector("div p");

That should not match any sibling p elements of foo, so such elements 
should not be checked.  However, this one would have to:

foo.querySelector("div :scope+p");
foo.querySelector("div h1:scope+p"); (assuming <h1 id="foo">)

But then this one would not

foo.querySelector("div h1+p");

Basically, this makes the implementation overly complex since it would 
need to determine whether or not to check descendant elements or sibling 
elements first based on whether the :scope pseudo-class is used and 
followed by a sibling combinator, and then evaluate the selector with 
each element.

However, there may be another solution to address this use case which I 
will discuss later.  See the appendix at the bottom.


*Solution 2*

   Define that selectors are only evaluated in the scope of the
   context node, rather than the whole document, but *excluding*
   the context node itself.  Selectors in the chain can only
   match descendants of the context node, but not the context
   node itself or its ancestors.

This would require modification of existing selector engines in browsers 
to be implemented.  In existing implementations, an element is evaluated 
against a selector in the context of the entire document.  It would 
require implementations to isolate the tree of nodes within the context 
node before evaluating each element.  But if we assume for the moment 
that this is technically possible, in spite of difficulty, we still need 
to determine whether or not it addresses the use cases.

1. Finding descendants of another element that is also a descendant of
    the context node.

foo.querySelector("div p");

Works as expected.

2. Finding a child of the context node.

Not possible, since there is no way to address the context node itself.

3. Finding a child of an element that is itself a descendant of the
    context node.

foo.querySelector("p>span");

Works as expected.

4. Finding a sibling of the context node.

Not possible, since it's still only searching descendants, not siblings.


*Solution 3*

   Define that selectors are only evaluated in the scope of the
   context node *including* the context node itself.

1. Finding descendants of another element that is also a descendant of
    the context node.

This will work in cases where the left most selector in the chain does 
not match context node itself.  e.g.

foo.querySelector("div p");

That would correctly match the p element in use case 1, example, and not 
match in example c.  But it would incorrectly match it in example b. 
This would require the use of a selector that matches the context node 
itself.

2. Finding a child of the context node.

This would also require selector to explicitly match the context node 
itself.

foo.querySelector("#foo>div");

That would work, but doesn't address the existing problem we have now.

3. Finding a child of an element that is itself a descendant of the
    context node.

This is similar to #1, and again, this will also require a selector to 
match the context node.

4. Finding a sibling of the context node.

Not possible because it's still only searching descendants, not siblings.

This solution also introduces another unintended consequence.

<div id="foo"><div>...</div></div>

foo.querySelector("div") == foo;

That doesn't match the behaviour of the existing API, nor of JQuery and 
Dojo.


*Solution 4*

   Define that selectors are only evaluated in the scope of the
   context node *including* the context node itself, but only
   descendants of the node are returned even if the context node
   matches the selector.

This solves the unintended consequence from solution 3, but for all the 
use cases, it still suffers from the same problems.


*Solution 5*

   Define the methods to behave as if an implicit :scope selector
   and descendant combinator were prepended to each selector in
   the group.

This would work by taking the selector and appending the equivalent of 
":scope " to the beginning of it, and then parsing as usual.  :scope 
could be implemented in any way the UA wanted here since it's not 
actually used by the author.

1. Finding descendants of another element that is also a descendant of
    the context node.

foo.querySelector("div p");

This becomes ":scope div p" and works is the same solution 1.

2. Finding a child of the context node.

foo.querySelector(">div");

This becomes ":scope >div" and also works the same as solution 1.

3. Finding a child of an element that is itself a descendant of the
    context node.

foo.querySelector("p>span");

This becomes ":scope p>span" works the same as solution 1.

4. Finding a sibling of the context node.

foo.querySelector("+p");

This becomes "scope +p" and fails for the same reason as solution 1.

While this looks promising on the surface, the real problem lies in the 
implementation of the selector parsing. This requires signifant changes 
to the grammar of selectors, and thus alteration of the selector engines 
in browsers.

Consider the following selector:

">strong, >em"

The expectation would be for this to become:

":scope >strong, :scope >em"

The question is how to parse and convert it.  Since it is invalid 
according to the grammar of selectors, current parsers would reject it 
with a parse error.  Modifying the grammar explicitly for use within 
this API is an unacceptable risk because it significantly increases the 
costs of defining the spec, reimplementing selector parsing; 
significantly increases the chance of introducing bugs and 
interoperability issues.  All browsers have had their fair share of 
selector parsing bugs over the years (it's been the basis of many CSS 
hacks in the past!), and repeating history is really not a good idea.


*Conclusion*

Solutions 1 and 5 both equally solve 3 out of 4 of the use cases, 
whereas 2, 3 and 4 all suffer from problems with them.  The 
implementation issues with solution 5 eliminate it as a possibility, 
leaving solution 1 as the only reasonable choice.

It is thus my firm belief that defining and implementing :scope (or 
whatever it gets called) is the best way to move forward and address 
this problem.  However, as I have stated before, this would belong in a 
separate spec and very likely end up in the next version of Selectors.


*Appendix*

The 4th use case isn't addressed by any of the solutions.  However, I 
mentioned ealier that there may be a separate solution to consider.

If the methods were defined to accept an additional parameter 
representing a context node in addition to introducing the :scope 
selector, it may work.

e.g.
document.querySelectorAll(":scope+p", foo, nsresolver);

Here, :scope would match the foo element and the adjacent sibling p 
element (if present) would be returned.

But it introduces some interesting consequences for use on elements.

var foo = document.getElementById("foo");
var bar = document.getElementById("bar");
foo.querySelector(":scope", bar, nsresolver);

Here, :scope would match the bar element instead of the foo element, 
which would be the default, but still only descendants of foo would be 
matched.

This may suffer from other problems, since I haven't fully evaluated it, 
and it does slightly increase the complexity.  But if the use cases for 
matching siblings of the context node can be deemed significant enough, 
it may be worth investigating further.  But keep in mind that my 
searches revealed very few real world examples attempting to do this, 
and so it may not be worth it.

-- 
Lachlan Hunt - Opera Software
http://lachy.id.au/
http://www.opera.com/

Received on Monday, 5 May 2008 21:14:30 UTC