@@ -11,31 +11,31 @@ |
||
11 | 11 | */ |
12 | 12 | interface Traverser |
13 | 13 | { |
14 | - /** |
|
15 | - * Process a CSS selector and find matches. |
|
16 | - * |
|
17 | - * This specifies a query to be run by the Traverser. A given |
|
18 | - * Traverser may, in practice, delay the finding until some later time |
|
19 | - * but must return the found results when getMatches() is called. |
|
20 | - * |
|
21 | - * @param string $selector |
|
22 | - * A selector. Typically this is a CSS 3 Selector. |
|
23 | - * |
|
24 | - * @return \Traverser |
|
25 | - * The Traverser that can return matches. |
|
26 | - */ |
|
27 | - public function find($selector); |
|
14 | + /** |
|
15 | + * Process a CSS selector and find matches. |
|
16 | + * |
|
17 | + * This specifies a query to be run by the Traverser. A given |
|
18 | + * Traverser may, in practice, delay the finding until some later time |
|
19 | + * but must return the found results when getMatches() is called. |
|
20 | + * |
|
21 | + * @param string $selector |
|
22 | + * A selector. Typically this is a CSS 3 Selector. |
|
23 | + * |
|
24 | + * @return \Traverser |
|
25 | + * The Traverser that can return matches. |
|
26 | + */ |
|
27 | + public function find($selector); |
|
28 | 28 | |
29 | - /** |
|
30 | - * Get the results of a find() operation. |
|
31 | - * |
|
32 | - * Return an array of matching items. |
|
33 | - * |
|
34 | - * @return array |
|
35 | - * An array of matched values. The specific data type in the matches |
|
36 | - * will differ depending on the data type searched, but in the core |
|
37 | - * QueryPath implementation, this will be an array of DOMNode |
|
38 | - * objects. |
|
39 | - */ |
|
40 | - public function matches(); |
|
29 | + /** |
|
30 | + * Get the results of a find() operation. |
|
31 | + * |
|
32 | + * Return an array of matching items. |
|
33 | + * |
|
34 | + * @return array |
|
35 | + * An array of matched values. The specific data type in the matches |
|
36 | + * will differ depending on the data type searched, but in the core |
|
37 | + * QueryPath implementation, this will be an array of DOMNode |
|
38 | + * objects. |
|
39 | + */ |
|
40 | + public function matches(); |
|
41 | 41 | } |
@@ -14,74 +14,74 @@ |
||
14 | 14 | */ |
15 | 15 | final class Token |
16 | 16 | { |
17 | - public const CHAR = 0x0; |
|
18 | - public const STAR = 0x1; |
|
19 | - public const RANGLE = 0x2; |
|
20 | - public const DOT = 0x3; |
|
21 | - public const OCTO = 0x4; |
|
22 | - public const RSQUARE = 0x5; |
|
23 | - public const LSQUARE = 0x6; |
|
24 | - public const COLON = 0x7; |
|
25 | - public const RPAREN = 0x8; |
|
26 | - public const LPAREN = 0x9; |
|
27 | - public const PLUS = 0xA; |
|
28 | - public const TILDE = 0xB; |
|
29 | - public const EQ = 0xC; |
|
30 | - public const PIPE = 0xD; |
|
31 | - public const COMMA = 0xE; |
|
32 | - public const WHITE = 0xF; |
|
33 | - public const QUOTE = 0x10; |
|
34 | - public const SQUOTE = 0x11; |
|
35 | - public const BSLASH = 0x12; |
|
36 | - public const CARAT = 0x13; |
|
37 | - public const DOLLAR = 0x14; |
|
38 | - public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it. |
|
17 | + public const CHAR = 0x0; |
|
18 | + public const STAR = 0x1; |
|
19 | + public const RANGLE = 0x2; |
|
20 | + public const DOT = 0x3; |
|
21 | + public const OCTO = 0x4; |
|
22 | + public const RSQUARE = 0x5; |
|
23 | + public const LSQUARE = 0x6; |
|
24 | + public const COLON = 0x7; |
|
25 | + public const RPAREN = 0x8; |
|
26 | + public const LPAREN = 0x9; |
|
27 | + public const PLUS = 0xA; |
|
28 | + public const TILDE = 0xB; |
|
29 | + public const EQ = 0xC; |
|
30 | + public const PIPE = 0xD; |
|
31 | + public const COMMA = 0xE; |
|
32 | + public const WHITE = 0xF; |
|
33 | + public const QUOTE = 0x10; |
|
34 | + public const SQUOTE = 0x11; |
|
35 | + public const BSLASH = 0x12; |
|
36 | + public const CARAT = 0x13; |
|
37 | + public const DOLLAR = 0x14; |
|
38 | + public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it. |
|
39 | 39 | |
40 | - // In legal range for string. |
|
41 | - public const STRING_LEGAL = 0x63; |
|
40 | + // In legal range for string. |
|
41 | + public const STRING_LEGAL = 0x63; |
|
42 | 42 | |
43 | - /** |
|
44 | - * Get a name for a given constant. Used for error handling. |
|
45 | - */ |
|
46 | - public static function name($const_int) |
|
47 | - { |
|
48 | - $a = [ |
|
49 | - 'character', |
|
50 | - 'star', |
|
51 | - 'right angle bracket', |
|
52 | - 'dot', |
|
53 | - 'octothorp', |
|
54 | - 'right square bracket', |
|
55 | - 'left square bracket', |
|
56 | - 'colon', |
|
57 | - 'right parenthesis', |
|
58 | - 'left parenthesis', |
|
59 | - 'plus', |
|
60 | - 'tilde', |
|
61 | - 'equals', |
|
62 | - 'vertical bar', |
|
63 | - 'comma', |
|
64 | - 'space', |
|
65 | - 'quote', |
|
66 | - 'single quote', |
|
67 | - 'backslash', |
|
68 | - 'carat', |
|
69 | - 'dollar', |
|
70 | - 'at', |
|
71 | - ]; |
|
43 | + /** |
|
44 | + * Get a name for a given constant. Used for error handling. |
|
45 | + */ |
|
46 | + public static function name($const_int) |
|
47 | + { |
|
48 | + $a = [ |
|
49 | + 'character', |
|
50 | + 'star', |
|
51 | + 'right angle bracket', |
|
52 | + 'dot', |
|
53 | + 'octothorp', |
|
54 | + 'right square bracket', |
|
55 | + 'left square bracket', |
|
56 | + 'colon', |
|
57 | + 'right parenthesis', |
|
58 | + 'left parenthesis', |
|
59 | + 'plus', |
|
60 | + 'tilde', |
|
61 | + 'equals', |
|
62 | + 'vertical bar', |
|
63 | + 'comma', |
|
64 | + 'space', |
|
65 | + 'quote', |
|
66 | + 'single quote', |
|
67 | + 'backslash', |
|
68 | + 'carat', |
|
69 | + 'dollar', |
|
70 | + 'at', |
|
71 | + ]; |
|
72 | 72 | |
73 | - if (isset($a[$const_int]) && is_numeric($const_int)) { |
|
74 | - return $a[$const_int]; |
|
75 | - } |
|
73 | + if (isset($a[$const_int]) && is_numeric($const_int)) { |
|
74 | + return $a[$const_int]; |
|
75 | + } |
|
76 | 76 | |
77 | - if ($const_int === self::STRING_LEGAL) { |
|
78 | - return 'a legal non-alphanumeric character'; |
|
79 | - } |
|
77 | + if ($const_int === self::STRING_LEGAL) { |
|
78 | + return 'a legal non-alphanumeric character'; |
|
79 | + } |
|
80 | 80 | |
81 | - if ($const_int === false) { |
|
82 | - return 'end of file'; |
|
83 | - } |
|
81 | + if ($const_int === false) { |
|
82 | + return 'end of file'; |
|
83 | + } |
|
84 | 84 | |
85 | - return sprintf('illegal character (%s)', $const_int); |
|
86 | - } |
|
85 | + return sprintf('illegal character (%s)', $const_int); |
|
86 | + } |
|
87 | 87 | } |
@@ -54,133 +54,133 @@ |
||
54 | 54 | class Selector implements EventHandler, IteratorAggregate, Countable |
55 | 55 | { |
56 | 56 | |
57 | - protected $selectors = []; |
|
58 | - protected $currSelector; |
|
59 | - protected $selectorGroups = []; |
|
60 | - protected $groupIndex = 0; |
|
61 | - |
|
62 | - public function __construct() |
|
63 | - { |
|
64 | - $this->currSelector = new SimpleSelector(); |
|
65 | - |
|
66 | - $this->selectors[$this->groupIndex][] = $this->currSelector; |
|
67 | - } |
|
68 | - |
|
69 | - public function getIterator(): Traversable |
|
70 | - { |
|
71 | - return new ArrayIterator($this->selectors); |
|
72 | - } |
|
73 | - |
|
74 | - /** |
|
75 | - * Get the array of SimpleSelector objects. |
|
76 | - * |
|
77 | - * Normally, one iterates over a Selector. However, if it is |
|
78 | - * necessary to get the selector array and manipulate it, this |
|
79 | - * method can be used. |
|
80 | - */ |
|
81 | - public function toArray() |
|
82 | - { |
|
83 | - return $this->selectors; |
|
84 | - } |
|
85 | - |
|
86 | - public function count(): int |
|
87 | - { |
|
88 | - return count($this->selectors); |
|
89 | - } |
|
90 | - |
|
91 | - public function elementID($id) |
|
92 | - { |
|
93 | - $this->currSelector->id = $id; |
|
94 | - } |
|
95 | - |
|
96 | - public function element($name) |
|
97 | - { |
|
98 | - $this->currSelector->element = $name; |
|
99 | - } |
|
100 | - |
|
101 | - public function elementNS($name, $namespace = null) |
|
102 | - { |
|
103 | - $this->currSelector->ns = $namespace; |
|
104 | - $this->currSelector->element = $name; |
|
105 | - } |
|
106 | - |
|
107 | - public function anyElement() |
|
108 | - { |
|
109 | - $this->currSelector->element = '*'; |
|
110 | - } |
|
111 | - |
|
112 | - public function anyElementInNS($ns) |
|
113 | - { |
|
114 | - $this->currSelector->ns = $ns; |
|
115 | - $this->currSelector->element = '*'; |
|
116 | - } |
|
117 | - |
|
118 | - public function elementClass($name) |
|
119 | - { |
|
120 | - $this->currSelector->classes[] = $name; |
|
121 | - } |
|
122 | - |
|
123 | - public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
124 | - { |
|
125 | - $this->currSelector->attributes[] = [ |
|
126 | - 'name' => $name, |
|
127 | - 'value' => $value, |
|
128 | - 'op' => $operation, |
|
129 | - ]; |
|
130 | - } |
|
131 | - |
|
132 | - public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
133 | - { |
|
134 | - $this->currSelector->attributes[] = [ |
|
135 | - 'name' => $name, |
|
136 | - 'value' => $value, |
|
137 | - 'op' => $operation, |
|
138 | - 'ns' => $ns, |
|
139 | - ]; |
|
140 | - } |
|
141 | - |
|
142 | - public function pseudoClass($name, $value = null) |
|
143 | - { |
|
144 | - $this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value]; |
|
145 | - } |
|
146 | - |
|
147 | - public function pseudoElement($name) |
|
148 | - { |
|
149 | - $this->currSelector->pseudoElements[] = $name; |
|
150 | - } |
|
151 | - |
|
152 | - public function combinator($combinatorName) |
|
153 | - { |
|
154 | - $this->currSelector->combinator = $combinatorName; |
|
155 | - $this->currSelector = new SimpleSelector(); |
|
156 | - array_unshift($this->selectors[$this->groupIndex], $this->currSelector); |
|
157 | - //$this->selectors[]= $this->currSelector; |
|
158 | - } |
|
159 | - |
|
160 | - public function directDescendant() |
|
161 | - { |
|
162 | - $this->combinator(SimpleSelector::DIRECT_DESCENDANT); |
|
163 | - } |
|
164 | - |
|
165 | - public function adjacent() |
|
166 | - { |
|
167 | - $this->combinator(SimpleSelector::ADJACENT); |
|
168 | - } |
|
169 | - |
|
170 | - public function anotherSelector() |
|
171 | - { |
|
172 | - $this->groupIndex++; |
|
173 | - $this->currSelector = new SimpleSelector(); |
|
174 | - $this->selectors[$this->groupIndex] = [$this->currSelector]; |
|
175 | - } |
|
176 | - |
|
177 | - public function sibling() |
|
178 | - { |
|
179 | - $this->combinator(SimpleSelector::SIBLING); |
|
180 | - } |
|
181 | - |
|
182 | - public function anyDescendant() |
|
183 | - { |
|
184 | - $this->combinator(SimpleSelector::ANY_DESCENDANT); |
|
185 | - } |
|
57 | + protected $selectors = []; |
|
58 | + protected $currSelector; |
|
59 | + protected $selectorGroups = []; |
|
60 | + protected $groupIndex = 0; |
|
61 | + |
|
62 | + public function __construct() |
|
63 | + { |
|
64 | + $this->currSelector = new SimpleSelector(); |
|
65 | + |
|
66 | + $this->selectors[$this->groupIndex][] = $this->currSelector; |
|
67 | + } |
|
68 | + |
|
69 | + public function getIterator(): Traversable |
|
70 | + { |
|
71 | + return new ArrayIterator($this->selectors); |
|
72 | + } |
|
73 | + |
|
74 | + /** |
|
75 | + * Get the array of SimpleSelector objects. |
|
76 | + * |
|
77 | + * Normally, one iterates over a Selector. However, if it is |
|
78 | + * necessary to get the selector array and manipulate it, this |
|
79 | + * method can be used. |
|
80 | + */ |
|
81 | + public function toArray() |
|
82 | + { |
|
83 | + return $this->selectors; |
|
84 | + } |
|
85 | + |
|
86 | + public function count(): int |
|
87 | + { |
|
88 | + return count($this->selectors); |
|
89 | + } |
|
90 | + |
|
91 | + public function elementID($id) |
|
92 | + { |
|
93 | + $this->currSelector->id = $id; |
|
94 | + } |
|
95 | + |
|
96 | + public function element($name) |
|
97 | + { |
|
98 | + $this->currSelector->element = $name; |
|
99 | + } |
|
100 | + |
|
101 | + public function elementNS($name, $namespace = null) |
|
102 | + { |
|
103 | + $this->currSelector->ns = $namespace; |
|
104 | + $this->currSelector->element = $name; |
|
105 | + } |
|
106 | + |
|
107 | + public function anyElement() |
|
108 | + { |
|
109 | + $this->currSelector->element = '*'; |
|
110 | + } |
|
111 | + |
|
112 | + public function anyElementInNS($ns) |
|
113 | + { |
|
114 | + $this->currSelector->ns = $ns; |
|
115 | + $this->currSelector->element = '*'; |
|
116 | + } |
|
117 | + |
|
118 | + public function elementClass($name) |
|
119 | + { |
|
120 | + $this->currSelector->classes[] = $name; |
|
121 | + } |
|
122 | + |
|
123 | + public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
124 | + { |
|
125 | + $this->currSelector->attributes[] = [ |
|
126 | + 'name' => $name, |
|
127 | + 'value' => $value, |
|
128 | + 'op' => $operation, |
|
129 | + ]; |
|
130 | + } |
|
131 | + |
|
132 | + public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY) |
|
133 | + { |
|
134 | + $this->currSelector->attributes[] = [ |
|
135 | + 'name' => $name, |
|
136 | + 'value' => $value, |
|
137 | + 'op' => $operation, |
|
138 | + 'ns' => $ns, |
|
139 | + ]; |
|
140 | + } |
|
141 | + |
|
142 | + public function pseudoClass($name, $value = null) |
|
143 | + { |
|
144 | + $this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value]; |
|
145 | + } |
|
146 | + |
|
147 | + public function pseudoElement($name) |
|
148 | + { |
|
149 | + $this->currSelector->pseudoElements[] = $name; |
|
150 | + } |
|
151 | + |
|
152 | + public function combinator($combinatorName) |
|
153 | + { |
|
154 | + $this->currSelector->combinator = $combinatorName; |
|
155 | + $this->currSelector = new SimpleSelector(); |
|
156 | + array_unshift($this->selectors[$this->groupIndex], $this->currSelector); |
|
157 | + //$this->selectors[]= $this->currSelector; |
|
158 | + } |
|
159 | + |
|
160 | + public function directDescendant() |
|
161 | + { |
|
162 | + $this->combinator(SimpleSelector::DIRECT_DESCENDANT); |
|
163 | + } |
|
164 | + |
|
165 | + public function adjacent() |
|
166 | + { |
|
167 | + $this->combinator(SimpleSelector::ADJACENT); |
|
168 | + } |
|
169 | + |
|
170 | + public function anotherSelector() |
|
171 | + { |
|
172 | + $this->groupIndex++; |
|
173 | + $this->currSelector = new SimpleSelector(); |
|
174 | + $this->selectors[$this->groupIndex] = [$this->currSelector]; |
|
175 | + } |
|
176 | + |
|
177 | + public function sibling() |
|
178 | + { |
|
179 | + $this->combinator(SimpleSelector::SIBLING); |
|
180 | + } |
|
181 | + |
|
182 | + public function anyDescendant() |
|
183 | + { |
|
184 | + $this->combinator(SimpleSelector::ANY_DESCENDANT); |
|
185 | + } |
|
186 | 186 | } |
@@ -60,166 +60,166 @@ discard block |
||
60 | 60 | */ |
61 | 61 | class DOMTraverser implements Traverser |
62 | 62 | { |
63 | - protected $matches = []; |
|
64 | - protected $selector; |
|
65 | - protected $dom; |
|
66 | - protected $initialized = true; |
|
67 | - protected $psHandler; |
|
68 | - protected $scopeNode; |
|
69 | - |
|
70 | - /** |
|
71 | - * Build a new DOMTraverser. |
|
72 | - * |
|
73 | - * This requires a DOM-like object or collection of DOM nodes. |
|
74 | - * |
|
75 | - * @param SplObjectStorage $splos |
|
76 | - * @param bool $initialized |
|
77 | - * @param null $scopeNode |
|
78 | - */ |
|
79 | - public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null) |
|
80 | - { |
|
81 | - $this->psHandler = new PseudoClass(); |
|
82 | - $this->initialized = $initialized; |
|
83 | - |
|
84 | - // Re-use the initial splos |
|
85 | - $this->matches = $splos; |
|
86 | - |
|
87 | - if (count($splos) !== 0) { |
|
88 | - $splos->rewind(); |
|
89 | - $first = $splos->current(); |
|
90 | - if ($first instanceof DOMDocument) { |
|
91 | - $this->dom = $first;//->documentElement; |
|
92 | - } else { |
|
93 | - $this->dom = $first->ownerDocument;//->documentElement; |
|
94 | - } |
|
95 | - |
|
96 | - $this->scopeNode = $scopeNode; |
|
97 | - if (empty($scopeNode)) { |
|
98 | - $this->scopeNode = $this->dom->documentElement; |
|
99 | - } |
|
100 | - } |
|
101 | - |
|
102 | - // This assumes a DOM. Need to also accomodate the case |
|
103 | - // where we get a set of elements. |
|
104 | - /* |
|
63 | + protected $matches = []; |
|
64 | + protected $selector; |
|
65 | + protected $dom; |
|
66 | + protected $initialized = true; |
|
67 | + protected $psHandler; |
|
68 | + protected $scopeNode; |
|
69 | + |
|
70 | + /** |
|
71 | + * Build a new DOMTraverser. |
|
72 | + * |
|
73 | + * This requires a DOM-like object or collection of DOM nodes. |
|
74 | + * |
|
75 | + * @param SplObjectStorage $splos |
|
76 | + * @param bool $initialized |
|
77 | + * @param null $scopeNode |
|
78 | + */ |
|
79 | + public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null) |
|
80 | + { |
|
81 | + $this->psHandler = new PseudoClass(); |
|
82 | + $this->initialized = $initialized; |
|
83 | + |
|
84 | + // Re-use the initial splos |
|
85 | + $this->matches = $splos; |
|
86 | + |
|
87 | + if (count($splos) !== 0) { |
|
88 | + $splos->rewind(); |
|
89 | + $first = $splos->current(); |
|
90 | + if ($first instanceof DOMDocument) { |
|
91 | + $this->dom = $first;//->documentElement; |
|
92 | + } else { |
|
93 | + $this->dom = $first->ownerDocument;//->documentElement; |
|
94 | + } |
|
95 | + |
|
96 | + $this->scopeNode = $scopeNode; |
|
97 | + if (empty($scopeNode)) { |
|
98 | + $this->scopeNode = $this->dom->documentElement; |
|
99 | + } |
|
100 | + } |
|
101 | + |
|
102 | + // This assumes a DOM. Need to also accomodate the case |
|
103 | + // where we get a set of elements. |
|
104 | + /* |
|
105 | 105 | $this->dom = $dom; |
106 | 106 | $this->matches = new \SplObjectStorage(); |
107 | 107 | $this->matches->attach($this->dom); |
108 | 108 | */ |
109 | - } |
|
110 | - |
|
111 | - public function debug($msg) |
|
112 | - { |
|
113 | - fwrite(STDOUT, PHP_EOL . $msg); |
|
114 | - } |
|
115 | - |
|
116 | - /** |
|
117 | - * Given a selector, find the matches in the given DOM. |
|
118 | - * |
|
119 | - * This is the main function for querying the DOM using a CSS |
|
120 | - * selector. |
|
121 | - * |
|
122 | - * @param string $selector |
|
123 | - * The selector. |
|
124 | - * |
|
125 | - * @return DOMTraverser a list of matched |
|
126 | - * DOMNode objects. |
|
127 | - * @throws ParseException |
|
128 | - */ |
|
129 | - public function find($selector): DOMTraverser |
|
130 | - { |
|
131 | - // Setup |
|
132 | - $handler = new Selector(); |
|
133 | - $parser = new Parser($selector, $handler); |
|
134 | - $parser->parse(); |
|
135 | - $this->selector = $handler; |
|
136 | - |
|
137 | - //$selector = $handler->toArray(); |
|
138 | - $found = $this->newMatches(); |
|
139 | - foreach ($handler as $selectorGroup) { |
|
140 | - // Initialize matches if necessary. |
|
141 | - if ($this->initialized) { |
|
142 | - $candidates = $this->matches; |
|
143 | - } else { |
|
144 | - $candidates = $this->initialMatch($selectorGroup[0], $this->matches); |
|
145 | - } |
|
146 | - |
|
147 | - /** @var DOMElement $candidate */ |
|
148 | - foreach ($candidates as $candidate) { |
|
149 | - // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]); |
|
150 | - if ($this->matchesSelector($candidate, $selectorGroup)) { |
|
151 | - // $this->debug('Attaching ' . $candidate->nodeName); |
|
152 | - $found->attach($candidate); |
|
153 | - } |
|
154 | - } |
|
155 | - } |
|
156 | - $this->setMatches($found); |
|
157 | - |
|
158 | - return $this; |
|
159 | - } |
|
160 | - |
|
161 | - public function matches() |
|
162 | - { |
|
163 | - return $this->matches; |
|
164 | - } |
|
165 | - |
|
166 | - /** |
|
167 | - * Check whether the given node matches the given selector. |
|
168 | - * |
|
169 | - * A selector is a group of one or more simple selectors combined |
|
170 | - * by combinators. This determines if a given selector |
|
171 | - * matches the given node. |
|
172 | - * |
|
173 | - * @attention |
|
174 | - * Evaluation of selectors is done recursively. Thus the length |
|
175 | - * of the selector is limited to the recursion depth allowed by |
|
176 | - * the PHP configuration. This should only cause problems for |
|
177 | - * absolutely huge selectors or for versions of PHP tuned to |
|
178 | - * strictly limit recursion depth. |
|
179 | - * |
|
180 | - * @param DOMElement $node |
|
181 | - * The DOMNode to check. |
|
182 | - * @param $selector |
|
183 | - * |
|
184 | - * @return boolean |
|
185 | - * A boolean TRUE if the node matches, false otherwise. |
|
186 | - */ |
|
187 | - public function matchesSelector(DOMElement $node, $selector) |
|
188 | - { |
|
189 | - return $this->matchesSimpleSelector($node, $selector, 0); |
|
190 | - } |
|
191 | - |
|
192 | - /** |
|
193 | - * Performs a match check on a SimpleSelector. |
|
194 | - * |
|
195 | - * Where matchesSelector() does a check on an entire selector, |
|
196 | - * this checks only a simple selector (plus an optional |
|
197 | - * combinator). |
|
198 | - * |
|
199 | - * @param DOMElement $node |
|
200 | - * @param $selectors |
|
201 | - * @param $index |
|
202 | - * |
|
203 | - * @return boolean |
|
204 | - * A boolean TRUE if the node matches, false otherwise. |
|
205 | - * @throws NotImplementedException |
|
206 | - */ |
|
207 | - public function matchesSimpleSelector(DOMElement $node, $selectors, $index) |
|
208 | - { |
|
209 | - $selector = $selectors[$index]; |
|
210 | - // Note that this will short circuit as soon as one of these |
|
211 | - // returns FALSE. |
|
212 | - $result = $this->matchElement($node, $selector->element, $selector->ns) |
|
213 | - && $this->matchAttributes($node, $selector->attributes) |
|
214 | - && $this->matchId($node, $selector->id) |
|
215 | - && $this->matchClasses($node, $selector->classes) |
|
216 | - && $this->matchPseudoClasses($node, $selector->pseudoClasses) |
|
217 | - && $this->matchPseudoElements($node, $selector->pseudoElements); |
|
218 | - |
|
219 | - $isNextRule = isset($selectors[++$index]); |
|
220 | - // If there is another selector, we process that if there a match |
|
221 | - // hasn't been found. |
|
222 | - /* |
|
109 | + } |
|
110 | + |
|
111 | + public function debug($msg) |
|
112 | + { |
|
113 | + fwrite(STDOUT, PHP_EOL . $msg); |
|
114 | + } |
|
115 | + |
|
116 | + /** |
|
117 | + * Given a selector, find the matches in the given DOM. |
|
118 | + * |
|
119 | + * This is the main function for querying the DOM using a CSS |
|
120 | + * selector. |
|
121 | + * |
|
122 | + * @param string $selector |
|
123 | + * The selector. |
|
124 | + * |
|
125 | + * @return DOMTraverser a list of matched |
|
126 | + * DOMNode objects. |
|
127 | + * @throws ParseException |
|
128 | + */ |
|
129 | + public function find($selector): DOMTraverser |
|
130 | + { |
|
131 | + // Setup |
|
132 | + $handler = new Selector(); |
|
133 | + $parser = new Parser($selector, $handler); |
|
134 | + $parser->parse(); |
|
135 | + $this->selector = $handler; |
|
136 | + |
|
137 | + //$selector = $handler->toArray(); |
|
138 | + $found = $this->newMatches(); |
|
139 | + foreach ($handler as $selectorGroup) { |
|
140 | + // Initialize matches if necessary. |
|
141 | + if ($this->initialized) { |
|
142 | + $candidates = $this->matches; |
|
143 | + } else { |
|
144 | + $candidates = $this->initialMatch($selectorGroup[0], $this->matches); |
|
145 | + } |
|
146 | + |
|
147 | + /** @var DOMElement $candidate */ |
|
148 | + foreach ($candidates as $candidate) { |
|
149 | + // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]); |
|
150 | + if ($this->matchesSelector($candidate, $selectorGroup)) { |
|
151 | + // $this->debug('Attaching ' . $candidate->nodeName); |
|
152 | + $found->attach($candidate); |
|
153 | + } |
|
154 | + } |
|
155 | + } |
|
156 | + $this->setMatches($found); |
|
157 | + |
|
158 | + return $this; |
|
159 | + } |
|
160 | + |
|
161 | + public function matches() |
|
162 | + { |
|
163 | + return $this->matches; |
|
164 | + } |
|
165 | + |
|
166 | + /** |
|
167 | + * Check whether the given node matches the given selector. |
|
168 | + * |
|
169 | + * A selector is a group of one or more simple selectors combined |
|
170 | + * by combinators. This determines if a given selector |
|
171 | + * matches the given node. |
|
172 | + * |
|
173 | + * @attention |
|
174 | + * Evaluation of selectors is done recursively. Thus the length |
|
175 | + * of the selector is limited to the recursion depth allowed by |
|
176 | + * the PHP configuration. This should only cause problems for |
|
177 | + * absolutely huge selectors or for versions of PHP tuned to |
|
178 | + * strictly limit recursion depth. |
|
179 | + * |
|
180 | + * @param DOMElement $node |
|
181 | + * The DOMNode to check. |
|
182 | + * @param $selector |
|
183 | + * |
|
184 | + * @return boolean |
|
185 | + * A boolean TRUE if the node matches, false otherwise. |
|
186 | + */ |
|
187 | + public function matchesSelector(DOMElement $node, $selector) |
|
188 | + { |
|
189 | + return $this->matchesSimpleSelector($node, $selector, 0); |
|
190 | + } |
|
191 | + |
|
192 | + /** |
|
193 | + * Performs a match check on a SimpleSelector. |
|
194 | + * |
|
195 | + * Where matchesSelector() does a check on an entire selector, |
|
196 | + * this checks only a simple selector (plus an optional |
|
197 | + * combinator). |
|
198 | + * |
|
199 | + * @param DOMElement $node |
|
200 | + * @param $selectors |
|
201 | + * @param $index |
|
202 | + * |
|
203 | + * @return boolean |
|
204 | + * A boolean TRUE if the node matches, false otherwise. |
|
205 | + * @throws NotImplementedException |
|
206 | + */ |
|
207 | + public function matchesSimpleSelector(DOMElement $node, $selectors, $index) |
|
208 | + { |
|
209 | + $selector = $selectors[$index]; |
|
210 | + // Note that this will short circuit as soon as one of these |
|
211 | + // returns FALSE. |
|
212 | + $result = $this->matchElement($node, $selector->element, $selector->ns) |
|
213 | + && $this->matchAttributes($node, $selector->attributes) |
|
214 | + && $this->matchId($node, $selector->id) |
|
215 | + && $this->matchClasses($node, $selector->classes) |
|
216 | + && $this->matchPseudoClasses($node, $selector->pseudoClasses) |
|
217 | + && $this->matchPseudoElements($node, $selector->pseudoElements); |
|
218 | + |
|
219 | + $isNextRule = isset($selectors[++$index]); |
|
220 | + // If there is another selector, we process that if there a match |
|
221 | + // hasn't been found. |
|
222 | + /* |
|
223 | 223 | if ($isNextRule && $selectors[$index]->combinator == SimpleSelector::anotherSelector) { |
224 | 224 | // We may need to re-initialize the match set for the next selector. |
225 | 225 | if (!$this->initialized) { |
@@ -231,457 +231,457 @@ discard block |
||
231 | 231 | // If we have a match and we have a combinator, we need to |
232 | 232 | // recurse up the tree. |
233 | 233 | else*/ |
234 | - if ($isNextRule && $result) { |
|
235 | - $result = $this->combine($node, $selectors, $index); |
|
236 | - } |
|
237 | - |
|
238 | - return $result; |
|
239 | - } |
|
240 | - |
|
241 | - /** |
|
242 | - * Combine the next selector with the given match |
|
243 | - * using the next combinator. |
|
244 | - * |
|
245 | - * If the next selector is combined with another |
|
246 | - * selector, that will be evaluated too, and so on. |
|
247 | - * So if this function returns TRUE, it means that all |
|
248 | - * child selectors are also matches. |
|
249 | - * |
|
250 | - * @param DOMNode $node |
|
251 | - * The DOMNode to test. |
|
252 | - * @param array $selectors |
|
253 | - * The array of simple selectors. |
|
254 | - * @param int $index |
|
255 | - * The index of the current selector. |
|
256 | - * |
|
257 | - * @return boolean |
|
258 | - * TRUE if the next selector(s) match. |
|
259 | - */ |
|
260 | - public function combine(DOMElement $node, $selectors, $index) |
|
261 | - { |
|
262 | - $selector = $selectors[$index]; |
|
263 | - //$this->debug(implode(' ', $selectors)); |
|
264 | - switch ($selector->combinator) { |
|
265 | - case SimpleSelector::ADJACENT: |
|
266 | - return $this->combineAdjacent($node, $selectors, $index); |
|
267 | - case SimpleSelector::SIBLING: |
|
268 | - return $this->combineSibling($node, $selectors, $index); |
|
269 | - case SimpleSelector::DIRECT_DESCENDANT: |
|
270 | - return $this->combineDirectDescendant($node, $selectors, $index); |
|
271 | - case SimpleSelector::ANY_DESCENDANT: |
|
272 | - return $this->combineAnyDescendant($node, $selectors, $index); |
|
273 | - case SimpleSelector::ANOTHER_SELECTOR: |
|
274 | - // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]); |
|
275 | - return $this->matchesSimpleSelector($node, $selectors, $index); |
|
276 | - } |
|
277 | - |
|
278 | - return false; |
|
279 | - } |
|
280 | - |
|
281 | - /** |
|
282 | - * Process an Adjacent Sibling. |
|
283 | - * |
|
284 | - * The spec does not indicate whether Adjacent should ignore non-Element |
|
285 | - * nodes, so we choose to ignore them. |
|
286 | - * |
|
287 | - * @param DOMNode $node |
|
288 | - * A DOM Node. |
|
289 | - * @param array $selectors |
|
290 | - * The selectors array. |
|
291 | - * @param int $index |
|
292 | - * The current index to the operative simple selector in the selectors |
|
293 | - * array. |
|
294 | - * |
|
295 | - * @return boolean |
|
296 | - * TRUE if the combination matches, FALSE otherwise. |
|
297 | - */ |
|
298 | - public function combineAdjacent($node, $selectors, $index) |
|
299 | - { |
|
300 | - while (! empty($node->previousSibling)) { |
|
301 | - $node = $node->previousSibling; |
|
302 | - if ($node->nodeType == XML_ELEMENT_NODE) { |
|
303 | - //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
|
304 | - return $this->matchesSimpleSelector($node, $selectors, $index); |
|
305 | - } |
|
306 | - } |
|
307 | - |
|
308 | - return false; |
|
309 | - } |
|
310 | - |
|
311 | - /** |
|
312 | - * Check all siblings. |
|
313 | - * |
|
314 | - * According to the spec, this only tests elements LEFT of the provided |
|
315 | - * node. |
|
316 | - * |
|
317 | - * @param DOMNode $node |
|
318 | - * A DOM Node. |
|
319 | - * @param array $selectors |
|
320 | - * The selectors array. |
|
321 | - * @param int $index |
|
322 | - * The current index to the operative simple selector in the selectors |
|
323 | - * array. |
|
324 | - * |
|
325 | - * @return boolean |
|
326 | - * TRUE if the combination matches, FALSE otherwise. |
|
327 | - */ |
|
328 | - public function combineSibling($node, $selectors, $index) |
|
329 | - { |
|
330 | - while (! empty($node->previousSibling)) { |
|
331 | - $node = $node->previousSibling; |
|
332 | - if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
|
333 | - return true; |
|
334 | - } |
|
335 | - } |
|
336 | - |
|
337 | - return false; |
|
338 | - } |
|
339 | - |
|
340 | - /** |
|
341 | - * Handle a Direct Descendant combination. |
|
342 | - * |
|
343 | - * Check whether the given node is a rightly-related descendant |
|
344 | - * of its parent node. |
|
345 | - * |
|
346 | - * @param DOMNode $node |
|
347 | - * A DOM Node. |
|
348 | - * @param array $selectors |
|
349 | - * The selectors array. |
|
350 | - * @param int $index |
|
351 | - * The current index to the operative simple selector in the selectors |
|
352 | - * array. |
|
353 | - * |
|
354 | - * @return boolean |
|
355 | - * TRUE if the combination matches, FALSE otherwise. |
|
356 | - */ |
|
357 | - public function combineDirectDescendant($node, $selectors, $index) |
|
358 | - { |
|
359 | - $parent = $node->parentNode; |
|
360 | - if (empty($parent)) { |
|
361 | - return false; |
|
362 | - } |
|
363 | - |
|
364 | - return $this->matchesSimpleSelector($parent, $selectors, $index); |
|
365 | - } |
|
366 | - |
|
367 | - /** |
|
368 | - * Handle Any Descendant combinations. |
|
369 | - * |
|
370 | - * This checks to see if there are any matching routes from the |
|
371 | - * selector beginning at the present node. |
|
372 | - * |
|
373 | - * @param DOMNode $node |
|
374 | - * A DOM Node. |
|
375 | - * @param array $selectors |
|
376 | - * The selectors array. |
|
377 | - * @param int $index |
|
378 | - * The current index to the operative simple selector in the selectors |
|
379 | - * array. |
|
380 | - * |
|
381 | - * @return boolean |
|
382 | - * TRUE if the combination matches, FALSE otherwise. |
|
383 | - */ |
|
384 | - public function combineAnyDescendant($node, $selectors, $index) |
|
385 | - { |
|
386 | - while (! empty($node->parentNode)) { |
|
387 | - $node = $node->parentNode; |
|
388 | - |
|
389 | - // Catch case where element is child of something |
|
390 | - // else. This should really only happen with a |
|
391 | - // document element. |
|
392 | - if ($node->nodeType != XML_ELEMENT_NODE) { |
|
393 | - continue; |
|
394 | - } |
|
395 | - |
|
396 | - if ($this->matchesSimpleSelector($node, $selectors, $index)) { |
|
397 | - return true; |
|
398 | - } |
|
399 | - } |
|
400 | - } |
|
401 | - |
|
402 | - /** |
|
403 | - * Get the intial match set. |
|
404 | - * |
|
405 | - * This should only be executed when not working with |
|
406 | - * an existing match set. |
|
407 | - * |
|
408 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
409 | - * @param SplObjectStorage $matches |
|
410 | - * |
|
411 | - * @return SplObjectStorage |
|
412 | - */ |
|
413 | - protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
414 | - { |
|
415 | - $element = $selector->element; |
|
416 | - |
|
417 | - // If no element is specified, we have to start with the |
|
418 | - // entire document. |
|
419 | - if ($element === null) { |
|
420 | - $element = '*'; |
|
421 | - } |
|
422 | - |
|
423 | - // We try to do some optimization here to reduce the |
|
424 | - // number of matches to the bare minimum. This will |
|
425 | - // reduce the subsequent number of operations that |
|
426 | - // must be performed in the query. |
|
427 | - |
|
428 | - // Experimental: ID queries use XPath to match, since |
|
429 | - // this should give us only a single matched element |
|
430 | - // to work with. |
|
431 | - if (/*$element == '*' &&*/ |
|
432 | - ! empty($selector->id)) { |
|
433 | - $initialMatches = $this->initialMatchOnID($selector, $matches); |
|
434 | - } // If a namespace is set, find the namespace matches. |
|
435 | - elseif (! empty($selector->ns)) { |
|
436 | - $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
|
437 | - } |
|
438 | - // If the element is a wildcard, using class can |
|
439 | - // substantially reduce the number of elements that |
|
440 | - // we start with. |
|
441 | - elseif ($element === '*' && ! empty($selector->classes)) { |
|
442 | - $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
|
443 | - } else { |
|
444 | - $initialMatches = $this->initialMatchOnElement($selector, $matches); |
|
445 | - } |
|
446 | - |
|
447 | - return $initialMatches; |
|
448 | - } |
|
449 | - |
|
450 | - /** |
|
451 | - * Shortcut for finding initial match by ID. |
|
452 | - * |
|
453 | - * If the element is set to '*' and an ID is |
|
454 | - * set, then this should be used to find by ID, |
|
455 | - * which will drastically reduce the amount of |
|
456 | - * comparison operations done in PHP. |
|
457 | - * |
|
458 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
459 | - * @param SplObjectStorage $matches |
|
460 | - * |
|
461 | - * @return SplObjectStorage |
|
462 | - */ |
|
463 | - protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
464 | - { |
|
465 | - $id = $selector->id; |
|
466 | - $found = $this->newMatches(); |
|
467 | - |
|
468 | - // Issue #145: DOMXPath will through an exception if the DOM is |
|
469 | - // not set. |
|
470 | - if (! ($this->dom instanceof DOMDocument)) { |
|
471 | - return $found; |
|
472 | - } |
|
473 | - $baseQuery = ".//*[@id='{$id}']"; |
|
474 | - $xpath = new DOMXPath($this->dom); |
|
475 | - |
|
476 | - // Now we try to find any matching IDs. |
|
477 | - /** @var DOMElement $node */ |
|
478 | - foreach ($matches as $node) { |
|
479 | - if ($node->getAttribute('id') === $id) { |
|
480 | - $found->attach($node); |
|
481 | - } |
|
482 | - |
|
483 | - $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
484 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
485 | - $this->attachNodeList($nl, $found); |
|
486 | - } |
|
487 | - } |
|
488 | - // Unset the ID selector. |
|
489 | - $selector->id = null; |
|
490 | - |
|
491 | - return $found; |
|
492 | - } |
|
493 | - |
|
494 | - /** |
|
495 | - * Shortcut for setting the intial match. |
|
496 | - * |
|
497 | - * This shortcut should only be used when the initial |
|
498 | - * element is '*' and there are classes set. |
|
499 | - * |
|
500 | - * In any other case, the element finding algo is |
|
501 | - * faster and should be used instead. |
|
502 | - * |
|
503 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
504 | - * @param $matches |
|
505 | - * |
|
506 | - * @return SplObjectStorage |
|
507 | - */ |
|
508 | - protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
509 | - { |
|
510 | - $found = $this->newMatches(); |
|
511 | - |
|
512 | - // Issue #145: DOMXPath will through an exception if the DOM is |
|
513 | - // not set. |
|
514 | - if (! ($this->dom instanceof DOMDocument)) { |
|
515 | - return $found; |
|
516 | - } |
|
517 | - $baseQuery = './/*[@class]'; |
|
518 | - $xpath = new DOMXPath($this->dom); |
|
519 | - |
|
520 | - // Now we try to find any matching IDs. |
|
521 | - /** @var DOMElement $node */ |
|
522 | - foreach ($matches as $node) { |
|
523 | - // Refactor me! |
|
524 | - if ($node->hasAttribute('class')) { |
|
525 | - $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class'))); |
|
526 | - if (count($intersect) === count($selector->classes)) { |
|
527 | - $found->attach($node); |
|
528 | - } |
|
529 | - } |
|
530 | - |
|
531 | - $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
532 | - /** @var DOMElement $subNode */ |
|
533 | - foreach ($nl as $subNode) { |
|
534 | - $classes = $subNode->getAttribute('class'); |
|
535 | - $classArray = explode(' ', $classes); |
|
536 | - |
|
537 | - $intersect = array_intersect($selector->classes, $classArray); |
|
538 | - if (count($intersect) === count($selector->classes)) { |
|
539 | - $found->attach($subNode); |
|
540 | - } |
|
541 | - } |
|
542 | - } |
|
543 | - |
|
544 | - // Unset the classes selector. |
|
545 | - $selector->classes = []; |
|
546 | - |
|
547 | - return $found; |
|
548 | - } |
|
549 | - |
|
550 | - /** |
|
551 | - * Internal xpath query. |
|
552 | - * |
|
553 | - * This is optimized for very specific use, and is not a general |
|
554 | - * purpose function. |
|
555 | - * |
|
556 | - * @param DOMXPath $xpath |
|
557 | - * @param DOMElement $node |
|
558 | - * @param string $query |
|
559 | - * |
|
560 | - * @return DOMNodeList |
|
561 | - */ |
|
562 | - private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList |
|
563 | - { |
|
564 | - // This works around a bug in which the document element |
|
565 | - // does not correctly search with the $baseQuery. |
|
566 | - if ($node->isSameNode($this->dom->documentElement)) { |
|
567 | - $query = mb_substr($query, 1); |
|
568 | - } |
|
569 | - |
|
570 | - return $xpath->query($query, $node); |
|
571 | - } |
|
572 | - |
|
573 | - /** |
|
574 | - * Shortcut for setting the initial match. |
|
575 | - * |
|
576 | - * @param $selector |
|
577 | - * @param $matches |
|
578 | - * |
|
579 | - * @return SplObjectStorage |
|
580 | - */ |
|
581 | - protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
582 | - { |
|
583 | - $element = $selector->element; |
|
584 | - if (null === $element) { |
|
585 | - $element = '*'; |
|
586 | - } |
|
587 | - $found = $this->newMatches(); |
|
588 | - /** @var DOMDocument $node */ |
|
589 | - foreach ($matches as $node) { |
|
590 | - // Capture the case where the initial element is the root element. |
|
591 | - if ($node->tagName === $element |
|
592 | - || ($element === '*' && $node->parentNode instanceof DOMDocument)) { |
|
593 | - $found->attach($node); |
|
594 | - } |
|
595 | - $nl = $node->getElementsByTagName($element); |
|
596 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
597 | - $this->attachNodeList($nl, $found); |
|
598 | - } |
|
599 | - } |
|
600 | - |
|
601 | - $selector->element = null; |
|
602 | - |
|
603 | - return $found; |
|
604 | - } |
|
605 | - |
|
606 | - /** |
|
607 | - * Get elements and filter by namespace. |
|
608 | - * |
|
609 | - * @param \QueryPath\CSS\SimpleSelector $selector |
|
610 | - * @param SplObjectStorage $matches |
|
611 | - * |
|
612 | - * @return SplObjectStorage |
|
613 | - */ |
|
614 | - protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
615 | - { |
|
616 | - $ns = $selector->ns; |
|
617 | - |
|
618 | - $elements = $this->initialMatchOnElement($selector, $matches); |
|
619 | - |
|
620 | - // "any namespace" matches anything. |
|
621 | - if ($ns === '*') { |
|
622 | - return $elements; |
|
623 | - } |
|
624 | - |
|
625 | - // Loop through and make a list of items that need to be filtered |
|
626 | - // out, then filter them. This is required b/c ObjectStorage iterates |
|
627 | - // wrongly when an item is detached in an access loop. |
|
628 | - $detach = []; |
|
629 | - foreach ($elements as $node) { |
|
630 | - // This lookup must be done PER NODE. |
|
631 | - $nsuri = $node->lookupNamespaceURI($ns); |
|
632 | - if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
633 | - $detach[] = $node; |
|
634 | - } |
|
635 | - } |
|
636 | - foreach ($detach as $rem) { |
|
637 | - $elements->detach($rem); |
|
638 | - } |
|
639 | - $selector->ns = null; |
|
640 | - |
|
641 | - return $elements; |
|
642 | - } |
|
643 | - |
|
644 | - /** |
|
645 | - * Checks to see if the DOMNode matches the given element selector. |
|
646 | - * |
|
647 | - * This handles the following cases: |
|
648 | - * |
|
649 | - * - element (foo) |
|
650 | - * - namespaced element (ns|foo) |
|
651 | - * - namespaced wildcard (ns|*) |
|
652 | - * - wildcard (* or *|*) |
|
653 | - * |
|
654 | - * @param DOMElement $node |
|
655 | - * @param $element |
|
656 | - * @param null $ns |
|
657 | - * |
|
658 | - * @return bool |
|
659 | - */ |
|
660 | - protected function matchElement(DOMElement $node, $element, $ns = null): bool |
|
661 | - { |
|
662 | - if (empty($element)) { |
|
663 | - return true; |
|
664 | - } |
|
665 | - |
|
666 | - // Handle namespace. |
|
667 | - if (! empty($ns) && $ns !== '*') { |
|
668 | - // Check whether we have a matching NS URI. |
|
669 | - $nsuri = $node->lookupNamespaceURI($ns); |
|
670 | - if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
671 | - return false; |
|
672 | - } |
|
673 | - } |
|
674 | - |
|
675 | - // Compare local name to given element name. |
|
676 | - return $element === '*' || $node->localName === $element; |
|
677 | - } |
|
678 | - |
|
679 | - /** |
|
680 | - * Checks to see if the given DOMNode matches an "any element" (*). |
|
681 | - * |
|
682 | - * This does not handle namespaced whildcards. |
|
683 | - */ |
|
684 | - /* |
|
234 | + if ($isNextRule && $result) { |
|
235 | + $result = $this->combine($node, $selectors, $index); |
|
236 | + } |
|
237 | + |
|
238 | + return $result; |
|
239 | + } |
|
240 | + |
|
241 | + /** |
|
242 | + * Combine the next selector with the given match |
|
243 | + * using the next combinator. |
|
244 | + * |
|
245 | + * If the next selector is combined with another |
|
246 | + * selector, that will be evaluated too, and so on. |
|
247 | + * So if this function returns TRUE, it means that all |
|
248 | + * child selectors are also matches. |
|
249 | + * |
|
250 | + * @param DOMNode $node |
|
251 | + * The DOMNode to test. |
|
252 | + * @param array $selectors |
|
253 | + * The array of simple selectors. |
|
254 | + * @param int $index |
|
255 | + * The index of the current selector. |
|
256 | + * |
|
257 | + * @return boolean |
|
258 | + * TRUE if the next selector(s) match. |
|
259 | + */ |
|
260 | + public function combine(DOMElement $node, $selectors, $index) |
|
261 | + { |
|
262 | + $selector = $selectors[$index]; |
|
263 | + //$this->debug(implode(' ', $selectors)); |
|
264 | + switch ($selector->combinator) { |
|
265 | + case SimpleSelector::ADJACENT: |
|
266 | + return $this->combineAdjacent($node, $selectors, $index); |
|
267 | + case SimpleSelector::SIBLING: |
|
268 | + return $this->combineSibling($node, $selectors, $index); |
|
269 | + case SimpleSelector::DIRECT_DESCENDANT: |
|
270 | + return $this->combineDirectDescendant($node, $selectors, $index); |
|
271 | + case SimpleSelector::ANY_DESCENDANT: |
|
272 | + return $this->combineAnyDescendant($node, $selectors, $index); |
|
273 | + case SimpleSelector::ANOTHER_SELECTOR: |
|
274 | + // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]); |
|
275 | + return $this->matchesSimpleSelector($node, $selectors, $index); |
|
276 | + } |
|
277 | + |
|
278 | + return false; |
|
279 | + } |
|
280 | + |
|
281 | + /** |
|
282 | + * Process an Adjacent Sibling. |
|
283 | + * |
|
284 | + * The spec does not indicate whether Adjacent should ignore non-Element |
|
285 | + * nodes, so we choose to ignore them. |
|
286 | + * |
|
287 | + * @param DOMNode $node |
|
288 | + * A DOM Node. |
|
289 | + * @param array $selectors |
|
290 | + * The selectors array. |
|
291 | + * @param int $index |
|
292 | + * The current index to the operative simple selector in the selectors |
|
293 | + * array. |
|
294 | + * |
|
295 | + * @return boolean |
|
296 | + * TRUE if the combination matches, FALSE otherwise. |
|
297 | + */ |
|
298 | + public function combineAdjacent($node, $selectors, $index) |
|
299 | + { |
|
300 | + while (! empty($node->previousSibling)) { |
|
301 | + $node = $node->previousSibling; |
|
302 | + if ($node->nodeType == XML_ELEMENT_NODE) { |
|
303 | + //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
|
304 | + return $this->matchesSimpleSelector($node, $selectors, $index); |
|
305 | + } |
|
306 | + } |
|
307 | + |
|
308 | + return false; |
|
309 | + } |
|
310 | + |
|
311 | + /** |
|
312 | + * Check all siblings. |
|
313 | + * |
|
314 | + * According to the spec, this only tests elements LEFT of the provided |
|
315 | + * node. |
|
316 | + * |
|
317 | + * @param DOMNode $node |
|
318 | + * A DOM Node. |
|
319 | + * @param array $selectors |
|
320 | + * The selectors array. |
|
321 | + * @param int $index |
|
322 | + * The current index to the operative simple selector in the selectors |
|
323 | + * array. |
|
324 | + * |
|
325 | + * @return boolean |
|
326 | + * TRUE if the combination matches, FALSE otherwise. |
|
327 | + */ |
|
328 | + public function combineSibling($node, $selectors, $index) |
|
329 | + { |
|
330 | + while (! empty($node->previousSibling)) { |
|
331 | + $node = $node->previousSibling; |
|
332 | + if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
|
333 | + return true; |
|
334 | + } |
|
335 | + } |
|
336 | + |
|
337 | + return false; |
|
338 | + } |
|
339 | + |
|
340 | + /** |
|
341 | + * Handle a Direct Descendant combination. |
|
342 | + * |
|
343 | + * Check whether the given node is a rightly-related descendant |
|
344 | + * of its parent node. |
|
345 | + * |
|
346 | + * @param DOMNode $node |
|
347 | + * A DOM Node. |
|
348 | + * @param array $selectors |
|
349 | + * The selectors array. |
|
350 | + * @param int $index |
|
351 | + * The current index to the operative simple selector in the selectors |
|
352 | + * array. |
|
353 | + * |
|
354 | + * @return boolean |
|
355 | + * TRUE if the combination matches, FALSE otherwise. |
|
356 | + */ |
|
357 | + public function combineDirectDescendant($node, $selectors, $index) |
|
358 | + { |
|
359 | + $parent = $node->parentNode; |
|
360 | + if (empty($parent)) { |
|
361 | + return false; |
|
362 | + } |
|
363 | + |
|
364 | + return $this->matchesSimpleSelector($parent, $selectors, $index); |
|
365 | + } |
|
366 | + |
|
367 | + /** |
|
368 | + * Handle Any Descendant combinations. |
|
369 | + * |
|
370 | + * This checks to see if there are any matching routes from the |
|
371 | + * selector beginning at the present node. |
|
372 | + * |
|
373 | + * @param DOMNode $node |
|
374 | + * A DOM Node. |
|
375 | + * @param array $selectors |
|
376 | + * The selectors array. |
|
377 | + * @param int $index |
|
378 | + * The current index to the operative simple selector in the selectors |
|
379 | + * array. |
|
380 | + * |
|
381 | + * @return boolean |
|
382 | + * TRUE if the combination matches, FALSE otherwise. |
|
383 | + */ |
|
384 | + public function combineAnyDescendant($node, $selectors, $index) |
|
385 | + { |
|
386 | + while (! empty($node->parentNode)) { |
|
387 | + $node = $node->parentNode; |
|
388 | + |
|
389 | + // Catch case where element is child of something |
|
390 | + // else. This should really only happen with a |
|
391 | + // document element. |
|
392 | + if ($node->nodeType != XML_ELEMENT_NODE) { |
|
393 | + continue; |
|
394 | + } |
|
395 | + |
|
396 | + if ($this->matchesSimpleSelector($node, $selectors, $index)) { |
|
397 | + return true; |
|
398 | + } |
|
399 | + } |
|
400 | + } |
|
401 | + |
|
402 | + /** |
|
403 | + * Get the intial match set. |
|
404 | + * |
|
405 | + * This should only be executed when not working with |
|
406 | + * an existing match set. |
|
407 | + * |
|
408 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
409 | + * @param SplObjectStorage $matches |
|
410 | + * |
|
411 | + * @return SplObjectStorage |
|
412 | + */ |
|
413 | + protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
414 | + { |
|
415 | + $element = $selector->element; |
|
416 | + |
|
417 | + // If no element is specified, we have to start with the |
|
418 | + // entire document. |
|
419 | + if ($element === null) { |
|
420 | + $element = '*'; |
|
421 | + } |
|
422 | + |
|
423 | + // We try to do some optimization here to reduce the |
|
424 | + // number of matches to the bare minimum. This will |
|
425 | + // reduce the subsequent number of operations that |
|
426 | + // must be performed in the query. |
|
427 | + |
|
428 | + // Experimental: ID queries use XPath to match, since |
|
429 | + // this should give us only a single matched element |
|
430 | + // to work with. |
|
431 | + if (/*$element == '*' &&*/ |
|
432 | + ! empty($selector->id)) { |
|
433 | + $initialMatches = $this->initialMatchOnID($selector, $matches); |
|
434 | + } // If a namespace is set, find the namespace matches. |
|
435 | + elseif (! empty($selector->ns)) { |
|
436 | + $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
|
437 | + } |
|
438 | + // If the element is a wildcard, using class can |
|
439 | + // substantially reduce the number of elements that |
|
440 | + // we start with. |
|
441 | + elseif ($element === '*' && ! empty($selector->classes)) { |
|
442 | + $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
|
443 | + } else { |
|
444 | + $initialMatches = $this->initialMatchOnElement($selector, $matches); |
|
445 | + } |
|
446 | + |
|
447 | + return $initialMatches; |
|
448 | + } |
|
449 | + |
|
450 | + /** |
|
451 | + * Shortcut for finding initial match by ID. |
|
452 | + * |
|
453 | + * If the element is set to '*' and an ID is |
|
454 | + * set, then this should be used to find by ID, |
|
455 | + * which will drastically reduce the amount of |
|
456 | + * comparison operations done in PHP. |
|
457 | + * |
|
458 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
459 | + * @param SplObjectStorage $matches |
|
460 | + * |
|
461 | + * @return SplObjectStorage |
|
462 | + */ |
|
463 | + protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
464 | + { |
|
465 | + $id = $selector->id; |
|
466 | + $found = $this->newMatches(); |
|
467 | + |
|
468 | + // Issue #145: DOMXPath will through an exception if the DOM is |
|
469 | + // not set. |
|
470 | + if (! ($this->dom instanceof DOMDocument)) { |
|
471 | + return $found; |
|
472 | + } |
|
473 | + $baseQuery = ".//*[@id='{$id}']"; |
|
474 | + $xpath = new DOMXPath($this->dom); |
|
475 | + |
|
476 | + // Now we try to find any matching IDs. |
|
477 | + /** @var DOMElement $node */ |
|
478 | + foreach ($matches as $node) { |
|
479 | + if ($node->getAttribute('id') === $id) { |
|
480 | + $found->attach($node); |
|
481 | + } |
|
482 | + |
|
483 | + $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
484 | + if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
485 | + $this->attachNodeList($nl, $found); |
|
486 | + } |
|
487 | + } |
|
488 | + // Unset the ID selector. |
|
489 | + $selector->id = null; |
|
490 | + |
|
491 | + return $found; |
|
492 | + } |
|
493 | + |
|
494 | + /** |
|
495 | + * Shortcut for setting the intial match. |
|
496 | + * |
|
497 | + * This shortcut should only be used when the initial |
|
498 | + * element is '*' and there are classes set. |
|
499 | + * |
|
500 | + * In any other case, the element finding algo is |
|
501 | + * faster and should be used instead. |
|
502 | + * |
|
503 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
504 | + * @param $matches |
|
505 | + * |
|
506 | + * @return SplObjectStorage |
|
507 | + */ |
|
508 | + protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
509 | + { |
|
510 | + $found = $this->newMatches(); |
|
511 | + |
|
512 | + // Issue #145: DOMXPath will through an exception if the DOM is |
|
513 | + // not set. |
|
514 | + if (! ($this->dom instanceof DOMDocument)) { |
|
515 | + return $found; |
|
516 | + } |
|
517 | + $baseQuery = './/*[@class]'; |
|
518 | + $xpath = new DOMXPath($this->dom); |
|
519 | + |
|
520 | + // Now we try to find any matching IDs. |
|
521 | + /** @var DOMElement $node */ |
|
522 | + foreach ($matches as $node) { |
|
523 | + // Refactor me! |
|
524 | + if ($node->hasAttribute('class')) { |
|
525 | + $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class'))); |
|
526 | + if (count($intersect) === count($selector->classes)) { |
|
527 | + $found->attach($node); |
|
528 | + } |
|
529 | + } |
|
530 | + |
|
531 | + $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
|
532 | + /** @var DOMElement $subNode */ |
|
533 | + foreach ($nl as $subNode) { |
|
534 | + $classes = $subNode->getAttribute('class'); |
|
535 | + $classArray = explode(' ', $classes); |
|
536 | + |
|
537 | + $intersect = array_intersect($selector->classes, $classArray); |
|
538 | + if (count($intersect) === count($selector->classes)) { |
|
539 | + $found->attach($subNode); |
|
540 | + } |
|
541 | + } |
|
542 | + } |
|
543 | + |
|
544 | + // Unset the classes selector. |
|
545 | + $selector->classes = []; |
|
546 | + |
|
547 | + return $found; |
|
548 | + } |
|
549 | + |
|
550 | + /** |
|
551 | + * Internal xpath query. |
|
552 | + * |
|
553 | + * This is optimized for very specific use, and is not a general |
|
554 | + * purpose function. |
|
555 | + * |
|
556 | + * @param DOMXPath $xpath |
|
557 | + * @param DOMElement $node |
|
558 | + * @param string $query |
|
559 | + * |
|
560 | + * @return DOMNodeList |
|
561 | + */ |
|
562 | + private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList |
|
563 | + { |
|
564 | + // This works around a bug in which the document element |
|
565 | + // does not correctly search with the $baseQuery. |
|
566 | + if ($node->isSameNode($this->dom->documentElement)) { |
|
567 | + $query = mb_substr($query, 1); |
|
568 | + } |
|
569 | + |
|
570 | + return $xpath->query($query, $node); |
|
571 | + } |
|
572 | + |
|
573 | + /** |
|
574 | + * Shortcut for setting the initial match. |
|
575 | + * |
|
576 | + * @param $selector |
|
577 | + * @param $matches |
|
578 | + * |
|
579 | + * @return SplObjectStorage |
|
580 | + */ |
|
581 | + protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
582 | + { |
|
583 | + $element = $selector->element; |
|
584 | + if (null === $element) { |
|
585 | + $element = '*'; |
|
586 | + } |
|
587 | + $found = $this->newMatches(); |
|
588 | + /** @var DOMDocument $node */ |
|
589 | + foreach ($matches as $node) { |
|
590 | + // Capture the case where the initial element is the root element. |
|
591 | + if ($node->tagName === $element |
|
592 | + || ($element === '*' && $node->parentNode instanceof DOMDocument)) { |
|
593 | + $found->attach($node); |
|
594 | + } |
|
595 | + $nl = $node->getElementsByTagName($element); |
|
596 | + if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
597 | + $this->attachNodeList($nl, $found); |
|
598 | + } |
|
599 | + } |
|
600 | + |
|
601 | + $selector->element = null; |
|
602 | + |
|
603 | + return $found; |
|
604 | + } |
|
605 | + |
|
606 | + /** |
|
607 | + * Get elements and filter by namespace. |
|
608 | + * |
|
609 | + * @param \QueryPath\CSS\SimpleSelector $selector |
|
610 | + * @param SplObjectStorage $matches |
|
611 | + * |
|
612 | + * @return SplObjectStorage |
|
613 | + */ |
|
614 | + protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage |
|
615 | + { |
|
616 | + $ns = $selector->ns; |
|
617 | + |
|
618 | + $elements = $this->initialMatchOnElement($selector, $matches); |
|
619 | + |
|
620 | + // "any namespace" matches anything. |
|
621 | + if ($ns === '*') { |
|
622 | + return $elements; |
|
623 | + } |
|
624 | + |
|
625 | + // Loop through and make a list of items that need to be filtered |
|
626 | + // out, then filter them. This is required b/c ObjectStorage iterates |
|
627 | + // wrongly when an item is detached in an access loop. |
|
628 | + $detach = []; |
|
629 | + foreach ($elements as $node) { |
|
630 | + // This lookup must be done PER NODE. |
|
631 | + $nsuri = $node->lookupNamespaceURI($ns); |
|
632 | + if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
633 | + $detach[] = $node; |
|
634 | + } |
|
635 | + } |
|
636 | + foreach ($detach as $rem) { |
|
637 | + $elements->detach($rem); |
|
638 | + } |
|
639 | + $selector->ns = null; |
|
640 | + |
|
641 | + return $elements; |
|
642 | + } |
|
643 | + |
|
644 | + /** |
|
645 | + * Checks to see if the DOMNode matches the given element selector. |
|
646 | + * |
|
647 | + * This handles the following cases: |
|
648 | + * |
|
649 | + * - element (foo) |
|
650 | + * - namespaced element (ns|foo) |
|
651 | + * - namespaced wildcard (ns|*) |
|
652 | + * - wildcard (* or *|*) |
|
653 | + * |
|
654 | + * @param DOMElement $node |
|
655 | + * @param $element |
|
656 | + * @param null $ns |
|
657 | + * |
|
658 | + * @return bool |
|
659 | + */ |
|
660 | + protected function matchElement(DOMElement $node, $element, $ns = null): bool |
|
661 | + { |
|
662 | + if (empty($element)) { |
|
663 | + return true; |
|
664 | + } |
|
665 | + |
|
666 | + // Handle namespace. |
|
667 | + if (! empty($ns) && $ns !== '*') { |
|
668 | + // Check whether we have a matching NS URI. |
|
669 | + $nsuri = $node->lookupNamespaceURI($ns); |
|
670 | + if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
|
671 | + return false; |
|
672 | + } |
|
673 | + } |
|
674 | + |
|
675 | + // Compare local name to given element name. |
|
676 | + return $element === '*' || $node->localName === $element; |
|
677 | + } |
|
678 | + |
|
679 | + /** |
|
680 | + * Checks to see if the given DOMNode matches an "any element" (*). |
|
681 | + * |
|
682 | + * This does not handle namespaced whildcards. |
|
683 | + */ |
|
684 | + /* |
|
685 | 685 | protected function matchAnyElement($node) { |
686 | 686 | $ancestors = $this->ancestors($node); |
687 | 687 | |
@@ -689,211 +689,211 @@ discard block |
||
689 | 689 | } |
690 | 690 | */ |
691 | 691 | |
692 | - /** |
|
693 | - * Get a list of ancestors to the present node. |
|
694 | - */ |
|
695 | - protected function ancestors($node) |
|
696 | - { |
|
697 | - $buffer = []; |
|
698 | - $parent = $node; |
|
699 | - while (($parent = $parent->parentNode) !== null) { |
|
700 | - $buffer[] = $parent; |
|
701 | - } |
|
702 | - |
|
703 | - return $buffer; |
|
704 | - } |
|
705 | - |
|
706 | - /** |
|
707 | - * Check to see if DOMNode has all of the given attributes. |
|
708 | - * |
|
709 | - * This can handle namespaced attributes, including namespace |
|
710 | - * wildcards. |
|
711 | - * |
|
712 | - * @param DOMElement $node |
|
713 | - * @param $attributes |
|
714 | - * |
|
715 | - * @return bool |
|
716 | - */ |
|
717 | - protected function matchAttributes(DOMElement $node, $attributes): bool |
|
718 | - { |
|
719 | - if (empty($attributes)) { |
|
720 | - return true; |
|
721 | - } |
|
722 | - |
|
723 | - foreach ($attributes as $attr) { |
|
724 | - $val = isset($attr['value']) ? $attr['value'] : null; |
|
725 | - |
|
726 | - // Namespaced attributes. |
|
727 | - if (isset($attr['ns']) && $attr['ns'] !== '*') { |
|
728 | - $nsuri = $node->lookupNamespaceURI($attr['ns']); |
|
729 | - if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
730 | - return false; |
|
731 | - } |
|
732 | - $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
|
733 | - } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) { |
|
734 | - // Cycle through all of the attributes in the node. Note that |
|
735 | - // these are DOMAttr objects. |
|
736 | - $matches = false; |
|
737 | - $name = $attr['name']; |
|
738 | - foreach ($node->attributes as $attrNode) { |
|
739 | - if ($attrNode->localName === $name) { |
|
740 | - $nsuri = $attrNode->namespaceURI; |
|
741 | - $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']); |
|
742 | - } |
|
743 | - } |
|
744 | - } // No namespace. |
|
745 | - else { |
|
746 | - $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
|
747 | - } |
|
748 | - |
|
749 | - if (! $matches) { |
|
750 | - return false; |
|
751 | - } |
|
752 | - } |
|
753 | - |
|
754 | - return true; |
|
755 | - } |
|
756 | - |
|
757 | - /** |
|
758 | - * Check that the given DOMNode has the given ID. |
|
759 | - * |
|
760 | - * @param DOMElement $node |
|
761 | - * @param $id |
|
762 | - * |
|
763 | - * @return bool |
|
764 | - */ |
|
765 | - protected function matchId(DOMElement $node, $id): bool |
|
766 | - { |
|
767 | - if (empty($id)) { |
|
768 | - return true; |
|
769 | - } |
|
770 | - |
|
771 | - return $node->hasAttribute('id') && $node->getAttribute('id') === $id; |
|
772 | - } |
|
773 | - |
|
774 | - /** |
|
775 | - * Check that the given DOMNode has all of the given classes. |
|
776 | - * |
|
777 | - * @param DOMElement $node |
|
778 | - * @param $classes |
|
779 | - * |
|
780 | - * @return bool |
|
781 | - */ |
|
782 | - protected function matchClasses(DOMElement $node, $classes): bool |
|
783 | - { |
|
784 | - if (empty($classes)) { |
|
785 | - return true; |
|
786 | - } |
|
787 | - |
|
788 | - if (! $node->hasAttribute('class')) { |
|
789 | - return false; |
|
790 | - } |
|
791 | - |
|
792 | - $eleClasses = preg_split('/\s+/', $node->getAttribute('class')); |
|
793 | - if (empty($eleClasses)) { |
|
794 | - return false; |
|
795 | - } |
|
796 | - |
|
797 | - // The intersection should match the given $classes. |
|
798 | - $missing = array_diff($classes, array_intersect($classes, $eleClasses)); |
|
799 | - |
|
800 | - return count($missing) === 0; |
|
801 | - } |
|
802 | - |
|
803 | - /** |
|
804 | - * @param DOMElement $node |
|
805 | - * @param $pseudoClasses |
|
806 | - * |
|
807 | - * @return bool |
|
808 | - * @throws NotImplementedException |
|
809 | - * @throws ParseException |
|
810 | - */ |
|
811 | - protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool |
|
812 | - { |
|
813 | - $ret = true; |
|
814 | - foreach ($pseudoClasses as $pseudoClass) { |
|
815 | - $name = $pseudoClass['name']; |
|
816 | - // Avoid E_STRICT violation. |
|
817 | - $value = $pseudoClass['value'] ?? null; |
|
818 | - $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
819 | - } |
|
820 | - |
|
821 | - return $ret; |
|
822 | - } |
|
823 | - |
|
824 | - /** |
|
825 | - * Test whether the given node matches the pseudoElements. |
|
826 | - * |
|
827 | - * If any pseudo-elements are passed, this will test to see |
|
828 | - * <i>if conditions obtain that would allow the pseudo-element |
|
829 | - * to be created</i>. This does not modify the match in any way. |
|
830 | - * |
|
831 | - * @param DOMElement $node |
|
832 | - * @param $pseudoElements |
|
833 | - * |
|
834 | - * @return bool |
|
835 | - * @throws NotImplementedException |
|
836 | - */ |
|
837 | - protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool |
|
838 | - { |
|
839 | - if (empty($pseudoElements)) { |
|
840 | - return true; |
|
841 | - } |
|
842 | - |
|
843 | - foreach ($pseudoElements as $pse) { |
|
844 | - switch ($pse) { |
|
845 | - case 'first-line': |
|
846 | - case 'first-letter': |
|
847 | - case 'before': |
|
848 | - case 'after': |
|
849 | - return strlen($node->textContent) > 0; |
|
850 | - case 'selection': |
|
851 | - throw new NotImplementedException("::$pse is not implemented."); |
|
852 | - } |
|
853 | - } |
|
854 | - |
|
855 | - return false; |
|
856 | - } |
|
857 | - |
|
858 | - protected function newMatches() |
|
859 | - { |
|
860 | - return new SplObjectStorage(); |
|
861 | - } |
|
862 | - |
|
863 | - /** |
|
864 | - * Get the internal match set. |
|
865 | - * Internal utility function. |
|
866 | - */ |
|
867 | - protected function getMatches() |
|
868 | - { |
|
869 | - return $this->matches(); |
|
870 | - } |
|
871 | - |
|
872 | - /** |
|
873 | - * Set the internal match set. |
|
874 | - * |
|
875 | - * Internal utility function. |
|
876 | - */ |
|
877 | - protected function setMatches($matches) |
|
878 | - { |
|
879 | - $this->matches = $matches; |
|
880 | - } |
|
881 | - |
|
882 | - /** |
|
883 | - * Attach all nodes in a node list to the given \SplObjectStorage. |
|
884 | - * |
|
885 | - * @param DOMNodeList $nodeList |
|
886 | - * @param SplObjectStorage $splos |
|
887 | - */ |
|
888 | - public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos) |
|
889 | - { |
|
890 | - foreach ($nodeList as $item) { |
|
891 | - $splos->attach($item); |
|
892 | - } |
|
893 | - } |
|
894 | - |
|
895 | - public function getDocument() |
|
896 | - { |
|
897 | - return $this->dom; |
|
898 | - } |
|
692 | + /** |
|
693 | + * Get a list of ancestors to the present node. |
|
694 | + */ |
|
695 | + protected function ancestors($node) |
|
696 | + { |
|
697 | + $buffer = []; |
|
698 | + $parent = $node; |
|
699 | + while (($parent = $parent->parentNode) !== null) { |
|
700 | + $buffer[] = $parent; |
|
701 | + } |
|
702 | + |
|
703 | + return $buffer; |
|
704 | + } |
|
705 | + |
|
706 | + /** |
|
707 | + * Check to see if DOMNode has all of the given attributes. |
|
708 | + * |
|
709 | + * This can handle namespaced attributes, including namespace |
|
710 | + * wildcards. |
|
711 | + * |
|
712 | + * @param DOMElement $node |
|
713 | + * @param $attributes |
|
714 | + * |
|
715 | + * @return bool |
|
716 | + */ |
|
717 | + protected function matchAttributes(DOMElement $node, $attributes): bool |
|
718 | + { |
|
719 | + if (empty($attributes)) { |
|
720 | + return true; |
|
721 | + } |
|
722 | + |
|
723 | + foreach ($attributes as $attr) { |
|
724 | + $val = isset($attr['value']) ? $attr['value'] : null; |
|
725 | + |
|
726 | + // Namespaced attributes. |
|
727 | + if (isset($attr['ns']) && $attr['ns'] !== '*') { |
|
728 | + $nsuri = $node->lookupNamespaceURI($attr['ns']); |
|
729 | + if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
730 | + return false; |
|
731 | + } |
|
732 | + $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
|
733 | + } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) { |
|
734 | + // Cycle through all of the attributes in the node. Note that |
|
735 | + // these are DOMAttr objects. |
|
736 | + $matches = false; |
|
737 | + $name = $attr['name']; |
|
738 | + foreach ($node->attributes as $attrNode) { |
|
739 | + if ($attrNode->localName === $name) { |
|
740 | + $nsuri = $attrNode->namespaceURI; |
|
741 | + $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']); |
|
742 | + } |
|
743 | + } |
|
744 | + } // No namespace. |
|
745 | + else { |
|
746 | + $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
|
747 | + } |
|
748 | + |
|
749 | + if (! $matches) { |
|
750 | + return false; |
|
751 | + } |
|
752 | + } |
|
753 | + |
|
754 | + return true; |
|
755 | + } |
|
756 | + |
|
757 | + /** |
|
758 | + * Check that the given DOMNode has the given ID. |
|
759 | + * |
|
760 | + * @param DOMElement $node |
|
761 | + * @param $id |
|
762 | + * |
|
763 | + * @return bool |
|
764 | + */ |
|
765 | + protected function matchId(DOMElement $node, $id): bool |
|
766 | + { |
|
767 | + if (empty($id)) { |
|
768 | + return true; |
|
769 | + } |
|
770 | + |
|
771 | + return $node->hasAttribute('id') && $node->getAttribute('id') === $id; |
|
772 | + } |
|
773 | + |
|
774 | + /** |
|
775 | + * Check that the given DOMNode has all of the given classes. |
|
776 | + * |
|
777 | + * @param DOMElement $node |
|
778 | + * @param $classes |
|
779 | + * |
|
780 | + * @return bool |
|
781 | + */ |
|
782 | + protected function matchClasses(DOMElement $node, $classes): bool |
|
783 | + { |
|
784 | + if (empty($classes)) { |
|
785 | + return true; |
|
786 | + } |
|
787 | + |
|
788 | + if (! $node->hasAttribute('class')) { |
|
789 | + return false; |
|
790 | + } |
|
791 | + |
|
792 | + $eleClasses = preg_split('/\s+/', $node->getAttribute('class')); |
|
793 | + if (empty($eleClasses)) { |
|
794 | + return false; |
|
795 | + } |
|
796 | + |
|
797 | + // The intersection should match the given $classes. |
|
798 | + $missing = array_diff($classes, array_intersect($classes, $eleClasses)); |
|
799 | + |
|
800 | + return count($missing) === 0; |
|
801 | + } |
|
802 | + |
|
803 | + /** |
|
804 | + * @param DOMElement $node |
|
805 | + * @param $pseudoClasses |
|
806 | + * |
|
807 | + * @return bool |
|
808 | + * @throws NotImplementedException |
|
809 | + * @throws ParseException |
|
810 | + */ |
|
811 | + protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool |
|
812 | + { |
|
813 | + $ret = true; |
|
814 | + foreach ($pseudoClasses as $pseudoClass) { |
|
815 | + $name = $pseudoClass['name']; |
|
816 | + // Avoid E_STRICT violation. |
|
817 | + $value = $pseudoClass['value'] ?? null; |
|
818 | + $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
819 | + } |
|
820 | + |
|
821 | + return $ret; |
|
822 | + } |
|
823 | + |
|
824 | + /** |
|
825 | + * Test whether the given node matches the pseudoElements. |
|
826 | + * |
|
827 | + * If any pseudo-elements are passed, this will test to see |
|
828 | + * <i>if conditions obtain that would allow the pseudo-element |
|
829 | + * to be created</i>. This does not modify the match in any way. |
|
830 | + * |
|
831 | + * @param DOMElement $node |
|
832 | + * @param $pseudoElements |
|
833 | + * |
|
834 | + * @return bool |
|
835 | + * @throws NotImplementedException |
|
836 | + */ |
|
837 | + protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool |
|
838 | + { |
|
839 | + if (empty($pseudoElements)) { |
|
840 | + return true; |
|
841 | + } |
|
842 | + |
|
843 | + foreach ($pseudoElements as $pse) { |
|
844 | + switch ($pse) { |
|
845 | + case 'first-line': |
|
846 | + case 'first-letter': |
|
847 | + case 'before': |
|
848 | + case 'after': |
|
849 | + return strlen($node->textContent) > 0; |
|
850 | + case 'selection': |
|
851 | + throw new NotImplementedException("::$pse is not implemented."); |
|
852 | + } |
|
853 | + } |
|
854 | + |
|
855 | + return false; |
|
856 | + } |
|
857 | + |
|
858 | + protected function newMatches() |
|
859 | + { |
|
860 | + return new SplObjectStorage(); |
|
861 | + } |
|
862 | + |
|
863 | + /** |
|
864 | + * Get the internal match set. |
|
865 | + * Internal utility function. |
|
866 | + */ |
|
867 | + protected function getMatches() |
|
868 | + { |
|
869 | + return $this->matches(); |
|
870 | + } |
|
871 | + |
|
872 | + /** |
|
873 | + * Set the internal match set. |
|
874 | + * |
|
875 | + * Internal utility function. |
|
876 | + */ |
|
877 | + protected function setMatches($matches) |
|
878 | + { |
|
879 | + $this->matches = $matches; |
|
880 | + } |
|
881 | + |
|
882 | + /** |
|
883 | + * Attach all nodes in a node list to the given \SplObjectStorage. |
|
884 | + * |
|
885 | + * @param DOMNodeList $nodeList |
|
886 | + * @param SplObjectStorage $splos |
|
887 | + */ |
|
888 | + public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos) |
|
889 | + { |
|
890 | + foreach ($nodeList as $item) { |
|
891 | + $splos->attach($item); |
|
892 | + } |
|
893 | + } |
|
894 | + |
|
895 | + public function getDocument() |
|
896 | + { |
|
897 | + return $this->dom; |
|
898 | + } |
|
899 | 899 | } |
@@ -88,9 +88,9 @@ discard block |
||
88 | 88 | $splos->rewind(); |
89 | 89 | $first = $splos->current(); |
90 | 90 | if ($first instanceof DOMDocument) { |
91 | - $this->dom = $first;//->documentElement; |
|
91 | + $this->dom = $first; //->documentElement; |
|
92 | 92 | } else { |
93 | - $this->dom = $first->ownerDocument;//->documentElement; |
|
93 | + $this->dom = $first->ownerDocument; //->documentElement; |
|
94 | 94 | } |
95 | 95 | |
96 | 96 | $this->scopeNode = $scopeNode; |
@@ -297,7 +297,7 @@ discard block |
||
297 | 297 | */ |
298 | 298 | public function combineAdjacent($node, $selectors, $index) |
299 | 299 | { |
300 | - while (! empty($node->previousSibling)) { |
|
300 | + while (!empty($node->previousSibling)) { |
|
301 | 301 | $node = $node->previousSibling; |
302 | 302 | if ($node->nodeType == XML_ELEMENT_NODE) { |
303 | 303 | //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index])); |
@@ -327,7 +327,7 @@ discard block |
||
327 | 327 | */ |
328 | 328 | public function combineSibling($node, $selectors, $index) |
329 | 329 | { |
330 | - while (! empty($node->previousSibling)) { |
|
330 | + while (!empty($node->previousSibling)) { |
|
331 | 331 | $node = $node->previousSibling; |
332 | 332 | if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) { |
333 | 333 | return true; |
@@ -383,7 +383,7 @@ discard block |
||
383 | 383 | */ |
384 | 384 | public function combineAnyDescendant($node, $selectors, $index) |
385 | 385 | { |
386 | - while (! empty($node->parentNode)) { |
|
386 | + while (!empty($node->parentNode)) { |
|
387 | 387 | $node = $node->parentNode; |
388 | 388 | |
389 | 389 | // Catch case where element is child of something |
@@ -429,16 +429,16 @@ discard block |
||
429 | 429 | // this should give us only a single matched element |
430 | 430 | // to work with. |
431 | 431 | if (/*$element == '*' &&*/ |
432 | - ! empty($selector->id)) { |
|
432 | + !empty($selector->id)) { |
|
433 | 433 | $initialMatches = $this->initialMatchOnID($selector, $matches); |
434 | 434 | } // If a namespace is set, find the namespace matches. |
435 | - elseif (! empty($selector->ns)) { |
|
435 | + elseif (!empty($selector->ns)) { |
|
436 | 436 | $initialMatches = $this->initialMatchOnElementNS($selector, $matches); |
437 | 437 | } |
438 | 438 | // If the element is a wildcard, using class can |
439 | 439 | // substantially reduce the number of elements that |
440 | 440 | // we start with. |
441 | - elseif ($element === '*' && ! empty($selector->classes)) { |
|
441 | + elseif ($element === '*' && !empty($selector->classes)) { |
|
442 | 442 | $initialMatches = $this->initialMatchOnClasses($selector, $matches); |
443 | 443 | } else { |
444 | 444 | $initialMatches = $this->initialMatchOnElement($selector, $matches); |
@@ -467,7 +467,7 @@ discard block |
||
467 | 467 | |
468 | 468 | // Issue #145: DOMXPath will through an exception if the DOM is |
469 | 469 | // not set. |
470 | - if (! ($this->dom instanceof DOMDocument)) { |
|
470 | + if (!($this->dom instanceof DOMDocument)) { |
|
471 | 471 | return $found; |
472 | 472 | } |
473 | 473 | $baseQuery = ".//*[@id='{$id}']"; |
@@ -481,7 +481,7 @@ discard block |
||
481 | 481 | } |
482 | 482 | |
483 | 483 | $nl = $this->initialXpathQuery($xpath, $node, $baseQuery); |
484 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
484 | + if (!empty($nl) && $nl instanceof DOMNodeList) { |
|
485 | 485 | $this->attachNodeList($nl, $found); |
486 | 486 | } |
487 | 487 | } |
@@ -511,7 +511,7 @@ discard block |
||
511 | 511 | |
512 | 512 | // Issue #145: DOMXPath will through an exception if the DOM is |
513 | 513 | // not set. |
514 | - if (! ($this->dom instanceof DOMDocument)) { |
|
514 | + if (!($this->dom instanceof DOMDocument)) { |
|
515 | 515 | return $found; |
516 | 516 | } |
517 | 517 | $baseQuery = './/*[@class]'; |
@@ -593,7 +593,7 @@ discard block |
||
593 | 593 | $found->attach($node); |
594 | 594 | } |
595 | 595 | $nl = $node->getElementsByTagName($element); |
596 | - if (! empty($nl) && $nl instanceof DOMNodeList) { |
|
596 | + if (!empty($nl) && $nl instanceof DOMNodeList) { |
|
597 | 597 | $this->attachNodeList($nl, $found); |
598 | 598 | } |
599 | 599 | } |
@@ -664,7 +664,7 @@ discard block |
||
664 | 664 | } |
665 | 665 | |
666 | 666 | // Handle namespace. |
667 | - if (! empty($ns) && $ns !== '*') { |
|
667 | + if (!empty($ns) && $ns !== '*') { |
|
668 | 668 | // Check whether we have a matching NS URI. |
669 | 669 | $nsuri = $node->lookupNamespaceURI($ns); |
670 | 670 | if (empty($nsuri) || $node->namespaceURI !== $nsuri) { |
@@ -726,7 +726,7 @@ discard block |
||
726 | 726 | // Namespaced attributes. |
727 | 727 | if (isset($attr['ns']) && $attr['ns'] !== '*') { |
728 | 728 | $nsuri = $node->lookupNamespaceURI($attr['ns']); |
729 | - if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) { |
|
729 | + if (empty($nsuri) || !$node->hasAttributeNS($nsuri, $attr['name'])) { |
|
730 | 730 | return false; |
731 | 731 | } |
732 | 732 | $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']); |
@@ -746,7 +746,7 @@ discard block |
||
746 | 746 | $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']); |
747 | 747 | } |
748 | 748 | |
749 | - if (! $matches) { |
|
749 | + if (!$matches) { |
|
750 | 750 | return false; |
751 | 751 | } |
752 | 752 | } |
@@ -785,7 +785,7 @@ discard block |
||
785 | 785 | return true; |
786 | 786 | } |
787 | 787 | |
788 | - if (! $node->hasAttribute('class')) { |
|
788 | + if (!$node->hasAttribute('class')) { |
|
789 | 789 | return false; |
790 | 790 | } |
791 | 791 | |
@@ -815,7 +815,7 @@ discard block |
||
815 | 815 | $name = $pseudoClass['name']; |
816 | 816 | // Avoid E_STRICT violation. |
817 | 817 | $value = $pseudoClass['value'] ?? null; |
818 | - $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
818 | + $ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value); |
|
819 | 819 | } |
820 | 820 | |
821 | 821 | return $ret; |
@@ -25,72 +25,72 @@ discard block |
||
25 | 25 | */ |
26 | 26 | class Parser |
27 | 27 | { |
28 | - protected $scanner; |
|
29 | - protected $buffer = ''; |
|
30 | - protected $handler; |
|
31 | - private $strict = false; |
|
32 | - |
|
33 | - protected $DEBUG = false; |
|
34 | - |
|
35 | - /** |
|
36 | - * Construct a new CSS parser object. This will attempt to |
|
37 | - * parse the string as a CSS selector. As it parses, it will |
|
38 | - * send events to the EventHandler implementation. |
|
39 | - * |
|
40 | - * @param string $string |
|
41 | - * @param EventHandler $handler |
|
42 | - */ |
|
43 | - public function __construct(string $string, EventHandler $handler) |
|
44 | - { |
|
45 | - $this->originalString = $string; |
|
46 | - $is = new InputStream($string); |
|
47 | - $this->scanner = new Scanner($is); |
|
48 | - $this->handler = $handler; |
|
49 | - } |
|
50 | - |
|
51 | - /** |
|
52 | - * Parse the selector. |
|
53 | - * |
|
54 | - * This begins an event-based parsing process that will |
|
55 | - * fire events as the selector is handled. A EventHandler |
|
56 | - * implementation will be responsible for handling the events. |
|
57 | - * |
|
58 | - * @throws ParseException |
|
59 | - * @throws Exception |
|
60 | - */ |
|
61 | - public function parse(): void |
|
62 | - { |
|
63 | - $this->scanner->nextToken(); |
|
64 | - |
|
65 | - while ($this->scanner->token !== false) { |
|
66 | - // Primitive recursion detection. |
|
67 | - $position = $this->scanner->position(); |
|
68 | - |
|
69 | - if ($this->DEBUG) { |
|
70 | - echo 'PARSE ' . $this->scanner->token . PHP_EOL; |
|
71 | - } |
|
72 | - $this->selector(); |
|
73 | - |
|
74 | - $finalPosition = $this->scanner->position(); |
|
75 | - if ($this->scanner->token !== false && $finalPosition === $position) { |
|
76 | - // If we get here, then the scanner did not pop a single character |
|
77 | - // off of the input stream during a full run of the parser, which |
|
78 | - // means that the current input does not match any recognizable |
|
79 | - // pattern. |
|
80 | - throw new ParseException('CSS selector is not well formed.'); |
|
81 | - } |
|
82 | - } |
|
83 | - } |
|
84 | - |
|
85 | - /** |
|
86 | - * A restricted parser that can only parse simple selectors. |
|
87 | - * The pseudoClass handler for this parser will throw an |
|
88 | - * exception if it encounters a pseudo-element or the |
|
89 | - * negation pseudo-class. |
|
90 | - * |
|
91 | - * @deprecated This is not used anywhere in QueryPath and |
|
92 | - * may be removed. |
|
93 | - *//* |
|
28 | + protected $scanner; |
|
29 | + protected $buffer = ''; |
|
30 | + protected $handler; |
|
31 | + private $strict = false; |
|
32 | + |
|
33 | + protected $DEBUG = false; |
|
34 | + |
|
35 | + /** |
|
36 | + * Construct a new CSS parser object. This will attempt to |
|
37 | + * parse the string as a CSS selector. As it parses, it will |
|
38 | + * send events to the EventHandler implementation. |
|
39 | + * |
|
40 | + * @param string $string |
|
41 | + * @param EventHandler $handler |
|
42 | + */ |
|
43 | + public function __construct(string $string, EventHandler $handler) |
|
44 | + { |
|
45 | + $this->originalString = $string; |
|
46 | + $is = new InputStream($string); |
|
47 | + $this->scanner = new Scanner($is); |
|
48 | + $this->handler = $handler; |
|
49 | + } |
|
50 | + |
|
51 | + /** |
|
52 | + * Parse the selector. |
|
53 | + * |
|
54 | + * This begins an event-based parsing process that will |
|
55 | + * fire events as the selector is handled. A EventHandler |
|
56 | + * implementation will be responsible for handling the events. |
|
57 | + * |
|
58 | + * @throws ParseException |
|
59 | + * @throws Exception |
|
60 | + */ |
|
61 | + public function parse(): void |
|
62 | + { |
|
63 | + $this->scanner->nextToken(); |
|
64 | + |
|
65 | + while ($this->scanner->token !== false) { |
|
66 | + // Primitive recursion detection. |
|
67 | + $position = $this->scanner->position(); |
|
68 | + |
|
69 | + if ($this->DEBUG) { |
|
70 | + echo 'PARSE ' . $this->scanner->token . PHP_EOL; |
|
71 | + } |
|
72 | + $this->selector(); |
|
73 | + |
|
74 | + $finalPosition = $this->scanner->position(); |
|
75 | + if ($this->scanner->token !== false && $finalPosition === $position) { |
|
76 | + // If we get here, then the scanner did not pop a single character |
|
77 | + // off of the input stream during a full run of the parser, which |
|
78 | + // means that the current input does not match any recognizable |
|
79 | + // pattern. |
|
80 | + throw new ParseException('CSS selector is not well formed.'); |
|
81 | + } |
|
82 | + } |
|
83 | + } |
|
84 | + |
|
85 | + /** |
|
86 | + * A restricted parser that can only parse simple selectors. |
|
87 | + * The pseudoClass handler for this parser will throw an |
|
88 | + * exception if it encounters a pseudo-element or the |
|
89 | + * negation pseudo-class. |
|
90 | + * |
|
91 | + * @deprecated This is not used anywhere in QueryPath and |
|
92 | + * may be removed. |
|
93 | + *//* |
|
94 | 94 | public function parseSimpleSelector() { |
95 | 95 | while ($this->scanner->token !== FALSE) { |
96 | 96 | if ($this->DEBUG) print "SIMPLE SELECTOR\n"; |
@@ -105,261 +105,261 @@ discard block |
||
105 | 105 | } |
106 | 106 | }*/ |
107 | 107 | |
108 | - /** |
|
109 | - * Handle an entire CSS selector. |
|
110 | - * |
|
111 | - * @throws ParseException |
|
112 | - * @throws Exception |
|
113 | - */ |
|
114 | - private function selector(): void |
|
115 | - { |
|
116 | - if ($this->DEBUG) { |
|
117 | - print 'SELECTOR' . $this->scanner->position() . PHP_EOL; |
|
118 | - } |
|
119 | - |
|
120 | - $this->consumeWhitespace(); // Remove leading whitespace |
|
121 | - $this->simpleSelectors(); |
|
122 | - $this->combinator(); |
|
123 | - } |
|
124 | - |
|
125 | - /** |
|
126 | - * Consume whitespace and return a count of the number of whitespace consumed. |
|
127 | - * |
|
128 | - * @throws ParseException |
|
129 | - * @throws Exception |
|
130 | - */ |
|
131 | - private function consumeWhitespace(): int |
|
132 | - { |
|
133 | - if ($this->DEBUG) { |
|
134 | - echo 'CONSUME WHITESPACE' . PHP_EOL; |
|
135 | - } |
|
136 | - |
|
137 | - $white = 0; |
|
138 | - while ($this->scanner->token === Token::WHITE) { |
|
139 | - $this->scanner->nextToken(); |
|
140 | - ++$white; |
|
141 | - } |
|
142 | - |
|
143 | - return $white; |
|
144 | - } |
|
145 | - |
|
146 | - /** |
|
147 | - * Handle one of the five combinators: '>', '+', ' ', '~', and ','. |
|
148 | - * This will call the appropriate event handlers. |
|
149 | - * |
|
150 | - * @throws ParseException |
|
151 | - * @throws Exception |
|
152 | - * @see EventHandler::anyDescendant(), |
|
153 | - * @see EventHandler::anotherSelector(). |
|
154 | - * @see EventHandler::directDescendant(), |
|
155 | - * @see EventHandler::adjacent(), |
|
156 | - */ |
|
157 | - private function combinator(): void |
|
158 | - { |
|
159 | - if ($this->DEBUG) { |
|
160 | - echo 'COMBINATOR' . PHP_EOL; |
|
161 | - } |
|
162 | - /* |
|
108 | + /** |
|
109 | + * Handle an entire CSS selector. |
|
110 | + * |
|
111 | + * @throws ParseException |
|
112 | + * @throws Exception |
|
113 | + */ |
|
114 | + private function selector(): void |
|
115 | + { |
|
116 | + if ($this->DEBUG) { |
|
117 | + print 'SELECTOR' . $this->scanner->position() . PHP_EOL; |
|
118 | + } |
|
119 | + |
|
120 | + $this->consumeWhitespace(); // Remove leading whitespace |
|
121 | + $this->simpleSelectors(); |
|
122 | + $this->combinator(); |
|
123 | + } |
|
124 | + |
|
125 | + /** |
|
126 | + * Consume whitespace and return a count of the number of whitespace consumed. |
|
127 | + * |
|
128 | + * @throws ParseException |
|
129 | + * @throws Exception |
|
130 | + */ |
|
131 | + private function consumeWhitespace(): int |
|
132 | + { |
|
133 | + if ($this->DEBUG) { |
|
134 | + echo 'CONSUME WHITESPACE' . PHP_EOL; |
|
135 | + } |
|
136 | + |
|
137 | + $white = 0; |
|
138 | + while ($this->scanner->token === Token::WHITE) { |
|
139 | + $this->scanner->nextToken(); |
|
140 | + ++$white; |
|
141 | + } |
|
142 | + |
|
143 | + return $white; |
|
144 | + } |
|
145 | + |
|
146 | + /** |
|
147 | + * Handle one of the five combinators: '>', '+', ' ', '~', and ','. |
|
148 | + * This will call the appropriate event handlers. |
|
149 | + * |
|
150 | + * @throws ParseException |
|
151 | + * @throws Exception |
|
152 | + * @see EventHandler::anyDescendant(), |
|
153 | + * @see EventHandler::anotherSelector(). |
|
154 | + * @see EventHandler::directDescendant(), |
|
155 | + * @see EventHandler::adjacent(), |
|
156 | + */ |
|
157 | + private function combinator(): void |
|
158 | + { |
|
159 | + if ($this->DEBUG) { |
|
160 | + echo 'COMBINATOR' . PHP_EOL; |
|
161 | + } |
|
162 | + /* |
|
163 | 163 | * Problem: ' ' and ' > ' are both valid combinators. |
164 | 164 | * So we have to track whitespace consumption to see |
165 | 165 | * if we are hitting the ' ' combinator or if the |
166 | 166 | * selector just has whitespace padding another combinator. |
167 | 167 | */ |
168 | 168 | |
169 | - // Flag to indicate that post-checks need doing |
|
170 | - $inCombinator = false; |
|
171 | - $white = $this->consumeWhitespace(); |
|
172 | - $t = $this->scanner->token; |
|
173 | - |
|
174 | - if ($t === Token::RANGLE) { |
|
175 | - $this->handler->directDescendant(); |
|
176 | - $this->scanner->nextToken(); |
|
177 | - $inCombinator = true; |
|
178 | - //$this->simpleSelectors(); |
|
179 | - } elseif ($t === Token::PLUS) { |
|
180 | - $this->handler->adjacent(); |
|
181 | - $this->scanner->nextToken(); |
|
182 | - $inCombinator = true; |
|
183 | - //$this->simpleSelectors(); |
|
184 | - } elseif ($t === Token::COMMA) { |
|
185 | - $this->handler->anotherSelector(); |
|
186 | - $this->scanner->nextToken(); |
|
187 | - $inCombinator = true; |
|
188 | - //$this->scanner->selectors(); |
|
189 | - } elseif ($t === Token::TILDE) { |
|
190 | - $this->handler->sibling(); |
|
191 | - $this->scanner->nextToken(); |
|
192 | - $inCombinator = true; |
|
193 | - } |
|
194 | - |
|
195 | - // Check that we don't get two combinators in a row. |
|
196 | - if ($inCombinator) { |
|
197 | - if ($this->DEBUG) { |
|
198 | - print 'COMBINATOR: ' . Token::name($t) . "\n"; |
|
199 | - } |
|
200 | - $this->consumeWhitespace(); |
|
201 | - if ($this->isCombinator($this->scanner->token)) { |
|
202 | - throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.'); |
|
203 | - } |
|
204 | - } // Check to see if we have whitespace combinator: |
|
205 | - elseif ($white > 0) { |
|
206 | - if ($this->DEBUG) { |
|
207 | - echo 'COMBINATOR: any descendant' . PHP_EOL; |
|
208 | - } |
|
209 | - $this->handler->anyDescendant(); |
|
210 | - } else { |
|
211 | - if ($this->DEBUG) { |
|
212 | - echo 'COMBINATOR: no combinator found.' . PHP_EOL; |
|
213 | - } |
|
214 | - } |
|
215 | - } |
|
216 | - |
|
217 | - /** |
|
218 | - * Check if the token is a combinator. |
|
219 | - * |
|
220 | - * @param int $tok |
|
221 | - * |
|
222 | - * @return bool |
|
223 | - */ |
|
224 | - private function isCombinator(int $tok): bool |
|
225 | - { |
|
226 | - return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true); |
|
227 | - } |
|
228 | - |
|
229 | - /** |
|
230 | - * Handle a simple selector. |
|
231 | - * |
|
232 | - * @throws ParseException |
|
233 | - */ |
|
234 | - private function simpleSelectors(): void |
|
235 | - { |
|
236 | - if ($this->DEBUG) { |
|
237 | - print 'SIMPLE SELECTOR' . PHP_EOL; |
|
238 | - } |
|
239 | - $this->allElements(); |
|
240 | - $this->elementName(); |
|
241 | - $this->elementClass(); |
|
242 | - $this->elementID(); |
|
243 | - $this->pseudoClass(); |
|
244 | - $this->attribute(); |
|
245 | - } |
|
246 | - |
|
247 | - /** |
|
248 | - * Handles CSS ID selectors. |
|
249 | - * This will call EventHandler::elementID(). |
|
250 | - * |
|
251 | - * @throws ParseException |
|
252 | - * @throws Exception |
|
253 | - */ |
|
254 | - private function elementID(): void |
|
255 | - { |
|
256 | - if ($this->DEBUG) { |
|
257 | - echo 'ELEMENT ID' . PHP_EOL; |
|
258 | - } |
|
259 | - |
|
260 | - if ($this->scanner->token === Token::OCTO) { |
|
261 | - $this->scanner->nextToken(); |
|
262 | - if ($this->scanner->token !== Token::CHAR) { |
|
263 | - throw new ParseException("Expected string after #"); |
|
264 | - } |
|
265 | - $id = $this->scanner->getNameString(); |
|
266 | - $this->handler->elementID($id); |
|
267 | - } |
|
268 | - } |
|
269 | - |
|
270 | - /** |
|
271 | - * Handles CSS class selectors. |
|
272 | - * This will call the EventHandler::elementClass() method. |
|
273 | - */ |
|
274 | - private function elementClass(): void |
|
275 | - { |
|
276 | - if ($this->DEBUG) { |
|
277 | - print 'ELEMENT CLASS' . PHP_EOL; |
|
278 | - } |
|
279 | - if ($this->scanner->token == Token::DOT) { |
|
280 | - $this->scanner->nextToken(); |
|
281 | - $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error. |
|
282 | - $cssClass = $this->scanner->getNameString(); |
|
283 | - $this->handler->elementClass($cssClass); |
|
284 | - } |
|
285 | - } |
|
286 | - |
|
287 | - /** |
|
288 | - * Handle a pseudo-class and pseudo-element. |
|
289 | - * |
|
290 | - * CSS 3 selectors support separate pseudo-elements, using :: instead |
|
291 | - * of : for separator. This is now supported, and calls the pseudoElement |
|
292 | - * handler, EventHandler::pseudoElement(). |
|
293 | - * |
|
294 | - * This will call EventHandler::pseudoClass() when a |
|
295 | - * pseudo-class is parsed. |
|
296 | - * |
|
297 | - * @throws ParseException |
|
298 | - * @throws Exception |
|
299 | - */ |
|
300 | - private function pseudoClass($restricted = false): void |
|
301 | - { |
|
302 | - if ($this->DEBUG) { |
|
303 | - echo 'PSEUDO-CLASS' . PHP_EOL; |
|
304 | - } |
|
305 | - if ($this->scanner->token === Token::COLON) { |
|
306 | - // Check for CSS 3 pseudo element: |
|
307 | - $isPseudoElement = false; |
|
308 | - if ($this->scanner->nextToken() === Token::COLON) { |
|
309 | - $isPseudoElement = true; |
|
310 | - $this->scanner->nextToken(); |
|
311 | - } |
|
312 | - |
|
313 | - $name = $this->scanner->getNameString(); |
|
314 | - if ($restricted && $name === 'not') { |
|
315 | - throw new ParseException("The 'not' pseudo-class is illegal in this context."); |
|
316 | - } |
|
317 | - |
|
318 | - $value = null; |
|
319 | - if ($this->scanner->token === Token::LPAREN) { |
|
320 | - if ($isPseudoElement) { |
|
321 | - throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.'); |
|
322 | - } |
|
323 | - $value = $this->pseudoClassValue(); |
|
324 | - } |
|
325 | - |
|
326 | - // FIXME: This should throw errors when pseudo element has values. |
|
327 | - if ($isPseudoElement) { |
|
328 | - if ($restricted) { |
|
329 | - throw new ParseException('Pseudo-Elements are illegal in this context.'); |
|
330 | - } |
|
331 | - $this->handler->pseudoElement($name); |
|
332 | - $this->consumeWhitespace(); |
|
333 | - |
|
334 | - // Per the spec, pseudo-elements must be the last items in a selector, so we |
|
335 | - // check to make sure that we are either at the end of the stream or that a |
|
336 | - // new selector is starting. Only one pseudo-element is allowed per selector. |
|
337 | - if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) { |
|
338 | - throw new ParseException('A Pseudo-Element must be the last item in a selector.'); |
|
339 | - } |
|
340 | - } else { |
|
341 | - $this->handler->pseudoClass($name, $value); |
|
342 | - } |
|
343 | - } |
|
344 | - } |
|
345 | - |
|
346 | - /** |
|
347 | - * Get the value of a pseudo-classes. |
|
348 | - * |
|
349 | - * @return string |
|
350 | - * Returns the value found from a pseudo-class. |
|
351 | - * |
|
352 | - * @todo Pseudoclasses can be passed pseudo-elements and |
|
353 | - * other pseudo-classes as values, which means :pseudo(::pseudo) |
|
354 | - * is legal. |
|
355 | - */ |
|
356 | - private function pseudoClassValue() |
|
357 | - { |
|
358 | - if ($this->scanner->token === Token::LPAREN) { |
|
359 | - $buf = ''; |
|
360 | - |
|
361 | - // For now, just leave pseudoClass value vague. |
|
362 | - /* |
|
169 | + // Flag to indicate that post-checks need doing |
|
170 | + $inCombinator = false; |
|
171 | + $white = $this->consumeWhitespace(); |
|
172 | + $t = $this->scanner->token; |
|
173 | + |
|
174 | + if ($t === Token::RANGLE) { |
|
175 | + $this->handler->directDescendant(); |
|
176 | + $this->scanner->nextToken(); |
|
177 | + $inCombinator = true; |
|
178 | + //$this->simpleSelectors(); |
|
179 | + } elseif ($t === Token::PLUS) { |
|
180 | + $this->handler->adjacent(); |
|
181 | + $this->scanner->nextToken(); |
|
182 | + $inCombinator = true; |
|
183 | + //$this->simpleSelectors(); |
|
184 | + } elseif ($t === Token::COMMA) { |
|
185 | + $this->handler->anotherSelector(); |
|
186 | + $this->scanner->nextToken(); |
|
187 | + $inCombinator = true; |
|
188 | + //$this->scanner->selectors(); |
|
189 | + } elseif ($t === Token::TILDE) { |
|
190 | + $this->handler->sibling(); |
|
191 | + $this->scanner->nextToken(); |
|
192 | + $inCombinator = true; |
|
193 | + } |
|
194 | + |
|
195 | + // Check that we don't get two combinators in a row. |
|
196 | + if ($inCombinator) { |
|
197 | + if ($this->DEBUG) { |
|
198 | + print 'COMBINATOR: ' . Token::name($t) . "\n"; |
|
199 | + } |
|
200 | + $this->consumeWhitespace(); |
|
201 | + if ($this->isCombinator($this->scanner->token)) { |
|
202 | + throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.'); |
|
203 | + } |
|
204 | + } // Check to see if we have whitespace combinator: |
|
205 | + elseif ($white > 0) { |
|
206 | + if ($this->DEBUG) { |
|
207 | + echo 'COMBINATOR: any descendant' . PHP_EOL; |
|
208 | + } |
|
209 | + $this->handler->anyDescendant(); |
|
210 | + } else { |
|
211 | + if ($this->DEBUG) { |
|
212 | + echo 'COMBINATOR: no combinator found.' . PHP_EOL; |
|
213 | + } |
|
214 | + } |
|
215 | + } |
|
216 | + |
|
217 | + /** |
|
218 | + * Check if the token is a combinator. |
|
219 | + * |
|
220 | + * @param int $tok |
|
221 | + * |
|
222 | + * @return bool |
|
223 | + */ |
|
224 | + private function isCombinator(int $tok): bool |
|
225 | + { |
|
226 | + return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true); |
|
227 | + } |
|
228 | + |
|
229 | + /** |
|
230 | + * Handle a simple selector. |
|
231 | + * |
|
232 | + * @throws ParseException |
|
233 | + */ |
|
234 | + private function simpleSelectors(): void |
|
235 | + { |
|
236 | + if ($this->DEBUG) { |
|
237 | + print 'SIMPLE SELECTOR' . PHP_EOL; |
|
238 | + } |
|
239 | + $this->allElements(); |
|
240 | + $this->elementName(); |
|
241 | + $this->elementClass(); |
|
242 | + $this->elementID(); |
|
243 | + $this->pseudoClass(); |
|
244 | + $this->attribute(); |
|
245 | + } |
|
246 | + |
|
247 | + /** |
|
248 | + * Handles CSS ID selectors. |
|
249 | + * This will call EventHandler::elementID(). |
|
250 | + * |
|
251 | + * @throws ParseException |
|
252 | + * @throws Exception |
|
253 | + */ |
|
254 | + private function elementID(): void |
|
255 | + { |
|
256 | + if ($this->DEBUG) { |
|
257 | + echo 'ELEMENT ID' . PHP_EOL; |
|
258 | + } |
|
259 | + |
|
260 | + if ($this->scanner->token === Token::OCTO) { |
|
261 | + $this->scanner->nextToken(); |
|
262 | + if ($this->scanner->token !== Token::CHAR) { |
|
263 | + throw new ParseException("Expected string after #"); |
|
264 | + } |
|
265 | + $id = $this->scanner->getNameString(); |
|
266 | + $this->handler->elementID($id); |
|
267 | + } |
|
268 | + } |
|
269 | + |
|
270 | + /** |
|
271 | + * Handles CSS class selectors. |
|
272 | + * This will call the EventHandler::elementClass() method. |
|
273 | + */ |
|
274 | + private function elementClass(): void |
|
275 | + { |
|
276 | + if ($this->DEBUG) { |
|
277 | + print 'ELEMENT CLASS' . PHP_EOL; |
|
278 | + } |
|
279 | + if ($this->scanner->token == Token::DOT) { |
|
280 | + $this->scanner->nextToken(); |
|
281 | + $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error. |
|
282 | + $cssClass = $this->scanner->getNameString(); |
|
283 | + $this->handler->elementClass($cssClass); |
|
284 | + } |
|
285 | + } |
|
286 | + |
|
287 | + /** |
|
288 | + * Handle a pseudo-class and pseudo-element. |
|
289 | + * |
|
290 | + * CSS 3 selectors support separate pseudo-elements, using :: instead |
|
291 | + * of : for separator. This is now supported, and calls the pseudoElement |
|
292 | + * handler, EventHandler::pseudoElement(). |
|
293 | + * |
|
294 | + * This will call EventHandler::pseudoClass() when a |
|
295 | + * pseudo-class is parsed. |
|
296 | + * |
|
297 | + * @throws ParseException |
|
298 | + * @throws Exception |
|
299 | + */ |
|
300 | + private function pseudoClass($restricted = false): void |
|
301 | + { |
|
302 | + if ($this->DEBUG) { |
|
303 | + echo 'PSEUDO-CLASS' . PHP_EOL; |
|
304 | + } |
|
305 | + if ($this->scanner->token === Token::COLON) { |
|
306 | + // Check for CSS 3 pseudo element: |
|
307 | + $isPseudoElement = false; |
|
308 | + if ($this->scanner->nextToken() === Token::COLON) { |
|
309 | + $isPseudoElement = true; |
|
310 | + $this->scanner->nextToken(); |
|
311 | + } |
|
312 | + |
|
313 | + $name = $this->scanner->getNameString(); |
|
314 | + if ($restricted && $name === 'not') { |
|
315 | + throw new ParseException("The 'not' pseudo-class is illegal in this context."); |
|
316 | + } |
|
317 | + |
|
318 | + $value = null; |
|
319 | + if ($this->scanner->token === Token::LPAREN) { |
|
320 | + if ($isPseudoElement) { |
|
321 | + throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.'); |
|
322 | + } |
|
323 | + $value = $this->pseudoClassValue(); |
|
324 | + } |
|
325 | + |
|
326 | + // FIXME: This should throw errors when pseudo element has values. |
|
327 | + if ($isPseudoElement) { |
|
328 | + if ($restricted) { |
|
329 | + throw new ParseException('Pseudo-Elements are illegal in this context.'); |
|
330 | + } |
|
331 | + $this->handler->pseudoElement($name); |
|
332 | + $this->consumeWhitespace(); |
|
333 | + |
|
334 | + // Per the spec, pseudo-elements must be the last items in a selector, so we |
|
335 | + // check to make sure that we are either at the end of the stream or that a |
|
336 | + // new selector is starting. Only one pseudo-element is allowed per selector. |
|
337 | + if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) { |
|
338 | + throw new ParseException('A Pseudo-Element must be the last item in a selector.'); |
|
339 | + } |
|
340 | + } else { |
|
341 | + $this->handler->pseudoClass($name, $value); |
|
342 | + } |
|
343 | + } |
|
344 | + } |
|
345 | + |
|
346 | + /** |
|
347 | + * Get the value of a pseudo-classes. |
|
348 | + * |
|
349 | + * @return string |
|
350 | + * Returns the value found from a pseudo-class. |
|
351 | + * |
|
352 | + * @todo Pseudoclasses can be passed pseudo-elements and |
|
353 | + * other pseudo-classes as values, which means :pseudo(::pseudo) |
|
354 | + * is legal. |
|
355 | + */ |
|
356 | + private function pseudoClassValue() |
|
357 | + { |
|
358 | + if ($this->scanner->token === Token::LPAREN) { |
|
359 | + $buf = ''; |
|
360 | + |
|
361 | + // For now, just leave pseudoClass value vague. |
|
362 | + /* |
|
363 | 363 | // We have to peek to see if next char is a colon because |
364 | 364 | // pseudo-classes and pseudo-elements are legal strings here. |
365 | 365 | print $this->scanner->peek(); |
@@ -390,242 +390,242 @@ discard block |
||
390 | 390 | } |
391 | 391 | return $buf; |
392 | 392 | */ |
393 | - //$buf .= $this->scanner->getQuotedString(); |
|
394 | - $buf .= $this->scanner->getPseudoClassString(); |
|
395 | - |
|
396 | - return $buf; |
|
397 | - } |
|
398 | - } |
|
399 | - |
|
400 | - /** |
|
401 | - * Handle element names. |
|
402 | - * This will call the EventHandler::elementName(). |
|
403 | - * |
|
404 | - * This handles: |
|
405 | - * <code> |
|
406 | - * name (EventHandler::element()) |
|
407 | - * |name (EventHandler::element()) |
|
408 | - * ns|name (EventHandler::elementNS()) |
|
409 | - * ns|* (EventHandler::elementNS()) |
|
410 | - * </code> |
|
411 | - */ |
|
412 | - private function elementName() |
|
413 | - { |
|
414 | - if ($this->DEBUG) { |
|
415 | - print "ELEMENT NAME\n"; |
|
416 | - } |
|
417 | - if ($this->scanner->token === Token::PIPE) { |
|
418 | - // We have '|name', which is equiv to 'name' |
|
419 | - $this->scanner->nextToken(); |
|
420 | - $this->consumeWhitespace(); |
|
421 | - $elementName = $this->scanner->getNameString(); |
|
422 | - $this->handler->element($elementName); |
|
423 | - } elseif ($this->scanner->token === Token::CHAR) { |
|
424 | - $elementName = $this->scanner->getNameString(); |
|
425 | - if ($this->scanner->token == Token::PIPE) { |
|
426 | - // Get ns|name |
|
427 | - $elementNS = $elementName; |
|
428 | - $this->scanner->nextToken(); |
|
429 | - $this->consumeWhitespace(); |
|
430 | - if ($this->scanner->token === Token::STAR) { |
|
431 | - // We have ns|* |
|
432 | - $this->handler->anyElementInNS($elementNS); |
|
433 | - $this->scanner->nextToken(); |
|
434 | - } elseif ($this->scanner->token !== Token::CHAR) { |
|
435 | - $this->throwError(Token::CHAR, $this->scanner->token); |
|
436 | - } else { |
|
437 | - $elementName = $this->scanner->getNameString(); |
|
438 | - // We have ns|name |
|
439 | - $this->handler->elementNS($elementName, $elementNS); |
|
440 | - } |
|
441 | - } else { |
|
442 | - $this->handler->element($elementName); |
|
443 | - } |
|
444 | - } |
|
445 | - } |
|
446 | - |
|
447 | - /** |
|
448 | - * Check for all elements designators. Due to the new CSS 3 namespace |
|
449 | - * support, this is slightly more complicated, now, as it handles |
|
450 | - * the *|name and *|* cases as well as *. |
|
451 | - * |
|
452 | - * Calls EventHandler::anyElement() or EventHandler::elementName(). |
|
453 | - */ |
|
454 | - private function allElements() |
|
455 | - { |
|
456 | - if ($this->scanner->token === Token::STAR) { |
|
457 | - $this->scanner->nextToken(); |
|
458 | - if ($this->scanner->token === Token::PIPE) { |
|
459 | - $this->scanner->nextToken(); |
|
460 | - if ($this->scanner->token === Token::STAR) { |
|
461 | - // We got *|*. According to spec, this requires |
|
462 | - // that the element has a namespace, so we pass it on |
|
463 | - // to the handler: |
|
464 | - $this->scanner->nextToken(); |
|
465 | - $this->handler->anyElementInNS('*'); |
|
466 | - } else { |
|
467 | - // We got *|name, which means the name MUST be in a namespce, |
|
468 | - // so we pass this off to elementNameNS(). |
|
469 | - $name = $this->scanner->getNameString(); |
|
470 | - $this->handler->elementNS($name, '*'); |
|
471 | - } |
|
472 | - } else { |
|
473 | - $this->handler->anyElement(); |
|
474 | - } |
|
475 | - } |
|
476 | - } |
|
477 | - |
|
478 | - /** |
|
479 | - * Handler an attribute. |
|
480 | - * An attribute can be in one of two forms: |
|
481 | - * <code>[attrName]</code> |
|
482 | - * or |
|
483 | - * <code>[attrName="AttrValue"]</code> |
|
484 | - * |
|
485 | - * This may call the following event handlers: EventHandler::attribute(). |
|
486 | - * |
|
487 | - * @throws ParseException |
|
488 | - * @throws Exception |
|
489 | - */ |
|
490 | - private function attribute() |
|
491 | - { |
|
492 | - if ($this->scanner->token === Token::LSQUARE) { |
|
493 | - $attrVal = $op = $ns = null; |
|
494 | - |
|
495 | - $this->scanner->nextToken(); |
|
496 | - $this->consumeWhitespace(); |
|
497 | - |
|
498 | - if ($this->scanner->token === Token::AT) { |
|
499 | - if ($this->strict) { |
|
500 | - throw new ParseException('The @ is illegal in attributes.'); |
|
501 | - } |
|
502 | - |
|
503 | - $this->scanner->nextToken(); |
|
504 | - $this->consumeWhitespace(); |
|
505 | - } |
|
506 | - |
|
507 | - if ($this->scanner->token === Token::STAR) { |
|
508 | - // Global namespace... requires that attr be prefixed, |
|
509 | - // so we pass this on to a namespace handler. |
|
510 | - $ns = '*'; |
|
511 | - $this->scanner->nextToken(); |
|
512 | - } |
|
513 | - if ($this->scanner->token === Token::PIPE) { |
|
514 | - // Skip this. It's a global namespace. |
|
515 | - $this->scanner->nextToken(); |
|
516 | - $this->consumeWhitespace(); |
|
517 | - } |
|
518 | - |
|
519 | - $attrName = $this->scanner->getNameString(); |
|
520 | - $this->consumeWhitespace(); |
|
521 | - |
|
522 | - // Check for namespace attribute: ns|attr. We have to peek() to make |
|
523 | - // sure that we haven't hit the |= operator, which looks the same. |
|
524 | - if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') { |
|
525 | - // We have a namespaced attribute. |
|
526 | - $ns = $attrName; |
|
527 | - $this->scanner->nextToken(); |
|
528 | - $attrName = $this->scanner->getNameString(); |
|
529 | - $this->consumeWhitespace(); |
|
530 | - } |
|
531 | - |
|
532 | - // Note: We require that operators do not have spaces |
|
533 | - // between characters, e.g. ~= , not ~ =. |
|
534 | - |
|
535 | - // Get the operator: |
|
536 | - switch ($this->scanner->token) { |
|
537 | - case Token::EQ: |
|
538 | - $this->consumeWhitespace(); |
|
539 | - $op = EventHandler::IS_EXACTLY; |
|
540 | - break; |
|
541 | - case Token::TILDE: |
|
542 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
543 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
544 | - } |
|
545 | - $op = EventHandler::CONTAINS_WITH_SPACE; |
|
546 | - break; |
|
547 | - case Token::PIPE: |
|
548 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
549 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
550 | - } |
|
551 | - $op = EventHandler::CONTAINS_WITH_HYPHEN; |
|
552 | - break; |
|
553 | - case Token::STAR: |
|
554 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
555 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
556 | - } |
|
557 | - $op = EventHandler::CONTAINS_IN_STRING; |
|
558 | - break; |
|
559 | - case Token::DOLLAR: |
|
560 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
561 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
562 | - } |
|
563 | - $op = EventHandler::ENDS_WITH; |
|
564 | - break; |
|
565 | - case Token::CARAT: |
|
566 | - if ($this->scanner->nextToken() !== Token::EQ) { |
|
567 | - $this->throwError(Token::EQ, $this->scanner->token); |
|
568 | - } |
|
569 | - $op = EventHandler::BEGINS_WITH; |
|
570 | - break; |
|
571 | - } |
|
572 | - |
|
573 | - if (isset($op)) { |
|
574 | - // Consume '=' and go on. |
|
575 | - $this->scanner->nextToken(); |
|
576 | - $this->consumeWhitespace(); |
|
577 | - |
|
578 | - // So... here we have a problem. The grammer suggests that the |
|
579 | - // value here is String1 or String2, both of which are enclosed |
|
580 | - // in quotes of some sort, and both of which allow lots of special |
|
581 | - // characters. But the spec itself includes examples like this: |
|
582 | - // [lang=fr] |
|
583 | - // So some bareword support is assumed. To get around this, we assume |
|
584 | - // that bare words follow the NAME rules, while quoted strings follow |
|
585 | - // the String1/String2 rules. |
|
586 | - |
|
587 | - if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) { |
|
588 | - $attrVal = $this->scanner->getQuotedString(); |
|
589 | - } else { |
|
590 | - $attrVal = $this->scanner->getNameString(); |
|
591 | - } |
|
592 | - |
|
593 | - if ($this->DEBUG) { |
|
594 | - print "ATTR: $attrVal AND OP: $op\n"; |
|
595 | - } |
|
596 | - } |
|
597 | - |
|
598 | - $this->consumeWhitespace(); |
|
599 | - |
|
600 | - if ($this->scanner->token !== Token::RSQUARE) { |
|
601 | - $this->throwError(Token::RSQUARE, $this->scanner->token); |
|
602 | - } |
|
603 | - |
|
604 | - if (isset($ns)) { |
|
605 | - $this->handler->attributeNS($attrName, $ns, $attrVal, $op); |
|
606 | - } elseif (isset($attrVal)) { |
|
607 | - $this->handler->attribute($attrName, $attrVal, $op); |
|
608 | - } else { |
|
609 | - $this->handler->attribute($attrName); |
|
610 | - } |
|
611 | - $this->scanner->nextToken(); |
|
612 | - } |
|
613 | - } |
|
614 | - |
|
615 | - /** |
|
616 | - * Utility for throwing a consistantly-formatted parse error. |
|
617 | - */ |
|
618 | - private function throwError($expected, $got) |
|
619 | - { |
|
620 | - $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got)); |
|
621 | - throw new ParseException($filter); |
|
622 | - } |
|
623 | - |
|
624 | - /** |
|
625 | - * @return Scanner |
|
626 | - */ |
|
627 | - public function getScanner(): Scanner |
|
628 | - { |
|
629 | - return $this->scanner; |
|
630 | - } |
|
393 | + //$buf .= $this->scanner->getQuotedString(); |
|
394 | + $buf .= $this->scanner->getPseudoClassString(); |
|
395 | + |
|
396 | + return $buf; |
|
397 | + } |
|
398 | + } |
|
399 | + |
|
400 | + /** |
|
401 | + * Handle element names. |
|
402 | + * This will call the EventHandler::elementName(). |
|
403 | + * |
|
404 | + * This handles: |
|
405 | + * <code> |
|
406 | + * name (EventHandler::element()) |
|
407 | + * |name (EventHandler::element()) |
|
408 | + * ns|name (EventHandler::elementNS()) |
|
409 | + * ns|* (EventHandler::elementNS()) |
|
410 | + * </code> |
|
411 | + */ |
|
412 | + private function elementName() |
|
413 | + { |
|
414 | + if ($this->DEBUG) { |
|
415 | + print "ELEMENT NAME\n"; |
|
416 | + } |
|
417 | + if ($this->scanner->token === Token::PIPE) { |
|
418 | + // We have '|name', which is equiv to 'name' |
|
419 | + $this->scanner->nextToken(); |
|
420 | + $this->consumeWhitespace(); |
|
421 | + $elementName = $this->scanner->getNameString(); |
|
422 | + $this->handler->element($elementName); |
|
423 | + } elseif ($this->scanner->token === Token::CHAR) { |
|
424 | + $elementName = $this->scanner->getNameString(); |
|
425 | + if ($this->scanner->token == Token::PIPE) { |
|
426 | + // Get ns|name |
|
427 | + $elementNS = $elementName; |
|
428 | + $this->scanner->nextToken(); |
|
429 | + $this->consumeWhitespace(); |
|
430 | + if ($this->scanner->token === Token::STAR) { |
|
431 | + // We have ns|* |
|
432 | + $this->handler->anyElementInNS($elementNS); |
|
433 | + $this->scanner->nextToken(); |
|
434 | + } elseif ($this->scanner->token !== Token::CHAR) { |
|
435 | + $this->throwError(Token::CHAR, $this->scanner->token); |
|
436 | + } else { |
|
437 | + $elementName = $this->scanner->getNameString(); |
|
438 | + // We have ns|name |
|
439 | + $this->handler->elementNS($elementName, $elementNS); |
|
440 | + } |
|
441 | + } else { |
|
442 | + $this->handler->element($elementName); |
|
443 | + } |
|
444 | + } |
|
445 | + } |
|
446 | + |
|
447 | + /** |
|
448 | + * Check for all elements designators. Due to the new CSS 3 namespace |
|
449 | + * support, this is slightly more complicated, now, as it handles |
|
450 | + * the *|name and *|* cases as well as *. |
|
451 | + * |
|
452 | + * Calls EventHandler::anyElement() or EventHandler::elementName(). |
|
453 | + */ |
|
454 | + private function allElements() |
|
455 | + { |
|
456 | + if ($this->scanner->token === Token::STAR) { |
|
457 | + $this->scanner->nextToken(); |
|
458 | + if ($this->scanner->token === Token::PIPE) { |
|
459 | + $this->scanner->nextToken(); |
|
460 | + if ($this->scanner->token === Token::STAR) { |
|
461 | + // We got *|*. According to spec, this requires |
|
462 | + // that the element has a namespace, so we pass it on |
|
463 | + // to the handler: |
|
464 | + $this->scanner->nextToken(); |
|
465 | + $this->handler->anyElementInNS('*'); |
|
466 | + } else { |
|
467 | + // We got *|name, which means the name MUST be in a namespce, |
|
468 | + // so we pass this off to elementNameNS(). |
|
469 | + $name = $this->scanner->getNameString(); |
|
470 | + $this->handler->elementNS($name, '*'); |
|
471 | + } |
|
472 | + } else { |
|
473 | + $this->handler->anyElement(); |
|
474 | + } |
|
475 | + } |
|
476 | + } |
|
477 | + |
|
478 | + /** |
|
479 | + * Handler an attribute. |
|
480 | + * An attribute can be in one of two forms: |
|
481 | + * <code>[attrName]</code> |
|
482 | + * or |
|
483 | + * <code>[attrName="AttrValue"]</code> |
|
484 | + * |
|
485 | + * This may call the following event handlers: EventHandler::attribute(). |
|
486 | + * |
|
487 | + * @throws ParseException |
|
488 | + * @throws Exception |
|
489 | + */ |
|
490 | + private function attribute() |
|
491 | + { |
|
492 | + if ($this->scanner->token === Token::LSQUARE) { |
|
493 | + $attrVal = $op = $ns = null; |
|
494 | + |
|
495 | + $this->scanner->nextToken(); |
|
496 | + $this->consumeWhitespace(); |
|
497 | + |
|
498 | + if ($this->scanner->token === Token::AT) { |
|
499 | + if ($this->strict) { |
|
500 | + throw new ParseException('The @ is illegal in attributes.'); |
|
501 | + } |
|
502 | + |
|
503 | + $this->scanner->nextToken(); |
|
504 | + $this->consumeWhitespace(); |
|
505 | + } |
|
506 | + |
|
507 | + if ($this->scanner->token === Token::STAR) { |
|
508 | + // Global namespace... requires that attr be prefixed, |
|
509 | + // so we pass this on to a namespace handler. |
|
510 | + $ns = '*'; |
|
511 | + $this->scanner->nextToken(); |
|
512 | + } |
|
513 | + if ($this->scanner->token === Token::PIPE) { |
|
514 | + // Skip this. It's a global namespace. |
|
515 | + $this->scanner->nextToken(); |
|
516 | + $this->consumeWhitespace(); |
|
517 | + } |
|
518 | + |
|
519 | + $attrName = $this->scanner->getNameString(); |
|
520 | + $this->consumeWhitespace(); |
|
521 | + |
|
522 | + // Check for namespace attribute: ns|attr. We have to peek() to make |
|
523 | + // sure that we haven't hit the |= operator, which looks the same. |
|
524 | + if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') { |
|
525 | + // We have a namespaced attribute. |
|
526 | + $ns = $attrName; |
|
527 | + $this->scanner->nextToken(); |
|
528 | + $attrName = $this->scanner->getNameString(); |
|
529 | + $this->consumeWhitespace(); |
|
530 | + } |
|
531 | + |
|
532 | + // Note: We require that operators do not have spaces |
|
533 | + // between characters, e.g. ~= , not ~ =. |
|
534 | + |
|
535 | + // Get the operator: |
|
536 | + switch ($this->scanner->token) { |
|
537 | + case Token::EQ: |
|
538 | + $this->consumeWhitespace(); |
|
539 | + $op = EventHandler::IS_EXACTLY; |
|
540 | + break; |
|
541 | + case Token::TILDE: |
|
542 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
543 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
544 | + } |
|
545 | + $op = EventHandler::CONTAINS_WITH_SPACE; |
|
546 | + break; |
|
547 | + case Token::PIPE: |
|
548 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
549 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
550 | + } |
|
551 | + $op = EventHandler::CONTAINS_WITH_HYPHEN; |
|
552 | + break; |
|
553 | + case Token::STAR: |
|
554 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
555 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
556 | + } |
|
557 | + $op = EventHandler::CONTAINS_IN_STRING; |
|
558 | + break; |
|
559 | + case Token::DOLLAR: |
|
560 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
561 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
562 | + } |
|
563 | + $op = EventHandler::ENDS_WITH; |
|
564 | + break; |
|
565 | + case Token::CARAT: |
|
566 | + if ($this->scanner->nextToken() !== Token::EQ) { |
|
567 | + $this->throwError(Token::EQ, $this->scanner->token); |
|
568 | + } |
|
569 | + $op = EventHandler::BEGINS_WITH; |
|
570 | + break; |
|
571 | + } |
|
572 | + |
|
573 | + if (isset($op)) { |
|
574 | + // Consume '=' and go on. |
|
575 | + $this->scanner->nextToken(); |
|
576 | + $this->consumeWhitespace(); |
|
577 | + |
|
578 | + // So... here we have a problem. The grammer suggests that the |
|
579 | + // value here is String1 or String2, both of which are enclosed |
|
580 | + // in quotes of some sort, and both of which allow lots of special |
|
581 | + // characters. But the spec itself includes examples like this: |
|
582 | + // [lang=fr] |
|
583 | + // So some bareword support is assumed. To get around this, we assume |
|
584 | + // that bare words follow the NAME rules, while quoted strings follow |
|
585 | + // the String1/String2 rules. |
|
586 | + |
|
587 | + if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) { |
|
588 | + $attrVal = $this->scanner->getQuotedString(); |
|
589 | + } else { |
|
590 | + $attrVal = $this->scanner->getNameString(); |
|
591 | + } |
|
592 | + |
|
593 | + if ($this->DEBUG) { |
|
594 | + print "ATTR: $attrVal AND OP: $op\n"; |
|
595 | + } |
|
596 | + } |
|
597 | + |
|
598 | + $this->consumeWhitespace(); |
|
599 | + |
|
600 | + if ($this->scanner->token !== Token::RSQUARE) { |
|
601 | + $this->throwError(Token::RSQUARE, $this->scanner->token); |
|
602 | + } |
|
603 | + |
|
604 | + if (isset($ns)) { |
|
605 | + $this->handler->attributeNS($attrName, $ns, $attrVal, $op); |
|
606 | + } elseif (isset($attrVal)) { |
|
607 | + $this->handler->attribute($attrName, $attrVal, $op); |
|
608 | + } else { |
|
609 | + $this->handler->attribute($attrName); |
|
610 | + } |
|
611 | + $this->scanner->nextToken(); |
|
612 | + } |
|
613 | + } |
|
614 | + |
|
615 | + /** |
|
616 | + * Utility for throwing a consistantly-formatted parse error. |
|
617 | + */ |
|
618 | + private function throwError($expected, $got) |
|
619 | + { |
|
620 | + $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got)); |
|
621 | + throw new ParseException($filter); |
|
622 | + } |
|
623 | + |
|
624 | + /** |
|
625 | + * @return Scanner |
|
626 | + */ |
|
627 | + public function getScanner(): Scanner |
|
628 | + { |
|
629 | + return $this->scanner; |
|
630 | + } |
|
631 | 631 | } |
@@ -14,55 +14,55 @@ |
||
14 | 14 | */ |
15 | 15 | class InputStream |
16 | 16 | { |
17 | - protected $stream; |
|
18 | - public $position = 0; |
|
17 | + protected $stream; |
|
18 | + public $position = 0; |
|
19 | 19 | |
20 | - /** |
|
21 | - * Build a new CSS input stream from a string. |
|
22 | - * |
|
23 | - * @param string |
|
24 | - * String to turn into an input stream. |
|
25 | - */ |
|
26 | - public function __construct($string) |
|
27 | - { |
|
28 | - $this->stream = str_split($string); |
|
29 | - } |
|
20 | + /** |
|
21 | + * Build a new CSS input stream from a string. |
|
22 | + * |
|
23 | + * @param string |
|
24 | + * String to turn into an input stream. |
|
25 | + */ |
|
26 | + public function __construct($string) |
|
27 | + { |
|
28 | + $this->stream = str_split($string); |
|
29 | + } |
|
30 | 30 | |
31 | - /** |
|
32 | - * Look ahead one character. |
|
33 | - * |
|
34 | - * @return char |
|
35 | - * Returns the next character, but does not remove it from |
|
36 | - * the stream. |
|
37 | - */ |
|
38 | - public function peek() |
|
39 | - { |
|
40 | - return $this->stream[0]; |
|
41 | - } |
|
31 | + /** |
|
32 | + * Look ahead one character. |
|
33 | + * |
|
34 | + * @return char |
|
35 | + * Returns the next character, but does not remove it from |
|
36 | + * the stream. |
|
37 | + */ |
|
38 | + public function peek() |
|
39 | + { |
|
40 | + return $this->stream[0]; |
|
41 | + } |
|
42 | 42 | |
43 | - /** |
|
44 | - * Get the next unconsumed character in the stream. |
|
45 | - * This will remove that character from the front of the |
|
46 | - * stream and return it. |
|
47 | - */ |
|
48 | - public function consume() |
|
49 | - { |
|
50 | - $ret = array_shift($this->stream); |
|
51 | - if (! empty($ret)) { |
|
52 | - $this->position++; |
|
53 | - } |
|
43 | + /** |
|
44 | + * Get the next unconsumed character in the stream. |
|
45 | + * This will remove that character from the front of the |
|
46 | + * stream and return it. |
|
47 | + */ |
|
48 | + public function consume() |
|
49 | + { |
|
50 | + $ret = array_shift($this->stream); |
|
51 | + if (! empty($ret)) { |
|
52 | + $this->position++; |
|
53 | + } |
|
54 | 54 | |
55 | - return $ret; |
|
56 | - } |
|
55 | + return $ret; |
|
56 | + } |
|
57 | 57 | |
58 | - /** |
|
59 | - * Check if the stream is empty. |
|
60 | - * |
|
61 | - * @return boolean |
|
62 | - * Returns TRUE when the stream is empty, FALSE otherwise. |
|
63 | - */ |
|
64 | - public function isEmpty() |
|
65 | - { |
|
66 | - return count($this->stream) === 0; |
|
67 | - } |
|
58 | + /** |
|
59 | + * Check if the stream is empty. |
|
60 | + * |
|
61 | + * @return boolean |
|
62 | + * Returns TRUE when the stream is empty, FALSE otherwise. |
|
63 | + */ |
|
64 | + public function isEmpty() |
|
65 | + { |
|
66 | + return count($this->stream) === 0; |
|
67 | + } |
|
68 | 68 | } |
@@ -48,7 +48,7 @@ |
||
48 | 48 | public function consume() |
49 | 49 | { |
50 | 50 | $ret = array_shift($this->stream); |
51 | - if (! empty($ret)) { |
|
51 | + if (!empty($ret)) { |
|
52 | 52 | $this->position++; |
53 | 53 | } |
54 | 54 |
@@ -15,1065 +15,1065 @@ |
||
15 | 15 | trait QueryMutators |
16 | 16 | { |
17 | 17 | |
18 | - /** |
|
19 | - * Empty everything within the specified element. |
|
20 | - * |
|
21 | - * A convenience function for removeChildren(). This is equivalent to jQuery's |
|
22 | - * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a |
|
23 | - * function name. |
|
24 | - * |
|
25 | - * @return DOMQuery |
|
26 | - * The DOMQuery object with the newly emptied elements. |
|
27 | - * @see removeChildren() |
|
28 | - * @since 2.1 |
|
29 | - * @author eabrand |
|
30 | - * @deprecated The removeChildren() function is the preferred method. |
|
31 | - */ |
|
32 | - public function emptyElement(): Query |
|
33 | - { |
|
34 | - $this->removeChildren(); |
|
35 | - |
|
36 | - return $this; |
|
37 | - } |
|
38 | - |
|
39 | - /** |
|
40 | - * Insert the given markup as the last child. |
|
41 | - * |
|
42 | - * The markup will be inserted into each match in the set. |
|
43 | - * |
|
44 | - * The same element cannot be inserted multiple times into a document. DOM |
|
45 | - * documents do not allow a single object to be inserted multiple times |
|
46 | - * into the DOM. To insert the same XML repeatedly, we must first clone |
|
47 | - * the object. This has one practical implication: Once you have inserted |
|
48 | - * an element into the object, you cannot further manipulate the original |
|
49 | - * element and expect the changes to be replciated in the appended object. |
|
50 | - * (They are not the same -- there is no shared reference.) Instead, you |
|
51 | - * will need to retrieve the appended object and operate on that. |
|
52 | - * |
|
53 | - * @param mixed $data |
|
54 | - * This can be either a string (the usual case), or a DOM Element. |
|
55 | - * |
|
56 | - * @return DOMQuery |
|
57 | - * The DOMQuery object. |
|
58 | - * @throws QueryPath::Exception |
|
59 | - * Thrown if $data is an unsupported object type. |
|
60 | - * @throws Exception |
|
61 | - * @see appendTo() |
|
62 | - * @see prepend() |
|
63 | - */ |
|
64 | - public function append($data): Query |
|
65 | - { |
|
66 | - $data = $this->prepareInsert($data); |
|
67 | - if (isset($data)) { |
|
68 | - if (empty($this->document->documentElement) && $this->matches->count() === 0) { |
|
69 | - // Then we assume we are writing to the doc root |
|
70 | - $this->document->appendChild($data); |
|
71 | - $found = new SplObjectStorage(); |
|
72 | - $found->attach($this->document->documentElement); |
|
73 | - $this->setMatches($found); |
|
74 | - } else { |
|
75 | - // You can only append in item once. So in cases where we |
|
76 | - // need to append multiple times, we have to clone the node. |
|
77 | - foreach ($this->matches as $m) { |
|
78 | - // DOMDocumentFragments are even more troublesome, as they don't |
|
79 | - // always clone correctly. So we have to clone their children. |
|
80 | - if ($data instanceof DOMDocumentFragment) { |
|
81 | - foreach ($data->childNodes as $n) { |
|
82 | - $m->appendChild($n->cloneNode(true)); |
|
83 | - } |
|
84 | - } else { |
|
85 | - // Otherwise a standard clone will do. |
|
86 | - $m->appendChild($data->cloneNode(true)); |
|
87 | - } |
|
88 | - |
|
89 | - } |
|
90 | - } |
|
91 | - |
|
92 | - } |
|
93 | - |
|
94 | - return $this; |
|
95 | - } |
|
96 | - |
|
97 | - /** |
|
98 | - * Insert the given markup as the first child. |
|
99 | - * |
|
100 | - * The markup will be inserted into each match in the set. |
|
101 | - * |
|
102 | - * @param mixed $data |
|
103 | - * This can be either a string (the usual case), or a DOM Element. |
|
104 | - * |
|
105 | - * @return DOMQuery |
|
106 | - * @throws QueryPath::Exception |
|
107 | - * Thrown if $data is an unsupported object type. |
|
108 | - * @throws Exception |
|
109 | - * @see after() |
|
110 | - * @see prependTo() |
|
111 | - * @see append() |
|
112 | - * @see before() |
|
113 | - */ |
|
114 | - public function prepend($data): Query |
|
115 | - { |
|
116 | - $data = $this->prepareInsert($data); |
|
117 | - if (isset($data)) { |
|
118 | - foreach ($this->matches as $m) { |
|
119 | - $ins = $data->cloneNode(true); |
|
120 | - if ($m->hasChildNodes()) { |
|
121 | - $m->insertBefore($ins, $m->childNodes->item(0)); |
|
122 | - } else { |
|
123 | - $m->appendChild($ins); |
|
124 | - } |
|
125 | - } |
|
126 | - } |
|
127 | - |
|
128 | - return $this; |
|
129 | - } |
|
130 | - |
|
131 | - /** |
|
132 | - * Take all nodes in the current object and prepend them to the children nodes of |
|
133 | - * each matched node in the passed-in DOMQuery object. |
|
134 | - * |
|
135 | - * This will iterate through each item in the current DOMQuery object and |
|
136 | - * add each item to the beginning of the children of each element in the |
|
137 | - * passed-in DOMQuery object. |
|
138 | - * |
|
139 | - * @param DOMQuery $dest |
|
140 | - * The destination DOMQuery object. |
|
141 | - * |
|
142 | - * @return DOMQuery |
|
143 | - * The original DOMQuery, unmodified. NOT the destination DOMQuery. |
|
144 | - * @throws QueryPath::Exception |
|
145 | - * Thrown if $data is an unsupported object type. |
|
146 | - * @see appendTo() |
|
147 | - * @see insertBefore() |
|
148 | - * @see insertAfter() |
|
149 | - * @see prepend() |
|
150 | - */ |
|
151 | - public function prependTo(Query $dest) |
|
152 | - { |
|
153 | - foreach ($this->matches as $m) { |
|
154 | - $dest->prepend($m); |
|
155 | - } |
|
156 | - |
|
157 | - return $this; |
|
158 | - } |
|
159 | - |
|
160 | - /** |
|
161 | - * Insert the given data before each element in the current set of matches. |
|
162 | - * |
|
163 | - * This will take the give data (XML or HTML) and put it before each of the items that |
|
164 | - * the DOMQuery object currently contains. Contrast this with after(). |
|
165 | - * |
|
166 | - * @param mixed $data |
|
167 | - * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement, |
|
168 | - * or the other usual suspects. (See {@link qp()}). |
|
169 | - * |
|
170 | - * @return DOMQuery |
|
171 | - * Returns the DOMQuery with the new modifications. The list of elements currently |
|
172 | - * selected will remain the same. |
|
173 | - * @throws QueryPath::Exception |
|
174 | - * Thrown if $data is an unsupported object type. |
|
175 | - * @throws Exception |
|
176 | - * @see append() |
|
177 | - * @see prepend() |
|
178 | - * @see insertBefore() |
|
179 | - * @see after() |
|
180 | - */ |
|
181 | - public function before($data): Query |
|
182 | - { |
|
183 | - $data = $this->prepareInsert($data); |
|
184 | - foreach ($this->matches as $m) { |
|
185 | - $ins = $data->cloneNode(true); |
|
186 | - $m->parentNode->insertBefore($ins, $m); |
|
187 | - } |
|
188 | - |
|
189 | - return $this; |
|
190 | - } |
|
191 | - |
|
192 | - /** |
|
193 | - * Insert the current elements into the destination document. |
|
194 | - * The items are inserted before each element in the given DOMQuery document. |
|
195 | - * That is, they will be siblings with the current elements. |
|
196 | - * |
|
197 | - * @param Query $dest |
|
198 | - * Destination DOMQuery document. |
|
199 | - * |
|
200 | - * @return DOMQuery |
|
201 | - * The current DOMQuery object, unaltered. Only the destination DOMQuery |
|
202 | - * object is altered. |
|
203 | - * @throws QueryPath::Exception |
|
204 | - * Thrown if $data is an unsupported object type. |
|
205 | - * @see insertAfter() |
|
206 | - * @see appendTo() |
|
207 | - * @see before() |
|
208 | - */ |
|
209 | - public function insertBefore(Query $dest): Query |
|
210 | - { |
|
211 | - foreach ($this->matches as $m) { |
|
212 | - $dest->before($m); |
|
213 | - } |
|
214 | - |
|
215 | - return $this; |
|
216 | - } |
|
217 | - |
|
218 | - /** |
|
219 | - * Insert the contents of the current DOMQuery after the nodes in the |
|
220 | - * destination DOMQuery object. |
|
221 | - * |
|
222 | - * @param Query $dest |
|
223 | - * Destination object where the current elements will be deposited. |
|
224 | - * |
|
225 | - * @return DOMQuery |
|
226 | - * The present DOMQuery, unaltered. Only the destination object is altered. |
|
227 | - * @throws QueryPath::Exception |
|
228 | - * Thrown if $data is an unsupported object type. |
|
229 | - * @see insertBefore() |
|
230 | - * @see append() |
|
231 | - * @see after() |
|
232 | - */ |
|
233 | - public function insertAfter(Query $dest): Query |
|
234 | - { |
|
235 | - foreach ($this->matches as $m) { |
|
236 | - $dest->after($m); |
|
237 | - } |
|
238 | - |
|
239 | - return $this; |
|
240 | - } |
|
241 | - |
|
242 | - /** |
|
243 | - * Insert the given data after each element in the current DOMQuery object. |
|
244 | - * |
|
245 | - * This inserts the element as a peer to the currently matched elements. |
|
246 | - * Contrast this with {@link append()}, which inserts the data as children |
|
247 | - * of matched elements. |
|
248 | - * |
|
249 | - * @param mixed $data |
|
250 | - * The data to be appended. |
|
251 | - * |
|
252 | - * @return DOMQuery |
|
253 | - * The DOMQuery object (with the items inserted). |
|
254 | - * @throws QueryPath::Exception |
|
255 | - * Thrown if $data is an unsupported object type. |
|
256 | - * @throws Exception |
|
257 | - * @see before() |
|
258 | - * @see append() |
|
259 | - */ |
|
260 | - public function after($data): Query |
|
261 | - { |
|
262 | - if (empty($data)) { |
|
263 | - return $this; |
|
264 | - } |
|
265 | - $data = $this->prepareInsert($data); |
|
266 | - foreach ($this->matches as $m) { |
|
267 | - $ins = $data->cloneNode(true); |
|
268 | - if (isset($m->nextSibling)) { |
|
269 | - $m->parentNode->insertBefore($ins, $m->nextSibling); |
|
270 | - } else { |
|
271 | - $m->parentNode->appendChild($ins); |
|
272 | - } |
|
273 | - } |
|
274 | - |
|
275 | - return $this; |
|
276 | - } |
|
277 | - |
|
278 | - /** |
|
279 | - * Replace the existing element(s) in the list with a new one. |
|
280 | - * |
|
281 | - * @param mixed $new |
|
282 | - * A DOMElement or XML in a string. This will replace all elements |
|
283 | - * currently wrapped in the DOMQuery object. |
|
284 | - * |
|
285 | - * @return DOMQuery |
|
286 | - * The DOMQuery object wrapping <b>the items that were removed</b>. |
|
287 | - * This remains consistent with the jQuery API. |
|
288 | - * @throws Exception |
|
289 | - * @throws ParseException |
|
290 | - * @throws QueryPath |
|
291 | - * @see append() |
|
292 | - * @see prepend() |
|
293 | - * @see before() |
|
294 | - * @see after() |
|
295 | - * @see remove() |
|
296 | - * @see replaceAll() |
|
297 | - */ |
|
298 | - public function replaceWith($new): Query |
|
299 | - { |
|
300 | - $data = $this->prepareInsert($new); |
|
301 | - $found = new SplObjectStorage(); |
|
302 | - foreach ($this->matches as $m) { |
|
303 | - $parent = $m->parentNode; |
|
304 | - $parent->insertBefore($data->cloneNode(true), $m); |
|
305 | - $found->attach($parent->removeChild($m)); |
|
306 | - } |
|
307 | - |
|
308 | - return $this->inst($found, null); |
|
309 | - } |
|
310 | - |
|
311 | - /** |
|
312 | - * Remove the parent element from the selected node or nodes. |
|
313 | - * |
|
314 | - * This takes the given list of nodes and "unwraps" them, moving them out of their parent |
|
315 | - * node, and then deleting the parent node. |
|
316 | - * |
|
317 | - * For example, consider this: |
|
318 | - * |
|
319 | - * @code |
|
320 | - * <root><wrapper><content/></wrapper></root> |
|
321 | - * @endcode |
|
322 | - * |
|
323 | - * Now we can run this code: |
|
324 | - * @code |
|
325 | - * qp($xml, 'content')->unwrap(); |
|
326 | - * @endcode |
|
327 | - * |
|
328 | - * This will result in: |
|
329 | - * |
|
330 | - * @code |
|
331 | - * <root><content/></root> |
|
332 | - * @endcode |
|
333 | - * This is the opposite of wrap(). |
|
334 | - * |
|
335 | - * <b>The root element cannot be unwrapped.</b> It has no parents. |
|
336 | - * If you attempt to use unwrap on a root element, this will throw a |
|
337 | - * QueryPath::Exception. (You can, however, "Unwrap" a child that is |
|
338 | - * a direct descendant of the root element. This will remove the root |
|
339 | - * element, and replace the child as the root element. Be careful, though. |
|
340 | - * You cannot set more than one child as a root element.) |
|
341 | - * |
|
342 | - * @return DOMQuery |
|
343 | - * The DOMQuery object, with the same element(s) selected. |
|
344 | - * @throws Exception |
|
345 | - * @see wrap() |
|
346 | - * @since 2.1 |
|
347 | - * @author mbutcher |
|
348 | - */ |
|
349 | - public function unwrap(): Query |
|
350 | - { |
|
351 | - // We do this in two loops in order to |
|
352 | - // capture the case where two matches are |
|
353 | - // under the same parent. Othwerwise we might |
|
354 | - // remove a match before we can move it. |
|
355 | - $parents = new SplObjectStorage(); |
|
356 | - foreach ($this->matches as $m) { |
|
357 | - |
|
358 | - // Cannot unwrap the root element. |
|
359 | - if ($m->isSameNode($m->ownerDocument->documentElement)) { |
|
360 | - throw new Exception('Cannot unwrap the root element.'); |
|
361 | - } |
|
362 | - |
|
363 | - // Move children to peer of parent. |
|
364 | - $parent = $m->parentNode; |
|
365 | - $old = $parent->removeChild($m); |
|
366 | - $parent->parentNode->insertBefore($old, $parent); |
|
367 | - $parents->attach($parent); |
|
368 | - } |
|
369 | - |
|
370 | - // Now that all the children are moved, we |
|
371 | - // remove all of the parents. |
|
372 | - foreach ($parents as $ele) { |
|
373 | - $ele->parentNode->removeChild($ele); |
|
374 | - } |
|
375 | - |
|
376 | - return $this; |
|
377 | - } |
|
378 | - |
|
379 | - /** |
|
380 | - * Wrap each element inside of the given markup. |
|
381 | - * |
|
382 | - * Markup is usually a string, but it can also be a DOMNode, a document |
|
383 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
384 | - * the first item in the list will be used.) |
|
385 | - * |
|
386 | - * @param mixed $markup |
|
387 | - * Markup that will wrap each element in the current list. |
|
388 | - * |
|
389 | - * @return DOMQuery |
|
390 | - * The DOMQuery object with the wrapping changes made. |
|
391 | - * @throws Exception |
|
392 | - * @throws QueryPath |
|
393 | - * @see wrapAll() |
|
394 | - * @see wrapInner() |
|
395 | - */ |
|
396 | - public function wrap($markup): Query |
|
397 | - { |
|
398 | - $data = $this->prepareInsert($markup); |
|
399 | - |
|
400 | - // If the markup passed in is empty, we don't do any wrapping. |
|
401 | - if (empty($data)) { |
|
402 | - return $this; |
|
403 | - } |
|
404 | - |
|
405 | - foreach ($this->matches as $m) { |
|
406 | - if ($data instanceof DOMDocumentFragment) { |
|
407 | - $copy = $data->firstChild->cloneNode(true); |
|
408 | - } else { |
|
409 | - $copy = $data->cloneNode(true); |
|
410 | - } |
|
411 | - |
|
412 | - // XXX: Should be able to avoid doing this over and over. |
|
413 | - if ($copy->hasChildNodes()) { |
|
414 | - $deepest = $this->deepestNode($copy); |
|
415 | - // FIXME: Does this need a different data structure? |
|
416 | - $bottom = $deepest[0]; |
|
417 | - } else { |
|
418 | - $bottom = $copy; |
|
419 | - } |
|
420 | - |
|
421 | - $parent = $m->parentNode; |
|
422 | - $parent->insertBefore($copy, $m); |
|
423 | - $m = $parent->removeChild($m); |
|
424 | - $bottom->appendChild($m); |
|
425 | - } |
|
426 | - |
|
427 | - return $this; |
|
428 | - } |
|
429 | - |
|
430 | - /** |
|
431 | - * Wrap all elements inside of the given markup. |
|
432 | - * |
|
433 | - * So all elements will be grouped together under this single marked up |
|
434 | - * item. This works by first determining the parent element of the first item |
|
435 | - * in the list. It then moves all of the matching elements under the wrapper |
|
436 | - * and inserts the wrapper where that first element was found. (This is in |
|
437 | - * accordance with the way jQuery works.) |
|
438 | - * |
|
439 | - * Markup is usually XML in a string, but it can also be a DOMNode, a document |
|
440 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
441 | - * the first item in the list will be used.) |
|
442 | - * |
|
443 | - * @param string $markup |
|
444 | - * Markup that will wrap all elements in the current list. |
|
445 | - * |
|
446 | - * @return DOMQuery |
|
447 | - * The DOMQuery object with the wrapping changes made. |
|
448 | - * @throws Exception |
|
449 | - * @throws QueryPath |
|
450 | - * @see wrap() |
|
451 | - * @see wrapInner() |
|
452 | - */ |
|
453 | - public function wrapAll($markup) |
|
454 | - { |
|
455 | - if ($this->matches->count() === 0) { |
|
456 | - return; |
|
457 | - } |
|
458 | - |
|
459 | - $data = $this->prepareInsert($markup); |
|
460 | - |
|
461 | - if (empty($data)) { |
|
462 | - return $this; |
|
463 | - } |
|
464 | - |
|
465 | - if ($data instanceof DOMDocumentFragment) { |
|
466 | - $data = $data->firstChild->cloneNode(true); |
|
467 | - } else { |
|
468 | - $data = $data->cloneNode(true); |
|
469 | - } |
|
470 | - |
|
471 | - if ($data->hasChildNodes()) { |
|
472 | - $deepest = $this->deepestNode($data); |
|
473 | - // FIXME: Does this need fixing? |
|
474 | - $bottom = $deepest[0]; |
|
475 | - } else { |
|
476 | - $bottom = $data; |
|
477 | - } |
|
478 | - |
|
479 | - $first = $this->getFirstMatch(); |
|
480 | - $parent = $first->parentNode; |
|
481 | - $parent->insertBefore($data, $first); |
|
482 | - foreach ($this->matches as $m) { |
|
483 | - $bottom->appendChild($m->parentNode->removeChild($m)); |
|
484 | - } |
|
485 | - |
|
486 | - return $this; |
|
487 | - } |
|
488 | - |
|
489 | - /** |
|
490 | - * Wrap the child elements of each item in the list with the given markup. |
|
491 | - * |
|
492 | - * Markup is usually a string, but it can also be a DOMNode, a document |
|
493 | - * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
494 | - * the first item in the list will be used.) |
|
495 | - * |
|
496 | - * @param string $markup |
|
497 | - * Markup that will wrap children of each element in the current list. |
|
498 | - * |
|
499 | - * @return DOMQuery |
|
500 | - * The DOMQuery object with the wrapping changes made. |
|
501 | - * @throws Exception |
|
502 | - * @throws QueryPath |
|
503 | - * @see wrap() |
|
504 | - * @see wrapAll() |
|
505 | - */ |
|
506 | - public function wrapInner($markup) |
|
507 | - { |
|
508 | - $data = $this->prepareInsert($markup); |
|
509 | - |
|
510 | - // No data? Short circuit. |
|
511 | - if (empty($data)) { |
|
512 | - return $this; |
|
513 | - } |
|
514 | - |
|
515 | - foreach ($this->matches as $m) { |
|
516 | - if ($data instanceof DOMDocumentFragment) { |
|
517 | - $wrapper = $data->firstChild->cloneNode(true); |
|
518 | - } else { |
|
519 | - $wrapper = $data->cloneNode(true); |
|
520 | - } |
|
521 | - |
|
522 | - if ($wrapper->hasChildNodes()) { |
|
523 | - $deepest = $this->deepestNode($wrapper); |
|
524 | - // FIXME: ??? |
|
525 | - $bottom = $deepest[0]; |
|
526 | - } else { |
|
527 | - $bottom = $wrapper; |
|
528 | - } |
|
529 | - |
|
530 | - if ($m->hasChildNodes()) { |
|
531 | - while ($m->firstChild) { |
|
532 | - $kid = $m->removeChild($m->firstChild); |
|
533 | - $bottom->appendChild($kid); |
|
534 | - } |
|
535 | - } |
|
536 | - |
|
537 | - $m->appendChild($wrapper); |
|
538 | - } |
|
539 | - |
|
540 | - return $this; |
|
541 | - } |
|
542 | - |
|
543 | - /** |
|
544 | - * Reduce the set of matches to the deepest child node in the tree. |
|
545 | - * |
|
546 | - * This loops through the matches and looks for the deepest child node of all of |
|
547 | - * the matches. "Deepest", here, is relative to the nodes in the list. It is |
|
548 | - * calculated as the distance from the starting node to the most distant child |
|
549 | - * node. In other words, it is not necessarily the farthest node from the root |
|
550 | - * element, but the farthest note from the matched element. |
|
551 | - * |
|
552 | - * In the case where there are multiple nodes at the same depth, all of the |
|
553 | - * nodes at that depth will be included. |
|
554 | - * |
|
555 | - * @return DOMQuery |
|
556 | - * The DOMQuery wrapping the single deepest node. |
|
557 | - * @throws ParseException |
|
558 | - */ |
|
559 | - public function deepest(): Query |
|
560 | - { |
|
561 | - $deepest = 0; |
|
562 | - $winner = new SplObjectStorage(); |
|
563 | - foreach ($this->matches as $m) { |
|
564 | - $local_deepest = 0; |
|
565 | - $local_ele = $this->deepestNode($m, 0, null, $local_deepest); |
|
566 | - |
|
567 | - // Replace with the new deepest. |
|
568 | - if ($local_deepest > $deepest) { |
|
569 | - $winner = new SplObjectStorage(); |
|
570 | - foreach ($local_ele as $lele) { |
|
571 | - $winner->attach($lele); |
|
572 | - } |
|
573 | - $deepest = $local_deepest; |
|
574 | - } // Augument with other equally deep elements. |
|
575 | - elseif ($local_deepest === $deepest) { |
|
576 | - foreach ($local_ele as $lele) { |
|
577 | - $winner->attach($lele); |
|
578 | - } |
|
579 | - } |
|
580 | - } |
|
581 | - |
|
582 | - return $this->inst($winner, null); |
|
583 | - } |
|
584 | - |
|
585 | - /** |
|
586 | - * Add a class to all elements in the current DOMQuery. |
|
587 | - * |
|
588 | - * This searchers for a class attribute on each item wrapped by the current |
|
589 | - * DOMNode object. If no attribute is found, a new one is added and its value |
|
590 | - * is set to $class. If a class attribute is found, then the value is appended |
|
591 | - * on to the end. |
|
592 | - * |
|
593 | - * @param string $class |
|
594 | - * The name of the class. |
|
595 | - * |
|
596 | - * @return DOMQuery |
|
597 | - * Returns the DOMQuery object. |
|
598 | - * @see css() |
|
599 | - * @see attr() |
|
600 | - * @see removeClass() |
|
601 | - * @see hasClass() |
|
602 | - */ |
|
603 | - public function addClass($class) |
|
604 | - { |
|
605 | - foreach ($this->matches as $m) { |
|
606 | - if ($m->hasAttribute('class')) { |
|
607 | - $val = $m->getAttribute('class'); |
|
608 | - $m->setAttribute('class', $val . ' ' . $class); |
|
609 | - } else { |
|
610 | - $m->setAttribute('class', $class); |
|
611 | - } |
|
612 | - } |
|
613 | - |
|
614 | - return $this; |
|
615 | - } |
|
616 | - |
|
617 | - /** |
|
618 | - * Remove the named class from any element in the DOMQuery that has it. |
|
619 | - * |
|
620 | - * This may result in the entire class attribute being removed. If there |
|
621 | - * are other items in the class attribute, though, they will not be removed. |
|
622 | - * |
|
623 | - * Example: |
|
624 | - * Consider this XML: |
|
625 | - * |
|
626 | - * @code |
|
627 | - * <element class="first second"/> |
|
628 | - * @endcode |
|
629 | - * |
|
630 | - * Executing this fragment of code will remove only the 'first' class: |
|
631 | - * @code |
|
632 | - * qp(document, 'element')->removeClass('first'); |
|
633 | - * @endcode |
|
634 | - * |
|
635 | - * The resulting XML will be: |
|
636 | - * @code |
|
637 | - * <element class="second"/> |
|
638 | - * @endcode |
|
639 | - * |
|
640 | - * To remove the entire 'class' attribute, you should use {@see removeAttr()}. |
|
641 | - * |
|
642 | - * @param string $class |
|
643 | - * The class name to remove. |
|
644 | - * |
|
645 | - * @return DOMQuery |
|
646 | - * The modified DOMNode object. |
|
647 | - * @see attr() |
|
648 | - * @see addClass() |
|
649 | - * @see hasClass() |
|
650 | - */ |
|
651 | - public function removeClass($class = false): Query |
|
652 | - { |
|
653 | - if (empty($class)) { |
|
654 | - foreach ($this->matches as $m) { |
|
655 | - $m->removeAttribute('class'); |
|
656 | - } |
|
657 | - } else { |
|
658 | - $to_remove = array_filter(explode(' ', $class)); |
|
659 | - foreach ($this->matches as $m) { |
|
660 | - if ($m->hasAttribute('class')) { |
|
661 | - $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
|
662 | - $buf = []; |
|
663 | - foreach ($vals as $v) { |
|
664 | - if (! in_array($v, $to_remove)) { |
|
665 | - $buf[] = $v; |
|
666 | - } |
|
667 | - } |
|
668 | - if (empty($buf)) { |
|
669 | - $m->removeAttribute('class'); |
|
670 | - } else { |
|
671 | - $m->setAttribute('class', implode(' ', $buf)); |
|
672 | - } |
|
673 | - } |
|
674 | - } |
|
675 | - } |
|
676 | - |
|
677 | - return $this; |
|
678 | - } |
|
679 | - |
|
680 | - /** |
|
681 | - * Detach any items from the list if they match the selector. |
|
682 | - * |
|
683 | - * In other words, each item that matches the selector will be removed |
|
684 | - * from the DOM document. The returned DOMQuery wraps the list of |
|
685 | - * removed elements. |
|
686 | - * |
|
687 | - * If no selector is specified, this will remove all current matches from |
|
688 | - * the document. |
|
689 | - * |
|
690 | - * @param string $selector |
|
691 | - * A CSS Selector. |
|
692 | - * |
|
693 | - * @return DOMQuery |
|
694 | - * The Query path wrapping a list of removed items. |
|
695 | - * @throws ParseException |
|
696 | - * @see replaceWith() |
|
697 | - * @see removeChildren() |
|
698 | - * @since 2.1 |
|
699 | - * @author eabrand |
|
700 | - * @see replaceAll() |
|
701 | - */ |
|
702 | - public function detach($selector = null): Query |
|
703 | - { |
|
704 | - if (null !== $selector) { |
|
705 | - $this->find($selector); |
|
706 | - } |
|
707 | - |
|
708 | - $found = new SplObjectStorage(); |
|
709 | - $this->last = $this->matches; |
|
710 | - foreach ($this->matches as $item) { |
|
711 | - // The item returned is (according to docs) different from |
|
712 | - // the one passed in, so we have to re-store it. |
|
713 | - $found->attach($item->parentNode->removeChild($item)); |
|
714 | - } |
|
715 | - |
|
716 | - return $this->inst($found, null); |
|
717 | - } |
|
718 | - |
|
719 | - /** |
|
720 | - * Attach any items from the list if they match the selector. |
|
721 | - * |
|
722 | - * If no selector is specified, this will remove all current matches from |
|
723 | - * the document. |
|
724 | - * |
|
725 | - * @param DOMQuery $dest |
|
726 | - * A DOMQuery Selector. |
|
727 | - * |
|
728 | - * @return DOMQuery |
|
729 | - * The Query path wrapping a list of removed items. |
|
730 | - * @throws QueryPath |
|
731 | - * @throws Exception |
|
732 | - * @see removeChildren() |
|
733 | - * @since 2.1 |
|
734 | - * @author eabrand |
|
735 | - * @see replaceAll() |
|
736 | - * @see replaceWith() |
|
737 | - */ |
|
738 | - public function attach(DOMQuery $dest): Query |
|
739 | - { |
|
740 | - foreach ($this->last as $m) { |
|
741 | - $dest->append($m); |
|
742 | - } |
|
743 | - |
|
744 | - return $this; |
|
745 | - } |
|
746 | - |
|
747 | - /** |
|
748 | - * Append the current elements to the destination passed into the function. |
|
749 | - * |
|
750 | - * This cycles through all of the current matches and appends them to |
|
751 | - * the context given in $destination. If a selector is provided then the |
|
752 | - * $destination is queried (using that selector) prior to the data being |
|
753 | - * appended. The data is then appended to the found items. |
|
754 | - * |
|
755 | - * @param DOMQuery $dest |
|
756 | - * A DOMQuery object that will be appended to. |
|
757 | - * |
|
758 | - * @return DOMQuery |
|
759 | - * The original DOMQuery, unaltered. Only the destination DOMQuery will |
|
760 | - * be modified. |
|
761 | - * @throws QueryPath::Exception |
|
762 | - * Thrown if $data is an unsupported object type. |
|
763 | - * @throws Exception |
|
764 | - * @see append() |
|
765 | - * @see prependTo() |
|
766 | - */ |
|
767 | - public function appendTo(DOMQuery $dest): Query |
|
768 | - { |
|
769 | - foreach ($this->matches as $m) { |
|
770 | - $dest->append($m); |
|
771 | - } |
|
772 | - |
|
773 | - return $this; |
|
774 | - } |
|
775 | - |
|
776 | - /** |
|
777 | - * Remove any items from the list if they match the selector. |
|
778 | - * |
|
779 | - * In other words, each item that matches the selector will be remove |
|
780 | - * from the DOM document. The returned DOMQuery wraps the list of |
|
781 | - * removed elements. |
|
782 | - * |
|
783 | - * If no selector is specified, this will remove all current matches from |
|
784 | - * the document. |
|
785 | - * |
|
786 | - * @param string $selector |
|
787 | - * A CSS Selector. |
|
788 | - * |
|
789 | - * @return DOMQuery |
|
790 | - * The Query path wrapping a list of removed items. |
|
791 | - * @throws ParseException |
|
792 | - * @see replaceWith() |
|
793 | - * @see removeChildren() |
|
794 | - * @see replaceAll() |
|
795 | - */ |
|
796 | - public function remove($selector = null): Query |
|
797 | - { |
|
798 | - if (! empty($selector)) { |
|
799 | - // Do a non-destructive find. |
|
800 | - $query = new QueryPathEventHandler($this->matches); |
|
801 | - $query->find($selector); |
|
802 | - $matches = $query->getMatches(); |
|
803 | - } else { |
|
804 | - $matches = $this->matches; |
|
805 | - } |
|
806 | - |
|
807 | - $found = new SplObjectStorage(); |
|
808 | - foreach ($matches as $item) { |
|
809 | - // The item returned is (according to docs) different from |
|
810 | - // the one passed in, so we have to re-store it. |
|
811 | - $found->attach($item->parentNode->removeChild($item)); |
|
812 | - } |
|
813 | - |
|
814 | - // Return a clone DOMQuery with just the removed items. If |
|
815 | - // no items are found, this will return an empty DOMQuery. |
|
816 | - return count($found) === 0 ? new static() : new static($found); |
|
817 | - } |
|
818 | - |
|
819 | - /** |
|
820 | - * This replaces everything that matches the selector with the first value |
|
821 | - * in the current list. |
|
822 | - * |
|
823 | - * This is the reverse of replaceWith. |
|
824 | - * |
|
825 | - * Unlike jQuery, DOMQuery cannot assume a default document. Consequently, |
|
826 | - * you must specify the intended destination document. If it is omitted, the |
|
827 | - * present document is assumed to be tthe document. However, that can result |
|
828 | - * in undefined behavior if the selector and the replacement are not sufficiently |
|
829 | - * distinct. |
|
830 | - * |
|
831 | - * @param string $selector |
|
832 | - * The selector. |
|
833 | - * @param DOMDocument $document |
|
834 | - * The destination document. |
|
835 | - * |
|
836 | - * @return DOMQuery |
|
837 | - * The DOMQuery wrapping the modified document. |
|
838 | - * @throws ParseException |
|
839 | - * @see remove() |
|
840 | - * @see replaceWith() |
|
841 | - * @deprecated Due to the fact that this is not a particularly friendly method, |
|
842 | - * and that it can be easily replicated using {@see replaceWith()}, it is to be |
|
843 | - * considered deprecated. |
|
844 | - */ |
|
845 | - public function replaceAll($selector, DOMDocument $document): Query |
|
846 | - { |
|
847 | - $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode(''); |
|
848 | - |
|
849 | - $c = new QueryPathEventHandler($document); |
|
850 | - $c->find($selector); |
|
851 | - $temp = $c->getMatches(); |
|
852 | - foreach ($temp as $item) { |
|
853 | - $node = $replacement->cloneNode(); |
|
854 | - $node = $document->importNode($node); |
|
855 | - $item->parentNode->replaceChild($node, $item); |
|
856 | - } |
|
857 | - |
|
858 | - return QueryPath::with($document, null, $this->options); |
|
859 | - } |
|
860 | - |
|
861 | - /** |
|
862 | - * Add more elements to the current set of matches. |
|
863 | - * |
|
864 | - * This begins the new query at the top of the DOM again. The results found |
|
865 | - * when running this selector are then merged into the existing results. In |
|
866 | - * this way, you can add additional elements to the existing set. |
|
867 | - * |
|
868 | - * @param string $selector |
|
869 | - * A valid selector. |
|
870 | - * |
|
871 | - * @return DOMQuery |
|
872 | - * The DOMQuery object with the newly added elements. |
|
873 | - * @see append() |
|
874 | - * @see after() |
|
875 | - * @see andSelf() |
|
876 | - * @see end() |
|
877 | - */ |
|
878 | - public function add($selector): Query |
|
879 | - { |
|
880 | - |
|
881 | - // This is destructive, so we need to set $last: |
|
882 | - $this->last = $this->matches; |
|
883 | - |
|
884 | - foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) { |
|
885 | - $this->matches->attach($item); |
|
886 | - } |
|
887 | - |
|
888 | - return $this; |
|
889 | - } |
|
890 | - |
|
891 | - /** |
|
892 | - * Remove all child nodes. |
|
893 | - * |
|
894 | - * This is equivalent to jQuery's empty() function. (However, empty() is a |
|
895 | - * PHP built-in, and cannot be used as a method name.) |
|
896 | - * |
|
897 | - * @return DOMQuery |
|
898 | - * The DOMQuery object with the child nodes removed. |
|
899 | - * @see replaceWith() |
|
900 | - * @see replaceAll() |
|
901 | - * @see remove() |
|
902 | - */ |
|
903 | - public function removeChildren(): Query |
|
904 | - { |
|
905 | - foreach ($this->matches as $m) { |
|
906 | - while ($kid = $m->firstChild) { |
|
907 | - $m->removeChild($kid); |
|
908 | - } |
|
909 | - } |
|
910 | - |
|
911 | - return $this; |
|
912 | - } |
|
913 | - |
|
914 | - /** |
|
915 | - * Get/set an attribute. |
|
916 | - * - If no parameters are specified, this returns an associative array of all |
|
917 | - * name/value pairs. |
|
918 | - * - If both $name and $value are set, then this will set the attribute name/value |
|
919 | - * pair for all items in this object. |
|
920 | - * - If $name is set, and is an array, then |
|
921 | - * all attributes in the array will be set for all items in this object. |
|
922 | - * - If $name is a string and is set, then the attribute value will be returned. |
|
923 | - * |
|
924 | - * When an attribute value is retrieved, only the attribute value of the FIRST |
|
925 | - * match is returned. |
|
926 | - * |
|
927 | - * @param mixed $name |
|
928 | - * The name of the attribute or an associative array of name/value pairs. |
|
929 | - * @param string $value |
|
930 | - * A value (used only when setting an individual property). |
|
931 | - * |
|
932 | - * @return mixed |
|
933 | - * If this was a setter request, return the DOMQuery object. If this was |
|
934 | - * an access request (getter), return the string value. |
|
935 | - * @see removeAttr() |
|
936 | - * @see tag() |
|
937 | - * @see hasAttr() |
|
938 | - * @see hasClass() |
|
939 | - */ |
|
940 | - public function attr($name = null, $value = null) |
|
941 | - { |
|
942 | - // Default case: Return all attributes as an assoc array. |
|
943 | - if (is_null($name)) { |
|
944 | - if ($this->matches->count() === 0) { |
|
945 | - return null; |
|
946 | - } |
|
947 | - $ele = $this->getFirstMatch(); |
|
948 | - $buffer = []; |
|
949 | - |
|
950 | - // This does not appear to be part of the DOM |
|
951 | - // spec. Nor is it documented. But it works. |
|
952 | - foreach ($ele->attributes as $name => $attrNode) { |
|
953 | - $buffer[$name] = $attrNode->value; |
|
954 | - } |
|
955 | - |
|
956 | - return $buffer; |
|
957 | - } |
|
958 | - |
|
959 | - // multi-setter |
|
960 | - if (is_array($name)) { |
|
961 | - foreach ($name as $k => $v) { |
|
962 | - foreach ($this->matches as $m) { |
|
963 | - $m->setAttribute($k, $v); |
|
964 | - } |
|
965 | - } |
|
966 | - |
|
967 | - return $this; |
|
968 | - } |
|
969 | - // setter |
|
970 | - if (isset($value)) { |
|
971 | - foreach ($this->matches as $m) { |
|
972 | - $m->setAttribute($name, $value); |
|
973 | - } |
|
974 | - |
|
975 | - return $this; |
|
976 | - } |
|
977 | - |
|
978 | - //getter |
|
979 | - if ($this->matches->count() === 0) { |
|
980 | - return null; |
|
981 | - } |
|
982 | - |
|
983 | - // Special node type handler: |
|
984 | - if ($name === 'nodeType') { |
|
985 | - return $this->getFirstMatch()->nodeType; |
|
986 | - } |
|
987 | - |
|
988 | - // Always return first match's attr. |
|
989 | - return $this->getFirstMatch()->getAttribute($name); |
|
990 | - } |
|
991 | - |
|
992 | - /** |
|
993 | - * Set/get a CSS value for the current element(s). |
|
994 | - * This sets the CSS value for each element in the DOMQuery object. |
|
995 | - * It does this by setting (or getting) the style attribute (without a namespace). |
|
996 | - * |
|
997 | - * For example, consider this code: |
|
998 | - * |
|
999 | - * @code |
|
1000 | - * <?php |
|
1001 | - * qp(HTML_STUB, 'body')->css('background-color','red')->html(); |
|
1002 | - * ?> |
|
1003 | - * @endcode |
|
1004 | - * This will return the following HTML: |
|
1005 | - * @code |
|
1006 | - * <body style="background-color: red"/> |
|
1007 | - * @endcode |
|
1008 | - * |
|
1009 | - * If no parameters are passed into this function, then the current style |
|
1010 | - * element will be returned unparsed. Example: |
|
1011 | - * @code |
|
1012 | - * <?php |
|
1013 | - * qp(HTML_STUB, 'body')->css('background-color','red')->css(); |
|
1014 | - * ?> |
|
1015 | - * @endcode |
|
1016 | - * This will return the following: |
|
1017 | - * @code |
|
1018 | - * background-color: red |
|
1019 | - * @endcode |
|
1020 | - * |
|
1021 | - * As of QueryPath 2.1, existing style attributes will be merged with new attributes. |
|
1022 | - * (In previous versions of QueryPath, a call to css() overwrite the existing style |
|
1023 | - * values). |
|
1024 | - * |
|
1025 | - * @param mixed $name |
|
1026 | - * If this is a string, it will be used as a CSS name. If it is an array, |
|
1027 | - * this will assume it is an array of name/value pairs of CSS rules. It will |
|
1028 | - * apply all rules to all elements in the set. |
|
1029 | - * @param string $value |
|
1030 | - * The value to set. This is only set if $name is a string. |
|
1031 | - * |
|
1032 | - * @return DOMQuery |
|
1033 | - */ |
|
1034 | - public function css($name = null, $value = '') |
|
1035 | - { |
|
1036 | - if (empty($name)) { |
|
1037 | - return $this->attr('style'); |
|
1038 | - } |
|
1039 | - |
|
1040 | - // Get any existing CSS. |
|
1041 | - $css = []; |
|
1042 | - foreach ($this->matches as $match) { |
|
1043 | - $style = $match->getAttribute('style'); |
|
1044 | - if (! empty($style)) { |
|
1045 | - // XXX: Is this sufficient? |
|
1046 | - $style_array = explode(';', $style); |
|
1047 | - foreach ($style_array as $item) { |
|
1048 | - $item = trim($item); |
|
1049 | - |
|
1050 | - // Skip empty attributes. |
|
1051 | - if ($item === '') { |
|
1052 | - continue; |
|
1053 | - } |
|
1054 | - |
|
1055 | - [$css_att, $css_val] = explode(':', $item, 2); |
|
1056 | - $css[$css_att] = trim($css_val); |
|
1057 | - } |
|
1058 | - } |
|
1059 | - } |
|
1060 | - |
|
1061 | - if (is_array($name)) { |
|
1062 | - // Use array_merge instead of + to preserve order. |
|
1063 | - $css = array_merge($css, $name); |
|
1064 | - } else { |
|
1065 | - $css[$name] = $value; |
|
1066 | - } |
|
1067 | - |
|
1068 | - // Collapse CSS into a string. |
|
1069 | - $format = '%s: %s;'; |
|
1070 | - $css_string = ''; |
|
1071 | - foreach ($css as $n => $v) { |
|
1072 | - $css_string .= sprintf($format, $n, trim($v)); |
|
1073 | - } |
|
1074 | - |
|
1075 | - $this->attr('style', $css_string); |
|
1076 | - |
|
1077 | - return $this; |
|
1078 | - } |
|
18 | + /** |
|
19 | + * Empty everything within the specified element. |
|
20 | + * |
|
21 | + * A convenience function for removeChildren(). This is equivalent to jQuery's |
|
22 | + * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a |
|
23 | + * function name. |
|
24 | + * |
|
25 | + * @return DOMQuery |
|
26 | + * The DOMQuery object with the newly emptied elements. |
|
27 | + * @see removeChildren() |
|
28 | + * @since 2.1 |
|
29 | + * @author eabrand |
|
30 | + * @deprecated The removeChildren() function is the preferred method. |
|
31 | + */ |
|
32 | + public function emptyElement(): Query |
|
33 | + { |
|
34 | + $this->removeChildren(); |
|
35 | + |
|
36 | + return $this; |
|
37 | + } |
|
38 | + |
|
39 | + /** |
|
40 | + * Insert the given markup as the last child. |
|
41 | + * |
|
42 | + * The markup will be inserted into each match in the set. |
|
43 | + * |
|
44 | + * The same element cannot be inserted multiple times into a document. DOM |
|
45 | + * documents do not allow a single object to be inserted multiple times |
|
46 | + * into the DOM. To insert the same XML repeatedly, we must first clone |
|
47 | + * the object. This has one practical implication: Once you have inserted |
|
48 | + * an element into the object, you cannot further manipulate the original |
|
49 | + * element and expect the changes to be replciated in the appended object. |
|
50 | + * (They are not the same -- there is no shared reference.) Instead, you |
|
51 | + * will need to retrieve the appended object and operate on that. |
|
52 | + * |
|
53 | + * @param mixed $data |
|
54 | + * This can be either a string (the usual case), or a DOM Element. |
|
55 | + * |
|
56 | + * @return DOMQuery |
|
57 | + * The DOMQuery object. |
|
58 | + * @throws QueryPath::Exception |
|
59 | + * Thrown if $data is an unsupported object type. |
|
60 | + * @throws Exception |
|
61 | + * @see appendTo() |
|
62 | + * @see prepend() |
|
63 | + */ |
|
64 | + public function append($data): Query |
|
65 | + { |
|
66 | + $data = $this->prepareInsert($data); |
|
67 | + if (isset($data)) { |
|
68 | + if (empty($this->document->documentElement) && $this->matches->count() === 0) { |
|
69 | + // Then we assume we are writing to the doc root |
|
70 | + $this->document->appendChild($data); |
|
71 | + $found = new SplObjectStorage(); |
|
72 | + $found->attach($this->document->documentElement); |
|
73 | + $this->setMatches($found); |
|
74 | + } else { |
|
75 | + // You can only append in item once. So in cases where we |
|
76 | + // need to append multiple times, we have to clone the node. |
|
77 | + foreach ($this->matches as $m) { |
|
78 | + // DOMDocumentFragments are even more troublesome, as they don't |
|
79 | + // always clone correctly. So we have to clone their children. |
|
80 | + if ($data instanceof DOMDocumentFragment) { |
|
81 | + foreach ($data->childNodes as $n) { |
|
82 | + $m->appendChild($n->cloneNode(true)); |
|
83 | + } |
|
84 | + } else { |
|
85 | + // Otherwise a standard clone will do. |
|
86 | + $m->appendChild($data->cloneNode(true)); |
|
87 | + } |
|
88 | + |
|
89 | + } |
|
90 | + } |
|
91 | + |
|
92 | + } |
|
93 | + |
|
94 | + return $this; |
|
95 | + } |
|
96 | + |
|
97 | + /** |
|
98 | + * Insert the given markup as the first child. |
|
99 | + * |
|
100 | + * The markup will be inserted into each match in the set. |
|
101 | + * |
|
102 | + * @param mixed $data |
|
103 | + * This can be either a string (the usual case), or a DOM Element. |
|
104 | + * |
|
105 | + * @return DOMQuery |
|
106 | + * @throws QueryPath::Exception |
|
107 | + * Thrown if $data is an unsupported object type. |
|
108 | + * @throws Exception |
|
109 | + * @see after() |
|
110 | + * @see prependTo() |
|
111 | + * @see append() |
|
112 | + * @see before() |
|
113 | + */ |
|
114 | + public function prepend($data): Query |
|
115 | + { |
|
116 | + $data = $this->prepareInsert($data); |
|
117 | + if (isset($data)) { |
|
118 | + foreach ($this->matches as $m) { |
|
119 | + $ins = $data->cloneNode(true); |
|
120 | + if ($m->hasChildNodes()) { |
|
121 | + $m->insertBefore($ins, $m->childNodes->item(0)); |
|
122 | + } else { |
|
123 | + $m->appendChild($ins); |
|
124 | + } |
|
125 | + } |
|
126 | + } |
|
127 | + |
|
128 | + return $this; |
|
129 | + } |
|
130 | + |
|
131 | + /** |
|
132 | + * Take all nodes in the current object and prepend them to the children nodes of |
|
133 | + * each matched node in the passed-in DOMQuery object. |
|
134 | + * |
|
135 | + * This will iterate through each item in the current DOMQuery object and |
|
136 | + * add each item to the beginning of the children of each element in the |
|
137 | + * passed-in DOMQuery object. |
|
138 | + * |
|
139 | + * @param DOMQuery $dest |
|
140 | + * The destination DOMQuery object. |
|
141 | + * |
|
142 | + * @return DOMQuery |
|
143 | + * The original DOMQuery, unmodified. NOT the destination DOMQuery. |
|
144 | + * @throws QueryPath::Exception |
|
145 | + * Thrown if $data is an unsupported object type. |
|
146 | + * @see appendTo() |
|
147 | + * @see insertBefore() |
|
148 | + * @see insertAfter() |
|
149 | + * @see prepend() |
|
150 | + */ |
|
151 | + public function prependTo(Query $dest) |
|
152 | + { |
|
153 | + foreach ($this->matches as $m) { |
|
154 | + $dest->prepend($m); |
|
155 | + } |
|
156 | + |
|
157 | + return $this; |
|
158 | + } |
|
159 | + |
|
160 | + /** |
|
161 | + * Insert the given data before each element in the current set of matches. |
|
162 | + * |
|
163 | + * This will take the give data (XML or HTML) and put it before each of the items that |
|
164 | + * the DOMQuery object currently contains. Contrast this with after(). |
|
165 | + * |
|
166 | + * @param mixed $data |
|
167 | + * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement, |
|
168 | + * or the other usual suspects. (See {@link qp()}). |
|
169 | + * |
|
170 | + * @return DOMQuery |
|
171 | + * Returns the DOMQuery with the new modifications. The list of elements currently |
|
172 | + * selected will remain the same. |
|
173 | + * @throws QueryPath::Exception |
|
174 | + * Thrown if $data is an unsupported object type. |
|
175 | + * @throws Exception |
|
176 | + * @see append() |
|
177 | + * @see prepend() |
|
178 | + * @see insertBefore() |
|
179 | + * @see after() |
|
180 | + */ |
|
181 | + public function before($data): Query |
|
182 | + { |
|
183 | + $data = $this->prepareInsert($data); |
|
184 | + foreach ($this->matches as $m) { |
|
185 | + $ins = $data->cloneNode(true); |
|
186 | + $m->parentNode->insertBefore($ins, $m); |
|
187 | + } |
|
188 | + |
|
189 | + return $this; |
|
190 | + } |
|
191 | + |
|
192 | + /** |
|
193 | + * Insert the current elements into the destination document. |
|
194 | + * The items are inserted before each element in the given DOMQuery document. |
|
195 | + * That is, they will be siblings with the current elements. |
|
196 | + * |
|
197 | + * @param Query $dest |
|
198 | + * Destination DOMQuery document. |
|
199 | + * |
|
200 | + * @return DOMQuery |
|
201 | + * The current DOMQuery object, unaltered. Only the destination DOMQuery |
|
202 | + * object is altered. |
|
203 | + * @throws QueryPath::Exception |
|
204 | + * Thrown if $data is an unsupported object type. |
|
205 | + * @see insertAfter() |
|
206 | + * @see appendTo() |
|
207 | + * @see before() |
|
208 | + */ |
|
209 | + public function insertBefore(Query $dest): Query |
|
210 | + { |
|
211 | + foreach ($this->matches as $m) { |
|
212 | + $dest->before($m); |
|
213 | + } |
|
214 | + |
|
215 | + return $this; |
|
216 | + } |
|
217 | + |
|
218 | + /** |
|
219 | + * Insert the contents of the current DOMQuery after the nodes in the |
|
220 | + * destination DOMQuery object. |
|
221 | + * |
|
222 | + * @param Query $dest |
|
223 | + * Destination object where the current elements will be deposited. |
|
224 | + * |
|
225 | + * @return DOMQuery |
|
226 | + * The present DOMQuery, unaltered. Only the destination object is altered. |
|
227 | + * @throws QueryPath::Exception |
|
228 | + * Thrown if $data is an unsupported object type. |
|
229 | + * @see insertBefore() |
|
230 | + * @see append() |
|
231 | + * @see after() |
|
232 | + */ |
|
233 | + public function insertAfter(Query $dest): Query |
|
234 | + { |
|
235 | + foreach ($this->matches as $m) { |
|
236 | + $dest->after($m); |
|
237 | + } |
|
238 | + |
|
239 | + return $this; |
|
240 | + } |
|
241 | + |
|
242 | + /** |
|
243 | + * Insert the given data after each element in the current DOMQuery object. |
|
244 | + * |
|
245 | + * This inserts the element as a peer to the currently matched elements. |
|
246 | + * Contrast this with {@link append()}, which inserts the data as children |
|
247 | + * of matched elements. |
|
248 | + * |
|
249 | + * @param mixed $data |
|
250 | + * The data to be appended. |
|
251 | + * |
|
252 | + * @return DOMQuery |
|
253 | + * The DOMQuery object (with the items inserted). |
|
254 | + * @throws QueryPath::Exception |
|
255 | + * Thrown if $data is an unsupported object type. |
|
256 | + * @throws Exception |
|
257 | + * @see before() |
|
258 | + * @see append() |
|
259 | + */ |
|
260 | + public function after($data): Query |
|
261 | + { |
|
262 | + if (empty($data)) { |
|
263 | + return $this; |
|
264 | + } |
|
265 | + $data = $this->prepareInsert($data); |
|
266 | + foreach ($this->matches as $m) { |
|
267 | + $ins = $data->cloneNode(true); |
|
268 | + if (isset($m->nextSibling)) { |
|
269 | + $m->parentNode->insertBefore($ins, $m->nextSibling); |
|
270 | + } else { |
|
271 | + $m->parentNode->appendChild($ins); |
|
272 | + } |
|
273 | + } |
|
274 | + |
|
275 | + return $this; |
|
276 | + } |
|
277 | + |
|
278 | + /** |
|
279 | + * Replace the existing element(s) in the list with a new one. |
|
280 | + * |
|
281 | + * @param mixed $new |
|
282 | + * A DOMElement or XML in a string. This will replace all elements |
|
283 | + * currently wrapped in the DOMQuery object. |
|
284 | + * |
|
285 | + * @return DOMQuery |
|
286 | + * The DOMQuery object wrapping <b>the items that were removed</b>. |
|
287 | + * This remains consistent with the jQuery API. |
|
288 | + * @throws Exception |
|
289 | + * @throws ParseException |
|
290 | + * @throws QueryPath |
|
291 | + * @see append() |
|
292 | + * @see prepend() |
|
293 | + * @see before() |
|
294 | + * @see after() |
|
295 | + * @see remove() |
|
296 | + * @see replaceAll() |
|
297 | + */ |
|
298 | + public function replaceWith($new): Query |
|
299 | + { |
|
300 | + $data = $this->prepareInsert($new); |
|
301 | + $found = new SplObjectStorage(); |
|
302 | + foreach ($this->matches as $m) { |
|
303 | + $parent = $m->parentNode; |
|
304 | + $parent->insertBefore($data->cloneNode(true), $m); |
|
305 | + $found->attach($parent->removeChild($m)); |
|
306 | + } |
|
307 | + |
|
308 | + return $this->inst($found, null); |
|
309 | + } |
|
310 | + |
|
311 | + /** |
|
312 | + * Remove the parent element from the selected node or nodes. |
|
313 | + * |
|
314 | + * This takes the given list of nodes and "unwraps" them, moving them out of their parent |
|
315 | + * node, and then deleting the parent node. |
|
316 | + * |
|
317 | + * For example, consider this: |
|
318 | + * |
|
319 | + * @code |
|
320 | + * <root><wrapper><content/></wrapper></root> |
|
321 | + * @endcode |
|
322 | + * |
|
323 | + * Now we can run this code: |
|
324 | + * @code |
|
325 | + * qp($xml, 'content')->unwrap(); |
|
326 | + * @endcode |
|
327 | + * |
|
328 | + * This will result in: |
|
329 | + * |
|
330 | + * @code |
|
331 | + * <root><content/></root> |
|
332 | + * @endcode |
|
333 | + * This is the opposite of wrap(). |
|
334 | + * |
|
335 | + * <b>The root element cannot be unwrapped.</b> It has no parents. |
|
336 | + * If you attempt to use unwrap on a root element, this will throw a |
|
337 | + * QueryPath::Exception. (You can, however, "Unwrap" a child that is |
|
338 | + * a direct descendant of the root element. This will remove the root |
|
339 | + * element, and replace the child as the root element. Be careful, though. |
|
340 | + * You cannot set more than one child as a root element.) |
|
341 | + * |
|
342 | + * @return DOMQuery |
|
343 | + * The DOMQuery object, with the same element(s) selected. |
|
344 | + * @throws Exception |
|
345 | + * @see wrap() |
|
346 | + * @since 2.1 |
|
347 | + * @author mbutcher |
|
348 | + */ |
|
349 | + public function unwrap(): Query |
|
350 | + { |
|
351 | + // We do this in two loops in order to |
|
352 | + // capture the case where two matches are |
|
353 | + // under the same parent. Othwerwise we might |
|
354 | + // remove a match before we can move it. |
|
355 | + $parents = new SplObjectStorage(); |
|
356 | + foreach ($this->matches as $m) { |
|
357 | + |
|
358 | + // Cannot unwrap the root element. |
|
359 | + if ($m->isSameNode($m->ownerDocument->documentElement)) { |
|
360 | + throw new Exception('Cannot unwrap the root element.'); |
|
361 | + } |
|
362 | + |
|
363 | + // Move children to peer of parent. |
|
364 | + $parent = $m->parentNode; |
|
365 | + $old = $parent->removeChild($m); |
|
366 | + $parent->parentNode->insertBefore($old, $parent); |
|
367 | + $parents->attach($parent); |
|
368 | + } |
|
369 | + |
|
370 | + // Now that all the children are moved, we |
|
371 | + // remove all of the parents. |
|
372 | + foreach ($parents as $ele) { |
|
373 | + $ele->parentNode->removeChild($ele); |
|
374 | + } |
|
375 | + |
|
376 | + return $this; |
|
377 | + } |
|
378 | + |
|
379 | + /** |
|
380 | + * Wrap each element inside of the given markup. |
|
381 | + * |
|
382 | + * Markup is usually a string, but it can also be a DOMNode, a document |
|
383 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
384 | + * the first item in the list will be used.) |
|
385 | + * |
|
386 | + * @param mixed $markup |
|
387 | + * Markup that will wrap each element in the current list. |
|
388 | + * |
|
389 | + * @return DOMQuery |
|
390 | + * The DOMQuery object with the wrapping changes made. |
|
391 | + * @throws Exception |
|
392 | + * @throws QueryPath |
|
393 | + * @see wrapAll() |
|
394 | + * @see wrapInner() |
|
395 | + */ |
|
396 | + public function wrap($markup): Query |
|
397 | + { |
|
398 | + $data = $this->prepareInsert($markup); |
|
399 | + |
|
400 | + // If the markup passed in is empty, we don't do any wrapping. |
|
401 | + if (empty($data)) { |
|
402 | + return $this; |
|
403 | + } |
|
404 | + |
|
405 | + foreach ($this->matches as $m) { |
|
406 | + if ($data instanceof DOMDocumentFragment) { |
|
407 | + $copy = $data->firstChild->cloneNode(true); |
|
408 | + } else { |
|
409 | + $copy = $data->cloneNode(true); |
|
410 | + } |
|
411 | + |
|
412 | + // XXX: Should be able to avoid doing this over and over. |
|
413 | + if ($copy->hasChildNodes()) { |
|
414 | + $deepest = $this->deepestNode($copy); |
|
415 | + // FIXME: Does this need a different data structure? |
|
416 | + $bottom = $deepest[0]; |
|
417 | + } else { |
|
418 | + $bottom = $copy; |
|
419 | + } |
|
420 | + |
|
421 | + $parent = $m->parentNode; |
|
422 | + $parent->insertBefore($copy, $m); |
|
423 | + $m = $parent->removeChild($m); |
|
424 | + $bottom->appendChild($m); |
|
425 | + } |
|
426 | + |
|
427 | + return $this; |
|
428 | + } |
|
429 | + |
|
430 | + /** |
|
431 | + * Wrap all elements inside of the given markup. |
|
432 | + * |
|
433 | + * So all elements will be grouped together under this single marked up |
|
434 | + * item. This works by first determining the parent element of the first item |
|
435 | + * in the list. It then moves all of the matching elements under the wrapper |
|
436 | + * and inserts the wrapper where that first element was found. (This is in |
|
437 | + * accordance with the way jQuery works.) |
|
438 | + * |
|
439 | + * Markup is usually XML in a string, but it can also be a DOMNode, a document |
|
440 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
441 | + * the first item in the list will be used.) |
|
442 | + * |
|
443 | + * @param string $markup |
|
444 | + * Markup that will wrap all elements in the current list. |
|
445 | + * |
|
446 | + * @return DOMQuery |
|
447 | + * The DOMQuery object with the wrapping changes made. |
|
448 | + * @throws Exception |
|
449 | + * @throws QueryPath |
|
450 | + * @see wrap() |
|
451 | + * @see wrapInner() |
|
452 | + */ |
|
453 | + public function wrapAll($markup) |
|
454 | + { |
|
455 | + if ($this->matches->count() === 0) { |
|
456 | + return; |
|
457 | + } |
|
458 | + |
|
459 | + $data = $this->prepareInsert($markup); |
|
460 | + |
|
461 | + if (empty($data)) { |
|
462 | + return $this; |
|
463 | + } |
|
464 | + |
|
465 | + if ($data instanceof DOMDocumentFragment) { |
|
466 | + $data = $data->firstChild->cloneNode(true); |
|
467 | + } else { |
|
468 | + $data = $data->cloneNode(true); |
|
469 | + } |
|
470 | + |
|
471 | + if ($data->hasChildNodes()) { |
|
472 | + $deepest = $this->deepestNode($data); |
|
473 | + // FIXME: Does this need fixing? |
|
474 | + $bottom = $deepest[0]; |
|
475 | + } else { |
|
476 | + $bottom = $data; |
|
477 | + } |
|
478 | + |
|
479 | + $first = $this->getFirstMatch(); |
|
480 | + $parent = $first->parentNode; |
|
481 | + $parent->insertBefore($data, $first); |
|
482 | + foreach ($this->matches as $m) { |
|
483 | + $bottom->appendChild($m->parentNode->removeChild($m)); |
|
484 | + } |
|
485 | + |
|
486 | + return $this; |
|
487 | + } |
|
488 | + |
|
489 | + /** |
|
490 | + * Wrap the child elements of each item in the list with the given markup. |
|
491 | + * |
|
492 | + * Markup is usually a string, but it can also be a DOMNode, a document |
|
493 | + * fragment, a SimpleXMLElement, or another DOMNode object (in which case |
|
494 | + * the first item in the list will be used.) |
|
495 | + * |
|
496 | + * @param string $markup |
|
497 | + * Markup that will wrap children of each element in the current list. |
|
498 | + * |
|
499 | + * @return DOMQuery |
|
500 | + * The DOMQuery object with the wrapping changes made. |
|
501 | + * @throws Exception |
|
502 | + * @throws QueryPath |
|
503 | + * @see wrap() |
|
504 | + * @see wrapAll() |
|
505 | + */ |
|
506 | + public function wrapInner($markup) |
|
507 | + { |
|
508 | + $data = $this->prepareInsert($markup); |
|
509 | + |
|
510 | + // No data? Short circuit. |
|
511 | + if (empty($data)) { |
|
512 | + return $this; |
|
513 | + } |
|
514 | + |
|
515 | + foreach ($this->matches as $m) { |
|
516 | + if ($data instanceof DOMDocumentFragment) { |
|
517 | + $wrapper = $data->firstChild->cloneNode(true); |
|
518 | + } else { |
|
519 | + $wrapper = $data->cloneNode(true); |
|
520 | + } |
|
521 | + |
|
522 | + if ($wrapper->hasChildNodes()) { |
|
523 | + $deepest = $this->deepestNode($wrapper); |
|
524 | + // FIXME: ??? |
|
525 | + $bottom = $deepest[0]; |
|
526 | + } else { |
|
527 | + $bottom = $wrapper; |
|
528 | + } |
|
529 | + |
|
530 | + if ($m->hasChildNodes()) { |
|
531 | + while ($m->firstChild) { |
|
532 | + $kid = $m->removeChild($m->firstChild); |
|
533 | + $bottom->appendChild($kid); |
|
534 | + } |
|
535 | + } |
|
536 | + |
|
537 | + $m->appendChild($wrapper); |
|
538 | + } |
|
539 | + |
|
540 | + return $this; |
|
541 | + } |
|
542 | + |
|
543 | + /** |
|
544 | + * Reduce the set of matches to the deepest child node in the tree. |
|
545 | + * |
|
546 | + * This loops through the matches and looks for the deepest child node of all of |
|
547 | + * the matches. "Deepest", here, is relative to the nodes in the list. It is |
|
548 | + * calculated as the distance from the starting node to the most distant child |
|
549 | + * node. In other words, it is not necessarily the farthest node from the root |
|
550 | + * element, but the farthest note from the matched element. |
|
551 | + * |
|
552 | + * In the case where there are multiple nodes at the same depth, all of the |
|
553 | + * nodes at that depth will be included. |
|
554 | + * |
|
555 | + * @return DOMQuery |
|
556 | + * The DOMQuery wrapping the single deepest node. |
|
557 | + * @throws ParseException |
|
558 | + */ |
|
559 | + public function deepest(): Query |
|
560 | + { |
|
561 | + $deepest = 0; |
|
562 | + $winner = new SplObjectStorage(); |
|
563 | + foreach ($this->matches as $m) { |
|
564 | + $local_deepest = 0; |
|
565 | + $local_ele = $this->deepestNode($m, 0, null, $local_deepest); |
|
566 | + |
|
567 | + // Replace with the new deepest. |
|
568 | + if ($local_deepest > $deepest) { |
|
569 | + $winner = new SplObjectStorage(); |
|
570 | + foreach ($local_ele as $lele) { |
|
571 | + $winner->attach($lele); |
|
572 | + } |
|
573 | + $deepest = $local_deepest; |
|
574 | + } // Augument with other equally deep elements. |
|
575 | + elseif ($local_deepest === $deepest) { |
|
576 | + foreach ($local_ele as $lele) { |
|
577 | + $winner->attach($lele); |
|
578 | + } |
|
579 | + } |
|
580 | + } |
|
581 | + |
|
582 | + return $this->inst($winner, null); |
|
583 | + } |
|
584 | + |
|
585 | + /** |
|
586 | + * Add a class to all elements in the current DOMQuery. |
|
587 | + * |
|
588 | + * This searchers for a class attribute on each item wrapped by the current |
|
589 | + * DOMNode object. If no attribute is found, a new one is added and its value |
|
590 | + * is set to $class. If a class attribute is found, then the value is appended |
|
591 | + * on to the end. |
|
592 | + * |
|
593 | + * @param string $class |
|
594 | + * The name of the class. |
|
595 | + * |
|
596 | + * @return DOMQuery |
|
597 | + * Returns the DOMQuery object. |
|
598 | + * @see css() |
|
599 | + * @see attr() |
|
600 | + * @see removeClass() |
|
601 | + * @see hasClass() |
|
602 | + */ |
|
603 | + public function addClass($class) |
|
604 | + { |
|
605 | + foreach ($this->matches as $m) { |
|
606 | + if ($m->hasAttribute('class')) { |
|
607 | + $val = $m->getAttribute('class'); |
|
608 | + $m->setAttribute('class', $val . ' ' . $class); |
|
609 | + } else { |
|
610 | + $m->setAttribute('class', $class); |
|
611 | + } |
|
612 | + } |
|
613 | + |
|
614 | + return $this; |
|
615 | + } |
|
616 | + |
|
617 | + /** |
|
618 | + * Remove the named class from any element in the DOMQuery that has it. |
|
619 | + * |
|
620 | + * This may result in the entire class attribute being removed. If there |
|
621 | + * are other items in the class attribute, though, they will not be removed. |
|
622 | + * |
|
623 | + * Example: |
|
624 | + * Consider this XML: |
|
625 | + * |
|
626 | + * @code |
|
627 | + * <element class="first second"/> |
|
628 | + * @endcode |
|
629 | + * |
|
630 | + * Executing this fragment of code will remove only the 'first' class: |
|
631 | + * @code |
|
632 | + * qp(document, 'element')->removeClass('first'); |
|
633 | + * @endcode |
|
634 | + * |
|
635 | + * The resulting XML will be: |
|
636 | + * @code |
|
637 | + * <element class="second"/> |
|
638 | + * @endcode |
|
639 | + * |
|
640 | + * To remove the entire 'class' attribute, you should use {@see removeAttr()}. |
|
641 | + * |
|
642 | + * @param string $class |
|
643 | + * The class name to remove. |
|
644 | + * |
|
645 | + * @return DOMQuery |
|
646 | + * The modified DOMNode object. |
|
647 | + * @see attr() |
|
648 | + * @see addClass() |
|
649 | + * @see hasClass() |
|
650 | + */ |
|
651 | + public function removeClass($class = false): Query |
|
652 | + { |
|
653 | + if (empty($class)) { |
|
654 | + foreach ($this->matches as $m) { |
|
655 | + $m->removeAttribute('class'); |
|
656 | + } |
|
657 | + } else { |
|
658 | + $to_remove = array_filter(explode(' ', $class)); |
|
659 | + foreach ($this->matches as $m) { |
|
660 | + if ($m->hasAttribute('class')) { |
|
661 | + $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
|
662 | + $buf = []; |
|
663 | + foreach ($vals as $v) { |
|
664 | + if (! in_array($v, $to_remove)) { |
|
665 | + $buf[] = $v; |
|
666 | + } |
|
667 | + } |
|
668 | + if (empty($buf)) { |
|
669 | + $m->removeAttribute('class'); |
|
670 | + } else { |
|
671 | + $m->setAttribute('class', implode(' ', $buf)); |
|
672 | + } |
|
673 | + } |
|
674 | + } |
|
675 | + } |
|
676 | + |
|
677 | + return $this; |
|
678 | + } |
|
679 | + |
|
680 | + /** |
|
681 | + * Detach any items from the list if they match the selector. |
|
682 | + * |
|
683 | + * In other words, each item that matches the selector will be removed |
|
684 | + * from the DOM document. The returned DOMQuery wraps the list of |
|
685 | + * removed elements. |
|
686 | + * |
|
687 | + * If no selector is specified, this will remove all current matches from |
|
688 | + * the document. |
|
689 | + * |
|
690 | + * @param string $selector |
|
691 | + * A CSS Selector. |
|
692 | + * |
|
693 | + * @return DOMQuery |
|
694 | + * The Query path wrapping a list of removed items. |
|
695 | + * @throws ParseException |
|
696 | + * @see replaceWith() |
|
697 | + * @see removeChildren() |
|
698 | + * @since 2.1 |
|
699 | + * @author eabrand |
|
700 | + * @see replaceAll() |
|
701 | + */ |
|
702 | + public function detach($selector = null): Query |
|
703 | + { |
|
704 | + if (null !== $selector) { |
|
705 | + $this->find($selector); |
|
706 | + } |
|
707 | + |
|
708 | + $found = new SplObjectStorage(); |
|
709 | + $this->last = $this->matches; |
|
710 | + foreach ($this->matches as $item) { |
|
711 | + // The item returned is (according to docs) different from |
|
712 | + // the one passed in, so we have to re-store it. |
|
713 | + $found->attach($item->parentNode->removeChild($item)); |
|
714 | + } |
|
715 | + |
|
716 | + return $this->inst($found, null); |
|
717 | + } |
|
718 | + |
|
719 | + /** |
|
720 | + * Attach any items from the list if they match the selector. |
|
721 | + * |
|
722 | + * If no selector is specified, this will remove all current matches from |
|
723 | + * the document. |
|
724 | + * |
|
725 | + * @param DOMQuery $dest |
|
726 | + * A DOMQuery Selector. |
|
727 | + * |
|
728 | + * @return DOMQuery |
|
729 | + * The Query path wrapping a list of removed items. |
|
730 | + * @throws QueryPath |
|
731 | + * @throws Exception |
|
732 | + * @see removeChildren() |
|
733 | + * @since 2.1 |
|
734 | + * @author eabrand |
|
735 | + * @see replaceAll() |
|
736 | + * @see replaceWith() |
|
737 | + */ |
|
738 | + public function attach(DOMQuery $dest): Query |
|
739 | + { |
|
740 | + foreach ($this->last as $m) { |
|
741 | + $dest->append($m); |
|
742 | + } |
|
743 | + |
|
744 | + return $this; |
|
745 | + } |
|
746 | + |
|
747 | + /** |
|
748 | + * Append the current elements to the destination passed into the function. |
|
749 | + * |
|
750 | + * This cycles through all of the current matches and appends them to |
|
751 | + * the context given in $destination. If a selector is provided then the |
|
752 | + * $destination is queried (using that selector) prior to the data being |
|
753 | + * appended. The data is then appended to the found items. |
|
754 | + * |
|
755 | + * @param DOMQuery $dest |
|
756 | + * A DOMQuery object that will be appended to. |
|
757 | + * |
|
758 | + * @return DOMQuery |
|
759 | + * The original DOMQuery, unaltered. Only the destination DOMQuery will |
|
760 | + * be modified. |
|
761 | + * @throws QueryPath::Exception |
|
762 | + * Thrown if $data is an unsupported object type. |
|
763 | + * @throws Exception |
|
764 | + * @see append() |
|
765 | + * @see prependTo() |
|
766 | + */ |
|
767 | + public function appendTo(DOMQuery $dest): Query |
|
768 | + { |
|
769 | + foreach ($this->matches as $m) { |
|
770 | + $dest->append($m); |
|
771 | + } |
|
772 | + |
|
773 | + return $this; |
|
774 | + } |
|
775 | + |
|
776 | + /** |
|
777 | + * Remove any items from the list if they match the selector. |
|
778 | + * |
|
779 | + * In other words, each item that matches the selector will be remove |
|
780 | + * from the DOM document. The returned DOMQuery wraps the list of |
|
781 | + * removed elements. |
|
782 | + * |
|
783 | + * If no selector is specified, this will remove all current matches from |
|
784 | + * the document. |
|
785 | + * |
|
786 | + * @param string $selector |
|
787 | + * A CSS Selector. |
|
788 | + * |
|
789 | + * @return DOMQuery |
|
790 | + * The Query path wrapping a list of removed items. |
|
791 | + * @throws ParseException |
|
792 | + * @see replaceWith() |
|
793 | + * @see removeChildren() |
|
794 | + * @see replaceAll() |
|
795 | + */ |
|
796 | + public function remove($selector = null): Query |
|
797 | + { |
|
798 | + if (! empty($selector)) { |
|
799 | + // Do a non-destructive find. |
|
800 | + $query = new QueryPathEventHandler($this->matches); |
|
801 | + $query->find($selector); |
|
802 | + $matches = $query->getMatches(); |
|
803 | + } else { |
|
804 | + $matches = $this->matches; |
|
805 | + } |
|
806 | + |
|
807 | + $found = new SplObjectStorage(); |
|
808 | + foreach ($matches as $item) { |
|
809 | + // The item returned is (according to docs) different from |
|
810 | + // the one passed in, so we have to re-store it. |
|
811 | + $found->attach($item->parentNode->removeChild($item)); |
|
812 | + } |
|
813 | + |
|
814 | + // Return a clone DOMQuery with just the removed items. If |
|
815 | + // no items are found, this will return an empty DOMQuery. |
|
816 | + return count($found) === 0 ? new static() : new static($found); |
|
817 | + } |
|
818 | + |
|
819 | + /** |
|
820 | + * This replaces everything that matches the selector with the first value |
|
821 | + * in the current list. |
|
822 | + * |
|
823 | + * This is the reverse of replaceWith. |
|
824 | + * |
|
825 | + * Unlike jQuery, DOMQuery cannot assume a default document. Consequently, |
|
826 | + * you must specify the intended destination document. If it is omitted, the |
|
827 | + * present document is assumed to be tthe document. However, that can result |
|
828 | + * in undefined behavior if the selector and the replacement are not sufficiently |
|
829 | + * distinct. |
|
830 | + * |
|
831 | + * @param string $selector |
|
832 | + * The selector. |
|
833 | + * @param DOMDocument $document |
|
834 | + * The destination document. |
|
835 | + * |
|
836 | + * @return DOMQuery |
|
837 | + * The DOMQuery wrapping the modified document. |
|
838 | + * @throws ParseException |
|
839 | + * @see remove() |
|
840 | + * @see replaceWith() |
|
841 | + * @deprecated Due to the fact that this is not a particularly friendly method, |
|
842 | + * and that it can be easily replicated using {@see replaceWith()}, it is to be |
|
843 | + * considered deprecated. |
|
844 | + */ |
|
845 | + public function replaceAll($selector, DOMDocument $document): Query |
|
846 | + { |
|
847 | + $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode(''); |
|
848 | + |
|
849 | + $c = new QueryPathEventHandler($document); |
|
850 | + $c->find($selector); |
|
851 | + $temp = $c->getMatches(); |
|
852 | + foreach ($temp as $item) { |
|
853 | + $node = $replacement->cloneNode(); |
|
854 | + $node = $document->importNode($node); |
|
855 | + $item->parentNode->replaceChild($node, $item); |
|
856 | + } |
|
857 | + |
|
858 | + return QueryPath::with($document, null, $this->options); |
|
859 | + } |
|
860 | + |
|
861 | + /** |
|
862 | + * Add more elements to the current set of matches. |
|
863 | + * |
|
864 | + * This begins the new query at the top of the DOM again. The results found |
|
865 | + * when running this selector are then merged into the existing results. In |
|
866 | + * this way, you can add additional elements to the existing set. |
|
867 | + * |
|
868 | + * @param string $selector |
|
869 | + * A valid selector. |
|
870 | + * |
|
871 | + * @return DOMQuery |
|
872 | + * The DOMQuery object with the newly added elements. |
|
873 | + * @see append() |
|
874 | + * @see after() |
|
875 | + * @see andSelf() |
|
876 | + * @see end() |
|
877 | + */ |
|
878 | + public function add($selector): Query |
|
879 | + { |
|
880 | + |
|
881 | + // This is destructive, so we need to set $last: |
|
882 | + $this->last = $this->matches; |
|
883 | + |
|
884 | + foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) { |
|
885 | + $this->matches->attach($item); |
|
886 | + } |
|
887 | + |
|
888 | + return $this; |
|
889 | + } |
|
890 | + |
|
891 | + /** |
|
892 | + * Remove all child nodes. |
|
893 | + * |
|
894 | + * This is equivalent to jQuery's empty() function. (However, empty() is a |
|
895 | + * PHP built-in, and cannot be used as a method name.) |
|
896 | + * |
|
897 | + * @return DOMQuery |
|
898 | + * The DOMQuery object with the child nodes removed. |
|
899 | + * @see replaceWith() |
|
900 | + * @see replaceAll() |
|
901 | + * @see remove() |
|
902 | + */ |
|
903 | + public function removeChildren(): Query |
|
904 | + { |
|
905 | + foreach ($this->matches as $m) { |
|
906 | + while ($kid = $m->firstChild) { |
|
907 | + $m->removeChild($kid); |
|
908 | + } |
|
909 | + } |
|
910 | + |
|
911 | + return $this; |
|
912 | + } |
|
913 | + |
|
914 | + /** |
|
915 | + * Get/set an attribute. |
|
916 | + * - If no parameters are specified, this returns an associative array of all |
|
917 | + * name/value pairs. |
|
918 | + * - If both $name and $value are set, then this will set the attribute name/value |
|
919 | + * pair for all items in this object. |
|
920 | + * - If $name is set, and is an array, then |
|
921 | + * all attributes in the array will be set for all items in this object. |
|
922 | + * - If $name is a string and is set, then the attribute value will be returned. |
|
923 | + * |
|
924 | + * When an attribute value is retrieved, only the attribute value of the FIRST |
|
925 | + * match is returned. |
|
926 | + * |
|
927 | + * @param mixed $name |
|
928 | + * The name of the attribute or an associative array of name/value pairs. |
|
929 | + * @param string $value |
|
930 | + * A value (used only when setting an individual property). |
|
931 | + * |
|
932 | + * @return mixed |
|
933 | + * If this was a setter request, return the DOMQuery object. If this was |
|
934 | + * an access request (getter), return the string value. |
|
935 | + * @see removeAttr() |
|
936 | + * @see tag() |
|
937 | + * @see hasAttr() |
|
938 | + * @see hasClass() |
|
939 | + */ |
|
940 | + public function attr($name = null, $value = null) |
|
941 | + { |
|
942 | + // Default case: Return all attributes as an assoc array. |
|
943 | + if (is_null($name)) { |
|
944 | + if ($this->matches->count() === 0) { |
|
945 | + return null; |
|
946 | + } |
|
947 | + $ele = $this->getFirstMatch(); |
|
948 | + $buffer = []; |
|
949 | + |
|
950 | + // This does not appear to be part of the DOM |
|
951 | + // spec. Nor is it documented. But it works. |
|
952 | + foreach ($ele->attributes as $name => $attrNode) { |
|
953 | + $buffer[$name] = $attrNode->value; |
|
954 | + } |
|
955 | + |
|
956 | + return $buffer; |
|
957 | + } |
|
958 | + |
|
959 | + // multi-setter |
|
960 | + if (is_array($name)) { |
|
961 | + foreach ($name as $k => $v) { |
|
962 | + foreach ($this->matches as $m) { |
|
963 | + $m->setAttribute($k, $v); |
|
964 | + } |
|
965 | + } |
|
966 | + |
|
967 | + return $this; |
|
968 | + } |
|
969 | + // setter |
|
970 | + if (isset($value)) { |
|
971 | + foreach ($this->matches as $m) { |
|
972 | + $m->setAttribute($name, $value); |
|
973 | + } |
|
974 | + |
|
975 | + return $this; |
|
976 | + } |
|
977 | + |
|
978 | + //getter |
|
979 | + if ($this->matches->count() === 0) { |
|
980 | + return null; |
|
981 | + } |
|
982 | + |
|
983 | + // Special node type handler: |
|
984 | + if ($name === 'nodeType') { |
|
985 | + return $this->getFirstMatch()->nodeType; |
|
986 | + } |
|
987 | + |
|
988 | + // Always return first match's attr. |
|
989 | + return $this->getFirstMatch()->getAttribute($name); |
|
990 | + } |
|
991 | + |
|
992 | + /** |
|
993 | + * Set/get a CSS value for the current element(s). |
|
994 | + * This sets the CSS value for each element in the DOMQuery object. |
|
995 | + * It does this by setting (or getting) the style attribute (without a namespace). |
|
996 | + * |
|
997 | + * For example, consider this code: |
|
998 | + * |
|
999 | + * @code |
|
1000 | + * <?php |
|
1001 | + * qp(HTML_STUB, 'body')->css('background-color','red')->html(); |
|
1002 | + * ?> |
|
1003 | + * @endcode |
|
1004 | + * This will return the following HTML: |
|
1005 | + * @code |
|
1006 | + * <body style="background-color: red"/> |
|
1007 | + * @endcode |
|
1008 | + * |
|
1009 | + * If no parameters are passed into this function, then the current style |
|
1010 | + * element will be returned unparsed. Example: |
|
1011 | + * @code |
|
1012 | + * <?php |
|
1013 | + * qp(HTML_STUB, 'body')->css('background-color','red')->css(); |
|
1014 | + * ?> |
|
1015 | + * @endcode |
|
1016 | + * This will return the following: |
|
1017 | + * @code |
|
1018 | + * background-color: red |
|
1019 | + * @endcode |
|
1020 | + * |
|
1021 | + * As of QueryPath 2.1, existing style attributes will be merged with new attributes. |
|
1022 | + * (In previous versions of QueryPath, a call to css() overwrite the existing style |
|
1023 | + * values). |
|
1024 | + * |
|
1025 | + * @param mixed $name |
|
1026 | + * If this is a string, it will be used as a CSS name. If it is an array, |
|
1027 | + * this will assume it is an array of name/value pairs of CSS rules. It will |
|
1028 | + * apply all rules to all elements in the set. |
|
1029 | + * @param string $value |
|
1030 | + * The value to set. This is only set if $name is a string. |
|
1031 | + * |
|
1032 | + * @return DOMQuery |
|
1033 | + */ |
|
1034 | + public function css($name = null, $value = '') |
|
1035 | + { |
|
1036 | + if (empty($name)) { |
|
1037 | + return $this->attr('style'); |
|
1038 | + } |
|
1039 | + |
|
1040 | + // Get any existing CSS. |
|
1041 | + $css = []; |
|
1042 | + foreach ($this->matches as $match) { |
|
1043 | + $style = $match->getAttribute('style'); |
|
1044 | + if (! empty($style)) { |
|
1045 | + // XXX: Is this sufficient? |
|
1046 | + $style_array = explode(';', $style); |
|
1047 | + foreach ($style_array as $item) { |
|
1048 | + $item = trim($item); |
|
1049 | + |
|
1050 | + // Skip empty attributes. |
|
1051 | + if ($item === '') { |
|
1052 | + continue; |
|
1053 | + } |
|
1054 | + |
|
1055 | + [$css_att, $css_val] = explode(':', $item, 2); |
|
1056 | + $css[$css_att] = trim($css_val); |
|
1057 | + } |
|
1058 | + } |
|
1059 | + } |
|
1060 | + |
|
1061 | + if (is_array($name)) { |
|
1062 | + // Use array_merge instead of + to preserve order. |
|
1063 | + $css = array_merge($css, $name); |
|
1064 | + } else { |
|
1065 | + $css[$name] = $value; |
|
1066 | + } |
|
1067 | + |
|
1068 | + // Collapse CSS into a string. |
|
1069 | + $format = '%s: %s;'; |
|
1070 | + $css_string = ''; |
|
1071 | + foreach ($css as $n => $v) { |
|
1072 | + $css_string .= sprintf($format, $n, trim($v)); |
|
1073 | + } |
|
1074 | + |
|
1075 | + $this->attr('style', $css_string); |
|
1076 | + |
|
1077 | + return $this; |
|
1078 | + } |
|
1079 | 1079 | } |
@@ -661,7 +661,7 @@ discard block |
||
661 | 661 | $vals = array_filter(explode(' ', $m->getAttribute('class'))); |
662 | 662 | $buf = []; |
663 | 663 | foreach ($vals as $v) { |
664 | - if (! in_array($v, $to_remove)) { |
|
664 | + if (!in_array($v, $to_remove)) { |
|
665 | 665 | $buf[] = $v; |
666 | 666 | } |
667 | 667 | } |
@@ -795,7 +795,7 @@ discard block |
||
795 | 795 | */ |
796 | 796 | public function remove($selector = null): Query |
797 | 797 | { |
798 | - if (! empty($selector)) { |
|
798 | + if (!empty($selector)) { |
|
799 | 799 | // Do a non-destructive find. |
800 | 800 | $query = new QueryPathEventHandler($this->matches); |
801 | 801 | $query->find($selector); |
@@ -1041,7 +1041,7 @@ discard block |
||
1041 | 1041 | $css = []; |
1042 | 1042 | foreach ($this->matches as $match) { |
1043 | 1043 | $style = $match->getAttribute('style'); |
1044 | - if (! empty($style)) { |
|
1044 | + if (!empty($style)) { |
|
1045 | 1045 | // XXX: Is this sufficient? |
1046 | 1046 | $style_array = explode(';', $style); |
1047 | 1047 | foreach ($style_array as $item) { |
@@ -20,182 +20,182 @@ |
||
20 | 20 | trait QueryChecks |
21 | 21 | { |
22 | 22 | |
23 | - /** |
|
24 | - * Given a selector, this checks to see if the current set has one or more matches. |
|
25 | - * |
|
26 | - * Unlike jQuery's version, this supports full selectors (not just simple ones). |
|
27 | - * |
|
28 | - * @param string|DOMNode $selector |
|
29 | - * The selector to search for. As of QueryPath 2.1.1, this also supports passing a |
|
30 | - * DOMNode object. |
|
31 | - * |
|
32 | - * @return boolean |
|
33 | - * TRUE if one or more elements match. FALSE if no match is found. |
|
34 | - * @throws Exception |
|
35 | - * @throws Exception |
|
36 | - * @see get() |
|
37 | - * @see eq() |
|
38 | - */ |
|
39 | - public function is($selector): bool |
|
40 | - { |
|
41 | - if (is_object($selector)) { |
|
42 | - if ($selector instanceof DOMNode) { |
|
43 | - return count($this->matches) === 1 && $selector->isSameNode($this->get(0)); |
|
44 | - } |
|
45 | - |
|
46 | - if ($selector instanceof Traversable) { |
|
47 | - if (count($selector) !== count($this->matches)) { |
|
48 | - return false; |
|
49 | - } |
|
50 | - // Without $seen, there is an edge case here if $selector contains the same object |
|
51 | - // more than once, but the counts are equal. For example, [a, a, a, a] will |
|
52 | - // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
|
53 | - $seen = new SplObjectStorage(); |
|
54 | - foreach ($selector as $item) { |
|
55 | - if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
56 | - return false; |
|
57 | - } |
|
58 | - $seen->attach($item); |
|
59 | - } |
|
60 | - |
|
61 | - return true; |
|
62 | - } |
|
63 | - throw new Exception('Cannot compare an object to a DOMQuery.'); |
|
64 | - } |
|
65 | - |
|
66 | - return $this->branch($selector)->count() > 0; |
|
67 | - } |
|
68 | - |
|
69 | - /** |
|
70 | - * Reduce the elements matched by DOMQuery to only those which contain the given item. |
|
71 | - * |
|
72 | - * There are two ways in which this is different from jQuery's implementation: |
|
73 | - * - We allow ANY DOMNode, not just DOMElements. That means this will work on |
|
74 | - * processor instructions, text nodes, comments, etc. |
|
75 | - * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior |
|
76 | - * and modifies the existing object. It does not create a brand new object. |
|
77 | - * |
|
78 | - * @param mixed $contained |
|
79 | - * - If $contained is a CSS selector (e.g. '#foo'), this will test to see |
|
80 | - * if the current DOMQuery has any elements that contain items that match |
|
81 | - * the selector. |
|
82 | - * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode |
|
83 | - * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the |
|
84 | - * same document.) |
|
85 | - * |
|
86 | - * @return DOMQuery |
|
87 | - * @throws ParseException |
|
88 | - * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes. |
|
89 | - * @since 2.1 |
|
90 | - * @author eabrand |
|
91 | - */ |
|
92 | - public function has($contained): Query |
|
93 | - { |
|
94 | - /* |
|
23 | + /** |
|
24 | + * Given a selector, this checks to see if the current set has one or more matches. |
|
25 | + * |
|
26 | + * Unlike jQuery's version, this supports full selectors (not just simple ones). |
|
27 | + * |
|
28 | + * @param string|DOMNode $selector |
|
29 | + * The selector to search for. As of QueryPath 2.1.1, this also supports passing a |
|
30 | + * DOMNode object. |
|
31 | + * |
|
32 | + * @return boolean |
|
33 | + * TRUE if one or more elements match. FALSE if no match is found. |
|
34 | + * @throws Exception |
|
35 | + * @throws Exception |
|
36 | + * @see get() |
|
37 | + * @see eq() |
|
38 | + */ |
|
39 | + public function is($selector): bool |
|
40 | + { |
|
41 | + if (is_object($selector)) { |
|
42 | + if ($selector instanceof DOMNode) { |
|
43 | + return count($this->matches) === 1 && $selector->isSameNode($this->get(0)); |
|
44 | + } |
|
45 | + |
|
46 | + if ($selector instanceof Traversable) { |
|
47 | + if (count($selector) !== count($this->matches)) { |
|
48 | + return false; |
|
49 | + } |
|
50 | + // Without $seen, there is an edge case here if $selector contains the same object |
|
51 | + // more than once, but the counts are equal. For example, [a, a, a, a] will |
|
52 | + // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
|
53 | + $seen = new SplObjectStorage(); |
|
54 | + foreach ($selector as $item) { |
|
55 | + if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
56 | + return false; |
|
57 | + } |
|
58 | + $seen->attach($item); |
|
59 | + } |
|
60 | + |
|
61 | + return true; |
|
62 | + } |
|
63 | + throw new Exception('Cannot compare an object to a DOMQuery.'); |
|
64 | + } |
|
65 | + |
|
66 | + return $this->branch($selector)->count() > 0; |
|
67 | + } |
|
68 | + |
|
69 | + /** |
|
70 | + * Reduce the elements matched by DOMQuery to only those which contain the given item. |
|
71 | + * |
|
72 | + * There are two ways in which this is different from jQuery's implementation: |
|
73 | + * - We allow ANY DOMNode, not just DOMElements. That means this will work on |
|
74 | + * processor instructions, text nodes, comments, etc. |
|
75 | + * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior |
|
76 | + * and modifies the existing object. It does not create a brand new object. |
|
77 | + * |
|
78 | + * @param mixed $contained |
|
79 | + * - If $contained is a CSS selector (e.g. '#foo'), this will test to see |
|
80 | + * if the current DOMQuery has any elements that contain items that match |
|
81 | + * the selector. |
|
82 | + * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode |
|
83 | + * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the |
|
84 | + * same document.) |
|
85 | + * |
|
86 | + * @return DOMQuery |
|
87 | + * @throws ParseException |
|
88 | + * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes. |
|
89 | + * @since 2.1 |
|
90 | + * @author eabrand |
|
91 | + */ |
|
92 | + public function has($contained): Query |
|
93 | + { |
|
94 | + /* |
|
95 | 95 | if (count($this->matches) == 0) { |
96 | 96 | return false; |
97 | 97 | } |
98 | 98 | */ |
99 | - $found = new SplObjectStorage(); |
|
100 | - |
|
101 | - // If it's a selector, we just get all of the DOMNodes that match the selector. |
|
102 | - $nodes = []; |
|
103 | - if (is_string($contained)) { |
|
104 | - // Get the list of nodes. |
|
105 | - $nodes = $this->branch($contained)->get(); |
|
106 | - } elseif ($contained instanceof DOMNode) { |
|
107 | - // Make a list with one node. |
|
108 | - $nodes = [$contained]; |
|
109 | - } |
|
110 | - |
|
111 | - // Now we go through each of the nodes that we are testing. We want to find |
|
112 | - // ALL PARENTS that are in our existing DOMQuery matches. Those are the |
|
113 | - // ones we add to our new matches. |
|
114 | - foreach ($nodes as $original_node) { |
|
115 | - $node = $original_node; |
|
116 | - while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
117 | - if ($this->matches->contains($node)) { |
|
118 | - $found->attach($node); |
|
119 | - } |
|
120 | - $node = $node->parentNode; |
|
121 | - } |
|
122 | - } |
|
123 | - |
|
124 | - return $this->inst($found, null); |
|
125 | - } |
|
126 | - |
|
127 | - /** |
|
128 | - * Returns TRUE if any of the elements in the DOMQuery have the specified class. |
|
129 | - * |
|
130 | - * @param string $class |
|
131 | - * The name of the class. |
|
132 | - * |
|
133 | - * @return boolean |
|
134 | - * TRUE if the class exists in one or more of the elements, FALSE otherwise. |
|
135 | - * @see addClass() |
|
136 | - * @see removeClass() |
|
137 | - */ |
|
138 | - public function hasClass($class): bool |
|
139 | - { |
|
140 | - foreach ($this->matches as $m) { |
|
141 | - if ($m->hasAttribute('class')) { |
|
142 | - $vals = explode(' ', $m->getAttribute('class')); |
|
143 | - if (in_array($class, $vals)) { |
|
144 | - return true; |
|
145 | - } |
|
146 | - } |
|
147 | - } |
|
148 | - |
|
149 | - return false; |
|
150 | - } |
|
151 | - |
|
152 | - /** |
|
153 | - * Check to see if the given attribute is present. |
|
154 | - * |
|
155 | - * This returns TRUE if <em>all</em> selected items have the attribute, or |
|
156 | - * FALSE if at least one item does not have the attribute. |
|
157 | - * |
|
158 | - * @param string $attrName |
|
159 | - * The attribute name. |
|
160 | - * |
|
161 | - * @return boolean |
|
162 | - * TRUE if all matches have the attribute, FALSE otherwise. |
|
163 | - * @since 2.0 |
|
164 | - * @see attr() |
|
165 | - * @see hasClass() |
|
166 | - */ |
|
167 | - public function hasAttr($attrName): bool |
|
168 | - { |
|
169 | - foreach ($this->matches as $match) { |
|
170 | - if (! $match->hasAttribute($attrName)) { |
|
171 | - return false; |
|
172 | - } |
|
173 | - } |
|
174 | - |
|
175 | - return true; |
|
176 | - } |
|
177 | - |
|
178 | - /** |
|
179 | - * Remove the named attribute from all elements in the current DOMQuery. |
|
180 | - * |
|
181 | - * This will remove any attribute with the given name. It will do this on each |
|
182 | - * item currently wrapped by DOMQuery. |
|
183 | - * |
|
184 | - * As is the case in jQuery, this operation is not considered destructive. |
|
185 | - * |
|
186 | - * @param string $name |
|
187 | - * Name of the parameter to remove. |
|
188 | - * |
|
189 | - * @return DOMQuery |
|
190 | - * The DOMQuery object with the same elements. |
|
191 | - * @see attr() |
|
192 | - */ |
|
193 | - public function removeAttr($name): Query |
|
194 | - { |
|
195 | - foreach ($this->matches as $m) { |
|
196 | - $m->removeAttribute($name); |
|
197 | - } |
|
198 | - |
|
199 | - return $this; |
|
200 | - } |
|
99 | + $found = new SplObjectStorage(); |
|
100 | + |
|
101 | + // If it's a selector, we just get all of the DOMNodes that match the selector. |
|
102 | + $nodes = []; |
|
103 | + if (is_string($contained)) { |
|
104 | + // Get the list of nodes. |
|
105 | + $nodes = $this->branch($contained)->get(); |
|
106 | + } elseif ($contained instanceof DOMNode) { |
|
107 | + // Make a list with one node. |
|
108 | + $nodes = [$contained]; |
|
109 | + } |
|
110 | + |
|
111 | + // Now we go through each of the nodes that we are testing. We want to find |
|
112 | + // ALL PARENTS that are in our existing DOMQuery matches. Those are the |
|
113 | + // ones we add to our new matches. |
|
114 | + foreach ($nodes as $original_node) { |
|
115 | + $node = $original_node; |
|
116 | + while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
117 | + if ($this->matches->contains($node)) { |
|
118 | + $found->attach($node); |
|
119 | + } |
|
120 | + $node = $node->parentNode; |
|
121 | + } |
|
122 | + } |
|
123 | + |
|
124 | + return $this->inst($found, null); |
|
125 | + } |
|
126 | + |
|
127 | + /** |
|
128 | + * Returns TRUE if any of the elements in the DOMQuery have the specified class. |
|
129 | + * |
|
130 | + * @param string $class |
|
131 | + * The name of the class. |
|
132 | + * |
|
133 | + * @return boolean |
|
134 | + * TRUE if the class exists in one or more of the elements, FALSE otherwise. |
|
135 | + * @see addClass() |
|
136 | + * @see removeClass() |
|
137 | + */ |
|
138 | + public function hasClass($class): bool |
|
139 | + { |
|
140 | + foreach ($this->matches as $m) { |
|
141 | + if ($m->hasAttribute('class')) { |
|
142 | + $vals = explode(' ', $m->getAttribute('class')); |
|
143 | + if (in_array($class, $vals)) { |
|
144 | + return true; |
|
145 | + } |
|
146 | + } |
|
147 | + } |
|
148 | + |
|
149 | + return false; |
|
150 | + } |
|
151 | + |
|
152 | + /** |
|
153 | + * Check to see if the given attribute is present. |
|
154 | + * |
|
155 | + * This returns TRUE if <em>all</em> selected items have the attribute, or |
|
156 | + * FALSE if at least one item does not have the attribute. |
|
157 | + * |
|
158 | + * @param string $attrName |
|
159 | + * The attribute name. |
|
160 | + * |
|
161 | + * @return boolean |
|
162 | + * TRUE if all matches have the attribute, FALSE otherwise. |
|
163 | + * @since 2.0 |
|
164 | + * @see attr() |
|
165 | + * @see hasClass() |
|
166 | + */ |
|
167 | + public function hasAttr($attrName): bool |
|
168 | + { |
|
169 | + foreach ($this->matches as $match) { |
|
170 | + if (! $match->hasAttribute($attrName)) { |
|
171 | + return false; |
|
172 | + } |
|
173 | + } |
|
174 | + |
|
175 | + return true; |
|
176 | + } |
|
177 | + |
|
178 | + /** |
|
179 | + * Remove the named attribute from all elements in the current DOMQuery. |
|
180 | + * |
|
181 | + * This will remove any attribute with the given name. It will do this on each |
|
182 | + * item currently wrapped by DOMQuery. |
|
183 | + * |
|
184 | + * As is the case in jQuery, this operation is not considered destructive. |
|
185 | + * |
|
186 | + * @param string $name |
|
187 | + * Name of the parameter to remove. |
|
188 | + * |
|
189 | + * @return DOMQuery |
|
190 | + * The DOMQuery object with the same elements. |
|
191 | + * @see attr() |
|
192 | + */ |
|
193 | + public function removeAttr($name): Query |
|
194 | + { |
|
195 | + foreach ($this->matches as $m) { |
|
196 | + $m->removeAttribute($name); |
|
197 | + } |
|
198 | + |
|
199 | + return $this; |
|
200 | + } |
|
201 | 201 | } |
@@ -52,7 +52,7 @@ discard block |
||
52 | 52 | // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this. |
53 | 53 | $seen = new SplObjectStorage(); |
54 | 54 | foreach ($selector as $item) { |
55 | - if (! $this->matches->contains($item) || $seen->contains($item)) { |
|
55 | + if (!$this->matches->contains($item) || $seen->contains($item)) { |
|
56 | 56 | return false; |
57 | 57 | } |
58 | 58 | $seen->attach($item); |
@@ -113,7 +113,7 @@ discard block |
||
113 | 113 | // ones we add to our new matches. |
114 | 114 | foreach ($nodes as $original_node) { |
115 | 115 | $node = $original_node; |
116 | - while (! empty($node)/* && $node != $node->ownerDocument*/) { |
|
116 | + while (!empty($node)/* && $node != $node->ownerDocument*/) { |
|
117 | 117 | if ($this->matches->contains($node)) { |
118 | 118 | $found->attach($node); |
119 | 119 | } |
@@ -167,7 +167,7 @@ discard block |
||
167 | 167 | public function hasAttr($attrName): bool |
168 | 168 | { |
169 | 169 | foreach ($this->matches as $match) { |
170 | - if (! $match->hasAttribute($attrName)) { |
|
170 | + if (!$match->hasAttribute($attrName)) { |
|
171 | 171 | return false; |
172 | 172 | } |
173 | 173 | } |
@@ -22,1128 +22,1128 @@ |
||
22 | 22 | trait QueryFilters |
23 | 23 | { |
24 | 24 | |
25 | - /** |
|
26 | - * Filter a list down to only elements that match the selector. |
|
27 | - * Use this, for example, to find all elements with a class, or with |
|
28 | - * certain children. |
|
29 | - * |
|
30 | - * @param string $selector |
|
31 | - * The selector to use as a filter. |
|
32 | - * |
|
33 | - * @return Query The DOMQuery with non-matching items filtered out.* The DOMQuery with non-matching items |
|
34 | - * filtered out. |
|
35 | - * @throws ParseException |
|
36 | - * @see filterCallback() |
|
37 | - * @see map() |
|
38 | - * @see find() |
|
39 | - * @see is() |
|
40 | - * @see filterLambda() |
|
41 | - */ |
|
42 | - public function filter($selector): Query |
|
43 | - { |
|
44 | - $found = new SplObjectStorage(); |
|
45 | - $tmp = new SplObjectStorage(); |
|
46 | - |
|
47 | - foreach ($this->matches as $m) { |
|
48 | - $tmp->attach($m); |
|
49 | - // Seems like this should be right... but it fails unit |
|
50 | - // tests. Need to compare to jQuery. |
|
51 | - // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m); |
|
52 | - $query = new DOMTraverser($tmp); |
|
53 | - $query->find($selector); |
|
54 | - if (count($query->matches())) { |
|
55 | - $found->attach($m); |
|
56 | - } |
|
57 | - $tmp->detach($m); |
|
58 | - } |
|
59 | - |
|
60 | - return $this->inst($found, null); |
|
61 | - } |
|
62 | - |
|
63 | - /** |
|
64 | - * Filter based on a lambda function. |
|
65 | - * |
|
66 | - * The function string will be executed as if it were the body of a |
|
67 | - * function. It is passed two arguments: |
|
68 | - * - $index: The index of the item. |
|
69 | - * - $item: The current Element. |
|
70 | - * If the function returns boolean FALSE, the item will be removed from |
|
71 | - * the list of elements. Otherwise it will be kept. |
|
72 | - * |
|
73 | - * Example: |
|
74 | - * |
|
75 | - * @code |
|
76 | - * qp('li')->filterLambda('qp($item)->attr("id") == "test"'); |
|
77 | - * @endcode |
|
78 | - * |
|
79 | - * The above would filter down the list to only an item whose ID is |
|
80 | - * 'text'. |
|
81 | - * |
|
82 | - * @param string $fn |
|
83 | - * Inline lambda function in a string. |
|
84 | - * |
|
85 | - * @return DOMQuery |
|
86 | - * @throws ParseException |
|
87 | - * |
|
88 | - * @see map() |
|
89 | - * @see mapLambda() |
|
90 | - * @see filterCallback() |
|
91 | - * @see filter() |
|
92 | - * @deprecated |
|
93 | - * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
94 | - * method is not necessary and should be avoided. |
|
95 | - */ |
|
96 | - public function filterLambda($fn): Query |
|
97 | - { |
|
98 | - $function = create_function('$index, $item', $fn); |
|
99 | - $found = new SplObjectStorage(); |
|
100 | - $i = 0; |
|
101 | - foreach ($this->matches as $item) { |
|
102 | - if ($function($i++, $item) !== false) { |
|
103 | - $found->attach($item); |
|
104 | - } |
|
105 | - } |
|
106 | - |
|
107 | - return $this->inst($found, null); |
|
108 | - } |
|
109 | - |
|
110 | - /** |
|
111 | - * Use regular expressions to filter based on the text content of matched elements. |
|
112 | - * |
|
113 | - * Only items that match the given regular expression will be kept. All others will |
|
114 | - * be removed. |
|
115 | - * |
|
116 | - * The regular expression is run against the <i>text content</i> (the PCDATA) of the |
|
117 | - * elements. This is a way of filtering elements based on their content. |
|
118 | - * |
|
119 | - * Example: |
|
120 | - * |
|
121 | - * @code |
|
122 | - * <?xml version="1.0"?> |
|
123 | - * <div>Hello <i>World</i></div> |
|
124 | - * @endcode |
|
125 | - * |
|
126 | - * @code |
|
127 | - * <?php |
|
128 | - * // This will be 1. |
|
129 | - * qp($xml, 'div')->filterPreg('/World/')->matches->count(); |
|
130 | - * ?> |
|
131 | - * @endcode |
|
132 | - * |
|
133 | - * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is |
|
134 | - * @codeHello World@endcode. |
|
135 | - * |
|
136 | - * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class. |
|
137 | - * |
|
138 | - * @param string $regex |
|
139 | - * A regular expression. |
|
140 | - * |
|
141 | - * @return DOMQuery |
|
142 | - * @throws ParseException |
|
143 | - * @see filterCallback() |
|
144 | - * @see preg_match() |
|
145 | - * @see filter() |
|
146 | - */ |
|
147 | - public function filterPreg($regex): Query |
|
148 | - { |
|
149 | - $found = new SplObjectStorage(); |
|
150 | - |
|
151 | - foreach ($this->matches as $item) { |
|
152 | - if (preg_match($regex, $item->textContent) > 0) { |
|
153 | - $found->attach($item); |
|
154 | - } |
|
155 | - } |
|
156 | - |
|
157 | - return $this->inst($found, null); |
|
158 | - } |
|
159 | - |
|
160 | - /** |
|
161 | - * Filter based on a callback function. |
|
162 | - * |
|
163 | - * A callback may be any of the following: |
|
164 | - * - a function: 'my_func'. |
|
165 | - * - an object/method combo: $obj, 'myMethod' |
|
166 | - * - a class/method combo: 'MyClass', 'myMethod' |
|
167 | - * Note that classes are passed in strings. Objects are not. |
|
168 | - * |
|
169 | - * Each callback is passed to arguments: |
|
170 | - * - $index: The index position of the object in the array. |
|
171 | - * - $item: The item to be operated upon. |
|
172 | - * |
|
173 | - * If the callback function returns FALSE, the item will be removed from the |
|
174 | - * set of matches. Otherwise the item will be considered a match and left alone. |
|
175 | - * |
|
176 | - * @param callback $callback . |
|
177 | - * A callback either as a string (function) or an array (object, method OR |
|
178 | - * classname, method). |
|
179 | - * |
|
180 | - * @return DOMQuery |
|
181 | - * Query path object augmented according to the function. |
|
182 | - * @throws ParseException |
|
183 | - * @throws Exception |
|
184 | - * @see map() |
|
185 | - * @see is() |
|
186 | - * @see find() |
|
187 | - * @see filter() |
|
188 | - * @see filterLambda() |
|
189 | - */ |
|
190 | - public function filterCallback($callback): Query |
|
191 | - { |
|
192 | - $found = new SplObjectStorage(); |
|
193 | - $i = 0; |
|
194 | - if (is_callable($callback)) { |
|
195 | - foreach ($this->matches as $item) { |
|
196 | - if ($callback($i++, $item) !== false) { |
|
197 | - $found->attach($item); |
|
198 | - } |
|
199 | - } |
|
200 | - } else { |
|
201 | - throw new Exception('The specified callback is not callable.'); |
|
202 | - } |
|
203 | - |
|
204 | - return $this->inst($found, null); |
|
205 | - } |
|
206 | - |
|
207 | - /** |
|
208 | - * Run a function on each item in a set. |
|
209 | - * |
|
210 | - * The mapping callback can return anything. Whatever it returns will be |
|
211 | - * stored as a match in the set, though. This means that afer a map call, |
|
212 | - * there is no guarantee that the elements in the set will behave correctly |
|
213 | - * with other DOMQuery functions. |
|
214 | - * |
|
215 | - * Callback rules: |
|
216 | - * - If the callback returns NULL, the item will be removed from the array. |
|
217 | - * - If the callback returns an array, the entire array will be stored in |
|
218 | - * the results. |
|
219 | - * - If the callback returns anything else, it will be appended to the array |
|
220 | - * of matches. |
|
221 | - * |
|
222 | - * @param callback $callback |
|
223 | - * The function or callback to use. The callback will be passed two params: |
|
224 | - * - $index: The index position in the list of items wrapped by this object. |
|
225 | - * - $item: The current item. |
|
226 | - * |
|
227 | - * @return DOMQuery |
|
228 | - * The DOMQuery object wrapping a list of whatever values were returned |
|
229 | - * by each run of the callback. |
|
230 | - * |
|
231 | - * @throws Exception |
|
232 | - * @throws ParseException |
|
233 | - * @see find() |
|
234 | - * @see DOMQuery::get() |
|
235 | - * @see filter() |
|
236 | - */ |
|
237 | - public function map($callback): Query |
|
238 | - { |
|
239 | - $found = new SplObjectStorage(); |
|
240 | - |
|
241 | - if (is_callable($callback)) { |
|
242 | - $i = 0; |
|
243 | - foreach ($this->matches as $item) { |
|
244 | - $c = call_user_func($callback, $i, $item); |
|
245 | - if (isset($c)) { |
|
246 | - if (is_array($c) || $c instanceof Iterable) { |
|
247 | - foreach ($c as $retval) { |
|
248 | - if (! is_object($retval)) { |
|
249 | - $tmp = new stdClass(); |
|
250 | - $tmp->textContent = $retval; |
|
251 | - $retval = $tmp; |
|
252 | - } |
|
253 | - $found->attach($retval); |
|
254 | - } |
|
255 | - } else { |
|
256 | - if (! is_object($c)) { |
|
257 | - $tmp = new stdClass(); |
|
258 | - $tmp->textContent = $c; |
|
259 | - $c = $tmp; |
|
260 | - } |
|
261 | - $found->attach($c); |
|
262 | - } |
|
263 | - } |
|
264 | - ++$i; |
|
265 | - } |
|
266 | - } else { |
|
267 | - throw new Exception('Callback is not callable.'); |
|
268 | - } |
|
269 | - |
|
270 | - return $this->inst($found, null); |
|
271 | - } |
|
272 | - |
|
273 | - /** |
|
274 | - * Narrow the items in this object down to only a slice of the starting items. |
|
275 | - * |
|
276 | - * @param integer $start |
|
277 | - * Where in the list of matches to begin the slice. |
|
278 | - * @param integer $length |
|
279 | - * The number of items to include in the slice. If nothing is specified, the |
|
280 | - * all remaining matches (from $start onward) will be included in the sliced |
|
281 | - * list. |
|
282 | - * |
|
283 | - * @return DOMQuery |
|
284 | - * @throws ParseException |
|
285 | - * @see array_slice() |
|
286 | - */ |
|
287 | - public function slice($start, $length = 0): Query |
|
288 | - { |
|
289 | - $end = $length; |
|
290 | - $found = new SplObjectStorage(); |
|
291 | - if ($start >= $this->count()) { |
|
292 | - return $this->inst($found, null); |
|
293 | - } |
|
294 | - |
|
295 | - $i = $j = 0; |
|
296 | - foreach ($this->matches as $m) { |
|
297 | - if ($i >= $start) { |
|
298 | - if ($end > 0 && $j >= $end) { |
|
299 | - break; |
|
300 | - } |
|
301 | - $found->attach($m); |
|
302 | - ++$j; |
|
303 | - } |
|
304 | - ++$i; |
|
305 | - } |
|
306 | - |
|
307 | - return $this->inst($found, null); |
|
308 | - } |
|
309 | - |
|
310 | - /** |
|
311 | - * Run a callback on each item in the list of items. |
|
312 | - * |
|
313 | - * Rules of the callback: |
|
314 | - * - A callback is passed two variables: $index and $item. (There is no |
|
315 | - * special treatment of $this, as there is in jQuery.) |
|
316 | - * - You will want to pass $item by reference if it is not an |
|
317 | - * object (DOMNodes are all objects). |
|
318 | - * - A callback that returns FALSE will stop execution of the each() loop. This |
|
319 | - * works like break in a standard loop. |
|
320 | - * - A TRUE return value from the callback is analogous to a continue statement. |
|
321 | - * - All other return values are ignored. |
|
322 | - * |
|
323 | - * @param callback $callback |
|
324 | - * The callback to run. |
|
325 | - * |
|
326 | - * @return DOMQuery |
|
327 | - * The DOMQuery. |
|
328 | - * @throws Exception |
|
329 | - * @see filter() |
|
330 | - * @see map() |
|
331 | - * @see eachLambda() |
|
332 | - */ |
|
333 | - public function each($callback): Query |
|
334 | - { |
|
335 | - if (is_callable($callback)) { |
|
336 | - $i = 0; |
|
337 | - foreach ($this->matches as $item) { |
|
338 | - if (call_user_func($callback, $i, $item) === false) { |
|
339 | - return $this; |
|
340 | - } |
|
341 | - ++$i; |
|
342 | - } |
|
343 | - } else { |
|
344 | - throw new Exception('Callback is not callable.'); |
|
345 | - } |
|
346 | - |
|
347 | - return $this; |
|
348 | - } |
|
349 | - |
|
350 | - /** |
|
351 | - * An each() iterator that takes a lambda function. |
|
352 | - * |
|
353 | - * @param string $lambda |
|
354 | - * The lambda function. This will be passed ($index, &$item). |
|
355 | - * |
|
356 | - * @return DOMQuery |
|
357 | - * The DOMQuery object. |
|
358 | - * @deprecated |
|
359 | - * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
360 | - * method is not necessary and should be avoided. |
|
361 | - * @see each() |
|
362 | - * @see filterLambda() |
|
363 | - * @see filterCallback() |
|
364 | - * @see map() |
|
365 | - */ |
|
366 | - public function eachLambda($lambda): Query |
|
367 | - { |
|
368 | - $index = 0; |
|
369 | - foreach ($this->matches as $item) { |
|
370 | - $fn = create_function('$index, &$item', $lambda); |
|
371 | - if ($fn($index, $item) === false) { |
|
372 | - return $this; |
|
373 | - } |
|
374 | - ++$index; |
|
375 | - } |
|
376 | - |
|
377 | - return $this; |
|
378 | - } |
|
379 | - |
|
380 | - /** |
|
381 | - * Get the even elements, so counter-intuitively 1, 3, 5, etc. |
|
382 | - * |
|
383 | - * @return DOMQuery |
|
384 | - * A DOMQuery wrapping all of the children. |
|
385 | - * @throws ParseException |
|
386 | - * @see parent() |
|
387 | - * @see parents() |
|
388 | - * @see next() |
|
389 | - * @see prev() |
|
390 | - * @since 2.1 |
|
391 | - * @author eabrand |
|
392 | - * @see removeChildren() |
|
393 | - */ |
|
394 | - public function even(): Query |
|
395 | - { |
|
396 | - $found = new SplObjectStorage(); |
|
397 | - $even = false; |
|
398 | - foreach ($this->matches as $m) { |
|
399 | - if ($even && $m->nodeType === XML_ELEMENT_NODE) { |
|
400 | - $found->attach($m); |
|
401 | - } |
|
402 | - $even = $even ? false : true; |
|
403 | - } |
|
404 | - |
|
405 | - return $this->inst($found, null); |
|
406 | - } |
|
407 | - |
|
408 | - /** |
|
409 | - * Get the odd elements, so counter-intuitively 0, 2, 4, etc. |
|
410 | - * |
|
411 | - * @return DOMQuery |
|
412 | - * A DOMQuery wrapping all of the children. |
|
413 | - * @throws ParseException |
|
414 | - * @see parent() |
|
415 | - * @see parents() |
|
416 | - * @see next() |
|
417 | - * @see prev() |
|
418 | - * @since 2.1 |
|
419 | - * @author eabrand |
|
420 | - * @see removeChildren() |
|
421 | - */ |
|
422 | - public function odd(): Query |
|
423 | - { |
|
424 | - $found = new SplObjectStorage(); |
|
425 | - $odd = true; |
|
426 | - foreach ($this->matches as $m) { |
|
427 | - if ($odd && $m->nodeType === XML_ELEMENT_NODE) { |
|
428 | - $found->attach($m); |
|
429 | - } |
|
430 | - $odd = $odd ? false : true; |
|
431 | - } |
|
432 | - |
|
433 | - return $this->inst($found, null); |
|
434 | - } |
|
435 | - |
|
436 | - /** |
|
437 | - * Get the first matching element. |
|
438 | - * |
|
439 | - * |
|
440 | - * @return DOMQuery |
|
441 | - * A DOMQuery wrapping all of the children. |
|
442 | - * @throws ParseException |
|
443 | - * @see prev() |
|
444 | - * @since 2.1 |
|
445 | - * @author eabrand |
|
446 | - * @see next() |
|
447 | - */ |
|
448 | - public function first(): Query |
|
449 | - { |
|
450 | - $found = new SplObjectStorage(); |
|
451 | - foreach ($this->matches as $m) { |
|
452 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
453 | - $found->attach($m); |
|
454 | - break; |
|
455 | - } |
|
456 | - } |
|
457 | - |
|
458 | - return $this->inst($found, null); |
|
459 | - } |
|
460 | - |
|
461 | - /** |
|
462 | - * Get the first child of the matching element. |
|
463 | - * |
|
464 | - * |
|
465 | - * @return DOMQuery |
|
466 | - * A DOMQuery wrapping all of the children. |
|
467 | - * @throws ParseException |
|
468 | - * @see prev() |
|
469 | - * @since 2.1 |
|
470 | - * @author eabrand |
|
471 | - * @see next() |
|
472 | - */ |
|
473 | - public function firstChild(): Query |
|
474 | - { |
|
475 | - // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php |
|
476 | - $found = new SplObjectStorage(); |
|
477 | - $flag = false; |
|
478 | - foreach ($this->matches as $m) { |
|
479 | - foreach ($m->childNodes as $c) { |
|
480 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
481 | - $found->attach($c); |
|
482 | - $flag = true; |
|
483 | - break; |
|
484 | - } |
|
485 | - } |
|
486 | - if ($flag) { |
|
487 | - break; |
|
488 | - } |
|
489 | - } |
|
490 | - |
|
491 | - return $this->inst($found, null); |
|
492 | - } |
|
493 | - |
|
494 | - /** |
|
495 | - * Get the last matching element. |
|
496 | - * |
|
497 | - * |
|
498 | - * @return DOMQuery |
|
499 | - * A DOMQuery wrapping all of the children. |
|
500 | - * @throws ParseException |
|
501 | - * @see prev() |
|
502 | - * @since 2.1 |
|
503 | - * @author eabrand |
|
504 | - * @see next() |
|
505 | - */ |
|
506 | - public function last(): Query |
|
507 | - { |
|
508 | - $found = new SplObjectStorage(); |
|
509 | - $item = null; |
|
510 | - foreach ($this->matches as $m) { |
|
511 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
512 | - $item = $m; |
|
513 | - } |
|
514 | - } |
|
515 | - if ($item) { |
|
516 | - $found->attach($item); |
|
517 | - } |
|
518 | - |
|
519 | - return $this->inst($found, null); |
|
520 | - } |
|
521 | - |
|
522 | - /** |
|
523 | - * Get the last child of the matching element. |
|
524 | - * |
|
525 | - * |
|
526 | - * @return DOMQuery |
|
527 | - * A DOMQuery wrapping all of the children. |
|
528 | - * @throws ParseException |
|
529 | - * @see prev() |
|
530 | - * @since 2.1 |
|
531 | - * @author eabrand |
|
532 | - * @see next() |
|
533 | - */ |
|
534 | - public function lastChild(): Query |
|
535 | - { |
|
536 | - $found = new SplObjectStorage(); |
|
537 | - $item = null; |
|
538 | - foreach ($this->matches as $m) { |
|
539 | - foreach ($m->childNodes as $c) { |
|
540 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
541 | - $item = $c; |
|
542 | - } |
|
543 | - } |
|
544 | - if ($item) { |
|
545 | - $found->attach($item); |
|
546 | - $item = null; |
|
547 | - } |
|
548 | - } |
|
549 | - |
|
550 | - return $this->inst($found, null); |
|
551 | - } |
|
552 | - |
|
553 | - /** |
|
554 | - * Get all siblings after an element until the selector is reached. |
|
555 | - * |
|
556 | - * For each element in the DOMQuery, get all siblings that appear after |
|
557 | - * it. If a selector is passed in, then only siblings that match the |
|
558 | - * selector will be included. |
|
559 | - * |
|
560 | - * @param string $selector |
|
561 | - * A valid CSS 3 selector. |
|
562 | - * |
|
563 | - * @return DOMQuery |
|
564 | - * The DOMQuery object, now containing the matching siblings. |
|
565 | - * @throws Exception |
|
566 | - * @see prevAll() |
|
567 | - * @see children() |
|
568 | - * @see siblings() |
|
569 | - * @since 2.1 |
|
570 | - * @author eabrand |
|
571 | - * @see next() |
|
572 | - */ |
|
573 | - public function nextUntil($selector = null): Query |
|
574 | - { |
|
575 | - $found = new SplObjectStorage(); |
|
576 | - foreach ($this->matches as $m) { |
|
577 | - while (isset($m->nextSibling)) { |
|
578 | - $m = $m->nextSibling; |
|
579 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
580 | - if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
581 | - break; |
|
582 | - } |
|
583 | - $found->attach($m); |
|
584 | - } |
|
585 | - } |
|
586 | - } |
|
587 | - |
|
588 | - return $this->inst($found, null); |
|
589 | - } |
|
590 | - |
|
591 | - /** |
|
592 | - * Get the previous siblings for each element in the DOMQuery |
|
593 | - * until the selector is reached. |
|
594 | - * |
|
595 | - * For each element in the DOMQuery, get all previous siblings. If a |
|
596 | - * selector is provided, only matching siblings will be retrieved. |
|
597 | - * |
|
598 | - * @param string $selector |
|
599 | - * A valid CSS 3 selector. |
|
600 | - * |
|
601 | - * @return DOMQuery |
|
602 | - * The DOMQuery object, now wrapping previous sibling elements. |
|
603 | - * @throws Exception |
|
604 | - * @see prev() |
|
605 | - * @see nextAll() |
|
606 | - * @see siblings() |
|
607 | - * @see contents() |
|
608 | - * @see children() |
|
609 | - * @since 2.1 |
|
610 | - * @author eabrand |
|
611 | - */ |
|
612 | - public function prevUntil($selector = null): Query |
|
613 | - { |
|
614 | - $found = new SplObjectStorage(); |
|
615 | - foreach ($this->matches as $m) { |
|
616 | - while (isset($m->previousSibling)) { |
|
617 | - $m = $m->previousSibling; |
|
618 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
619 | - if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) { |
|
620 | - break; |
|
621 | - } |
|
622 | - |
|
623 | - $found->attach($m); |
|
624 | - } |
|
625 | - } |
|
626 | - } |
|
627 | - |
|
628 | - return $this->inst($found, null); |
|
629 | - } |
|
630 | - |
|
631 | - /** |
|
632 | - * Get all ancestors of each element in the DOMQuery until the selector is reached. |
|
633 | - * |
|
634 | - * If a selector is present, only matching ancestors will be retrieved. |
|
635 | - * |
|
636 | - * @param string $selector |
|
637 | - * A valid CSS 3 Selector. |
|
638 | - * |
|
639 | - * @return DOMQuery |
|
640 | - * A DOMNode object containing the matching ancestors. |
|
641 | - * @throws Exception |
|
642 | - * @see siblings() |
|
643 | - * @see children() |
|
644 | - * @since 2.1 |
|
645 | - * @author eabrand |
|
646 | - * @see parent() |
|
647 | - */ |
|
648 | - public function parentsUntil($selector = null): Query |
|
649 | - { |
|
650 | - $found = new SplObjectStorage(); |
|
651 | - foreach ($this->matches as $m) { |
|
652 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
653 | - $m = $m->parentNode; |
|
654 | - // Is there any case where parent node is not an element? |
|
655 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
656 | - if (! empty($selector)) { |
|
657 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
658 | - break; |
|
659 | - } |
|
660 | - $found->attach($m); |
|
661 | - } else { |
|
662 | - $found->attach($m); |
|
663 | - } |
|
664 | - } |
|
665 | - } |
|
666 | - } |
|
667 | - |
|
668 | - return $this->inst($found, null); |
|
669 | - } |
|
670 | - |
|
671 | - /** |
|
672 | - * Reduce the matched set to just one. |
|
673 | - * |
|
674 | - * This will take a matched set and reduce it to just one item -- the item |
|
675 | - * at the index specified. This is a destructive operation, and can be undone |
|
676 | - * with {@link end()}. |
|
677 | - * |
|
678 | - * @param $index |
|
679 | - * The index of the element to keep. The rest will be |
|
680 | - * discarded. |
|
681 | - * |
|
682 | - * @return DOMQuery |
|
683 | - * @throws ParseException |
|
684 | - * @see is() |
|
685 | - * @see end() |
|
686 | - * @see get() |
|
687 | - */ |
|
688 | - public function eq($index): Query |
|
689 | - { |
|
690 | - return $this->inst($this->getNthMatch($index), null); |
|
691 | - } |
|
692 | - |
|
693 | - /** |
|
694 | - * Filter a list to contain only items that do NOT match. |
|
695 | - * |
|
696 | - * @param string $selector |
|
697 | - * A selector to use as a negation filter. If the filter is matched, the |
|
698 | - * element will be removed from the list. |
|
699 | - * |
|
700 | - * @return DOMQuery |
|
701 | - * The DOMQuery object with matching items filtered out. |
|
702 | - * @throws ParseException |
|
703 | - * @throws Exception |
|
704 | - * @see find() |
|
705 | - */ |
|
706 | - public function not($selector): Query |
|
707 | - { |
|
708 | - $found = new SplObjectStorage(); |
|
709 | - if ($selector instanceof DOMElement) { |
|
710 | - foreach ($this->matches as $m) { |
|
711 | - if ($m !== $selector) { |
|
712 | - $found->attach($m); |
|
713 | - } |
|
714 | - } |
|
715 | - } elseif (is_array($selector)) { |
|
716 | - foreach ($this->matches as $m) { |
|
717 | - if (! in_array($m, $selector, true)) { |
|
718 | - $found->attach($m); |
|
719 | - } |
|
720 | - } |
|
721 | - } elseif ($selector instanceof SplObjectStorage) { |
|
722 | - foreach ($this->matches as $m) { |
|
723 | - if ($selector->contains($m)) { |
|
724 | - $found->attach($m); |
|
725 | - } |
|
726 | - } |
|
727 | - } else { |
|
728 | - foreach ($this->matches as $m) { |
|
729 | - if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
730 | - $found->attach($m); |
|
731 | - } |
|
732 | - } |
|
733 | - } |
|
734 | - |
|
735 | - return $this->inst($found, null); |
|
736 | - } |
|
737 | - |
|
738 | - /** |
|
739 | - * Find the closest element matching the selector. |
|
740 | - * |
|
741 | - * This finds the closest match in the ancestry chain. It first checks the |
|
742 | - * present element. If the present element does not match, this traverses up |
|
743 | - * the ancestry chain (e.g. checks each parent) looking for an item that matches. |
|
744 | - * |
|
745 | - * It is provided for jQuery 1.3 compatibility. |
|
746 | - * |
|
747 | - * @param string $selector |
|
748 | - * A CSS Selector to match. |
|
749 | - * |
|
750 | - * @return DOMQuery |
|
751 | - * The set of matches. |
|
752 | - * @throws Exception |
|
753 | - * @since 2.0 |
|
754 | - */ |
|
755 | - public function closest($selector): Query |
|
756 | - { |
|
757 | - $found = new SplObjectStorage(); |
|
758 | - foreach ($this->matches as $m) { |
|
759 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
760 | - $found->attach($m); |
|
761 | - } else { |
|
762 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
763 | - $m = $m->parentNode; |
|
764 | - // Is there any case where parent node is not an element? |
|
765 | - if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with( |
|
766 | - $m, |
|
767 | - null, |
|
768 | - $this->options |
|
769 | - )->is($selector) > 0) { |
|
770 | - $found->attach($m); |
|
771 | - break; |
|
772 | - } |
|
773 | - } |
|
774 | - } |
|
775 | - } |
|
776 | - |
|
777 | - // XXX: Should this be an in-place modification? |
|
778 | - return $this->inst($found, null); |
|
779 | - } |
|
780 | - |
|
781 | - /** |
|
782 | - * Get the immediate parent of each element in the DOMQuery. |
|
783 | - * |
|
784 | - * If a selector is passed, this will return the nearest matching parent for |
|
785 | - * each element in the DOMQuery. |
|
786 | - * |
|
787 | - * @param string $selector |
|
788 | - * A valid CSS3 selector. |
|
789 | - * |
|
790 | - * @return DOMQuery |
|
791 | - * A DOMNode object wrapping the matching parents. |
|
792 | - * @throws Exception |
|
793 | - * @see siblings() |
|
794 | - * @see parents() |
|
795 | - * @see children() |
|
796 | - */ |
|
797 | - public function parent($selector = null): Query |
|
798 | - { |
|
799 | - $found = new SplObjectStorage(); |
|
800 | - foreach ($this->matches as $m) { |
|
801 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
802 | - $m = $m->parentNode; |
|
803 | - // Is there any case where parent node is not an element? |
|
804 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
805 | - if (! empty($selector)) { |
|
806 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
807 | - $found->attach($m); |
|
808 | - break; |
|
809 | - } |
|
810 | - } else { |
|
811 | - $found->attach($m); |
|
812 | - break; |
|
813 | - } |
|
814 | - } |
|
815 | - } |
|
816 | - } |
|
817 | - |
|
818 | - return $this->inst($found, null); |
|
819 | - } |
|
820 | - |
|
821 | - /** |
|
822 | - * Get all ancestors of each element in the DOMQuery. |
|
823 | - * |
|
824 | - * If a selector is present, only matching ancestors will be retrieved. |
|
825 | - * |
|
826 | - * @param string $selector |
|
827 | - * A valid CSS 3 Selector. |
|
828 | - * |
|
829 | - * @return DOMQuery |
|
830 | - * A DOMNode object containing the matching ancestors. |
|
831 | - * @throws ParseException |
|
832 | - * @throws Exception |
|
833 | - * @see children() |
|
834 | - * @see parent() |
|
835 | - * @see siblings() |
|
836 | - */ |
|
837 | - public function parents($selector = null): Query |
|
838 | - { |
|
839 | - $found = new SplObjectStorage(); |
|
840 | - foreach ($this->matches as $m) { |
|
841 | - while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
842 | - $m = $m->parentNode; |
|
843 | - // Is there any case where parent node is not an element? |
|
844 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
845 | - if (! empty($selector)) { |
|
846 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
847 | - $found->attach($m); |
|
848 | - } |
|
849 | - } else { |
|
850 | - $found->attach($m); |
|
851 | - } |
|
852 | - } |
|
853 | - } |
|
854 | - } |
|
855 | - |
|
856 | - return $this->inst($found, null); |
|
857 | - } |
|
858 | - |
|
859 | - /** |
|
860 | - * Get the next sibling of each element in the DOMQuery. |
|
861 | - * |
|
862 | - * If a selector is provided, the next matching sibling will be returned. |
|
863 | - * |
|
864 | - * @param string $selector |
|
865 | - * A CSS3 selector. |
|
866 | - * |
|
867 | - * @return DOMQuery |
|
868 | - * The DOMQuery object. |
|
869 | - * @throws Exception |
|
870 | - * @throws ParseException |
|
871 | - * @see nextAll() |
|
872 | - * @see prev() |
|
873 | - * @see children() |
|
874 | - * @see contents() |
|
875 | - * @see parent() |
|
876 | - * @see parents() |
|
877 | - */ |
|
878 | - public function next($selector = null): Query |
|
879 | - { |
|
880 | - $found = new SplObjectStorage(); |
|
881 | - foreach ($this->matches as $m) { |
|
882 | - while (isset($m->nextSibling)) { |
|
883 | - $m = $m->nextSibling; |
|
884 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
885 | - if (! empty($selector)) { |
|
886 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
887 | - $found->attach($m); |
|
888 | - break; |
|
889 | - } |
|
890 | - } else { |
|
891 | - $found->attach($m); |
|
892 | - break; |
|
893 | - } |
|
894 | - } |
|
895 | - } |
|
896 | - } |
|
897 | - |
|
898 | - return $this->inst($found, null); |
|
899 | - } |
|
900 | - |
|
901 | - /** |
|
902 | - * Get all siblings after an element. |
|
903 | - * |
|
904 | - * For each element in the DOMQuery, get all siblings that appear after |
|
905 | - * it. If a selector is passed in, then only siblings that match the |
|
906 | - * selector will be included. |
|
907 | - * |
|
908 | - * @param string $selector |
|
909 | - * A valid CSS 3 selector. |
|
910 | - * |
|
911 | - * @return DOMQuery |
|
912 | - * The DOMQuery object, now containing the matching siblings. |
|
913 | - * @throws Exception |
|
914 | - * @throws ParseException |
|
915 | - * @see next() |
|
916 | - * @see prevAll() |
|
917 | - * @see children() |
|
918 | - * @see siblings() |
|
919 | - */ |
|
920 | - public function nextAll($selector = null): Query |
|
921 | - { |
|
922 | - $found = new SplObjectStorage(); |
|
923 | - foreach ($this->matches as $m) { |
|
924 | - while (isset($m->nextSibling)) { |
|
925 | - $m = $m->nextSibling; |
|
926 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
927 | - if (! empty($selector)) { |
|
928 | - if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
929 | - $found->attach($m); |
|
930 | - } |
|
931 | - } else { |
|
932 | - $found->attach($m); |
|
933 | - } |
|
934 | - } |
|
935 | - } |
|
936 | - } |
|
937 | - |
|
938 | - return $this->inst($found, null); |
|
939 | - } |
|
940 | - |
|
941 | - /** |
|
942 | - * Get the next sibling before each element in the DOMQuery. |
|
943 | - * |
|
944 | - * For each element in the DOMQuery, this retrieves the previous sibling |
|
945 | - * (if any). If a selector is supplied, it retrieves the first matching |
|
946 | - * sibling (if any is found). |
|
947 | - * |
|
948 | - * @param string $selector |
|
949 | - * A valid CSS 3 selector. |
|
950 | - * |
|
951 | - * @return DOMQuery |
|
952 | - * A DOMNode object, now containing any previous siblings that have been |
|
953 | - * found. |
|
954 | - * @throws Exception |
|
955 | - * @throws ParseException |
|
956 | - * @see prevAll() |
|
957 | - * @see next() |
|
958 | - * @see siblings() |
|
959 | - * @see children() |
|
960 | - */ |
|
961 | - public function prev($selector = null): Query |
|
962 | - { |
|
963 | - $found = new SplObjectStorage(); |
|
964 | - foreach ($this->matches as $m) { |
|
965 | - while (isset($m->previousSibling)) { |
|
966 | - $m = $m->previousSibling; |
|
967 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
968 | - if (! empty($selector)) { |
|
969 | - if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
970 | - $found->attach($m); |
|
971 | - break; |
|
972 | - } |
|
973 | - } else { |
|
974 | - $found->attach($m); |
|
975 | - break; |
|
976 | - } |
|
977 | - } |
|
978 | - } |
|
979 | - } |
|
980 | - |
|
981 | - return $this->inst($found, null); |
|
982 | - } |
|
983 | - |
|
984 | - /** |
|
985 | - * Get the previous siblings for each element in the DOMQuery. |
|
986 | - * |
|
987 | - * For each element in the DOMQuery, get all previous siblings. If a |
|
988 | - * selector is provided, only matching siblings will be retrieved. |
|
989 | - * |
|
990 | - * @param string $selector |
|
991 | - * A valid CSS 3 selector. |
|
992 | - * |
|
993 | - * @return DOMQuery |
|
994 | - * The DOMQuery object, now wrapping previous sibling elements. |
|
995 | - * @throws ParseException |
|
996 | - * @throws Exception |
|
997 | - * @see siblings() |
|
998 | - * @see contents() |
|
999 | - * @see children() |
|
1000 | - * @see prev() |
|
1001 | - * @see nextAll() |
|
1002 | - */ |
|
1003 | - public function prevAll($selector = null): Query |
|
1004 | - { |
|
1005 | - $found = new SplObjectStorage(); |
|
1006 | - foreach ($this->matches as $m) { |
|
1007 | - while (isset($m->previousSibling)) { |
|
1008 | - $m = $m->previousSibling; |
|
1009 | - if ($m->nodeType === XML_ELEMENT_NODE) { |
|
1010 | - if (! empty($selector)) { |
|
1011 | - if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
1012 | - $found->attach($m); |
|
1013 | - } |
|
1014 | - } else { |
|
1015 | - $found->attach($m); |
|
1016 | - } |
|
1017 | - } |
|
1018 | - } |
|
1019 | - } |
|
1020 | - |
|
1021 | - return $this->inst($found, null); |
|
1022 | - } |
|
1023 | - |
|
1024 | - /** |
|
1025 | - * Get the children of the elements in the DOMQuery object. |
|
1026 | - * |
|
1027 | - * If a selector is provided, the list of children will be filtered through |
|
1028 | - * the selector. |
|
1029 | - * |
|
1030 | - * @param string $selector |
|
1031 | - * A valid selector. |
|
1032 | - * |
|
1033 | - * @return DOMQuery |
|
1034 | - * A DOMQuery wrapping all of the children. |
|
1035 | - * @throws ParseException |
|
1036 | - * @see parent() |
|
1037 | - * @see parents() |
|
1038 | - * @see next() |
|
1039 | - * @see prev() |
|
1040 | - * @see removeChildren() |
|
1041 | - */ |
|
1042 | - public function children($selector = null): Query |
|
1043 | - { |
|
1044 | - $found = new SplObjectStorage(); |
|
1045 | - $filter = is_string($selector) && strlen($selector) > 0; |
|
1046 | - |
|
1047 | - if ($filter) { |
|
1048 | - $tmp = new SplObjectStorage(); |
|
1049 | - } |
|
1050 | - foreach ($this->matches as $m) { |
|
1051 | - foreach ($m->childNodes as $c) { |
|
1052 | - if ($c->nodeType === XML_ELEMENT_NODE) { |
|
1053 | - // This is basically an optimized filter() just for children(). |
|
1054 | - if ($filter) { |
|
1055 | - $tmp->attach($c); |
|
1056 | - $query = new DOMTraverser($tmp, true, $c); |
|
1057 | - $query->find($selector); |
|
1058 | - if (count($query->matches()) > 0) { |
|
1059 | - $found->attach($c); |
|
1060 | - } |
|
1061 | - $tmp->detach($c); |
|
1062 | - } // No filter. Just attach it. |
|
1063 | - else { |
|
1064 | - $found->attach($c); |
|
1065 | - } |
|
1066 | - } |
|
1067 | - } |
|
1068 | - } |
|
1069 | - |
|
1070 | - return $this->inst($found, null); |
|
1071 | - } |
|
1072 | - |
|
1073 | - /** |
|
1074 | - * Get all child nodes (not just elements) of all items in the matched set. |
|
1075 | - * |
|
1076 | - * It gets only the immediate children, not all nodes in the subtree. |
|
1077 | - * |
|
1078 | - * This does not process iframes. Xinclude processing is dependent on the |
|
1079 | - * DOM implementation and configuration. |
|
1080 | - * |
|
1081 | - * @return DOMQuery |
|
1082 | - * A DOMNode object wrapping all child nodes for all elements in the |
|
1083 | - * DOMNode object. |
|
1084 | - * @throws ParseException |
|
1085 | - * @see text() |
|
1086 | - * @see html() |
|
1087 | - * @see innerHTML() |
|
1088 | - * @see xml() |
|
1089 | - * @see innerXML() |
|
1090 | - * @see find() |
|
1091 | - */ |
|
1092 | - public function contents(): Query |
|
1093 | - { |
|
1094 | - $found = new SplObjectStorage(); |
|
1095 | - foreach ($this->matches as $m) { |
|
1096 | - if (empty($m->childNodes)) { |
|
1097 | - continue; |
|
1098 | - } |
|
1099 | - foreach ($m->childNodes as $c) { |
|
1100 | - $found->attach($c); |
|
1101 | - } |
|
1102 | - } |
|
1103 | - |
|
1104 | - return $this->inst($found, null); |
|
1105 | - } |
|
1106 | - |
|
1107 | - /** |
|
1108 | - * Get a list of siblings for elements currently wrapped by this object. |
|
1109 | - * |
|
1110 | - * This will compile a list of every sibling of every element in the |
|
1111 | - * current list of elements. |
|
1112 | - * |
|
1113 | - * Note that if two siblings are present in the DOMQuery object to begin with, |
|
1114 | - * then both will be returned in the matched set, since they are siblings of each |
|
1115 | - * other. In other words,if the matches contain a and b, and a and b are siblings of |
|
1116 | - * each other, than running siblings will return a set that contains |
|
1117 | - * both a and b. |
|
1118 | - * |
|
1119 | - * @param string $selector |
|
1120 | - * If the optional selector is provided, siblings will be filtered through |
|
1121 | - * this expression. |
|
1122 | - * |
|
1123 | - * @return DOMQuery |
|
1124 | - * The DOMQuery containing the matched siblings. |
|
1125 | - * @throws ParseException |
|
1126 | - * @throws ParseException |
|
1127 | - * @see parent() |
|
1128 | - * @see parents() |
|
1129 | - * @see contents() |
|
1130 | - * @see children() |
|
1131 | - */ |
|
1132 | - public function siblings($selector = null): Query |
|
1133 | - { |
|
1134 | - $found = new SplObjectStorage(); |
|
1135 | - foreach ($this->matches as $m) { |
|
1136 | - $parent = $m->parentNode; |
|
1137 | - foreach ($parent->childNodes as $n) { |
|
1138 | - if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { |
|
1139 | - $found->attach($n); |
|
1140 | - } |
|
1141 | - } |
|
1142 | - } |
|
1143 | - if (empty($selector)) { |
|
1144 | - return $this->inst($found, null); |
|
1145 | - } |
|
1146 | - |
|
1147 | - return $this->inst($found, null)->filter($selector); |
|
1148 | - } |
|
25 | + /** |
|
26 | + * Filter a list down to only elements that match the selector. |
|
27 | + * Use this, for example, to find all elements with a class, or with |
|
28 | + * certain children. |
|
29 | + * |
|
30 | + * @param string $selector |
|
31 | + * The selector to use as a filter. |
|
32 | + * |
|
33 | + * @return Query The DOMQuery with non-matching items filtered out.* The DOMQuery with non-matching items |
|
34 | + * filtered out. |
|
35 | + * @throws ParseException |
|
36 | + * @see filterCallback() |
|
37 | + * @see map() |
|
38 | + * @see find() |
|
39 | + * @see is() |
|
40 | + * @see filterLambda() |
|
41 | + */ |
|
42 | + public function filter($selector): Query |
|
43 | + { |
|
44 | + $found = new SplObjectStorage(); |
|
45 | + $tmp = new SplObjectStorage(); |
|
46 | + |
|
47 | + foreach ($this->matches as $m) { |
|
48 | + $tmp->attach($m); |
|
49 | + // Seems like this should be right... but it fails unit |
|
50 | + // tests. Need to compare to jQuery. |
|
51 | + // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m); |
|
52 | + $query = new DOMTraverser($tmp); |
|
53 | + $query->find($selector); |
|
54 | + if (count($query->matches())) { |
|
55 | + $found->attach($m); |
|
56 | + } |
|
57 | + $tmp->detach($m); |
|
58 | + } |
|
59 | + |
|
60 | + return $this->inst($found, null); |
|
61 | + } |
|
62 | + |
|
63 | + /** |
|
64 | + * Filter based on a lambda function. |
|
65 | + * |
|
66 | + * The function string will be executed as if it were the body of a |
|
67 | + * function. It is passed two arguments: |
|
68 | + * - $index: The index of the item. |
|
69 | + * - $item: The current Element. |
|
70 | + * If the function returns boolean FALSE, the item will be removed from |
|
71 | + * the list of elements. Otherwise it will be kept. |
|
72 | + * |
|
73 | + * Example: |
|
74 | + * |
|
75 | + * @code |
|
76 | + * qp('li')->filterLambda('qp($item)->attr("id") == "test"'); |
|
77 | + * @endcode |
|
78 | + * |
|
79 | + * The above would filter down the list to only an item whose ID is |
|
80 | + * 'text'. |
|
81 | + * |
|
82 | + * @param string $fn |
|
83 | + * Inline lambda function in a string. |
|
84 | + * |
|
85 | + * @return DOMQuery |
|
86 | + * @throws ParseException |
|
87 | + * |
|
88 | + * @see map() |
|
89 | + * @see mapLambda() |
|
90 | + * @see filterCallback() |
|
91 | + * @see filter() |
|
92 | + * @deprecated |
|
93 | + * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
94 | + * method is not necessary and should be avoided. |
|
95 | + */ |
|
96 | + public function filterLambda($fn): Query |
|
97 | + { |
|
98 | + $function = create_function('$index, $item', $fn); |
|
99 | + $found = new SplObjectStorage(); |
|
100 | + $i = 0; |
|
101 | + foreach ($this->matches as $item) { |
|
102 | + if ($function($i++, $item) !== false) { |
|
103 | + $found->attach($item); |
|
104 | + } |
|
105 | + } |
|
106 | + |
|
107 | + return $this->inst($found, null); |
|
108 | + } |
|
109 | + |
|
110 | + /** |
|
111 | + * Use regular expressions to filter based on the text content of matched elements. |
|
112 | + * |
|
113 | + * Only items that match the given regular expression will be kept. All others will |
|
114 | + * be removed. |
|
115 | + * |
|
116 | + * The regular expression is run against the <i>text content</i> (the PCDATA) of the |
|
117 | + * elements. This is a way of filtering elements based on their content. |
|
118 | + * |
|
119 | + * Example: |
|
120 | + * |
|
121 | + * @code |
|
122 | + * <?xml version="1.0"?> |
|
123 | + * <div>Hello <i>World</i></div> |
|
124 | + * @endcode |
|
125 | + * |
|
126 | + * @code |
|
127 | + * <?php |
|
128 | + * // This will be 1. |
|
129 | + * qp($xml, 'div')->filterPreg('/World/')->matches->count(); |
|
130 | + * ?> |
|
131 | + * @endcode |
|
132 | + * |
|
133 | + * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is |
|
134 | + * @codeHello World@endcode. |
|
135 | + * |
|
136 | + * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class. |
|
137 | + * |
|
138 | + * @param string $regex |
|
139 | + * A regular expression. |
|
140 | + * |
|
141 | + * @return DOMQuery |
|
142 | + * @throws ParseException |
|
143 | + * @see filterCallback() |
|
144 | + * @see preg_match() |
|
145 | + * @see filter() |
|
146 | + */ |
|
147 | + public function filterPreg($regex): Query |
|
148 | + { |
|
149 | + $found = new SplObjectStorage(); |
|
150 | + |
|
151 | + foreach ($this->matches as $item) { |
|
152 | + if (preg_match($regex, $item->textContent) > 0) { |
|
153 | + $found->attach($item); |
|
154 | + } |
|
155 | + } |
|
156 | + |
|
157 | + return $this->inst($found, null); |
|
158 | + } |
|
159 | + |
|
160 | + /** |
|
161 | + * Filter based on a callback function. |
|
162 | + * |
|
163 | + * A callback may be any of the following: |
|
164 | + * - a function: 'my_func'. |
|
165 | + * - an object/method combo: $obj, 'myMethod' |
|
166 | + * - a class/method combo: 'MyClass', 'myMethod' |
|
167 | + * Note that classes are passed in strings. Objects are not. |
|
168 | + * |
|
169 | + * Each callback is passed to arguments: |
|
170 | + * - $index: The index position of the object in the array. |
|
171 | + * - $item: The item to be operated upon. |
|
172 | + * |
|
173 | + * If the callback function returns FALSE, the item will be removed from the |
|
174 | + * set of matches. Otherwise the item will be considered a match and left alone. |
|
175 | + * |
|
176 | + * @param callback $callback . |
|
177 | + * A callback either as a string (function) or an array (object, method OR |
|
178 | + * classname, method). |
|
179 | + * |
|
180 | + * @return DOMQuery |
|
181 | + * Query path object augmented according to the function. |
|
182 | + * @throws ParseException |
|
183 | + * @throws Exception |
|
184 | + * @see map() |
|
185 | + * @see is() |
|
186 | + * @see find() |
|
187 | + * @see filter() |
|
188 | + * @see filterLambda() |
|
189 | + */ |
|
190 | + public function filterCallback($callback): Query |
|
191 | + { |
|
192 | + $found = new SplObjectStorage(); |
|
193 | + $i = 0; |
|
194 | + if (is_callable($callback)) { |
|
195 | + foreach ($this->matches as $item) { |
|
196 | + if ($callback($i++, $item) !== false) { |
|
197 | + $found->attach($item); |
|
198 | + } |
|
199 | + } |
|
200 | + } else { |
|
201 | + throw new Exception('The specified callback is not callable.'); |
|
202 | + } |
|
203 | + |
|
204 | + return $this->inst($found, null); |
|
205 | + } |
|
206 | + |
|
207 | + /** |
|
208 | + * Run a function on each item in a set. |
|
209 | + * |
|
210 | + * The mapping callback can return anything. Whatever it returns will be |
|
211 | + * stored as a match in the set, though. This means that afer a map call, |
|
212 | + * there is no guarantee that the elements in the set will behave correctly |
|
213 | + * with other DOMQuery functions. |
|
214 | + * |
|
215 | + * Callback rules: |
|
216 | + * - If the callback returns NULL, the item will be removed from the array. |
|
217 | + * - If the callback returns an array, the entire array will be stored in |
|
218 | + * the results. |
|
219 | + * - If the callback returns anything else, it will be appended to the array |
|
220 | + * of matches. |
|
221 | + * |
|
222 | + * @param callback $callback |
|
223 | + * The function or callback to use. The callback will be passed two params: |
|
224 | + * - $index: The index position in the list of items wrapped by this object. |
|
225 | + * - $item: The current item. |
|
226 | + * |
|
227 | + * @return DOMQuery |
|
228 | + * The DOMQuery object wrapping a list of whatever values were returned |
|
229 | + * by each run of the callback. |
|
230 | + * |
|
231 | + * @throws Exception |
|
232 | + * @throws ParseException |
|
233 | + * @see find() |
|
234 | + * @see DOMQuery::get() |
|
235 | + * @see filter() |
|
236 | + */ |
|
237 | + public function map($callback): Query |
|
238 | + { |
|
239 | + $found = new SplObjectStorage(); |
|
240 | + |
|
241 | + if (is_callable($callback)) { |
|
242 | + $i = 0; |
|
243 | + foreach ($this->matches as $item) { |
|
244 | + $c = call_user_func($callback, $i, $item); |
|
245 | + if (isset($c)) { |
|
246 | + if (is_array($c) || $c instanceof Iterable) { |
|
247 | + foreach ($c as $retval) { |
|
248 | + if (! is_object($retval)) { |
|
249 | + $tmp = new stdClass(); |
|
250 | + $tmp->textContent = $retval; |
|
251 | + $retval = $tmp; |
|
252 | + } |
|
253 | + $found->attach($retval); |
|
254 | + } |
|
255 | + } else { |
|
256 | + if (! is_object($c)) { |
|
257 | + $tmp = new stdClass(); |
|
258 | + $tmp->textContent = $c; |
|
259 | + $c = $tmp; |
|
260 | + } |
|
261 | + $found->attach($c); |
|
262 | + } |
|
263 | + } |
|
264 | + ++$i; |
|
265 | + } |
|
266 | + } else { |
|
267 | + throw new Exception('Callback is not callable.'); |
|
268 | + } |
|
269 | + |
|
270 | + return $this->inst($found, null); |
|
271 | + } |
|
272 | + |
|
273 | + /** |
|
274 | + * Narrow the items in this object down to only a slice of the starting items. |
|
275 | + * |
|
276 | + * @param integer $start |
|
277 | + * Where in the list of matches to begin the slice. |
|
278 | + * @param integer $length |
|
279 | + * The number of items to include in the slice. If nothing is specified, the |
|
280 | + * all remaining matches (from $start onward) will be included in the sliced |
|
281 | + * list. |
|
282 | + * |
|
283 | + * @return DOMQuery |
|
284 | + * @throws ParseException |
|
285 | + * @see array_slice() |
|
286 | + */ |
|
287 | + public function slice($start, $length = 0): Query |
|
288 | + { |
|
289 | + $end = $length; |
|
290 | + $found = new SplObjectStorage(); |
|
291 | + if ($start >= $this->count()) { |
|
292 | + return $this->inst($found, null); |
|
293 | + } |
|
294 | + |
|
295 | + $i = $j = 0; |
|
296 | + foreach ($this->matches as $m) { |
|
297 | + if ($i >= $start) { |
|
298 | + if ($end > 0 && $j >= $end) { |
|
299 | + break; |
|
300 | + } |
|
301 | + $found->attach($m); |
|
302 | + ++$j; |
|
303 | + } |
|
304 | + ++$i; |
|
305 | + } |
|
306 | + |
|
307 | + return $this->inst($found, null); |
|
308 | + } |
|
309 | + |
|
310 | + /** |
|
311 | + * Run a callback on each item in the list of items. |
|
312 | + * |
|
313 | + * Rules of the callback: |
|
314 | + * - A callback is passed two variables: $index and $item. (There is no |
|
315 | + * special treatment of $this, as there is in jQuery.) |
|
316 | + * - You will want to pass $item by reference if it is not an |
|
317 | + * object (DOMNodes are all objects). |
|
318 | + * - A callback that returns FALSE will stop execution of the each() loop. This |
|
319 | + * works like break in a standard loop. |
|
320 | + * - A TRUE return value from the callback is analogous to a continue statement. |
|
321 | + * - All other return values are ignored. |
|
322 | + * |
|
323 | + * @param callback $callback |
|
324 | + * The callback to run. |
|
325 | + * |
|
326 | + * @return DOMQuery |
|
327 | + * The DOMQuery. |
|
328 | + * @throws Exception |
|
329 | + * @see filter() |
|
330 | + * @see map() |
|
331 | + * @see eachLambda() |
|
332 | + */ |
|
333 | + public function each($callback): Query |
|
334 | + { |
|
335 | + if (is_callable($callback)) { |
|
336 | + $i = 0; |
|
337 | + foreach ($this->matches as $item) { |
|
338 | + if (call_user_func($callback, $i, $item) === false) { |
|
339 | + return $this; |
|
340 | + } |
|
341 | + ++$i; |
|
342 | + } |
|
343 | + } else { |
|
344 | + throw new Exception('Callback is not callable.'); |
|
345 | + } |
|
346 | + |
|
347 | + return $this; |
|
348 | + } |
|
349 | + |
|
350 | + /** |
|
351 | + * An each() iterator that takes a lambda function. |
|
352 | + * |
|
353 | + * @param string $lambda |
|
354 | + * The lambda function. This will be passed ($index, &$item). |
|
355 | + * |
|
356 | + * @return DOMQuery |
|
357 | + * The DOMQuery object. |
|
358 | + * @deprecated |
|
359 | + * Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this |
|
360 | + * method is not necessary and should be avoided. |
|
361 | + * @see each() |
|
362 | + * @see filterLambda() |
|
363 | + * @see filterCallback() |
|
364 | + * @see map() |
|
365 | + */ |
|
366 | + public function eachLambda($lambda): Query |
|
367 | + { |
|
368 | + $index = 0; |
|
369 | + foreach ($this->matches as $item) { |
|
370 | + $fn = create_function('$index, &$item', $lambda); |
|
371 | + if ($fn($index, $item) === false) { |
|
372 | + return $this; |
|
373 | + } |
|
374 | + ++$index; |
|
375 | + } |
|
376 | + |
|
377 | + return $this; |
|
378 | + } |
|
379 | + |
|
380 | + /** |
|
381 | + * Get the even elements, so counter-intuitively 1, 3, 5, etc. |
|
382 | + * |
|
383 | + * @return DOMQuery |
|
384 | + * A DOMQuery wrapping all of the children. |
|
385 | + * @throws ParseException |
|
386 | + * @see parent() |
|
387 | + * @see parents() |
|
388 | + * @see next() |
|
389 | + * @see prev() |
|
390 | + * @since 2.1 |
|
391 | + * @author eabrand |
|
392 | + * @see removeChildren() |
|
393 | + */ |
|
394 | + public function even(): Query |
|
395 | + { |
|
396 | + $found = new SplObjectStorage(); |
|
397 | + $even = false; |
|
398 | + foreach ($this->matches as $m) { |
|
399 | + if ($even && $m->nodeType === XML_ELEMENT_NODE) { |
|
400 | + $found->attach($m); |
|
401 | + } |
|
402 | + $even = $even ? false : true; |
|
403 | + } |
|
404 | + |
|
405 | + return $this->inst($found, null); |
|
406 | + } |
|
407 | + |
|
408 | + /** |
|
409 | + * Get the odd elements, so counter-intuitively 0, 2, 4, etc. |
|
410 | + * |
|
411 | + * @return DOMQuery |
|
412 | + * A DOMQuery wrapping all of the children. |
|
413 | + * @throws ParseException |
|
414 | + * @see parent() |
|
415 | + * @see parents() |
|
416 | + * @see next() |
|
417 | + * @see prev() |
|
418 | + * @since 2.1 |
|
419 | + * @author eabrand |
|
420 | + * @see removeChildren() |
|
421 | + */ |
|
422 | + public function odd(): Query |
|
423 | + { |
|
424 | + $found = new SplObjectStorage(); |
|
425 | + $odd = true; |
|
426 | + foreach ($this->matches as $m) { |
|
427 | + if ($odd && $m->nodeType === XML_ELEMENT_NODE) { |
|
428 | + $found->attach($m); |
|
429 | + } |
|
430 | + $odd = $odd ? false : true; |
|
431 | + } |
|
432 | + |
|
433 | + return $this->inst($found, null); |
|
434 | + } |
|
435 | + |
|
436 | + /** |
|
437 | + * Get the first matching element. |
|
438 | + * |
|
439 | + * |
|
440 | + * @return DOMQuery |
|
441 | + * A DOMQuery wrapping all of the children. |
|
442 | + * @throws ParseException |
|
443 | + * @see prev() |
|
444 | + * @since 2.1 |
|
445 | + * @author eabrand |
|
446 | + * @see next() |
|
447 | + */ |
|
448 | + public function first(): Query |
|
449 | + { |
|
450 | + $found = new SplObjectStorage(); |
|
451 | + foreach ($this->matches as $m) { |
|
452 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
453 | + $found->attach($m); |
|
454 | + break; |
|
455 | + } |
|
456 | + } |
|
457 | + |
|
458 | + return $this->inst($found, null); |
|
459 | + } |
|
460 | + |
|
461 | + /** |
|
462 | + * Get the first child of the matching element. |
|
463 | + * |
|
464 | + * |
|
465 | + * @return DOMQuery |
|
466 | + * A DOMQuery wrapping all of the children. |
|
467 | + * @throws ParseException |
|
468 | + * @see prev() |
|
469 | + * @since 2.1 |
|
470 | + * @author eabrand |
|
471 | + * @see next() |
|
472 | + */ |
|
473 | + public function firstChild(): Query |
|
474 | + { |
|
475 | + // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php |
|
476 | + $found = new SplObjectStorage(); |
|
477 | + $flag = false; |
|
478 | + foreach ($this->matches as $m) { |
|
479 | + foreach ($m->childNodes as $c) { |
|
480 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
481 | + $found->attach($c); |
|
482 | + $flag = true; |
|
483 | + break; |
|
484 | + } |
|
485 | + } |
|
486 | + if ($flag) { |
|
487 | + break; |
|
488 | + } |
|
489 | + } |
|
490 | + |
|
491 | + return $this->inst($found, null); |
|
492 | + } |
|
493 | + |
|
494 | + /** |
|
495 | + * Get the last matching element. |
|
496 | + * |
|
497 | + * |
|
498 | + * @return DOMQuery |
|
499 | + * A DOMQuery wrapping all of the children. |
|
500 | + * @throws ParseException |
|
501 | + * @see prev() |
|
502 | + * @since 2.1 |
|
503 | + * @author eabrand |
|
504 | + * @see next() |
|
505 | + */ |
|
506 | + public function last(): Query |
|
507 | + { |
|
508 | + $found = new SplObjectStorage(); |
|
509 | + $item = null; |
|
510 | + foreach ($this->matches as $m) { |
|
511 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
512 | + $item = $m; |
|
513 | + } |
|
514 | + } |
|
515 | + if ($item) { |
|
516 | + $found->attach($item); |
|
517 | + } |
|
518 | + |
|
519 | + return $this->inst($found, null); |
|
520 | + } |
|
521 | + |
|
522 | + /** |
|
523 | + * Get the last child of the matching element. |
|
524 | + * |
|
525 | + * |
|
526 | + * @return DOMQuery |
|
527 | + * A DOMQuery wrapping all of the children. |
|
528 | + * @throws ParseException |
|
529 | + * @see prev() |
|
530 | + * @since 2.1 |
|
531 | + * @author eabrand |
|
532 | + * @see next() |
|
533 | + */ |
|
534 | + public function lastChild(): Query |
|
535 | + { |
|
536 | + $found = new SplObjectStorage(); |
|
537 | + $item = null; |
|
538 | + foreach ($this->matches as $m) { |
|
539 | + foreach ($m->childNodes as $c) { |
|
540 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
541 | + $item = $c; |
|
542 | + } |
|
543 | + } |
|
544 | + if ($item) { |
|
545 | + $found->attach($item); |
|
546 | + $item = null; |
|
547 | + } |
|
548 | + } |
|
549 | + |
|
550 | + return $this->inst($found, null); |
|
551 | + } |
|
552 | + |
|
553 | + /** |
|
554 | + * Get all siblings after an element until the selector is reached. |
|
555 | + * |
|
556 | + * For each element in the DOMQuery, get all siblings that appear after |
|
557 | + * it. If a selector is passed in, then only siblings that match the |
|
558 | + * selector will be included. |
|
559 | + * |
|
560 | + * @param string $selector |
|
561 | + * A valid CSS 3 selector. |
|
562 | + * |
|
563 | + * @return DOMQuery |
|
564 | + * The DOMQuery object, now containing the matching siblings. |
|
565 | + * @throws Exception |
|
566 | + * @see prevAll() |
|
567 | + * @see children() |
|
568 | + * @see siblings() |
|
569 | + * @since 2.1 |
|
570 | + * @author eabrand |
|
571 | + * @see next() |
|
572 | + */ |
|
573 | + public function nextUntil($selector = null): Query |
|
574 | + { |
|
575 | + $found = new SplObjectStorage(); |
|
576 | + foreach ($this->matches as $m) { |
|
577 | + while (isset($m->nextSibling)) { |
|
578 | + $m = $m->nextSibling; |
|
579 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
580 | + if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
581 | + break; |
|
582 | + } |
|
583 | + $found->attach($m); |
|
584 | + } |
|
585 | + } |
|
586 | + } |
|
587 | + |
|
588 | + return $this->inst($found, null); |
|
589 | + } |
|
590 | + |
|
591 | + /** |
|
592 | + * Get the previous siblings for each element in the DOMQuery |
|
593 | + * until the selector is reached. |
|
594 | + * |
|
595 | + * For each element in the DOMQuery, get all previous siblings. If a |
|
596 | + * selector is provided, only matching siblings will be retrieved. |
|
597 | + * |
|
598 | + * @param string $selector |
|
599 | + * A valid CSS 3 selector. |
|
600 | + * |
|
601 | + * @return DOMQuery |
|
602 | + * The DOMQuery object, now wrapping previous sibling elements. |
|
603 | + * @throws Exception |
|
604 | + * @see prev() |
|
605 | + * @see nextAll() |
|
606 | + * @see siblings() |
|
607 | + * @see contents() |
|
608 | + * @see children() |
|
609 | + * @since 2.1 |
|
610 | + * @author eabrand |
|
611 | + */ |
|
612 | + public function prevUntil($selector = null): Query |
|
613 | + { |
|
614 | + $found = new SplObjectStorage(); |
|
615 | + foreach ($this->matches as $m) { |
|
616 | + while (isset($m->previousSibling)) { |
|
617 | + $m = $m->previousSibling; |
|
618 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
619 | + if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) { |
|
620 | + break; |
|
621 | + } |
|
622 | + |
|
623 | + $found->attach($m); |
|
624 | + } |
|
625 | + } |
|
626 | + } |
|
627 | + |
|
628 | + return $this->inst($found, null); |
|
629 | + } |
|
630 | + |
|
631 | + /** |
|
632 | + * Get all ancestors of each element in the DOMQuery until the selector is reached. |
|
633 | + * |
|
634 | + * If a selector is present, only matching ancestors will be retrieved. |
|
635 | + * |
|
636 | + * @param string $selector |
|
637 | + * A valid CSS 3 Selector. |
|
638 | + * |
|
639 | + * @return DOMQuery |
|
640 | + * A DOMNode object containing the matching ancestors. |
|
641 | + * @throws Exception |
|
642 | + * @see siblings() |
|
643 | + * @see children() |
|
644 | + * @since 2.1 |
|
645 | + * @author eabrand |
|
646 | + * @see parent() |
|
647 | + */ |
|
648 | + public function parentsUntil($selector = null): Query |
|
649 | + { |
|
650 | + $found = new SplObjectStorage(); |
|
651 | + foreach ($this->matches as $m) { |
|
652 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
653 | + $m = $m->parentNode; |
|
654 | + // Is there any case where parent node is not an element? |
|
655 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
656 | + if (! empty($selector)) { |
|
657 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
658 | + break; |
|
659 | + } |
|
660 | + $found->attach($m); |
|
661 | + } else { |
|
662 | + $found->attach($m); |
|
663 | + } |
|
664 | + } |
|
665 | + } |
|
666 | + } |
|
667 | + |
|
668 | + return $this->inst($found, null); |
|
669 | + } |
|
670 | + |
|
671 | + /** |
|
672 | + * Reduce the matched set to just one. |
|
673 | + * |
|
674 | + * This will take a matched set and reduce it to just one item -- the item |
|
675 | + * at the index specified. This is a destructive operation, and can be undone |
|
676 | + * with {@link end()}. |
|
677 | + * |
|
678 | + * @param $index |
|
679 | + * The index of the element to keep. The rest will be |
|
680 | + * discarded. |
|
681 | + * |
|
682 | + * @return DOMQuery |
|
683 | + * @throws ParseException |
|
684 | + * @see is() |
|
685 | + * @see end() |
|
686 | + * @see get() |
|
687 | + */ |
|
688 | + public function eq($index): Query |
|
689 | + { |
|
690 | + return $this->inst($this->getNthMatch($index), null); |
|
691 | + } |
|
692 | + |
|
693 | + /** |
|
694 | + * Filter a list to contain only items that do NOT match. |
|
695 | + * |
|
696 | + * @param string $selector |
|
697 | + * A selector to use as a negation filter. If the filter is matched, the |
|
698 | + * element will be removed from the list. |
|
699 | + * |
|
700 | + * @return DOMQuery |
|
701 | + * The DOMQuery object with matching items filtered out. |
|
702 | + * @throws ParseException |
|
703 | + * @throws Exception |
|
704 | + * @see find() |
|
705 | + */ |
|
706 | + public function not($selector): Query |
|
707 | + { |
|
708 | + $found = new SplObjectStorage(); |
|
709 | + if ($selector instanceof DOMElement) { |
|
710 | + foreach ($this->matches as $m) { |
|
711 | + if ($m !== $selector) { |
|
712 | + $found->attach($m); |
|
713 | + } |
|
714 | + } |
|
715 | + } elseif (is_array($selector)) { |
|
716 | + foreach ($this->matches as $m) { |
|
717 | + if (! in_array($m, $selector, true)) { |
|
718 | + $found->attach($m); |
|
719 | + } |
|
720 | + } |
|
721 | + } elseif ($selector instanceof SplObjectStorage) { |
|
722 | + foreach ($this->matches as $m) { |
|
723 | + if ($selector->contains($m)) { |
|
724 | + $found->attach($m); |
|
725 | + } |
|
726 | + } |
|
727 | + } else { |
|
728 | + foreach ($this->matches as $m) { |
|
729 | + if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
730 | + $found->attach($m); |
|
731 | + } |
|
732 | + } |
|
733 | + } |
|
734 | + |
|
735 | + return $this->inst($found, null); |
|
736 | + } |
|
737 | + |
|
738 | + /** |
|
739 | + * Find the closest element matching the selector. |
|
740 | + * |
|
741 | + * This finds the closest match in the ancestry chain. It first checks the |
|
742 | + * present element. If the present element does not match, this traverses up |
|
743 | + * the ancestry chain (e.g. checks each parent) looking for an item that matches. |
|
744 | + * |
|
745 | + * It is provided for jQuery 1.3 compatibility. |
|
746 | + * |
|
747 | + * @param string $selector |
|
748 | + * A CSS Selector to match. |
|
749 | + * |
|
750 | + * @return DOMQuery |
|
751 | + * The set of matches. |
|
752 | + * @throws Exception |
|
753 | + * @since 2.0 |
|
754 | + */ |
|
755 | + public function closest($selector): Query |
|
756 | + { |
|
757 | + $found = new SplObjectStorage(); |
|
758 | + foreach ($this->matches as $m) { |
|
759 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
760 | + $found->attach($m); |
|
761 | + } else { |
|
762 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
763 | + $m = $m->parentNode; |
|
764 | + // Is there any case where parent node is not an element? |
|
765 | + if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with( |
|
766 | + $m, |
|
767 | + null, |
|
768 | + $this->options |
|
769 | + )->is($selector) > 0) { |
|
770 | + $found->attach($m); |
|
771 | + break; |
|
772 | + } |
|
773 | + } |
|
774 | + } |
|
775 | + } |
|
776 | + |
|
777 | + // XXX: Should this be an in-place modification? |
|
778 | + return $this->inst($found, null); |
|
779 | + } |
|
780 | + |
|
781 | + /** |
|
782 | + * Get the immediate parent of each element in the DOMQuery. |
|
783 | + * |
|
784 | + * If a selector is passed, this will return the nearest matching parent for |
|
785 | + * each element in the DOMQuery. |
|
786 | + * |
|
787 | + * @param string $selector |
|
788 | + * A valid CSS3 selector. |
|
789 | + * |
|
790 | + * @return DOMQuery |
|
791 | + * A DOMNode object wrapping the matching parents. |
|
792 | + * @throws Exception |
|
793 | + * @see siblings() |
|
794 | + * @see parents() |
|
795 | + * @see children() |
|
796 | + */ |
|
797 | + public function parent($selector = null): Query |
|
798 | + { |
|
799 | + $found = new SplObjectStorage(); |
|
800 | + foreach ($this->matches as $m) { |
|
801 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
802 | + $m = $m->parentNode; |
|
803 | + // Is there any case where parent node is not an element? |
|
804 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
805 | + if (! empty($selector)) { |
|
806 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
807 | + $found->attach($m); |
|
808 | + break; |
|
809 | + } |
|
810 | + } else { |
|
811 | + $found->attach($m); |
|
812 | + break; |
|
813 | + } |
|
814 | + } |
|
815 | + } |
|
816 | + } |
|
817 | + |
|
818 | + return $this->inst($found, null); |
|
819 | + } |
|
820 | + |
|
821 | + /** |
|
822 | + * Get all ancestors of each element in the DOMQuery. |
|
823 | + * |
|
824 | + * If a selector is present, only matching ancestors will be retrieved. |
|
825 | + * |
|
826 | + * @param string $selector |
|
827 | + * A valid CSS 3 Selector. |
|
828 | + * |
|
829 | + * @return DOMQuery |
|
830 | + * A DOMNode object containing the matching ancestors. |
|
831 | + * @throws ParseException |
|
832 | + * @throws Exception |
|
833 | + * @see children() |
|
834 | + * @see parent() |
|
835 | + * @see siblings() |
|
836 | + */ |
|
837 | + public function parents($selector = null): Query |
|
838 | + { |
|
839 | + $found = new SplObjectStorage(); |
|
840 | + foreach ($this->matches as $m) { |
|
841 | + while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) { |
|
842 | + $m = $m->parentNode; |
|
843 | + // Is there any case where parent node is not an element? |
|
844 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
845 | + if (! empty($selector)) { |
|
846 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
847 | + $found->attach($m); |
|
848 | + } |
|
849 | + } else { |
|
850 | + $found->attach($m); |
|
851 | + } |
|
852 | + } |
|
853 | + } |
|
854 | + } |
|
855 | + |
|
856 | + return $this->inst($found, null); |
|
857 | + } |
|
858 | + |
|
859 | + /** |
|
860 | + * Get the next sibling of each element in the DOMQuery. |
|
861 | + * |
|
862 | + * If a selector is provided, the next matching sibling will be returned. |
|
863 | + * |
|
864 | + * @param string $selector |
|
865 | + * A CSS3 selector. |
|
866 | + * |
|
867 | + * @return DOMQuery |
|
868 | + * The DOMQuery object. |
|
869 | + * @throws Exception |
|
870 | + * @throws ParseException |
|
871 | + * @see nextAll() |
|
872 | + * @see prev() |
|
873 | + * @see children() |
|
874 | + * @see contents() |
|
875 | + * @see parent() |
|
876 | + * @see parents() |
|
877 | + */ |
|
878 | + public function next($selector = null): Query |
|
879 | + { |
|
880 | + $found = new SplObjectStorage(); |
|
881 | + foreach ($this->matches as $m) { |
|
882 | + while (isset($m->nextSibling)) { |
|
883 | + $m = $m->nextSibling; |
|
884 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
885 | + if (! empty($selector)) { |
|
886 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
887 | + $found->attach($m); |
|
888 | + break; |
|
889 | + } |
|
890 | + } else { |
|
891 | + $found->attach($m); |
|
892 | + break; |
|
893 | + } |
|
894 | + } |
|
895 | + } |
|
896 | + } |
|
897 | + |
|
898 | + return $this->inst($found, null); |
|
899 | + } |
|
900 | + |
|
901 | + /** |
|
902 | + * Get all siblings after an element. |
|
903 | + * |
|
904 | + * For each element in the DOMQuery, get all siblings that appear after |
|
905 | + * it. If a selector is passed in, then only siblings that match the |
|
906 | + * selector will be included. |
|
907 | + * |
|
908 | + * @param string $selector |
|
909 | + * A valid CSS 3 selector. |
|
910 | + * |
|
911 | + * @return DOMQuery |
|
912 | + * The DOMQuery object, now containing the matching siblings. |
|
913 | + * @throws Exception |
|
914 | + * @throws ParseException |
|
915 | + * @see next() |
|
916 | + * @see prevAll() |
|
917 | + * @see children() |
|
918 | + * @see siblings() |
|
919 | + */ |
|
920 | + public function nextAll($selector = null): Query |
|
921 | + { |
|
922 | + $found = new SplObjectStorage(); |
|
923 | + foreach ($this->matches as $m) { |
|
924 | + while (isset($m->nextSibling)) { |
|
925 | + $m = $m->nextSibling; |
|
926 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
927 | + if (! empty($selector)) { |
|
928 | + if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
|
929 | + $found->attach($m); |
|
930 | + } |
|
931 | + } else { |
|
932 | + $found->attach($m); |
|
933 | + } |
|
934 | + } |
|
935 | + } |
|
936 | + } |
|
937 | + |
|
938 | + return $this->inst($found, null); |
|
939 | + } |
|
940 | + |
|
941 | + /** |
|
942 | + * Get the next sibling before each element in the DOMQuery. |
|
943 | + * |
|
944 | + * For each element in the DOMQuery, this retrieves the previous sibling |
|
945 | + * (if any). If a selector is supplied, it retrieves the first matching |
|
946 | + * sibling (if any is found). |
|
947 | + * |
|
948 | + * @param string $selector |
|
949 | + * A valid CSS 3 selector. |
|
950 | + * |
|
951 | + * @return DOMQuery |
|
952 | + * A DOMNode object, now containing any previous siblings that have been |
|
953 | + * found. |
|
954 | + * @throws Exception |
|
955 | + * @throws ParseException |
|
956 | + * @see prevAll() |
|
957 | + * @see next() |
|
958 | + * @see siblings() |
|
959 | + * @see children() |
|
960 | + */ |
|
961 | + public function prev($selector = null): Query |
|
962 | + { |
|
963 | + $found = new SplObjectStorage(); |
|
964 | + foreach ($this->matches as $m) { |
|
965 | + while (isset($m->previousSibling)) { |
|
966 | + $m = $m->previousSibling; |
|
967 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
968 | + if (! empty($selector)) { |
|
969 | + if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
970 | + $found->attach($m); |
|
971 | + break; |
|
972 | + } |
|
973 | + } else { |
|
974 | + $found->attach($m); |
|
975 | + break; |
|
976 | + } |
|
977 | + } |
|
978 | + } |
|
979 | + } |
|
980 | + |
|
981 | + return $this->inst($found, null); |
|
982 | + } |
|
983 | + |
|
984 | + /** |
|
985 | + * Get the previous siblings for each element in the DOMQuery. |
|
986 | + * |
|
987 | + * For each element in the DOMQuery, get all previous siblings. If a |
|
988 | + * selector is provided, only matching siblings will be retrieved. |
|
989 | + * |
|
990 | + * @param string $selector |
|
991 | + * A valid CSS 3 selector. |
|
992 | + * |
|
993 | + * @return DOMQuery |
|
994 | + * The DOMQuery object, now wrapping previous sibling elements. |
|
995 | + * @throws ParseException |
|
996 | + * @throws Exception |
|
997 | + * @see siblings() |
|
998 | + * @see contents() |
|
999 | + * @see children() |
|
1000 | + * @see prev() |
|
1001 | + * @see nextAll() |
|
1002 | + */ |
|
1003 | + public function prevAll($selector = null): Query |
|
1004 | + { |
|
1005 | + $found = new SplObjectStorage(); |
|
1006 | + foreach ($this->matches as $m) { |
|
1007 | + while (isset($m->previousSibling)) { |
|
1008 | + $m = $m->previousSibling; |
|
1009 | + if ($m->nodeType === XML_ELEMENT_NODE) { |
|
1010 | + if (! empty($selector)) { |
|
1011 | + if (QueryPath::with($m, null, $this->options)->is($selector)) { |
|
1012 | + $found->attach($m); |
|
1013 | + } |
|
1014 | + } else { |
|
1015 | + $found->attach($m); |
|
1016 | + } |
|
1017 | + } |
|
1018 | + } |
|
1019 | + } |
|
1020 | + |
|
1021 | + return $this->inst($found, null); |
|
1022 | + } |
|
1023 | + |
|
1024 | + /** |
|
1025 | + * Get the children of the elements in the DOMQuery object. |
|
1026 | + * |
|
1027 | + * If a selector is provided, the list of children will be filtered through |
|
1028 | + * the selector. |
|
1029 | + * |
|
1030 | + * @param string $selector |
|
1031 | + * A valid selector. |
|
1032 | + * |
|
1033 | + * @return DOMQuery |
|
1034 | + * A DOMQuery wrapping all of the children. |
|
1035 | + * @throws ParseException |
|
1036 | + * @see parent() |
|
1037 | + * @see parents() |
|
1038 | + * @see next() |
|
1039 | + * @see prev() |
|
1040 | + * @see removeChildren() |
|
1041 | + */ |
|
1042 | + public function children($selector = null): Query |
|
1043 | + { |
|
1044 | + $found = new SplObjectStorage(); |
|
1045 | + $filter = is_string($selector) && strlen($selector) > 0; |
|
1046 | + |
|
1047 | + if ($filter) { |
|
1048 | + $tmp = new SplObjectStorage(); |
|
1049 | + } |
|
1050 | + foreach ($this->matches as $m) { |
|
1051 | + foreach ($m->childNodes as $c) { |
|
1052 | + if ($c->nodeType === XML_ELEMENT_NODE) { |
|
1053 | + // This is basically an optimized filter() just for children(). |
|
1054 | + if ($filter) { |
|
1055 | + $tmp->attach($c); |
|
1056 | + $query = new DOMTraverser($tmp, true, $c); |
|
1057 | + $query->find($selector); |
|
1058 | + if (count($query->matches()) > 0) { |
|
1059 | + $found->attach($c); |
|
1060 | + } |
|
1061 | + $tmp->detach($c); |
|
1062 | + } // No filter. Just attach it. |
|
1063 | + else { |
|
1064 | + $found->attach($c); |
|
1065 | + } |
|
1066 | + } |
|
1067 | + } |
|
1068 | + } |
|
1069 | + |
|
1070 | + return $this->inst($found, null); |
|
1071 | + } |
|
1072 | + |
|
1073 | + /** |
|
1074 | + * Get all child nodes (not just elements) of all items in the matched set. |
|
1075 | + * |
|
1076 | + * It gets only the immediate children, not all nodes in the subtree. |
|
1077 | + * |
|
1078 | + * This does not process iframes. Xinclude processing is dependent on the |
|
1079 | + * DOM implementation and configuration. |
|
1080 | + * |
|
1081 | + * @return DOMQuery |
|
1082 | + * A DOMNode object wrapping all child nodes for all elements in the |
|
1083 | + * DOMNode object. |
|
1084 | + * @throws ParseException |
|
1085 | + * @see text() |
|
1086 | + * @see html() |
|
1087 | + * @see innerHTML() |
|
1088 | + * @see xml() |
|
1089 | + * @see innerXML() |
|
1090 | + * @see find() |
|
1091 | + */ |
|
1092 | + public function contents(): Query |
|
1093 | + { |
|
1094 | + $found = new SplObjectStorage(); |
|
1095 | + foreach ($this->matches as $m) { |
|
1096 | + if (empty($m->childNodes)) { |
|
1097 | + continue; |
|
1098 | + } |
|
1099 | + foreach ($m->childNodes as $c) { |
|
1100 | + $found->attach($c); |
|
1101 | + } |
|
1102 | + } |
|
1103 | + |
|
1104 | + return $this->inst($found, null); |
|
1105 | + } |
|
1106 | + |
|
1107 | + /** |
|
1108 | + * Get a list of siblings for elements currently wrapped by this object. |
|
1109 | + * |
|
1110 | + * This will compile a list of every sibling of every element in the |
|
1111 | + * current list of elements. |
|
1112 | + * |
|
1113 | + * Note that if two siblings are present in the DOMQuery object to begin with, |
|
1114 | + * then both will be returned in the matched set, since they are siblings of each |
|
1115 | + * other. In other words,if the matches contain a and b, and a and b are siblings of |
|
1116 | + * each other, than running siblings will return a set that contains |
|
1117 | + * both a and b. |
|
1118 | + * |
|
1119 | + * @param string $selector |
|
1120 | + * If the optional selector is provided, siblings will be filtered through |
|
1121 | + * this expression. |
|
1122 | + * |
|
1123 | + * @return DOMQuery |
|
1124 | + * The DOMQuery containing the matched siblings. |
|
1125 | + * @throws ParseException |
|
1126 | + * @throws ParseException |
|
1127 | + * @see parent() |
|
1128 | + * @see parents() |
|
1129 | + * @see contents() |
|
1130 | + * @see children() |
|
1131 | + */ |
|
1132 | + public function siblings($selector = null): Query |
|
1133 | + { |
|
1134 | + $found = new SplObjectStorage(); |
|
1135 | + foreach ($this->matches as $m) { |
|
1136 | + $parent = $m->parentNode; |
|
1137 | + foreach ($parent->childNodes as $n) { |
|
1138 | + if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { |
|
1139 | + $found->attach($n); |
|
1140 | + } |
|
1141 | + } |
|
1142 | + } |
|
1143 | + if (empty($selector)) { |
|
1144 | + return $this->inst($found, null); |
|
1145 | + } |
|
1146 | + |
|
1147 | + return $this->inst($found, null)->filter($selector); |
|
1148 | + } |
|
1149 | 1149 | } |
@@ -245,7 +245,7 @@ discard block |
||
245 | 245 | if (isset($c)) { |
246 | 246 | if (is_array($c) || $c instanceof Iterable) { |
247 | 247 | foreach ($c as $retval) { |
248 | - if (! is_object($retval)) { |
|
248 | + if (!is_object($retval)) { |
|
249 | 249 | $tmp = new stdClass(); |
250 | 250 | $tmp->textContent = $retval; |
251 | 251 | $retval = $tmp; |
@@ -253,7 +253,7 @@ discard block |
||
253 | 253 | $found->attach($retval); |
254 | 254 | } |
255 | 255 | } else { |
256 | - if (! is_object($c)) { |
|
256 | + if (!is_object($c)) { |
|
257 | 257 | $tmp = new stdClass(); |
258 | 258 | $tmp->textContent = $c; |
259 | 259 | $c = $tmp; |
@@ -653,7 +653,7 @@ discard block |
||
653 | 653 | $m = $m->parentNode; |
654 | 654 | // Is there any case where parent node is not an element? |
655 | 655 | if ($m->nodeType === XML_ELEMENT_NODE) { |
656 | - if (! empty($selector)) { |
|
656 | + if (!empty($selector)) { |
|
657 | 657 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
658 | 658 | break; |
659 | 659 | } |
@@ -714,7 +714,7 @@ discard block |
||
714 | 714 | } |
715 | 715 | } elseif (is_array($selector)) { |
716 | 716 | foreach ($this->matches as $m) { |
717 | - if (! in_array($m, $selector, true)) { |
|
717 | + if (!in_array($m, $selector, true)) { |
|
718 | 718 | $found->attach($m); |
719 | 719 | } |
720 | 720 | } |
@@ -726,7 +726,7 @@ discard block |
||
726 | 726 | } |
727 | 727 | } else { |
728 | 728 | foreach ($this->matches as $m) { |
729 | - if (! QueryPath::with($m, null, $this->options)->is($selector)) { |
|
729 | + if (!QueryPath::with($m, null, $this->options)->is($selector)) { |
|
730 | 730 | $found->attach($m); |
731 | 731 | } |
732 | 732 | } |
@@ -802,7 +802,7 @@ discard block |
||
802 | 802 | $m = $m->parentNode; |
803 | 803 | // Is there any case where parent node is not an element? |
804 | 804 | if ($m->nodeType === XML_ELEMENT_NODE) { |
805 | - if (! empty($selector)) { |
|
805 | + if (!empty($selector)) { |
|
806 | 806 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
807 | 807 | $found->attach($m); |
808 | 808 | break; |
@@ -842,7 +842,7 @@ discard block |
||
842 | 842 | $m = $m->parentNode; |
843 | 843 | // Is there any case where parent node is not an element? |
844 | 844 | if ($m->nodeType === XML_ELEMENT_NODE) { |
845 | - if (! empty($selector)) { |
|
845 | + if (!empty($selector)) { |
|
846 | 846 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
847 | 847 | $found->attach($m); |
848 | 848 | } |
@@ -882,7 +882,7 @@ discard block |
||
882 | 882 | while (isset($m->nextSibling)) { |
883 | 883 | $m = $m->nextSibling; |
884 | 884 | if ($m->nodeType === XML_ELEMENT_NODE) { |
885 | - if (! empty($selector)) { |
|
885 | + if (!empty($selector)) { |
|
886 | 886 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
887 | 887 | $found->attach($m); |
888 | 888 | break; |
@@ -924,7 +924,7 @@ discard block |
||
924 | 924 | while (isset($m->nextSibling)) { |
925 | 925 | $m = $m->nextSibling; |
926 | 926 | if ($m->nodeType === XML_ELEMENT_NODE) { |
927 | - if (! empty($selector)) { |
|
927 | + if (!empty($selector)) { |
|
928 | 928 | if (QueryPath::with($m, null, $this->options)->is($selector) > 0) { |
929 | 929 | $found->attach($m); |
930 | 930 | } |
@@ -965,7 +965,7 @@ discard block |
||
965 | 965 | while (isset($m->previousSibling)) { |
966 | 966 | $m = $m->previousSibling; |
967 | 967 | if ($m->nodeType === XML_ELEMENT_NODE) { |
968 | - if (! empty($selector)) { |
|
968 | + if (!empty($selector)) { |
|
969 | 969 | if (QueryPath::with($m, null, $this->options)->is($selector)) { |
970 | 970 | $found->attach($m); |
971 | 971 | break; |
@@ -1007,7 +1007,7 @@ discard block |
||
1007 | 1007 | while (isset($m->previousSibling)) { |
1008 | 1008 | $m = $m->previousSibling; |
1009 | 1009 | if ($m->nodeType === XML_ELEMENT_NODE) { |
1010 | - if (! empty($selector)) { |
|
1010 | + if (!empty($selector)) { |
|
1011 | 1011 | if (QueryPath::with($m, null, $this->options)->is($selector)) { |
1012 | 1012 | $found->attach($m); |
1013 | 1013 | } |