Code

< 40 %
40-60 %
> 60 %
1
<?php
2
/**
3
 * @author Niels A.D.
4
 * @author Todd Burry <[email protected]>
5
 * @copyright 2010 Niels A.D., 2014 Todd Burry
6
 * @license http://opensource.org/licenses/LGPL-2.1 LGPL-2.1
7
 * @package pQuery
8
 */
9
10
namespace pQuery;
11
12
/**
13
 * Tokenizes a css selector query
14
 */
15
class CSSQueryTokenizer extends TokenizerBase {
16
17
	/**
18
	 * Opening bracket token, used for "["
19
	 */
20
	const TOK_BRACKET_OPEN = 100;
21
	/**
22
	 * Closing bracket token, used for "]"
23
	 */
24
	const TOK_BRACKET_CLOSE = 101;
25
	/**
26
	 * Opening brace token, used for "("
27
	 */
28
	const TOK_BRACE_OPEN = 102;
29
	/**
30
	 * Closing brace token, used for ")"
31
	 */
32
	const TOK_BRACE_CLOSE = 103;
33
	/**
34
	 * String token
35
	 */
36
	const TOK_STRING = 104;
37
	/**
38
	 * Colon token, used for ":"
39
	 */
40
	const TOK_COLON = 105;
41
	/**
42
	 * Comma token, used for ","
43
	 */
44
	const TOK_COMMA = 106;
45
	/**
46
	 * "Not" token, used for "!"
47
	 */
48
	const TOK_NOT = 107;
49
50
	/**
51
	 * "All" token, used for "*" in query
52
	 */
53
	const TOK_ALL = 108;
54
	/**
55
	 * Pipe token, used for "|"
56
	 */
57
	const TOK_PIPE = 109;
58
	/**
59
	 * Plus token, used for "+"
60
	 */
61
	const TOK_PLUS = 110;
62
	/**
63
	 * "Sibling" token, used for "~" in query
64
	 */
65
	const TOK_SIBLING = 111;
66
	/**
67
	 * Class token, used for "." in query
68
	 */
69
	const TOK_CLASS = 112;
70
	/**
71
	 * ID token, used for "#" in query
72
	 */
73
	const TOK_ID = 113;
74
	/**
75
	 * Child token, used for ">" in query
76
	 */
77
	const TOK_CHILD = 114;
78
79
	/**
80
	 * Attribute compare prefix token, used for "|="
81
	 */
82
	const TOK_COMPARE_PREFIX = 115;
83
	/**
84
	 * Attribute contains token, used for "*="
85
	 */
86
	const TOK_COMPARE_CONTAINS = 116;
87
	/**
88
	 * Attribute contains word token, used for "~="
89
	 */
90
	const TOK_COMPARE_CONTAINS_WORD = 117;
91
	/**
92
	 * Attribute compare end token, used for "$="
93
	 */
94
	const TOK_COMPARE_ENDS = 118;
95
	/**
96
	 * Attribute equals token, used for "="
97
	 */
98
	const TOK_COMPARE_EQUALS = 119;
99
	/**
100
	 * Attribute not equal token, used for "!="
101
	 */
102
	const TOK_COMPARE_NOT_EQUAL = 120;
103
	/**
104
	 * Attribute compare bigger than token, used for ">="
105
	 */
106
	const TOK_COMPARE_BIGGER_THAN = 121;
107
	/**
108
	 * Attribute compare smaller than token, used for "<="
109
	 */
110
	const TOK_COMPARE_SMALLER_THAN = 122;
111
	/**
112
	 * Attribute compare with regex, used for "%="
113
	 */
114
	const TOK_COMPARE_REGEX = 123;
115
	/**
116
	 * Attribute compare start token, used for "^="
117
	 */
118
	const TOK_COMPARE_STARTS = 124;
119
120
	/**
121
	 * Sets query identifiers
122
	 * @see TokenizerBase::$identifiers
123
	 * @access private
124
	 */
125
	var $identifiers = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890_-?';
126
127
	/**
128
	 * Map characters to match their tokens
129
	 * @see TokenizerBase::$custom_char_map
130
	 * @access private
131
	 */
132
	var $custom_char_map = array(
133
		'.' => self::TOK_CLASS,
134
		'#' => self::TOK_ID,
135
		',' => self::TOK_COMMA,
136
		'>' => 'parse_gt',//self::TOK_CHILD,
137
138
		'+' => self::TOK_PLUS,
139
		'~' => 'parse_sibling',
140
141
		'|' => 'parse_pipe',
142
		'*' => 'parse_star',
143
		'$' => 'parse_compare',
144
		'=' => self::TOK_COMPARE_EQUALS,
145
		'!' => 'parse_not',
146
		'%' => 'parse_compare',
147
		'^' => 'parse_compare',
148
		'<' => 'parse_compare',
149
150
		'"' => 'parse_string',
151
		"'" => 'parse_string',
152
		'(' => self::TOK_BRACE_OPEN,
153
		')' => self::TOK_BRACE_CLOSE,
154
		'[' => self::TOK_BRACKET_OPEN,
155
		']' => self::TOK_BRACKET_CLOSE,
156
		':' => self::TOK_COLON
157
	);
158
159
	/**
160
	 * Parse ">" character
161
	 * @internal Could be {@link TOK_CHILD} or {@link TOK_COMPARE_BIGGER_THAN}
162
	 * @return int
163
	 */
164
	protected function parse_gt() {
165
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
166
			++$this->pos;
167
			return ($this->token = self::TOK_COMPARE_BIGGER_THAN);
168
		} else {
169
			return ($this->token = self::TOK_CHILD);
170
		}
171
	}
172
173
	/**
174
	 * Parse "~" character
175
	 * @internal Could be {@link TOK_SIBLING} or {@link TOK_COMPARE_CONTAINS_WORD}
176
	 * @return int
177
	 */
178
	protected function parse_sibling() {
179
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
180
			++$this->pos;
181
			return ($this->token = self::TOK_COMPARE_CONTAINS_WORD);
182
		} else {
183
			return ($this->token = self::TOK_SIBLING);
184
		}
185
	}
186
187
	/**
188
	 * Parse "|" character
189
	 * @internal Could be {@link TOK_PIPE} or {@link TOK_COMPARE_PREFIX}
190
	 * @return int
191
	 */
192 1
	protected function parse_pipe() {
193 1
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
194 1
			++$this->pos;
195 1
			return ($this->token = self::TOK_COMPARE_PREFIX);
196
		} else {
197
			return ($this->token = self::TOK_PIPE);
198
		}
199
	}
200
201
	/**
202
	 * Parse "*" character
203
	 * @internal Could be {@link TOK_ALL} or {@link TOK_COMPARE_CONTAINS}
204
	 * @return int
205
	 */
206 2
	protected function parse_star() {
207 2
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
208
			++$this->pos;
209
			return ($this->token = self::TOK_COMPARE_CONTAINS);
210
		} else {
211 2
			return ($this->token = self::TOK_ALL);
212
		}
213
	}
214
215
	/**
216
	 * Parse "!" character
217
	 * @internal Could be {@link TOK_NOT} or {@link TOK_COMPARE_NOT_EQUAL}
218
	 * @return int
219
	 */
220
	protected function parse_not() {
221
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
222
			++$this->pos;
223
			return ($this->token = self::TOK_COMPARE_NOT_EQUAL);
224
		} else {
225
			return ($this->token = self::TOK_NOT);
226
		}
227
	}
228
229
	/**
230
	 * Parse several compare characters
231
	 * @return int
232
	 */
233
	protected function parse_compare() {
234
		if ((($this->pos + 1) < $this->size) && ($this->doc[$this->pos + 1] === '=')) {
235
			switch($this->doc[$this->pos++]) {
236
				case '$':
237
					return ($this->token = self::TOK_COMPARE_ENDS);
238
				case '%':
239
					return ($this->token = self::TOK_COMPARE_REGEX);
240
				case '^':
241
					return ($this->token = self::TOK_COMPARE_STARTS);
242
				case '<':
243
					return ($this->token = self::TOK_COMPARE_SMALLER_THAN);
244
			}
245
		}
246
		return false;
247
	}
248
249
	/**
250
	 * Parse strings (" and ')
251
	 * @return int
252
	 */
253 5
	protected function parse_string() {
254 5
		$char = $this->doc[$this->pos];
255
256 5
		while (true) {
257 5
			if ($this->next_search($char.'\\', false) !== self::TOK_NULL) {
258 5
				if($this->doc[$this->pos] === $char) {
259 5
					break;
260
				} else {
261
					++$this->pos;
262
				}
263
			} else {
264
				$this->pos = $this->size - 1;
265
				break;
266
			}
267
		}
268
269 5
		return ($this->token = self::TOK_STRING);
270
	}
271
272
}
273
274
/**
275
 * Performs a css select query on HTML nodes
276
 */
277
class HtmlSelector {
278
279
	/**
280
	 * Parser object
281
	 * @internal If string, then it will create a new instance as parser
282
	 * @var CSSQueryTokenizer
283
	 */
284
	var $parser = 'pQuery\\CSSQueryTokenizer';
285
286
	/**
287
	 * Target of queries
288
	 * @var DomNode
289
	 */
290
	var $root = null;
291
292
	/**
293
	 * Last performed query, result in {@link $result}
294
	 * @var string
295
	 */
296
	var $query = '';
297
298
	/**
299
	 * Array of matching nodes
300
	 * @var array
301
	 */
302
	var $result = array();
303
304
	/**
305
	 * Include root in search, if false the only child nodes are evaluated
306
	 * @var bool
307
	 */
308
	var $search_root = false;
309
310
	/**
311
	 * Search recursively
312
	 * @var bool
313
	 */
314
	var $search_recursive = true;
315
316
	/**
317
	 * Extra function map for custom filters
318
	 * @var array
319
	 * @internal array('root' => 'filter_root') will cause the
320
	 * selector to call $this->filter_root at :root
321
	 * @see DomNode::$filter_map
322
	 */
323
	var $custom_filter_map = array();
324
325
	/**
326
	 * Class constructor
327
	 * @param DomNode $root {@link $root}
328
	 * @param string $query
329
	 * @param bool $search_root {@link $search_root}
330
	 * @param bool $search_recursive {@link $search_recursive}
331
	 * @param CSSQueryTokenizer $parser If null, then default class will be used
332
	 */
333 37
	function __construct($root, $query = '*', $search_root = false, $search_recursive = true, $parser = null) {
334 37
		if ($parser === null) {
335 37
			$parser = new $this->parser();
336 37
		}
337 37
		$this->parser = $parser;
338 37
		$this->root =& $root;
339
340 37
		$this->search_root = $search_root;
341 37
		$this->search_recursive = $search_recursive;
342
343 37
		$this->select($query);
344 37
	}
345
346
	#php4 PHP4 class constructor compatibility
347
	#function HtmlSelector($root, $query = '*', $search_root = false, $search_recursive = true, $parser = null) {return $this->__construct($root, $query, $search_root, $search_recursive, $parser);}
348
	#php4e
349
350
	/**
351
	 * toString method, returns {@link $query}
352
	 * @return string
353
	 * @access private
354
	 */
355
	function __toString() {
356
		return $this->query;
357
	}
358
359
	/**
360
	 * Class magic invoke method, performs {@link select()}
361
	 * @return array
362
	 * @access private
363
	 */
364
	function __invoke($query = '*') {
365
		return $this->select($query);
366
	}
367
368
	/**
369
	 * Perform query
370
	 * @param string $query
371
	 * @return array False on failure
372
	 */
373 37
	function select($query = '*') {
374 37
		$this->parser->setDoc($query);
375 37
		$this->query = $query;
376 37
		return (($this->parse()) ? $this->result : false);
377
	}
378
379
	/**
380
	 * Trigger error
381
	 * @param string $error
382
	 * @internal %pos% and %tok% will be replace in string with position and token(string)
383
	 * @access private
384
	 */
385
	protected function error($error) {
386
		$error = htmlentities(str_replace(
387
			array('%tok%', '%pos%'),
388
			array($this->parser->getTokenString(), (int) $this->parser->getPos()),
389
			$error
390
		));
391
392
		trigger_error($error);
393
	}
394
395
	/**
396
	 * Get identifier (parse identifier or string)
397
	 * @param bool $do_error Error on failure
398
	 * @return string False on failure
399
	 * @access private
400
	 */
401 36
	protected function parse_getIdentifier($do_error = true) {
402 36
		$p =& $this->parser;
403 36
		$tok = $p->token;
404
405 36
		if ($tok === CSSQueryTokenizer::TOK_IDENTIFIER) {
406 36
			return $p->getTokenString();
407 17
		} elseif($tok === CSSQueryTokenizer::TOK_STRING) {
408 5
			return str_replace(array('\\\'', '\\"', '\\\\'), array('\'', '"', '\\'), $p->getTokenString(1, -1));
409 17
		} elseif ($do_error) {
410
			$this->error('Expected identifier at %pos%!');
411
		}
412 17
		return false;
413
	}
414
415
	/**
416
	 * Get query conditions (tag, attribute and filter conditions)
417
	 * @return array False on failure
418
	 * @see DomNode::match()
419
	 * @access private
420
	 */
421 37
	protected function parse_conditions() {
422 37
		$p =& $this->parser;
423 37
		$tok = $p->token;
424
425 37
		if ($tok === CSSQueryTokenizer::TOK_NULL) {
426
			$this->error('Invalid search pattern(1): Empty string!');
427
			return false;
428
		}
429 37
		$conditions_all = array();
430
431
		//Tags
432 37
		while ($tok !== CSSQueryTokenizer::TOK_NULL) {
433 37
			$conditions = array('tags' => array(), 'attributes' => array());
434
435 37
			if ($tok === CSSQueryTokenizer::TOK_ALL) {
436 2
				$tok = $p->next();
437 2
				if (($tok === CSSQueryTokenizer::TOK_PIPE) && ($tok = $p->next()) && ($tok !== CSSQueryTokenizer::TOK_ALL)) {
438
					if (($tag = $this->parse_getIdentifier()) === false) {
439
						return false;
440
					}
441
					$conditions['tags'][] = array(
442
						'tag' => $tag,
443
						'compare' => 'name'
444
					);
445
					$tok = $p->next_no_whitespace();
446
				} else {
447 2
					$conditions['tags'][''] = array(
448 2
						'tag' => '',
449
						'match' => false
450 2
					);
451 2
					if ($tok === CSSQueryTokenizer::TOK_ALL) {
452
						$tok = $p->next_no_whitespace();
453
					}
454
				}
455 37
			} elseif ($tok === CSSQueryTokenizer::TOK_PIPE) {
456
				$tok = $p->next();
457
				if ($tok === CSSQueryTokenizer::TOK_ALL) {
458
					$conditions['tags'][] = array(
459
						'tag' => '',
460
						'compare' => 'namespace',
461
					);
462
				} elseif (($tag = $this->parse_getIdentifier()) !== false) {
463
					$conditions['tags'][] = array(
464
						'tag' => $tag,
465
						'compare' => 'total',
466
					);
467
				} else {
468
					return false;
469
				}
470
				$tok = $p->next_no_whitespace();
471 36
			} elseif ($tok === CSSQueryTokenizer::TOK_BRACE_OPEN) {
472
				$tok = $p->next_no_whitespace();
473
				$last_mode = 'or';
474
475
				while (true) {
476
					$match = true;
477
					$compare = 'total';
478
479
					if ($tok === CSSQueryTokenizer::TOK_NOT) {
480
						$match = false;
481
						$tok = $p->next_no_whitespace();
482
					}
483
484
					if ($tok === CSSQueryTokenizer::TOK_ALL) {
485
						$tok = $p->next();
486
						if ($tok === CSSQueryTokenizer::TOK_PIPE) {
487
							$this->next();
488
							$compare = 'name';
489
							if (($tag = $this->parse_getIdentifier()) === false) {
490
								return false;
491
							}
492
						}
493
					} elseif ($tok === CSSQueryTokenizer::TOK_PIPE) {
494
						$tok = $p->next();
495
						if ($tok === CSSQueryTokenizer::TOK_ALL) {
496
							$tag = '';
497
							$compare = 'namespace';
498
						} elseif (($tag = $this->parse_getIdentifier()) === false) {
499
							return false;
500
						}
501
						$tok = $p->next_no_whitespace();
502
					} else {
503
						if (($tag = $this->parse_getIdentifier()) === false) {
504
							return false;
505
						}
506
						$tok = $p->next();
507
						if ($tok === CSSQueryTokenizer::TOK_PIPE) {
508
							$tok = $p->next();
509
510
							if ($tok === CSSQueryTokenizer::TOK_ALL) {
511
								$compare = 'namespace';
512
							} elseif (($tag_name = $this->parse_getIdentifier()) !== false) {
513
								$tag = $tag.':'.$tag_name;
514
							} else {
515
								return false;
516
							}
517
518
							$tok = $p->next_no_whitespace();
519
						}
520
					}
521
					if ($tok === CSSQueryTokenizer::TOK_WHITESPACE) {
522
						$tok = $p->next_no_whitespace();
523
					}
524
525
					$conditions['tags'][] = array(
526
						'tag' => $tag,
527
						'match' => $match,
528
						'operator' => $last_mode,
529
						'compare' => $compare
530
					);
531
					switch($tok) {
532
						case CSSQueryTokenizer::TOK_COMMA:
533
							$tok = $p->next_no_whitespace();
534
							$last_mode = 'or';
535
							continue 2;
536
						case CSSQueryTokenizer::TOK_PLUS:
537
							$tok = $p->next_no_whitespace();
538
							$last_mode = 'and';
539
							continue 2;
540
						case CSSQueryTokenizer::TOK_BRACE_CLOSE:
541
							$tok = $p->next();
542
							break 2;
543
						default:
544
							$this->error('Expected closing brace or comma at pos %pos%!');
545
							return false;
546
					}
547
				}
548 36
			} elseif (($tag = $this->parse_getIdentifier(false)) !== false) {
549 22
				$tok = $p->next();
550 22
				if ($tok === CSSQueryTokenizer::TOK_PIPE) {
551
					$tok = $p->next();
552
553
					if ($tok === CSSQueryTokenizer::TOK_ALL) {
554
						$conditions['tags'][] = array(
555
							'tag' => $tag,
556
							'compare' => 'namespace'
557
						);
558
					} elseif (($tag_name = $this->parse_getIdentifier()) !== false) {
559
						$tag = $tag.':'.$tag_name;
560
						$conditions['tags'][] = array(
561
							'tag' => $tag,
562
							'match' => true
563
						);
564
					} else {
565
						return false;
566
					}
567
568
					$tok = $p->next();
569 22
                } elseif ($tag === 'text' && $tok === CSSQueryTokenizer::TOK_BRACE_OPEN) {
570 1
                    $pos = $p->getPos();
571 1
                    $tok = $p->next();
572 1
                    if ($tok === CSSQueryTokenizer::TOK_BRACE_CLOSE) {
573 1
                        $conditions['tags'][] = array(
574 1
                            'tag' => '~text~',
575
                            'match' => true
576 1
                        );
577 1
                        $p->next();
578 1
                    } else {
579
                        $p->setPos($pos);
580
                    }
581 1
				} else {
582 21
					$conditions['tags'][] = array(
583 21
						'tag' => $tag,
584
						'match' => true
585 21
					);
586
				}
587 22
			} else {
588 17
				unset($conditions['tags']);
589
			}
590
591
			//Class
592 37
			$last_mode = 'or';
593 37
			if ($tok === CSSQueryTokenizer::TOK_CLASS) {
594 12
				$p->next();
595 12
				if (($class = $this->parse_getIdentifier()) === false) {
596
					return false;
597
				}
598
599 12
				$conditions['attributes'][] = array(
600 12
					'attribute' => 'class',
601 12
					'operator_value' => 'contains_word',
602 12
					'value' => $class,
603
					'operator_result' => $last_mode
604 12
				);
605 12
				$last_mode = 'and';
606 12
				$tok = $p->next();
607 12
			}
608
609
			//ID
610 37
			if ($tok === CSSQueryTokenizer::TOK_ID) {
611 1
				$p->next();
612 1
				if (($id = $this->parse_getIdentifier()) === false) {
613
					return false;
614
				}
615
616 1
				$conditions['attributes'][] = array(
617 1
					'attribute' => 'id',
618 1
					'operator_value' => 'equals',
619 1
					'value' => $id,
620
					'operator_result' => $last_mode
621 1
				);
622 1
				$last_mode = 'and';
623 1
				$tok = $p->next();
624 1
			}
625
626
			//Attributes
627 37
			if ($tok === CSSQueryTokenizer::TOK_BRACKET_OPEN) {
628 6
				$tok = $p->next_no_whitespace();
629
630 6
				while (true) {
631 6
					$match = true;
632 6
					$compare = 'total';
633 6
					if ($tok === CSSQueryTokenizer::TOK_NOT) {
634
						$match = false;
635
						$tok = $p->next_no_whitespace();
636
					}
637
638 6
					if ($tok === CSSQueryTokenizer::TOK_ALL) {
639
						$tok = $p->next();
640
						if ($tok === CSSQueryTokenizer::TOK_PIPE) {
641
							$tok = $p->next();
642
							if (($attribute = $this->parse_getIdentifier()) === false) {
643
								return false;
644
							}
645
							$compare = 'name';
646
							$tok = $p->next();
647
						} else {
648
							$this->error('Expected pipe at pos %pos%!');
649
							return false;
650
						}
651 6
					} elseif ($tok === CSSQueryTokenizer::TOK_PIPE) {
652
						$tok = $p->next();
653
						if (($tag = $this->parse_getIdentifier()) === false) {
654
							return false;
655
						}
656
						$tok = $p->next_no_whitespace();
657 6
					} elseif (($attribute = $this->parse_getIdentifier()) !== false) {
658 6
						$tok = $p->next();
659 6
						if ($tok === CSSQueryTokenizer::TOK_PIPE) {
660
							$tok = $p->next();
661
662
							if (($attribute_name = $this->parse_getIdentifier()) !== false) {
663
								$attribute = $attribute.':'.$attribute_name;
664
							} else {
665
								return false;
666
							}
667
668
							$tok = $p->next();
669
						}
670 6
					} else {
671
						return false;
672
					}
673 6
					if ($tok === CSSQueryTokenizer::TOK_WHITESPACE) {
674
						$tok = $p->next_no_whitespace();
675
					}
676
677 6
					$operator_value = '';
678 6
					$val = '';
679
					switch($tok) {
680 6
						case CSSQueryTokenizer::TOK_COMPARE_PREFIX:
681 6
						case CSSQueryTokenizer::TOK_COMPARE_CONTAINS:
682 6
						case CSSQueryTokenizer::TOK_COMPARE_CONTAINS_WORD:
683 6
						case CSSQueryTokenizer::TOK_COMPARE_ENDS:
684 6
						case CSSQueryTokenizer::TOK_COMPARE_EQUALS:
685 6
						case CSSQueryTokenizer::TOK_COMPARE_NOT_EQUAL:
686 6
						case CSSQueryTokenizer::TOK_COMPARE_REGEX:
687 6
						case CSSQueryTokenizer::TOK_COMPARE_STARTS:
688 6
						case CSSQueryTokenizer::TOK_COMPARE_BIGGER_THAN:
689 6
						case CSSQueryTokenizer::TOK_COMPARE_SMALLER_THAN:
690 6
							$operator_value = $p->getTokenString(($tok === CSSQueryTokenizer::TOK_COMPARE_EQUALS) ? 0 : -1);
691 6
							$p->next_no_whitespace();
692
693 6
							if (($val = $this->parse_getIdentifier()) === false) {
694
								return false;
695
							}
696
697 6
							$tok = $p->next_no_whitespace();
698 6
							break;
699
					}
700
701 6
					if ($operator_value && $val) {
702 6
						$conditions['attributes'][] = array(
703 6
							'attribute' => $attribute,
704 6
							'operator_value' => $operator_value,
705 6
							'value' => $val,
706 6
							'match' => $match,
707 6
							'operator_result' => $last_mode,
708
							'compare' => $compare
709 6
						);
710 6
					} else {
711
						$conditions['attributes'][] = array(
712
							'attribute' => $attribute,
713
							'value' => $match,
714
							'operator_result' => $last_mode,
715
							'compare' => $compare
716
						);
717
					}
718
719
					switch($tok) {
720 6
						case CSSQueryTokenizer::TOK_COMMA:
721
							$tok = $p->next_no_whitespace();
722
							$last_mode = 'or';
723
							continue 2;
724 6
						case CSSQueryTokenizer::TOK_PLUS:
725
							$tok = $p->next_no_whitespace();
726
							$last_mode = 'and';
727
							continue 2;
728 6
						case CSSQueryTokenizer::TOK_BRACKET_CLOSE:
729 6
							$tok = $p->next();
730 6
							break 2;
731
						default:
732
							$this->error('Expected closing bracket or comma at pos %pos%!');
733
							return false;
734
					}
735
				}
736 6
			}
737
738 37
			if (count($conditions['attributes']) < 1) {
739 23
				unset($conditions['attributes']);
740 23
			}
741
742 37
			while($tok === CSSQueryTokenizer::TOK_COLON) {
743 3
				if (count($conditions) < 1) {
744 2
					$conditions['tags'] = array(array(
745 2
						'tag' => '',
746
						'match' => false
747 2
					));
748 2
				}
749
750 3
				$tok = $p->next();
751 3
				if (($filter = $this->parse_getIdentifier()) === false) {
752
					return false;
753
				}
754
755 3
				if (($tok = $p->next()) === CSSQueryTokenizer::TOK_BRACE_OPEN) {
756
					$start = $p->pos;
757
					$count = 1;
758
					while ((($tok = $p->next()) !== CSSQueryTokenizer::TOK_NULL) && !(($tok === CSSQueryTokenizer::TOK_BRACE_CLOSE) && (--$count === 0))) {
759
						if ($tok === CSSQueryTokenizer::TOK_BRACE_OPEN) {
760
							++$count;
761
						}
762
					}
763
764
765
					if ($tok !== CSSQueryTokenizer::TOK_BRACE_CLOSE) {
766
						$this->error('Expected closing brace at pos %pos%!');
767
						return false;
768
					}
769
					$len = $p->pos - 1 - $start;
770
					$params = (($len > 0) ? substr($p->doc, $start + 1, $len) : '');
771
					$tok = $p->next();
772
				} else {
773 3
					$params = '';
774
				}
775
776 3
				$conditions['filters'][] = array('filter' => $filter, 'params' => $params);
777 3
			}
778 37
			if (count($conditions) < 1) {
779
				$this->error('Invalid search pattern(2): No conditions found!');
780
				return false;
781
			}
782 37
			$conditions_all[] = $conditions;
783
784 37
			if ($tok === CSSQueryTokenizer::TOK_WHITESPACE) {
785
				$tok = $p->next_no_whitespace();
786
			}
787
788 37
			if ($tok === CSSQueryTokenizer::TOK_COMMA) {
789 2
				$tok = $p->next_no_whitespace();
790 2
				continue;
791
			} else {
792 37
				break;
793
			}
794
		}
795
796 37
		return $conditions_all;
797
	}
798
799
800
	/**
801
	 * Evaluate root node using custom callback
802
	 * @param array $conditions {@link parse_conditions()}
803
	 * @param bool|int $recursive
804
	 * @param bool $check_root
805
	 * @return array
806
	 * @access private
807
	 */
808 37
	protected function parse_callback($conditions, $recursive = true, $check_root = false) {
809 37
		return ($this->result = $this->root->getChildrenByMatch(
810 37
			$conditions,
811 37
			$recursive,
812 37
			$check_root,
813 37
			$this->custom_filter_map
814 37
		));
815
	}
816
817
	/**
818
	 * Parse first bit of query, only root node has to be evaluated now
819
	 * @param bool|int $recursive
820
	 * @return bool
821
	 * @internal Result of query is set in {@link $result}
822
	 * @access private
823
	 */
824 37
	protected function parse_single($recursive = true) {
825 37
		if (($c = $this->parse_conditions()) === false) {
826
			return false;
827
		}
828
829 37
		$this->parse_callback($c, $recursive, $this->search_root);
830 37
		return true;
831
	}
832
833
	/**
834
	 * Evaluate sibling nodes
835
	 * @return bool
836
	 * @internal Result of query is set in {@link $result}
837
	 * @access private
838
	 */
839
	protected function parse_adjacent() {
840
		$tmp = $this->result;
841
		$this->result = array();
842
		if (($c = $this->parse_conditions()) === false) {
843
			return false;
844
		}
845
846
		foreach($tmp as $t) {
847
			if (($sibling = $t->getNextSibling()) !== false) {
848
				if ($sibling->match($c, true, $this->custom_filter_map)) {
849
					$this->result[] = $sibling;
850
				}
851
			}
852
		}
853
854
		return true;
855
	}
856
857
	/**
858
	 * Evaluate {@link $result}
859
	 * @param bool $parent Evaluate parent nodes
860
	 * @param bool|int $recursive
861
	 * @return bool
862
	 * @internal Result of query is set in {@link $result}
863
	 * @access private
864
	 */
865
	protected function parse_result($parent = false, $recursive = true) {
866
		$tmp = $this->result;
867
		$tmp_res = array();
868
		if (($c = $this->parse_conditions()) === false) {
869
			return false;
870
		}
871
872
		foreach(array_keys($tmp) as $t) {
873
			$this->root = (($parent) ? $tmp[$t]->parent : $tmp[$t]);
874
			$this->parse_callback($c, $recursive);
875
			foreach(array_keys($this->result) as $r) {
876
				if (!in_array($this->result[$r], $tmp_res, true)) {
877
					$tmp_res[] = $this->result[$r];
878
				}
879
			}
880
		}
881
		$this->result = $tmp_res;
882
		return true;
883
	}
884
885
	/**
886
	 * Parse full query
887
	 * @return bool
888
	 * @internal Result of query is set in {@link $result}
889
	 * @access private
890
	 */
891 37
	protected function parse() {
892 37
		$p =& $this->parser;
893 37
		$p->setPos(0);
894 37
		$this->result = array();
895
896 37
		if (!$this->parse_single()) {
897
			return false;
898
		}
899
900 37
		while (count($this->result) > 0) {
901 37
			switch($p->token) {
902 37
				case CSSQueryTokenizer::TOK_CHILD:
903
					$this->parser->next_no_whitespace();
904
					if (!$this->parse_result(false, 1)) {
905
						return false;
906
					}
907
					break;
908
909 37
				case CSSQueryTokenizer::TOK_SIBLING:
910
					$this->parser->next_no_whitespace();
911
					if (!$this->parse_result(true, 1)) {
912
						return false;
913
					}
914
					break;
915
916 37
				case CSSQueryTokenizer::TOK_PLUS:
917
					$this->parser->next_no_whitespace();
918
					if (!$this->parse_adjacent()) {
919
						return false;
920
					}
921
					break;
922
923 37
				case CSSQueryTokenizer::TOK_ALL:
924 37
				case CSSQueryTokenizer::TOK_IDENTIFIER:
925 37
				case CSSQueryTokenizer::TOK_STRING:
926 37
				case CSSQueryTokenizer::TOK_BRACE_OPEN:
927 37
				case CSSQueryTokenizer::TOK_BRACKET_OPEN:
928 37
				case CSSQueryTokenizer::TOK_ID:
929 37
				case CSSQueryTokenizer::TOK_CLASS:
930 37
				case CSSQueryTokenizer::TOK_COLON:
931
					if (!$this->parse_result()) {
932
						return false;
933
					}
934
					break;
935
936 37
				case CSSQueryTokenizer::TOK_NULL:
937 37
					break 2;
938
939
				default:
940
					$this->error('Invalid search pattern(3): No result modifier found!');
941
					return false;
942
			}
943
		}
944
945 37
		return true;
946
	}
947
}
948
949
?>
950