Completed
Push — master ( 872339...c28159 )
by Todd
03:07
created

gan_selector_html.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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);}
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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