Subversion Repositories Applications.papyrus

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
2150 mathias 1
if(!dojo._hasResource["dojo._base.query"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
2
dojo._hasResource["dojo._base.query"] = true;
3
dojo.provide("dojo._base.query");
4
dojo.require("dojo._base.NodeList");
5
 
6
/*
7
	dojo.query() architectural overview:
8
 
9
		dojo.query is a relatively full-featured CSS3 query library. It is
10
		designed to take any valid CSS3 selector and return the nodes matching
11
		the selector. To do this quickly, it processes queries in several
12
		steps, applying caching where profitable.
13
 
14
		The steps (roughly in reverse order of the way they appear in the code):
15
			1.) check to see if we already have a "query dispatcher"
16
				- if so, use that with the given parameterization. Skip to step 4.
17
			2.) attempt to determine which branch to dispatch the query to:
18
				- JS (optimized DOM iteration)
19
				- xpath (for browsers that support it and where it's fast)
20
				- native (not available in any browser yet)
21
			3.) tokenize and convert to executable "query dispatcher"
22
				- this is where the lion's share of the complexity in the
23
				  system lies. In the DOM version, the query dispatcher is
24
				  assembled as a chain of "yes/no" test functions pertaining to
25
				  a section of a simple query statement (".blah:nth-child(odd)"
26
				  but not "div div", which is 2 simple statements). Individual
27
				  statement dispatchers are cached (to prevent re-definition)
28
				  as are entire dispatch chains (to make re-execution of the
29
				  same query fast)
30
				- in the xpath path, tokenization yeilds a concatenation of
31
				  parameterized xpath selectors. As with the DOM version, both
32
				  simple selector blocks and overall evaluators are cached to
33
				  prevent re-defintion
34
			4.) the resulting query dispatcher is called in the passed scope (by default the top-level document)
35
				- for DOM queries, this results in a recursive, top-down
36
				  evaluation of nodes based on each simple query section
37
				- xpath queries can, thankfully, be executed in one shot
38
			5.) matched nodes are pruned to ensure they are unique
39
*/
40
 
41
;(function(){
42
	// define everything in a closure for compressability reasons. "d" is an
43
	// alias to "dojo" since it's so frequently used. This seems a
44
	// transformation that the build system could perform on a per-file basis.
45
 
46
	////////////////////////////////////////////////////////////////////////
47
	// Utility code
48
	////////////////////////////////////////////////////////////////////////
49
 
50
	var d = dojo;
51
	var childNodesName = dojo.isIE ? "children" : "childNodes";
52
 
53
	var getQueryParts = function(query){
54
		// summary: state machine for query tokenization
55
		if(query.charAt(query.length-1) == ">"){
56
			query += " *"
57
		}
58
		query += " "; // ensure that we terminate the state machine
59
 
60
		var ts = function(s, e){
61
			return d.trim(query.slice(s, e));
62
		}
63
 
64
		// the overall data graph of the full query, as represented by queryPart objects
65
		var qparts = [];
66
		// state keeping vars
67
		var inBrackets = -1;
68
		var inParens = -1;
69
		var inMatchFor = -1;
70
		var inPseudo = -1;
71
		var inClass = -1;
72
		var inId = -1;
73
		var inTag = -1;
74
		var lc = ""; // the last character
75
		var cc = ""; // the current character
76
		var pStart;
77
		// iteration vars
78
		var x = 0; // index in the query
79
		var ql = query.length;
80
		var currentPart = null; // data structure representing the entire clause
81
		var _cp = null; // the current pseudo or attr matcher
82
 
83
		var endTag = function(){
84
			if(inTag >= 0){
85
				var tv = (inTag == x) ? null : ts(inTag, x).toLowerCase();
86
				currentPart[ (">~+".indexOf(tv) < 0)? "tag" : "oper" ] = tv;
87
				inTag = -1;
88
			}
89
		}
90
 
91
		var endId = function(){
92
			if(inId >= 0){
93
				currentPart.id = ts(inId, x).replace(/\\/g, "");
94
				inId = -1;
95
			}
96
		}
97
 
98
		var endClass = function(){
99
			if(inClass >= 0){
100
				currentPart.classes.push(ts(inClass+1, x).replace(/\\/g, ""));
101
				inClass = -1;
102
			}
103
		}
104
 
105
		var endAll = function(){
106
			endId(); endTag(); endClass();
107
		}
108
 
109
		for(; x<ql, lc=cc, cc=query.charAt(x); x++){
110
			if(lc == "\\"){ continue; }
111
			if(!currentPart){
112
				// NOTE: I hate all this alloc, but it's shorter than writing tons of if's
113
				pStart = x;
114
				currentPart = {
115
					query: null,
116
					pseudos: [],
117
					attrs: [],
118
					classes: [],
119
					tag: null,
120
					oper: null,
121
					id: null
122
				};
123
				inTag = x;
124
			}
125
 
126
			if(inBrackets >= 0){
127
				// look for a the close first
128
				if(cc == "]"){
129
					if(!_cp.attr){
130
						_cp.attr = ts(inBrackets+1, x);
131
					}else{
132
						_cp.matchFor = ts((inMatchFor||inBrackets+1), x);
133
					}
134
					var cmf = _cp.matchFor;
135
					if(cmf){
136
						if(	(cmf.charAt(0) == '"') || (cmf.charAt(0)  == "'") ){
137
							_cp.matchFor = cmf.substring(1, cmf.length-1);
138
						}
139
					}
140
					currentPart.attrs.push(_cp);
141
					_cp = null; // necessaray?
142
					inBrackets = inMatchFor = -1;
143
				}else if(cc == "="){
144
					var addToCc = ("|~^$*".indexOf(lc) >=0 ) ? lc : "";
145
					_cp.type = addToCc+cc;
146
					_cp.attr = ts(inBrackets+1, x-addToCc.length);
147
					inMatchFor = x+1;
148
				}
149
				// now look for other clause parts
150
			}else if(inParens >= 0){
151
				if(cc == ")"){
152
					if(inPseudo >= 0){
153
						_cp.value = ts(inParens+1, x);
154
					}
155
					inPseudo = inParens = -1;
156
				}
157
			}else if(cc == "#"){
158
				endAll();
159
				inId = x+1;
160
			}else if(cc == "."){
161
				endAll();
162
				inClass = x;
163
			}else if(cc == ":"){
164
				endAll();
165
				inPseudo = x;
166
			}else if(cc == "["){
167
				endAll();
168
				inBrackets = x;
169
				_cp = {
170
					/*=====
171
					attr: null, type: null, matchFor: null
172
					=====*/
173
				};
174
			}else if(cc == "("){
175
				if(inPseudo >= 0){
176
					_cp = {
177
						name: ts(inPseudo+1, x),
178
						value: null
179
					}
180
					currentPart.pseudos.push(_cp);
181
				}
182
				inParens = x;
183
			}else if(cc == " " && lc != cc){
184
				// note that we expect the string to be " " terminated
185
				endAll();
186
				if(inPseudo >= 0){
187
					currentPart.pseudos.push({ name: ts(inPseudo+1, x) });
188
				}
189
				currentPart.hasLoops = (
190
						currentPart.pseudos.length ||
191
						currentPart.attrs.length ||
192
						currentPart.classes.length	);
193
				currentPart.query = ts(pStart, x);
194
				currentPart.tag = (currentPart["oper"]) ? null : (currentPart.tag || "*");
195
				qparts.push(currentPart);
196
				currentPart = null;
197
			}
198
		}
199
		return qparts;
200
	};
201
 
202
 
203
	////////////////////////////////////////////////////////////////////////
204
	// XPath query code
205
	////////////////////////////////////////////////////////////////////////
206
 
207
	// this array is a lookup used to generate an attribute matching function.
208
	// There is a similar lookup/generator list for the DOM branch with similar
209
	// calling semantics.
210
	var xPathAttrs = {
211
		"*=": function(attr, value){
212
			return "[contains(@"+attr+", '"+ value +"')]";
213
		},
214
		"^=": function(attr, value){
215
			return "[starts-with(@"+attr+", '"+ value +"')]";
216
		},
217
		"$=": function(attr, value){
218
			return "[substring(@"+attr+", string-length(@"+attr+")-"+(value.length-1)+")='"+value+"']";
219
		},
220
		"~=": function(attr, value){
221
			return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]";
222
		},
223
		"|=": function(attr, value){
224
			return "[contains(concat(' ',@"+attr+",' '), ' "+ value +"-')]";
225
		},
226
		"=": function(attr, value){
227
			return "[@"+attr+"='"+ value +"']";
228
		}
229
	};
230
 
231
	// takes a list of attribute searches, the overall query, a function to
232
	// generate a default matcher, and a closure-bound method for providing a
233
	// matching function that generates whatever type of yes/no distinguisher
234
	// the query method needs. The method is a bit tortured and hard to read
235
	// because it needs to be used in both the XPath and DOM branches.
236
	var handleAttrs = function(	attrList,
237
								query,
238
								getDefault,
239
								handleMatch){
240
		d.forEach(query.attrs, function(attr){
241
			var matcher;
242
			// type, attr, matchFor
243
			if(attr.type && attrList[attr.type]){
244
				matcher = attrList[attr.type](attr.attr, attr.matchFor);
245
			}else if(attr.attr.length){
246
				matcher = getDefault(attr.attr);
247
			}
248
			if(matcher){ handleMatch(matcher); }
249
		});
250
	}
251
 
252
	var buildPath = function(query){
253
		var xpath = ".";
254
		var qparts = getQueryParts(d.trim(query));
255
		while(qparts.length){
256
			var tqp = qparts.shift();
257
			var prefix;
258
			// FIXME: need to add support for ~ and +
259
			if(tqp.oper == ">"){
260
				prefix = "/";
261
				// prefix = "/child::node()";
262
				tqp = qparts.shift();
263
			}else{
264
				prefix = "//";
265
				// prefix = "/descendant::node()"
266
			}
267
 
268
			// get the tag name (if any)
269
 
270
			xpath += prefix + tqp.tag;
271
 
272
			// check to see if it's got an id. Needs to come first in xpath.
273
			if(tqp.id){
274
				xpath += "[@id='"+tqp.id+"'][1]";
275
			}
276
 
277
			d.forEach(tqp.classes, function(cn){
278
				var cnl = cn.length;
279
				var padding = " ";
280
				if(cn.charAt(cnl-1) == "*"){
281
					padding = ""; cn = cn.substr(0, cnl-1);
282
				}
283
				xpath +=
284
					"[contains(concat(' ',@class,' '), ' "+
285
					cn + padding + "')]";
286
			});
287
 
288
			handleAttrs(xPathAttrs, tqp,
289
				function(condition){
290
						return "[@"+condition+"]";
291
				},
292
				function(matcher){
293
					xpath += matcher;
294
				}
295
			);
296
 
297
			// FIXME: need to implement pseudo-class checks!!
298
		};
299
		return xpath;
300
	};
301
 
302
	var _xpathFuncCache = {};
303
	var getXPathFunc = function(path){
304
		if(_xpathFuncCache[path]){
305
			return _xpathFuncCache[path];
306
		}
307
 
308
		var doc = d.doc;
309
		// var parent = d.body(); // FIXME
310
		// FIXME: don't need to memoize. The closure scope handles it for us.
311
		var xpath = buildPath(path);
312
 
313
		var tf = function(parent){
314
			// XPath query strings are memoized.
315
			var ret = [];
316
			var xpathResult;
317
			try{
318
				xpathResult = doc.evaluate(xpath, parent, null,
319
												// XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
320
												XPathResult.ANY_TYPE, null);
321
			}catch(e){
322
				console.debug("failure in exprssion:", xpath, "under:", parent);
323
				console.debug(e);
324
			}
325
			var result = xpathResult.iterateNext();
326
			while(result){
327
				ret.push(result);
328
				result = xpathResult.iterateNext();
329
			}
330
			return ret;
331
		}
332
		return _xpathFuncCache[path] = tf;
333
	};
334
 
335
	/*
336
	d.xPathMatch = function(query){
337
		// XPath based DOM query system. Handles a small subset of CSS
338
		// selectors, subset is identical to the non-XPath version of this
339
		// function.
340
 
341
		// FIXME: need to add support for alternate roots
342
		return getXPathFunc(query)();
343
	}
344
	*/
345
 
346
	////////////////////////////////////////////////////////////////////////
347
	// DOM query code
348
	////////////////////////////////////////////////////////////////////////
349
 
350
	var _filtersCache = {};
351
	var _simpleFiltersCache = {};
352
 
353
	// the basic building block of the yes/no chaining system. agree(f1, f2)
354
	// generates a new function which returns the boolean results of both of
355
	// the passed functions to a single logical-anded result.
356
	var agree = function(first, second){
357
		if(!first){ return second; }
358
		if(!second){ return first; }
359
 
360
		return function(){
361
			return first.apply(window, arguments) && second.apply(window, arguments);
362
		}
363
	}
364
 
365
	var _filterDown = function(element, queryParts, matchArr, idx){
366
		var nidx = idx+1;
367
		var isFinal = (queryParts.length == nidx);
368
		var tqp = queryParts[idx];
369
 
370
		// see if we can constrain our next level to direct children
371
		if(tqp.oper == ">"){
372
			var ecn = element[childNodesName];
373
			if(!ecn || !ecn.length){
374
				return;
375
			}
376
			nidx++;
377
			isFinal = (queryParts.length == nidx);
378
			// kinda janky, too much array alloc
379
			var tf = getFilterFunc(queryParts[idx+1]);
380
			// for(var x=ecn.length-1, te; x>=0, te=ecn[x]; x--){
381
			for(var x=0, ecnl=ecn.length, te; x<ecnl, te=ecn[x]; x++){
382
				if(tf(te)){
383
					if(isFinal){
384
						matchArr.push(te);
385
					}else{
386
						_filterDown(te, queryParts, matchArr, nidx);
387
					}
388
				}
389
				/*
390
				if(x==0){
391
					break;
392
				}
393
				*/
394
			}
395
		}
396
 
397
		// otherwise, keep going down, unless we'er at the end
398
		var candidates = getElementsFunc(tqp)(element);
399
		if(isFinal){
400
			while(candidates.length){
401
				matchArr.push(candidates.shift());
402
			}
403
			/*
404
			candidates.unshift(0, matchArr.length-1);
405
			matchArr.splice.apply(matchArr, candidates);
406
			*/
407
		}else{
408
			// if we're not yet at the bottom, keep going!
409
			while(candidates.length){
410
				_filterDown(candidates.shift(), queryParts, matchArr, nidx);
411
			}
412
		}
413
	}
414
 
415
	var filterDown = function(elements, queryParts){
416
		var ret = [];
417
 
418
		// for every root, get the elements that match the descendant selector
419
		// for(var x=elements.length-1, te; x>=0, te=elements[x]; x--){
420
		var x = elements.length - 1, te;
421
		while(te = elements[x--]){
422
			_filterDown(te, queryParts, ret, 0);
423
		}
424
		return ret;
425
	}
426
 
427
	var getFilterFunc = function(q){
428
		// note: query can't have spaces!
429
		if(_filtersCache[q.query]){
430
			return _filtersCache[q.query];
431
		}
432
		var ff = null;
433
 
434
		// does it have a tagName component?
435
		if(q.tag){
436
			if(q.tag == "*"){
437
				ff = agree(ff,
438
					function(elem){
439
						return (elem.nodeType == 1);
440
					}
441
				);
442
			}else{
443
				// tag name match
444
				ff = agree(ff,
445
					function(elem){
446
						return (
447
							(elem.nodeType == 1) &&
448
							(q.tag == elem.tagName.toLowerCase())
449
						);
450
						// return isTn;
451
					}
452
				);
453
			}
454
		}
455
 
456
		// does the node have an ID?
457
		if(q.id){
458
			ff = agree(ff,
459
				function(elem){
460
					return (
461
						(elem.nodeType == 1) &&
462
						(elem.id == q.id)
463
					);
464
				}
465
			);
466
		}
467
 
468
		if(q.hasLoops){
469
			// if we have other query param parts, make sure we add them to the
470
			// filter chain
471
			ff = agree(ff, getSimpleFilterFunc(q));
472
		}
473
 
474
		return _filtersCache[q.query] = ff;
475
	}
476
 
477
	var getNodeIndex = function(node){
478
		// NOTE:
479
		//		we could have a more accurate caching mechanism by invalidating
480
		//		caches after the query has finished, but I think that'd lead to
481
		//		significantly more cache churn than the cache would provide
482
		//		value for in the common case. Generally, we're more
483
		//		conservative (and therefore, more accurate) than jQuery and
484
		//		DomQuery WRT node node indexes, but there may be corner cases
485
		//		in which we fall down.  How much we care about them is TBD.
486
 
487
		var pn = node.parentNode;
488
		var pnc = pn.childNodes;
489
 
490
		// check to see if we can trust the cache. If not, re-key the whole
491
		// thing and return our node match from that.
492
 
493
		var nidx = -1;
494
		var child = pn.firstChild;
495
		if(!child){
496
			return nidx;
497
		}
498
 
499
		var ci = node["__cachedIndex"];
500
		var cl = pn["__cachedLength"];
501
 
502
		// only handle cache building if we've gone out of sync
503
		if(((typeof cl == "number")&&(cl != pnc.length))||(typeof ci != "number")){
504
			// rip though the whole set, building cache indexes as we go
505
			pn["__cachedLength"] = pnc.length;
506
			var idx = 1;
507
			do{
508
				// we only assign indexes for nodes with nodeType == 1, as per:
509
				//		http://www.w3.org/TR/css3-selectors/#nth-child-pseudo
510
				// only elements are counted in the search order, and they
511
				// begin at 1 for the first child's index
512
 
513
				if(child === node){
514
					nidx = idx;
515
				}
516
				if(child.nodeType == 1){
517
					child["__cachedIndex"] = idx;
518
					idx++;
519
				}
520
				child = child.nextSibling;
521
			}while(child);
522
		}else{
523
			// NOTE:
524
			//		could be incorrect in some cases (node swaps involving the
525
			//		passed node, etc.), but we ignore those due to the relative
526
			//		unlikelihood of that occuring
527
			nidx = ci;
528
		}
529
		return nidx;
530
	}
531
 
532
	var firedCount = 0;
533
 
534
	var blank = "";
535
	var _getAttr = function(elem, attr){
536
		if(attr == "class"){
537
			return elem.className || blank;
538
		}
539
		if(attr == "for"){
540
			return elem.htmlFor || blank;
541
		}
542
		return elem.getAttribute(attr, 2) || blank;
543
	}
544
 
545
	var attrs = {
546
		"*=": function(attr, value){
547
			return function(elem){
548
				// E[foo*="bar"]
549
				//		an E element whose "foo" attribute value contains
550
				//		the substring "bar"
551
				return (_getAttr(elem, attr).indexOf(value)>=0);
552
			}
553
		},
554
		"^=": function(attr, value){
555
			// E[foo^="bar"]
556
			//		an E element whose "foo" attribute value begins exactly
557
			//		with the string "bar"
558
			return function(elem){
559
				return (_getAttr(elem, attr).indexOf(value)==0);
560
			}
561
		},
562
		"$=": function(attr, value){
563
			// E[foo$="bar"]
564
			//		an E element whose "foo" attribute value ends exactly
565
			//		with the string "bar"
566
			var tval = " "+value;
567
			return function(elem){
568
				var ea = " "+_getAttr(elem, attr);
569
				return (ea.lastIndexOf(value)==(ea.length-value.length));
570
			}
571
		},
572
		"~=": function(attr, value){
573
			// E[foo~="bar"]
574
			//		an E element whose "foo" attribute value is a list of
575
			//		space-separated values, one of which is exactly equal
576
			//		to "bar"
577
 
578
			// return "[contains(concat(' ',@"+attr+",' '), ' "+ value +" ')]";
579
			var tval = " "+value+" ";
580
			return function(elem){
581
				var ea = " "+_getAttr(elem, attr)+" ";
582
				return (ea.indexOf(tval)>=0);
583
			}
584
		},
585
		"|=": function(attr, value){
586
			// E[hreflang|="en"]
587
			//		an E element whose "hreflang" attribute has a
588
			//		hyphen-separated list of values beginning (from the
589
			//		left) with "en"
590
			var valueDash = " "+value+"-";
591
			return function(elem){
592
				var ea = " "+(elem.getAttribute(attr, 2) || "");
593
				return (
594
					(ea == value) ||
595
					(ea.indexOf(valueDash)==0)
596
				);
597
			}
598
		},
599
		"=": function(attr, value){
600
			return function(elem){
601
				return (_getAttr(elem, attr) == value);
602
			}
603
		}
604
	};
605
 
606
	var pseudos = {
607
		"first-child": function(name, condition){
608
			return function(elem){
609
				if(elem.nodeType != 1){ return false; }
610
				// check to see if any of the previous siblings are elements
611
				var fc = elem.previousSibling;
612
				while(fc && (fc.nodeType != 1)){
613
					fc = fc.previousSibling;
614
				}
615
				return (!fc);
616
			}
617
		},
618
		"last-child": function(name, condition){
619
			return function(elem){
620
				if(elem.nodeType != 1){ return false; }
621
				// check to see if any of the next siblings are elements
622
				var nc = elem.nextSibling;
623
				while(nc && (nc.nodeType != 1)){
624
					nc = nc.nextSibling;
625
				}
626
				return (!nc);
627
			}
628
		},
629
		"empty": function(name, condition){
630
			return function(elem){
631
				// DomQuery and jQuery get this wrong, oddly enough.
632
				// The CSS 3 selectors spec is pretty explicit about
633
				// it, too.
634
				var cn = elem.childNodes;
635
				var cnl = elem.childNodes.length;
636
				// if(!cnl){ return true; }
637
				for(var x=cnl-1; x >= 0; x--){
638
					var nt = cn[x].nodeType;
639
					if((nt == 1)||(nt == 3)){ return false; }
640
				}
641
				return true;
642
			}
643
		},
644
		/* non standard!
645
		"contains": function(name, condition){
646
			return function(elem){
647
				// FIXME: I dislike this version of "contains", as
648
				// whimsical attribute could set it off. An inner-text
649
				// based version might be more accurate, but since
650
				// jQuery and DomQuery also potentially get this wrong,
651
				// I'm leaving it for now.
652
				return (elem.innerHTML.indexOf(condition) >= 0);
653
			}
654
		},
655
		*/
656
		"not": function(name, condition){
657
			var ntf = getFilterFunc(getQueryParts(condition)[0]);
658
			return function(elem){
659
				return (!ntf(elem));
660
			}
661
		},
662
		"nth-child": function(name, condition){
663
			var pi = parseInt;
664
			if(condition == "odd"){
665
				return function(elem){
666
					return (
667
						((getNodeIndex(elem)) % 2) == 1
668
					);
669
				}
670
			}else if((condition == "2n")||
671
				(condition == "even")){
672
				return function(elem){
673
					return ((getNodeIndex(elem) % 2) == 0);
674
				}
675
			}else if(condition.indexOf("0n+") == 0){
676
				var ncount = pi(condition.substr(3));
677
				return function(elem){
678
					return (elem.parentNode[childNodesName][ncount-1] === elem);
679
				}
680
			}else if(	(condition.indexOf("n+") > 0) &&
681
						(condition.length > 3) ){
682
				var tparts = condition.split("n+", 2);
683
				var pred = pi(tparts[0]);
684
				var idx = pi(tparts[1]);
685
				return function(elem){
686
					return ((getNodeIndex(elem) % pred) == idx);
687
				}
688
			}else if(condition.indexOf("n") == -1){
689
				var ncount = pi(condition);
690
				return function(elem){
691
					return (getNodeIndex(elem) == ncount);
692
				}
693
			}
694
		}
695
	};
696
 
697
	var defaultGetter = (d.isIE) ? function(cond){
698
		var clc = cond.toLowerCase();
699
		return function(elem){
700
			return elem[cond]||elem[clc];
701
		}
702
	} : function(cond){
703
		return function(elem){
704
			return (elem && elem.getAttribute && elem.hasAttribute(cond));
705
		}
706
	};
707
 
708
	var getSimpleFilterFunc = function(query){
709
 
710
		var fcHit = (_simpleFiltersCache[query.query]||_filtersCache[query.query]);
711
		if(fcHit){ return fcHit; }
712
 
713
		var ff = null;
714
 
715
		// the only case where we'll need the tag name is if we came from an ID query
716
		if(query.id){ // do we have an ID component?
717
			if(query.tag != "*"){
718
				ff = agree(ff, function(elem){
719
					return (elem.tagName.toLowerCase() == query.tag);
720
				});
721
			}
722
		}
723
 
724
		// if there's a class in our query, generate a match function for it
725
		d.forEach(query.classes, function(cname, idx, arr){
726
			// get the class name
727
			var isWildcard = cname.charAt(cname.length-1) == "*";
728
			if(isWildcard){
729
				cname = cname.substr(0, cname.length-1);
730
			}
731
			// I dislike the regex thing, even if memozied in a cache, but it's VERY short
732
			var re = new RegExp("(?:^|\\s)" + cname + (isWildcard ? ".*" : "") + "(?:\\s|$)");
733
			ff = agree(ff, function(elem){
734
				return re.test(elem.className);
735
			});
736
			ff.count = idx;
737
		});
738
 
739
		d.forEach(query.pseudos, function(pseudo){
740
			if(pseudos[pseudo.name]){
741
				ff = agree(ff, pseudos[pseudo.name](pseudo.name, pseudo.value));
742
			}
743
		});
744
 
745
		handleAttrs(attrs, query, defaultGetter,
746
			function(tmatcher){ ff = agree(ff, tmatcher); }
747
		);
748
		if(!ff){
749
			ff = function(){ return true; };
750
		}
751
		return _simpleFiltersCache[query.query] = ff;
752
	}
753
 
754
	var _getElementsFuncCache = { };
755
 
756
	var getElementsFunc = function(query, root){
757
		var fHit = _getElementsFuncCache[query.query];
758
		if(fHit){ return fHit; }
759
 
760
		// NOTE: this function is in the fast path! not memoized!!!
761
 
762
		// the query doesn't contain any spaces, so there's only so many
763
		// things it could be
764
 
765
		if(query.id && !query.hasLoops && !query.tag){
766
			// ID-only query. Easy.
767
			return _getElementsFuncCache[query.query] = function(root){
768
				// FIXME: if root != document, check for parenting!
769
				return [ d.byId(query.id) ];
770
			}
771
		}
772
 
773
		var filterFunc = getSimpleFilterFunc(query);
774
 
775
		var retFunc;
776
		if(query.tag && query.id && !query.hasLoops){
777
			// we got a filtered ID search (e.g., "h4#thinger")
778
			retFunc = function(root){
779
				var te = d.byId(query.id);
780
				if(filterFunc(te)){
781
					return [ te ];
782
				}
783
			}
784
		}else{
785
			var tret;
786
 
787
			if(!query.hasLoops){
788
				// it's just a plain-ol elements-by-tag-name query from the root
789
				retFunc = function(root){
790
					var ret = [];
791
					var te, x=0, tret = root.getElementsByTagName(query.tag);
792
					while(te=tret[x++]){
793
						ret.push(te);
794
					}
795
					return ret;
796
				}
797
			}else{
798
				retFunc = function(root){
799
					var ret = [];
800
					var te, x=0, tret = root.getElementsByTagName(query.tag);
801
					while(te=tret[x++]){
802
						if(filterFunc(te)){
803
							ret.push(te);
804
						}
805
					}
806
					return ret;
807
				}
808
			}
809
		}
810
		return _getElementsFuncCache[query.query] = retFunc;
811
	}
812
 
813
	var _partsCache = {};
814
 
815
	////////////////////////////////////////////////////////////////////////
816
	// the query runner
817
	////////////////////////////////////////////////////////////////////////
818
 
819
	// this is the second level of spliting, from full-length queries (e.g.,
820
	// "div.foo .bar") into simple query expressions (e.g., ["div.foo",
821
	// ".bar"])
822
	var _queryFuncCache = {
823
		"*": d.isIE ?
824
			function(root){
825
					return root.all;
826
			} :
827
			function(root){
828
				 return root.getElementsByTagName("*");
829
			},
830
		">": function(root){
831
			var ret = [];
832
			var te, x=0, tret = root[childNodesName];
833
			while(te=tret[x++]){
834
				if(te.nodeType == 1){ ret.push(te); }
835
			}
836
			return ret;
837
		}
838
	};
839
 
840
	var getStepQueryFunc = function(query){
841
		// if it's trivial, get a fast-path dispatcher
842
		var qparts = getQueryParts(d.trim(query));
843
		// if(query[query.length-1] == ">"){ query += " *"; }
844
		if(qparts.length == 1){
845
			var tt = getElementsFunc(qparts[0]);
846
			tt.nozip = true;
847
			return tt;
848
		}
849
 
850
		// otherwise, break it up and return a runner that iterates over the parts recursively
851
		var sqf = function(root){
852
			var localQueryParts = qparts.slice(0); // clone the src arr
853
			var candidates;
854
			if(localQueryParts[0].oper == ">"){
855
				candidates = [ root ];
856
				// root = document;
857
			}else{
858
				candidates = getElementsFunc(localQueryParts.shift())(root);
859
			}
860
			return filterDown(candidates, localQueryParts);
861
		}
862
		return sqf;
863
	}
864
 
865
	// a specialized method that implements our primoridal "query optimizer".
866
	// This allows us to dispatch queries to the fastest subsystem we can get.
867
	var _getQueryFunc = (
868
		// NOTE:
869
		//		XPath on the Webkit nighlies is slower than it's DOM iteration
870
		//		for most test cases
871
		// FIXME:
872
		//		we should try to capture some runtime speed data for each query
873
		//		function to determine on the fly if we should stick w/ the
874
		//		potentially optimized variant or if we should try something
875
		//		new.
876
		(document["evaluate"] && !d.isSafari) ?
877
		function(query){
878
			// has xpath support that's faster than DOM
879
			var qparts = query.split(" ");
880
			// can we handle it?
881
			if(	(document["evaluate"])&&
882
				(query.indexOf(":") == -1)&&
883
				(
884
					(true) // ||
885
					// (query.indexOf("[") == -1) ||
886
					// (query.indexOf("=") == -1)
887
				)
888
			){
889
				// dojo.debug(query);
890
				// should we handle it?
891
 
892
				// kind of a lame heuristic, but it works
893
				if(
894
					// a "div div div" style query
895
					((qparts.length > 2)&&(query.indexOf(">") == -1))||
896
					// or something else with moderate complexity. kinda janky
897
					(qparts.length > 3)||
898
					(query.indexOf("[")>=0)||
899
					// or if it's a ".thinger" query
900
					((1 == qparts.length)&&(0 <= query.indexOf(".")))
901
 
902
				){
903
					// use get and cache a xpath runner for this selector
904
					return getXPathFunc(query);
905
				}
906
			}
907
 
908
			// fallthrough
909
			return getStepQueryFunc(query);
910
		} : getStepQueryFunc
911
	);
912
	// uncomment to disable XPath for testing and tuning the DOM path
913
	// _getQueryFunc = getStepQueryFunc;
914
 
915
	// FIXME: we've got problems w/ the NodeList query()/filter() functions if we go XPath for everything
916
 
917
	// uncomment to disable DOM queries for testing and tuning XPath
918
	// _getQueryFunc = getXPathFunc;
919
 
920
	// this is the primary caching for full-query results. The query dispatcher
921
	// functions are generated here and then pickled for hash lookup in the
922
	// future
923
	var getQueryFunc = function(query){
924
		// return a cached version if one is available
925
		if(_queryFuncCache[query]){ return _queryFuncCache[query]; }
926
		if(0 > query.indexOf(",")){
927
			// if it's not a compound query (e.g., ".foo, .bar"), cache and return a dispatcher
928
			return _queryFuncCache[query] = _getQueryFunc(query);
929
		}else{
930
			// if it's a complex query, break it up into it's constituent parts
931
			// and return a dispatcher that will merge the parts when run
932
 
933
			// var parts = query.split(", ");
934
			var parts = query.split(/\s*,\s*/);
935
			var tf = function(root){
936
				var pindex = 0; // avoid array alloc for every invocation
937
				var ret = [];
938
				var tp;
939
				while(tp = parts[pindex++]){
940
					ret = ret.concat(_getQueryFunc(tp, tp.indexOf(" "))(root));
941
				}
942
				return ret;
943
			}
944
			// ...cache and return
945
			return _queryFuncCache[query] = tf;
946
		}
947
	}
948
 
949
	// FIXME:
950
	//		Dean's new Base2 uses a system whereby queries themselves note if
951
	//		they'll need duplicate filtering. We need to get on that plan!!
952
 
953
	// attempt to efficiently determine if an item in a list is a dupe,
954
	// returning a list of "uniques", hopefully in doucment order
955
	var _zipIdx = 0;
956
	var _zip = function(arr){
957
		if(arr && arr.nozip){ return d.NodeList._wrap(arr); }
958
		var ret = new d.NodeList();
959
		if(!arr){ return ret; }
960
		if(arr[0]){
961
			ret.push(arr[0]);
962
		}
963
		if(arr.length < 2){ return ret; }
964
		_zipIdx++;
965
		arr[0]["_zipIdx"] = _zipIdx;
966
		for(var x=1, te; te = arr[x]; x++){
967
			if(arr[x]["_zipIdx"] != _zipIdx){
968
				ret.push(te);
969
			}
970
			te["_zipIdx"] = _zipIdx;
971
		}
972
		// FIXME: should we consider stripping these properties?
973
		return ret;
974
	}
975
 
976
	// the main exectuor
977
	d.query = function(query, root){
978
		//	summary:
979
		//		returns nodes which match the given CSS3 selector, searching the
980
		//		entire document by default but optionally taking a node to scope
981
		//		the search by. Returns an instance of dojo.NodeList.
982
		//	description:
983
		//		dojo.query() is the swiss army knife of DOM node manipulation in
984
		//		Dojo. Much like Prototype's "$$" (bling-bling) function or JQuery's
985
		//		"$" function, dojo.query provides robust, high-performance
986
		//		CSS-based node selector support with the option of scoping searches
987
		//		to a particular sub-tree of a document.
988
		//
989
		//		Supported Selectors:
990
		//		--------------------
991
		//
992
		//		dojo.query() supports a rich set of CSS3 selectors, including:
993
		//
994
		//			* class selectors (e.g., ".foo")
995
		//			* node type selectors like "span"
996
		//			* " " descendant selectors
997
		//			* ">" child element selectors
998
		//			* "#foo" style ID selectors
999
		//			* "*" universal selector
1000
		//			* attribute queries:
1001
		//				* "[foo]" attribute presence selector
1002
		//				* "[foo='bar']" attribute value exact match
1003
		//				* "[foo~='bar']" attribute value list item match
1004
		//				* "[foo^='bar']" attribute start match
1005
		//				* "[foo$='bar']" attribute end match
1006
		//				* "[foo*='bar']" attribute substring match
1007
		//			* ":first-child", ":last-child" positional selectors
1008
		//			* ":nth-child(n)", ":nth-child(2n+1)" style positional calculations
1009
		//			* ":nth-child(even)", ":nth-child(odd)" positional selectors
1010
		//			* ":not(...)" negation pseudo selectors
1011
		//
1012
		//		Any legal combination of those selector types as per the CSS 3 sepc
1013
		//		will work with dojo.query(), including compound selectors (","
1014
		//		delimited). Very complex and useful searches can be constructed
1015
		//		with this palette of selectors and when combined with functions for
1016
		//		maniplation presented by dojo.NodeList, many types of DOM
1017
		//		manipulation operations become very straightforward.
1018
		//
1019
		//		Unsupported Selectors:
1020
		//		--------------------
1021
		//
1022
		//		While dojo.query handles many CSS3 selectors, some fall outside of
1023
		//		what's resaonable for a programmatic node querying engine to
1024
		//		handle. Currently unsupported selectors include:
1025
		//
1026
		//			* namespace-differentiated selectors of any form
1027
		//			* "~", the immediately preceeded-by sibling selector
1028
		//			* "+", the preceeded-by sibling selector
1029
		//			* all "::" pseduo-element selectors
1030
		//			* certain pseduo-selectors which don't get a lot of day-to-day use:
1031
		//				* :root, :lang(), :target, :focus
1032
		//			* all visual and state selectors:
1033
		//				* :root, :active, :hover, :visisted, :link, :enabled, :disabled, :checked
1034
		//			* :*-of-type pseudo selectors
1035
		//
1036
		//		dojo.query and XML Documents:
1037
		//		-----------------------------
1038
		//		FIXME
1039
		//
1040
		//	query: String
1041
		//		The CSS3 expression to match against. For details on the syntax of
1042
		//		CSS3 selectors, see:
1043
		//			http://www.w3.org/TR/css3-selectors/#selectors
1044
		//	root: String|DOMNode?
1045
		//		A node (or string ID of a node) to scope the search from. Optional.
1046
		//	returns:
1047
		//		An instance of dojo.NodeList. Many methods are available on
1048
		//		NodeLists for searching, iterating, manipulating, and handling
1049
		//		events on the matched nodes in the returned list.
1050
 
1051
		// return is always an array
1052
		// NOTE: elementsById is not currently supported
1053
		// NOTE: ignores xpath-ish queries for now
1054
		if(query.constructor == d.NodeList){
1055
			return query;
1056
		}
1057
		if(!d.isString(query)){
1058
			return new d.NodeList(query); // dojo.NodeList
1059
		}
1060
		if(d.isString(root)){
1061
			root = d.byId(root);
1062
		}
1063
 
1064
		// FIXME: should support more methods on the return than the stock array.
1065
		return _zip(getQueryFunc(query)(root||d.doc));
1066
	}
1067
 
1068
	/*
1069
	// exposing these was a mistake
1070
	d.query.attrs = attrs;
1071
	d.query.pseudos = pseudos;
1072
	*/
1073
 
1074
	// one-off function for filtering a NodeList based on a simple selector
1075
	d._filterQueryResult = function(nodeList, simpleFilter){
1076
		var tnl = new d.NodeList();
1077
		var ff = (simpleFilter) ? getFilterFunc(getQueryParts(simpleFilter)[0]) : function(){ return true; };
1078
		for(var x=0, te; te = nodeList[x]; x++){
1079
			if(ff(te)){ tnl.push(te); }
1080
		}
1081
		return tnl;
1082
	}
1083
})();
1084
 
1085
}