- From: Lachlan Hunt <lachlan.hunt@lachy.id.au>
- Date: Mon, 05 May 2008 23:13:49 +0200
- To: John Resig <jresig@mozilla.com>
- Cc: public-webapi@w3.org
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