root / trunk / src / query.js

Revision 7802, 26.0 kB (checked in by alex, 3 years ago)

more re-ordering to survive shrinksafe-ing. Refs #2649

Line 
1dojo.provide("dojo.query");
2dojo.require("dojo.NodeList");
3dojo.require("dojo.experimental");
4dojo.experimental("dojo.query");
5
6;(function(){
7        var d = dojo;
8        var h = d.render.html;
9
10        ////////////////////////////////////////////////////////////////////////
11        // Utility code
12        ////////////////////////////////////////////////////////////////////////
13
14        var _getIndexes = function(q){
15                return [ q.indexOf("#"), q.indexOf("."), q.indexOf("["), q.indexOf(":") ];
16        }
17
18        var _lowestFromIndex = function(query, index){
19                // spammy and verbose, but fast
20                var ql = query.length;
21                var i = _getIndexes(query);
22                var end = ql;
23                for(var x=index; x<i.length; x++){
24                        if(i[x] >= 0){
25                                if(i[x] < end){
26                                        end = i[x];
27                                }
28                        }
29                }
30                return (end < 0) ? ql : end;
31        }
32
33        var getIdEnd = function(query){
34                return _lowestFromIndex(query, 1);
35        }
36
37        var getId = function(query){
38                var i = _getIndexes(query);
39                if(i[0] != -1){
40                        return query.substring(i[0]+1, getIdEnd(query));
41                }else{
42                        return "";
43                }
44        }
45
46        var getTagNameEnd = function(query){
47                var i = _getIndexes(query);
48                if((i[0] == 0)||(i[1] == 0)){
49                        // hash or dot at the front, no tagname
50                        return 0;
51                }else{
52                        return _lowestFromIndex(query, 0);
53                }
54        }
55
56        var getTagName = function(query){
57                var tagNameEnd = getTagNameEnd(query);
58                // FIXME: should this be ">=" to account for tags like <a> ?
59                return ((tagNameEnd > 0) ? query.substr(0, tagNameEnd).toLowerCase() : "*");
60        }
61
62        var smallest = function(arr){
63                var ret = -1;
64                for(var x=0; x<arr.length; x++){
65                        var ta = arr[x];
66                        if(ta >= 0){
67                                if((ta > ret)||(ret == -1)){
68                                        ret = ta;
69                                }
70                        }
71                }
72                return ret;
73        }
74
75        var getClassName = function(query){
76                // [ "#", ".", "[", ":" ];
77                var i = _getIndexes(query);
78                if(-1 == i[1]){ return ""; } // no class component
79                var di = i[1]+1;
80
81                var othersStart = smallest(i.slice(2));
82                if(di < othersStart){
83                        return query.substring(di, othersStart);
84                }else if(-1 == othersStart){
85                        return query.substr(di);
86                }else{
87                        return "";
88                }
89        }
90
91        ////////////////////////////////////////////////////////////////////////
92        // XPath query code
93        ////////////////////////////////////////////////////////////////////////
94
95        var xPathAttrs = [
96                // FIXME: need to re-order in order of likelyness to be used in matches
97                {
98                        key: "|=",
99                        match: function(attr, value){
100                                return "[contains(concat(' ',@"+attr+",' '), ' "+ value +"-')]";
101                        }
102                },
103                {
104                        key: "^=",
105                        match: function(attr, value){
106                                return "[starts-with(@"+attr+", '"+ value +"')]";
107                        }
108                },
109                {
110                        key: "*=",
111                        match: function(attr, value){
112                                return "[contains(@"+attr+", '"+ value +"')]";
113                        }
114                },
115                {
116                        key: "$=",
117                        match: function(attr, value){
118                                return "[substring(@"+attr+", string-length(@"+attr+")-"+(value.length-1)+")='"+value+"']";
119                        }
120                },
121                {
122                        key: "!=",
123                        match: function(attr, value){
124                                return "[not(@"+attr+"='"+ value +"')]";
125                        }
126                },
127                // NOTE: the "=" match MUST come last!
128                {
129                        key: "=",
130                        match: function(attr, value){
131                                return "[@"+attr+"='"+ value +"']";
132                        }
133                }
134        ];
135
136        var handleAttrs = function(     attrList, 
137                                                                query, 
138                                                                getDefault, 
139                                                                handleMatch){
140                var matcher;
141                var i = _getIndexes(query);
142                if(i[2] >= 0){
143                        var lBktIdx = query.indexOf("]", i[2]);
144                        var condition = query.substring(i[2]+1, lBktIdx);
145                        while(condition && condition.length){
146                                if(condition.charAt(0) == "@"){
147                                        condition = condition.slice(1);
148                                }
149
150                                matcher = null;
151                                // http://www.w3.org/TR/css3-selectors/#attribute-selectors
152                                for(var x=0; x<attrList.length; x++){
153                                        var ta = attrList[x];
154                                        var tci = condition.indexOf(ta.key);
155                                        if(tci >= 0){
156                                                var attr = condition.substring(0, tci);
157                                                var value = condition.substring(tci+ta.key.length);
158                                                if(     (value.charAt(0) == "\"")||
159                                                        (value.charAt(0) == "\'")){
160                                                        value = value.substring(1, value.length-1);
161                                                }
162                                                matcher = ta.match(attr, value);
163                                                break;
164                                        }
165                                }
166                                if((!matcher)&&(condition.length)){
167                                        matcher = getDefault(condition);
168                                }
169                                if(matcher){
170                                        handleMatch(matcher);
171                                        // ff = agree(ff, matcher);
172                                }
173
174                                condition = null;
175                                var nbktIdx = query.indexOf("[", lBktIdx);
176                                if(0 <= nbktIdx){
177                                        lBktIdx = query.indexOf("]", nbktIdx);
178                                        if(0 <= lBktIdx){
179                                                condition = query.substring(nbktIdx+1, lBktIdx);
180                                        }
181                                }
182                        }
183                }
184        }
185
186        var buildPath = function(query){
187                var xpath = "";
188                var qparts = query.split(" ");
189                while(qparts.length){
190                        var tqp = qparts.shift();
191                        var prefix;
192                        if(tqp == ">"){
193                                prefix = "/";
194                                tqp = qparts.shift();
195                        }else{
196                                prefix = "//";
197                        }
198                        // get the tag name (if any)
199                        var tagName = getTagName(tqp);
200
201                        xpath += prefix + tagName;
202                       
203                        // check to see if it's got an id. Needs to come first in xpath.
204                        var id = getId(tqp);
205                        if(id.length){
206                                xpath += "[@id='"+id+"']";
207                        }
208
209                        var cn = getClassName(tqp);
210                        // check the class name component
211                        if(cn.length){
212                                var padding = " ";
213                                if(cn.charAt(cn.length-1) == "*"){
214                                        padding = ""; cn = cn.substr(0, cn.length-1);
215                                }
216                                xpath += 
217                                        "[contains(concat(' ',@class,' '), ' "+
218                                        cn + padding + "')]";
219                        }
220
221                        handleAttrs(xPathAttrs, tqp, 
222                                function(condition){
223                                                return "[@"+condition+"]";
224                                },
225                                function(matcher){
226                                        xpath += matcher;
227                                }
228                        );
229
230                        // FIXME: need to implement pseudo-class checks!!
231                };
232                // dojo.debug(xpath);
233                return xpath;
234        };
235
236        var _xpathFuncCache = {};
237        var getXPathFunc = function(path){
238                if(_xpathFuncCache[path]){
239                        return _xpathFuncCache[path];
240                }
241
242                var doc = d.doc();
243                var parent = d.body(); // FIXME
244                // FIXME: don't need to memoize. The closure scope handles it for us.
245                var xpath = buildPath(path);
246
247                var tf = function(){
248                        // XPath query strings are memoized.
249                        var ret = [];
250                        var xpathResult = doc.evaluate(xpath, parent, null, 
251                                                                                        // XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
252                                                                                        XPathResult.ANY_TYPE, null);
253                        var result = xpathResult.iterateNext();
254                        while(result){
255                                ret.push(result);
256                                result = xpathResult.iterateNext();
257                        }
258                        return ret;
259                }
260                return _xpathFuncCache[path] = tf;
261        };
262
263        /*
264        d.xPathMatch = function(query){
265                // XPath based DOM query system. Handles a small subset of CSS
266                // selectors, subset is identical to the non-XPath version of this
267                // function.
268
269                // FIXME: need to add support for alternate roots
270                return getXPathFunc(query)();
271        }
272        */
273
274        ////////////////////////////////////////////////////////////////////////
275        // DOM query code
276        ////////////////////////////////////////////////////////////////////////
277
278        var _filtersCache = {};
279        var _simpleFiltersCache = {};
280
281        var agree = function(first, second){
282                if(!first){
283                        return second;
284                }
285                if(!second){
286                        return first;
287                }
288
289                return function(){
290                        return first.apply(window, arguments) && second.apply(window, arguments);
291                }
292        }
293
294        var _filterDown = function(element, queryParts, matchArr, idx){
295                var nidx = idx+1;
296                var isFinal = (queryParts.length == nidx);
297                var tqp = queryParts[idx];
298
299                // see if we can constrain our next level to direct children
300                if(tqp == ">"){
301                        var ecn = element.childNodes;
302                        if(!ecn.length){
303                                return;
304                        }
305                        nidx++;
306                        // kinda janky, too much array alloc
307                        var isFinal = (queryParts.length == nidx);
308
309                        var tf = getFilterFunc(queryParts[idx+1]);
310                        for(var x=ecn.length-1, te; x>=0, te=ecn[x]; x--){
311                                if(tf(te)){
312                                        if(isFinal){
313                                                matchArr.push(te);
314                                        }else{
315                                                _filterDown(te, queryParts, matchArr, nidx);
316                                        }
317                                }
318                                if(x==0){
319                                        break;
320                                }
321                        }
322                }
323
324                // otherwise, keep going down, unless we'er at the end
325                var candidates = getElementsFunc(tqp)(element);
326                if(isFinal){
327                        while(candidates.length){
328                                matchArr.push(candidates.shift());
329                        }
330                        /*
331                        candidates.unshift(0, matchArr.length-1);
332                        matchArr.splice.apply(matchArr, candidates);
333                        */
334                }else{
335                        // if we're not yet at the bottom, keep going!
336                        while(candidates.length){
337                                _filterDown(candidates.shift(), queryParts, matchArr, nidx);
338                        }
339                }
340        }
341
342        var filterDown = function(elements, queryParts){
343                ret = [];
344
345                // for every root, get the elements that match the descendant selector
346                // for(var x=elements.length-1, te; x>=0, te=elements[x]; x--){
347                var x=elements.length-1, te;
348                while(te=elements[x--]){
349                        _filterDown(te, queryParts, ret, 0);
350                }
351                return ret;
352        }
353
354        var getFilterFunc = function(query){
355                // note: query can't have spaces!
356                if(_filtersCache[query]){
357                        return _filtersCache[query];
358                }
359                var ff = null;
360                var tagName = getTagName(query);
361
362                // does it have a tagName component?
363                if(tagName != "*"){
364                        // tag name match
365                        ff = agree(ff, 
366                                function(elem){
367                                        var isTn = (
368                                                (elem.nodeType == 1) &&
369                                                (tagName == elem.tagName.toLowerCase())
370                                        );
371                                        return isTn;
372                                }
373                        );
374                }
375
376                var idComponent = getId(query);
377
378                // does the node have an ID?
379                if(idComponent.length){
380                        ff = agree(ff, 
381                                function(elem){
382                                        return (
383                                                (elem.nodeType == 1) &&
384                                                (elem.id == idComponent)
385                                        );
386                                }
387                        );
388                }
389
390                if(     Math.max.apply(this, _getIndexes(query).slice(1)) >= 0){
391                        // if we have other query param parts, make sure we add them to the
392                        // filter chain
393                        ff = agree(ff, getSimpleFilterFunc(query));
394                }
395
396                return _filtersCache[query] = ff;
397        }
398
399        var getNodeIndex = function(node){
400                // NOTE: we could have a more accurate caching mechanism by
401                // invalidating caches after the query has finished, but I think that'd
402                // lead to significantly more cache churn than the cache would provide
403                // value for in the common case. Generally, we're more conservative
404                // (and therefore, more accurate) than jQuery and DomQuery WRT node
405                // node indexes, but there may be corner cases in which we fall down.
406                // How much we care about them is TBD.
407
408                var pn = node.parentNode;
409                var pnc = pn.childNodes;
410
411                // check to see if we can trust the cache. If not, re-key the whole
412                // thing and return our node match from that.
413
414                var nidx = -1;
415                var child = pn.firstChild;
416                if(!child){
417                        return nidx;
418                }
419
420                var ci = node["__cachedIndex"];
421                var cl = pn["__cachedLength"];
422
423                // only handle cache building if we've gone out of sync
424                if(((typeof cl == "number")&&(cl != pnc.length))||(typeof ci != "number")){
425                        // rip though the whole set, building cache indexes as we go
426                        pn["__cachedLength"] = pnc.length;
427                        var idx = 1;
428                        do{
429                                // we only assign indexes for nodes with nodeType == 1, as per:
430                                //              http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
431                                // only elements are counted in the search order, and they
432                                // begin at 1 for the first child's index
433
434                                if(child === node){
435                                        nidx = idx;
436                                }
437                                if(child.nodeType == 1){
438                                        child["__cachedIndex"] = idx;
439                                        idx++;
440                                }
441                                child = child.nextSibling;
442                        }while(child);
443                }else{
444                        // FIXME: could be incorrect in some cases (node swaps involving
445                        // the passed node, etc.), but we ignore those for now.
446                        nidx = ci;
447                }
448                return nidx;
449        }
450
451        var firedCount = 0;
452
453        var _getAttr = function(elem, attr){
454                var blank = "";
455                if(attr == "class"){
456                        return elem.className || blank;
457                }
458                if(attr == "for"){
459                        return elem.htmlFor || blank;
460                }
461                return elem.getAttribute(attr, 2) || blank;
462        }
463
464        var attrs = [
465                // FIXME: need to re-order in order of likelyness to be used in matches
466                {
467                        key: "|=",
468                        match: function(attr, value){
469                                // E[hreflang|="en"]
470                                //              an E element whose "hreflang" attribute has a
471                                //              hyphen-separated list of values beginning (from the
472                                //              left) with "en"
473                                var valueDash = value+"-";
474                                return function(elem){
475                                        var ea = elem.getAttribute(attr, 2) || "";
476                                        return (
477                                                (ea == value) ||
478                                                (ea.indexOf(valueDash)==0)
479                                        );
480                                }
481                        }
482                },
483                {
484                        key: "^=",
485                        match: function(attr, value){
486                                return function(elem){
487                                        return (_getAttr(elem, attr).indexOf(value)==0);
488                                }
489                        }
490                },
491                {
492                        key: "*=",
493                        match: function(attr, value){
494                                return function(elem){
495                                        return (_getAttr(elem, attr).indexOf(value)>=0);
496                                }
497                        }
498                },
499                {
500                        key: "$=",
501                        match: function(attr, value){
502                                return function(elem){
503                                        var ea = _getAttr(elem, attr);
504                                        return (ea.lastIndexOf(value)==(ea.length-value.length));
505                                }
506                        }
507                },
508                {
509                        key: "!=",
510                        match: function(attr, value){
511                                return function(elem){
512                                        return (_getAttr(elem, attr) != value);
513                                }
514                        }
515                },
516                // NOTE: the "=" match MUST come last!
517                {
518                        key: "=",
519                        match: function(attr, value){
520                                return function(elem){
521                                        return (_getAttr(elem, attr) == value);
522                                }
523                        }
524                }
525        ];
526
527        var pseudos = [
528                {
529                        key: "first-child",
530                        match: function(name, condition){
531                                return function(elem){
532                                        if(elem.nodeType != 1){ return false; }
533                                        // check to see if any of the previous siblings are elements
534                                        var fc = elem.previousSibling;
535                                        while(fc && (fc.nodeType != 1)){
536                                                fc = fc.previousSibling;
537                                        }
538                                        return (!fc);
539                                }
540                        }
541                },
542                {
543                        key: "last-child",
544                        match: function(name, condition){
545                                return function(elem){
546                                        if(elem.nodeType != 1){ return false; }
547                                        // check to see if any of the next siblings are elements
548                                        var nc = elem.nextSibling;
549                                        while(nc && (nc.nodeType != 1)){
550                                                nc = nc.nextSibling;
551                                        }
552                                        return (!nc);
553                                }
554                        }
555                },
556                {
557                        key: "empty",
558                        match: function(name, condition){
559                                return function(elem){
560                                        // DomQuery and jQuery get this wrong, oddly enough.
561                                        // The CSS 3 selectors spec is pretty explicit about
562                                        // it, too.
563                                        var cn = elem.childNodes;
564                                        var cnl = elem.childNodes.length;
565                                        // if(!cnl){ return true; }
566                                        for(var x=cnl-1; x >= 0; x--){
567                                                var nt = cn[x].nodeType;
568                                                if((nt == 1)||(nt == 3)){ return false; }
569                                        }
570                                        return true;
571                                }
572                        }
573                },
574                {
575                        key: "contains",
576                        match: function(name, condition){
577                                return function(elem){
578                                        // FIXME: I dislike this version of "contains", as
579                                        // whimsical attribute could set it off. An inner-text
580                                        // based version might be more accurate, but since
581                                        // jQuery and DomQuery also potentially get this wrong,
582                                        // I'm leaving it for now.
583                                        return (elem.innerHTML.indexOf(condition) >= 0);
584                                }
585                        }
586                },
587                {
588                        key: "not",
589                        match: function(name, condition){
590                                var ntf = getFilterFunc(condition);
591                                return function(elem){
592                                        // FIXME: I dislike this version of "contains", as
593                                        // whimsical attribute could set it off. An inner-text
594                                        // based version might be more accurate, but since
595                                        // jQuery and DomQuery also potentially get this wrong,
596                                        // I'm leaving it for now.
597                                        return (!ntf(elem));
598                                }
599                        }
600                },
601                {
602                        key: "nth-child",
603                        match: function(name, condition){
604                                var pi = parseInt;
605                                if(condition == "odd"){
606                                        return function(elem){
607                                                return (
608                                                        ((getNodeIndex(elem)) % 2) == 1
609                                                );
610                                        }
611                                }else if((condition == "2n")||
612                                        (condition == "even")){
613                                        return function(elem){
614                                                return ((getNodeIndex(elem) % 2) == 0);
615                                        }
616                                }else if(condition.indexOf("0n+") == 0){
617                                        var ncount = pi(condition.substr(3));
618                                        return function(elem){
619                                                return (elem.parentNode.childNodes[ncount-1] === elem);
620                                        }
621                                }else if(       (condition.indexOf("n+") > 0) &&
622                                                        (condition.length > 3) ){
623                                        var tparts = condition.split("n+", 2);
624                                        var pred = pi(tparts[0]);
625                                        var idx = pi(tparts[1]);
626                                        return function(elem){
627                                                return ((getNodeIndex(elem) % pred) == idx);
628                                        }
629                                }else if(condition.indexOf("n") == -1){
630                                        var ncount = pi(condition);
631                                        return function(elem){
632                                                // removeChaffNodes(elem.parentNode);
633                                                return (elem.parentNode.childNodes[ncount-1] === elem);
634                                        }
635                                }
636                        }
637                }
638        ];
639
640        var getSimpleFilterFunc = function(query){
641                // FIXME: this function currently doesn't support chaining of the same
642                // sub-selector. E.g., we can't yet search on
643                //              span.thinger.thud
644                // or
645                //              div:nth-child(even):last-child
646
647                var fcHit = (_simpleFiltersCache[query]||_filtersCache[query]);
648                if(fcHit){ return fcHit; }
649
650                var ff = null;
651
652                // [ q.indexOf("#"), q.indexOf("."), q.indexOf("["), q.indexOf(":") ];
653                var i = _getIndexes(query);
654
655                // the only case where we'll need the tag name is if we came from an ID query
656                if(i[0] >= 0){
657                        var tn = getTagName(query);
658                        if(tn != "*"){
659                                ff = agree(ff, function(elem){
660                                        return (elem.tagName.toLowerCase() == tn);
661                                });
662                        }
663                }
664
665                // if there's a class in our query, generate a match function for it
666                var className = getClassName(query);
667                if(className.length){
668                        // get the class name
669                        var isWildcard = className.charAt(className.length-1) == "*";
670                        if(isWildcard){
671                                className = className.substr(0, className.length-1);
672                        }
673                        // I dislike the regex thing, even if memozied in a cache, but it's VERY short
674                        var re = new RegExp("(?:^|\\s)" + className + (isWildcard ? ".*" : "") + "(?:\\s|$)");
675                        ff = agree(ff, function(elem){
676                                return re.test(elem.className);
677                        });
678
679                }
680                // [ "#", ".", "[", ":" ];
681                var defaultGetter = (h.ie) ?
682                        function(cond){
683                                return function(elem){
684                                        return elem[cond];
685                                }
686                        } : function(cond){
687                                return function(elem){
688                                        return elem.hasAttribute(cond);
689                                }
690                        };
691                handleAttrs(attrs, query, defaultGetter,
692                        function(tmatcher){
693                                ff = agree(ff, tmatcher);
694                        }
695                );
696
697                var matcher;
698                if(i[3]>= 0){
699                        // NOTE: we count on the pseudo name being at the end
700                        var pseudoName = query.substr(i[3]+1);
701                        var condition = "";
702                        var obi = pseudoName.indexOf("(");
703                        var cbi = pseudoName.lastIndexOf(")");
704                        if(     (0 <= obi)&&
705                                (0 <= cbi)&&
706                                (cbi > obi)){
707                                condition = pseudoName.substring(obi+1, cbi);
708                                pseudoName = pseudoName.substr(0, obi);
709                        }
710
711                        // NOTE: NOT extensible on purpose until I figure out
712                        // the portable xpath pseudos extensibility plan.
713
714                        // http://www.w3.org/TR/css3-selectors/#structural-pseudos
715                        matcher = null;
716                        for(var x=0; x<pseudos.length; x++){
717                                var ta = pseudos[x];
718                                if(ta.key == pseudoName){
719                                        matcher = ta.match(pseudoName, condition);
720                                        break;
721                                }
722                        }
723                        if(matcher){
724                                ff = agree(ff, matcher);
725                        }
726                }
727                if(!ff){
728                        ff = function(){ return true; };
729                }
730                return _simpleFiltersCache[query] = ff;
731        }
732
733        var isTagOnly = function(query){
734                return (Math.max.apply(this, _getIndexes(query)) == -1);
735        }
736
737        var _getElementsFuncCache = {};
738
739        var getElementsFunc = function(query, root){
740                var fHit = _getElementsFuncCache[query];
741                if(fHit){ return fHit; }
742                // NOTE: this function is in the fast path! not memoized!!!
743
744                // the query doesn't contain any spaces, so there's only so many
745                // things it could be
746                // [ q.indexOf("#"), q.indexOf("."), q.indexOf("["), q.indexOf(":") ];
747                var i = _getIndexes(query);
748                var id = getId(query);
749                if(i[0] == 0){
750                        // ID query. Easy.
751                        return _getElementsFuncCache[query] = function(root){
752                                return [ d.byId(id) ];
753                        }
754                }
755
756                var filterFunc = getSimpleFilterFunc(query);
757
758                var retFunc;
759                if(i[0] >= 0){
760                        // we got a filtered ID search (e.g., "h4#thinger")
761                        retFunc = function(root){
762                                var te = d.byId(id);
763                                if(filterFunc(te)){
764                                        return [ te ];
765                                }
766                        }
767                }else{
768                        // var ret = [];
769                        var tret;
770                        var tn = getTagName(query);
771
772                        /*
773                        if(-1 != i[3]){
774                                var pseudoName = (0 <= i[3]) ? query.substr(i[3]+1) : "";
775                                switch(pseudoName){
776                                        case "first":
777                                                retFunc = function(root){
778                                                        // for(var x=0, te; te = tret[x]; x++){
779                                                        var te, x=0, tret = root.getElementsByTagName(tn);
780                                                        while(te=tret[x++]){
781                                                                if(filterFunc(te)){
782                                                                        return [ te ];
783                                                                }
784                                                        }
785                                                        return [];
786                                                }
787                                                break;
788                                        case "last":
789                                                retFunc = function(root){
790                                                        var tret = root.getElementsByTagName(tn);
791                                                        var te, x=tret.length-1;
792                                                        while(te=tret[x--]){
793                                                                if(filterFunc(te)){
794                                                                        return [ te ];
795                                                                }
796                                                        }
797                                                        return [];
798                                                }
799                                                break;
800                                        default:
801                                                retFunc = function(root){
802                                                        var ret = [];
803                                                        var te, x=0, tret = root.getElementsByTagName(tn);
804                                                        while(te=tret[x++]){
805                                                                if(filterFunc(te)){
806                                                                        ret[ret.length] = te;
807                                                                        // ret.push(te);
808                                                                }
809                                                        }
810                                                        return ret;
811                                                }
812                                                break;
813                                }
814                        }else
815                        */
816                        if(isTagOnly(query)){
817                                // it's just a plain-ol elements-by-tag-name query from the root
818                                retFunc = function(root){
819                                        var ret = [];
820                                        var te, x=0, tret = root.getElementsByTagName(tn);
821                                        while(te=tret[x++]){
822                                                ret.push(te);
823                                        }
824                                        return ret;
825                                }
826                        }else{
827                                retFunc = function(root){
828                                        var ret = [];
829                                        var te, x=0, tret = root.getElementsByTagName(tn);
830                                        while(te=tret[x++]){
831                                                if(filterFunc(te)){
832                                                        ret.push(te);
833                                                }
834                                        }
835                                        return ret;
836                                }
837                        }
838                }
839                return _getElementsFuncCache[query] = retFunc;
840        }
841
842        var _partsCache = {};
843
844        ////////////////////////////////////////////////////////////////////////
845        // the query runner
846        ////////////////////////////////////////////////////////////////////////
847
848        var _queryFuncCache = {};
849        var getStepQueryFunc = function(query){
850                if(0 > query.indexOf(" ")){
851                        return getElementsFunc(query);
852                }
853
854                var sqf = function(root){
855                        var qparts = query.split(" ");
856
857                        // FIXME: need to make root popping more explicit and cache it somehow
858
859                        // see if we can't pop a root off the front
860                        var partIndex = 0;
861                        var lastRoot;
862                        while((partIndex < qparts.length)&&(0 <= qparts[partIndex].indexOf("#"))){
863                                // FIXME: should we try to cache such that the step function
864                                // only ever looks for the last ID-based query part, thereby
865                                // avoiding re-runs and potential array alloc?
866
867                                lastRoot = root;
868                                root = getElementsFunc(qparts[partIndex])()[0];
869                                if(!root){ root = lastRoot; break; }
870                                partIndex++;
871                        }
872                        if(qparts.length == partIndex){
873                                return [ root ];
874                        }
875                        // FIXME: need to handle tight-loop "div div div" style queries
876                        // here so as to avoid huge amounts of array alloc in repeated
877                        // getElements calls by filterDown
878                        var candidates;
879
880                        // FIXME: this isn't very generic. It lets us run like a bat outta
881                        // hell on queries like:
882                        //              div div span
883                        // and:
884                        //              #thinger span#blah div span span
885                        // but we might still fall apart on searches like:
886                        //              foo.bar span[blah="thonk"] div div span code.example
887                        // in short, we need to move the look-ahead logic into _filterDown()
888                        if( isTagOnly(qparts[partIndex]) && 
889                                (qparts[partIndex+1] != ">")
890                        ){
891                                // go as far as we can down the chain without any intermediate
892                                // array allocation
893                                qparts = qparts.slice(partIndex);
894                                var searchParts = [];
895                                var idx = 0;
896                                while(qparts[idx] && isTagOnly(qparts[idx]) && (qparts[idx+1] != ">" )){
897                                        searchParts.push(qparts[idx]);
898                                        idx++;
899                                }
900                                var curLevelItems = [ root ];
901                                var nextLevelItems;
902                                for(var x=0; x<searchParts.length; x++){
903                                        nextLevelItems = [];
904                                        var tsp = qparts.shift();
905                                        for(var y=0; y<curLevelItems.length; y++){
906                                                var tze, z=0, tcol = curLevelItems[y].getElementsByTagName(tsp);
907                                                while(tze=tcol[z++]){
908                                                        nextLevelItems.push(tze);
909                                                }
910                                        }
911                                        curLevelItems = nextLevelItems;
912                                }
913                                candidates = curLevelItems;
914                                if(!qparts.length){
915                                        return candidates;
916                                }
917                        }else{
918                                candidates = getElementsFunc(qparts.shift())(root);
919                        }
920                        return filterDown(candidates, qparts);
921                }
922                return sqf;
923        }
924
925        var _getQueryFunc = (
926                // NOTE:
927                //              XPath on the Webkit nighlies is slower than it's DOM iteration
928                //              for most test cases
929                // FIXME:
930                //              we should try to capture some runtime speed data for each query
931                //              function to determine on the fly if we should stick w/ the
932                //              potentially optimized variant or if we should try something
933                //              new.
934                (document["evaluate"] && !h.safari) ? 
935                function(query){
936                        // has xpath support that's faster than DOM
937                        var qparts = query.split(" ");
938                        // can we handle it?
939                        if(     (document["evaluate"])&&
940                                (query.indexOf(":") == -1)&&
941                                (
942                                        (true) // ||
943                                        // (query.indexOf("[") == -1) ||
944                                        // (query.indexOf("=") == -1)
945                                )
946                        ){
947                                // dojo.debug(query);
948                                // should we handle it?
949                                var gtIdx = query.indexOf(">")
950                                // kind of a lame heuristic, but it works
951                                if(     
952                                        // a "div div div" style query
953                                        ((qparts.length > 2)&&(query.indexOf(">") == -1))||
954                                        // or something else with moderate complexity. kinda janky
955                                        (qparts.length > 3)||
956                                        (query.indexOf("[")>=0)||
957                                        // or if it's a ".thinger" query
958                                        ((1 == qparts.length)&&(0 <= query.indexOf(".")))
959
960                                ){
961                                        // use get and cache a xpath runner for this selector
962                                        return getXPathFunc(query);
963                                }
964                                // return getXPathFunc(query);
965                        }
966
967                        return getStepQueryFunc(query);
968                } : getStepQueryFunc
969        );
970        // FIXME: disable XPath for testing and tuning the DOM path
971        // _getQueryFunc = getStepQueryFunc;
972
973        var getQueryFunc = function(query){
974                if(_queryFuncCache[query]){ return _queryFuncCache[query]; }
975                if(0 > query.indexOf(",")){
976                        return _queryFuncCache[query] = _getQueryFunc(query);
977                }else{
978                        var parts = query.split(", ");
979                        var tf = function(root){
980                                var pindex = 0; // avoid array alloc for every invocation
981                                var ret = [];
982                                var tp;
983                                while(tp = parts[pindex++]){
984                                        ret = ret.concat(_getQueryFunc(tp, tp.indexOf(" "))(root));
985                                }
986                                return ret;
987                        }
988                        return _queryFuncCache[query] = tf;
989                }
990        }
991
992        /*
993        // experimental implementation of _zip for IE
994        var _zip = function(arr){
995                if(!arr){ return []; }
996                var al = arr.length;
997                if(al < 2){ return arr; }
998                var ret = [arr[0]];
999                var lastIdx = arr[0].sourceIndex;
1000                for(var x=1, te; te = arr[x]; x++){
1001                // for(var x=1; x<arr.length; x++){
1002                        var nsi = te.sourceIndex;
1003                        if(nsi > lastIdx){
1004                                ret.push(te);
1005                                lastIdx = nsi;
1006                        }
1007                }
1008                return ret;
1009        }
1010        */
1011
1012        var _zipIdx = 0;
1013        var _zip = function(arr){
1014                var ret = new d.NodeList();
1015                if(!arr){ return ret; }
1016                ret.push(arr[0]);
1017                if(arr.length < 2){ return ret; }
1018                _zipIdx++;
1019                arr[0]["_zipIdx"] = _zipIdx;
1020                for(var x=1, te; te = arr[x]; x++){
1021                        if(arr[x]["_zipIdx"] != _zipIdx){ 
1022                                ret.push(te);
1023                        }
1024                        te["_zipIdx"] = _zipIdx;
1025                }
1026                // FIXME: should we consider stripping these properties?
1027                return ret;
1028        }
1029
1030        d.query = function(query, root){
1031                // return is always an array
1032                // NOTE: elementsById is not currently supported
1033                // NOTE: ignores xpath-ish queries for now
1034                if(typeof query != "string"){
1035                        return new d.NodeList(query);
1036                }
1037
1038                // FIXME: should support more methods on the return than the stock array.
1039                return _zip(getQueryFunc(query)(root||document));
1040        }
1041
1042        /*
1043        // exposing these was a mistake
1044        d.query.attrs = attrs;
1045        d.query.pseudos = pseudos;
1046        */
1047
1048        d._filterQueryResult = function(nodeList, simpleFilter){
1049                var tnl = new d.NodeList();
1050                var ff = (simpleFilter) ? getFilterFunc(simpleFilter) : function(){ return true; };
1051                // dojo.debug(ff);
1052                for(var x=0, te; te = nodeList[x]; x++){
1053                        if(ff(te)){ tnl.push(te); }
1054                }
1055                return tnl;
1056        }
1057})();
Note: See TracBrowser for help on using the browser.