These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * FluentDOM\Query implements a jQuery like replacement for DOMNodeList |
||
4 | * |
||
5 | * @license http://www.opensource.org/licenses/mit-license.php The MIT License |
||
6 | * @copyright Copyright (c) 2009-2014 Bastian Feder, Thomas Weinert |
||
7 | */ |
||
8 | |||
9 | namespace FluentDOM { |
||
10 | use FluentDOM\Nodes\Fetcher; |
||
11 | |||
12 | /** |
||
13 | * FluentDOM\Query implements a jQuery like replacement for DOMNodeList |
||
14 | * |
||
15 | * @property Query\Attributes $attr |
||
16 | * @property Query\Data $data |
||
17 | * @property Query\Css $css |
||
18 | * |
||
19 | * @method Query clone() Clone matched nodes and select the clones. |
||
20 | * @method Query empty() Remove all child nodes from the set of matched elements. |
||
21 | * |
||
22 | * @method Query spawn($elements = NULL) |
||
23 | * @method Query find($selector, $options = 0) |
||
24 | * @method Query end() |
||
25 | */ |
||
26 | class Query extends Nodes { |
||
27 | |||
28 | /** |
||
29 | * @var Nodes\Builder |
||
30 | */ |
||
31 | private $_builder; |
||
32 | |||
33 | /** |
||
34 | * Virtual properties, validate existence |
||
35 | * |
||
36 | * @param string $name |
||
37 | * @return bool |
||
38 | */ |
||
39 | 4 | public function __isset($name) { |
|
40 | switch ($name) { |
||
41 | 4 | case 'attr' : |
|
42 | 4 | case 'css' : |
|
43 | 4 | case 'data' : |
|
44 | 3 | return TRUE; |
|
45 | } |
||
46 | 1 | return parent::__isset($name); |
|
47 | } |
||
48 | |||
49 | /** |
||
50 | * Virtual properties, read property |
||
51 | * |
||
52 | * @param string $name |
||
53 | * @throws \UnexpectedValueException |
||
54 | * @return mixed |
||
55 | */ |
||
56 | 57 | public function __get($name) { |
|
57 | switch ($name) { |
||
58 | 57 | case 'attr' : |
|
59 | 1 | return new Query\Attributes($this); |
|
60 | 56 | case 'css' : |
|
61 | 1 | return new Query\Css($this); |
|
62 | 55 | case 'data' : |
|
63 | 2 | if ($node = $this->getFirstElement()) { |
|
64 | 1 | return new Query\Data($node); |
|
65 | } else { |
||
66 | 1 | throw new \UnexpectedValueException( |
|
67 | 'UnexpectedValueException: first selected node is no element.' |
||
68 | 1 | ); |
|
69 | } |
||
70 | } |
||
71 | 53 | return parent::__get($name); |
|
72 | } |
||
73 | |||
74 | /** |
||
75 | * Block changing the readonly dynamic property |
||
76 | * |
||
77 | * @param string $name |
||
78 | * @param mixed $value |
||
79 | */ |
||
80 | 5 | public function __set($name, $value) { |
|
81 | switch ($name) { |
||
82 | 5 | case 'attr' : |
|
83 | 2 | $this->attr( |
|
84 | 2 | $value instanceOf Query\Attributes ? $value->toArray() : $value |
|
85 | 2 | ); |
|
86 | 2 | break; |
|
87 | 3 | case 'css' : |
|
88 | 2 | $this->css($value); |
|
89 | 2 | break; |
|
90 | 1 | case 'data' : |
|
91 | 1 | $this->data( |
|
92 | 1 | $value instanceOf Query\Data ? $value->toArray() : $value |
|
93 | 1 | ); |
|
94 | 1 | break; |
|
95 | } |
||
96 | 5 | parent::__set($name, $value); |
|
97 | 5 | } |
|
98 | |||
99 | /** |
||
100 | * Throws an exception if somebody tries to unset one |
||
101 | * of the dynamic properties |
||
102 | * |
||
103 | * @param string $name |
||
104 | * @throws \BadMethodCallException |
||
105 | */ |
||
106 | 4 | public function __unset($name) { |
|
107 | switch ($name) { |
||
108 | 4 | case 'attr' : |
|
109 | 4 | case 'css' : |
|
110 | 4 | case 'data' : |
|
111 | 3 | throw new \BadMethodCallException( |
|
112 | 3 | sprintf( |
|
113 | 3 | 'Can not unset property %s::$%s', |
|
114 | 3 | get_class($this), |
|
115 | $name |
||
116 | 3 | ) |
|
117 | 3 | ); |
|
118 | } |
||
119 | 1 | parent::__unset($name); |
|
120 | // @codeCoverageIgnoreStart |
||
121 | } |
||
122 | // @codeCoverageIgnoreEnd |
||
123 | |||
124 | /** |
||
125 | * declaring an empty() or clone() method will crash the parser so we use some magic |
||
126 | * |
||
127 | * @param string $name |
||
128 | * @param array $arguments |
||
129 | * @throws \BadMethodCallException |
||
130 | * @return Query |
||
131 | */ |
||
132 | 4 | public function __call($name, $arguments) { |
|
133 | 4 | switch (strtolower($name)) { |
|
134 | 4 | case 'empty' : |
|
135 | 2 | return $this->emptyNodes(); |
|
136 | 2 | case 'clone' : |
|
137 | 1 | return $this->cloneNodes(); |
|
138 | 1 | default : |
|
139 | 1 | throw new \BadMethodCallException('Unknown method '.get_class($this).'::'.$name); |
|
140 | 1 | } |
|
141 | } |
||
142 | |||
143 | /****************** |
||
144 | * Internal |
||
145 | *****************/ |
||
146 | |||
147 | /** |
||
148 | * Returns the item from the internal array if |
||
149 | * if the index exists and is an DOMElement |
||
150 | * |
||
151 | * @param array|\Traversable |
||
152 | * @return NULL|\DOMElement |
||
153 | */ |
||
154 | 12 | private function getFirstElement() { |
|
155 | 4 | foreach ($this->_nodes as $node) { |
|
156 | 12 | if ($node instanceof \DOMElement) { |
|
157 | 3 | return $node; |
|
158 | } |
||
159 | 1 | } |
|
160 | 1 | return NULL; |
|
161 | } |
||
162 | |||
163 | /** |
||
164 | * Wrap $content around a set of elements |
||
165 | * |
||
166 | * @param array $elements |
||
167 | * @param string|array|\DOMNode|\Traversable|callable $content |
||
168 | * @return Query |
||
169 | */ |
||
170 | 8 | private function wrapNodes($elements, $content) { |
|
171 | 8 | $result = array(); |
|
172 | 8 | $wrapperTemplate = NULL; |
|
173 | 8 | $callback = Constraints::isCallable($content, FALSE, TRUE); |
|
174 | 8 | if (!$callback) { |
|
175 | 6 | $wrapperTemplate = $this->build()->getContentElement($content); |
|
176 | 5 | } |
|
177 | 7 | $simple = FALSE; |
|
178 | 7 | foreach ($elements as $index => $node) { |
|
179 | 7 | if ($callback) { |
|
180 | 2 | $wrapperTemplate = NULL; |
|
181 | 2 | $wrapContent = $callback($node, $index); |
|
182 | 2 | if (!empty($wrapContent)) { |
|
183 | 2 | $wrapperTemplate = $this->build()->getContentElement($wrapContent); |
|
184 | 2 | } |
|
185 | 2 | } |
|
186 | 7 | if ($wrapperTemplate instanceof \DOMElement) { |
|
187 | /** |
||
188 | * @var \DOMElement $target |
||
189 | * @var \DOMElement $wrapper |
||
190 | */ |
||
191 | 7 | list($target, $wrapper) = $this->build()->getWrapperNodes( |
|
192 | 7 | $wrapperTemplate, |
|
193 | $simple |
||
194 | 7 | ); |
|
195 | 7 | if ($node->parentNode instanceof \DOMNode) { |
|
196 | 7 | $node->parentNode->insertBefore($wrapper, $node); |
|
197 | 7 | } |
|
198 | 7 | $target->appendChild($node); |
|
199 | 7 | $result[] = $node; |
|
200 | 7 | } |
|
201 | 7 | } |
|
202 | 7 | return $result; |
|
203 | } |
||
204 | |||
205 | /********************* |
||
206 | * Core |
||
207 | ********************/ |
||
208 | |||
209 | /** |
||
210 | * @param \DOMNode $node |
||
211 | * @return Nodes\Modifier |
||
212 | */ |
||
213 | 32 | private function modify(\DOMNode $node) { |
|
214 | 32 | return new Nodes\Modifier($node); |
|
215 | } |
||
216 | |||
217 | /** |
||
218 | * @return Nodes\Builder |
||
219 | */ |
||
220 | 57 | private function build() { |
|
221 | 57 | if (NULL === $this->_builder) { |
|
222 | 57 | $this->_builder = new Nodes\Builder($this); |
|
223 | 57 | } |
|
224 | 57 | return $this->_builder; |
|
225 | } |
||
226 | |||
227 | /** |
||
228 | * Use a handler callback to apply a content argument to each node $targetNodes. The content |
||
229 | * argument can be an easy setter function |
||
230 | * |
||
231 | * @param array|\DOMNodeList $targetNodes |
||
232 | * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
||
233 | * @param callable $handler |
||
234 | * @return array |
||
235 | */ |
||
236 | 21 | private function apply($targetNodes, $content, callable $handler) { |
|
237 | 21 | $result = array(); |
|
238 | 21 | $isSetterFunction = FALSE; |
|
239 | 21 | if ($callback = Constraints::isCallable($content)) { |
|
240 | 6 | $isSetterFunction = TRUE; |
|
241 | 6 | } else { |
|
242 | 15 | $contentNodes = $this->build()->getContentNodes($content); |
|
243 | } |
||
244 | 21 | foreach ($targetNodes as $index => $node) { |
|
245 | 21 | if ($isSetterFunction) { |
|
246 | 6 | $contentData = $callback($node, $index, $this->build()->getInnerXml($node)); |
|
247 | 6 | if (!empty($contentData)) { |
|
248 | 6 | $contentNodes = $this->build()->getContentNodes($contentData); |
|
249 | 6 | } |
|
250 | 6 | } |
|
251 | 21 | if (!empty($contentNodes)) { |
|
252 | 21 | $resultNodes = call_user_func($handler, $node, $contentNodes); |
|
253 | 21 | if (is_array($resultNodes)) { |
|
254 | 21 | $result = array_merge($result, $resultNodes); |
|
255 | 21 | } |
|
256 | 21 | } |
|
257 | 21 | } |
|
258 | 21 | return $result; |
|
259 | } |
||
260 | |||
261 | /** |
||
262 | * Apply the content to the target nodes using the handler callback |
||
263 | * and push them into a spawned Query object. |
||
264 | * |
||
265 | * @param array|\DOMNodeList $targetNodes |
||
266 | * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
||
267 | * @param callable $handler |
||
268 | * @param bool $remove Call remove() on $this, remove the current selection from the DOM |
||
269 | * @return Query |
||
270 | */ |
||
271 | 19 | private function applyToSpawn($targetNodes, $content, callable $handler, $remove = FALSE) { |
|
272 | 19 | $result = $this->spawn( |
|
273 | 19 | $this->apply($targetNodes, $content, $handler) |
|
274 | 19 | ); |
|
275 | 19 | if ($remove) { |
|
276 | 7 | $this->remove(); |
|
277 | 7 | } |
|
278 | 19 | return $result; |
|
279 | } |
||
280 | |||
281 | /** |
||
282 | * Apply the handler the $handler to nodes defined by selector, using |
||
283 | * the currently selected nodes as context. |
||
284 | * |
||
285 | * @param string|array|\DOMNode|\Traversable $selector |
||
286 | * @param callable $handler |
||
287 | * @param bool $remove Call remove() on $this, remove the current selection from the DOM |
||
288 | * @return Query |
||
289 | */ |
||
290 | 4 | private function applyToSelector($selector, callable $handler, $remove = FALSE) { |
|
291 | 4 | return $this->applyToSpawn( |
|
292 | 4 | $this->build()->getTargetNodes($selector), |
|
293 | 4 | $this->_nodes, |
|
294 | 4 | $handler, |
|
295 | $remove |
||
296 | 4 | ); |
|
297 | } |
||
298 | |||
299 | /********************* |
||
300 | * Traversing |
||
301 | ********************/ |
||
302 | |||
303 | /** |
||
304 | * Adds more elements, matched by the given expression, to the set of matched elements. |
||
305 | * |
||
306 | * @example add.php Usage Examples: FluentDOM::add() |
||
307 | * @param string $selector selector |
||
308 | * @param array|\Traversable $context |
||
309 | * @return Query |
||
310 | */ |
||
311 | 7 | public function add($selector, $context = NULL) { |
|
312 | 7 | $result = $this->spawn($this); |
|
313 | 7 | if (isset($context)) { |
|
314 | 1 | $result->push($this->spawn($context)->find($selector)); |
|
315 | 1 | } elseif ( |
|
316 | 6 | is_object($selector) || |
|
317 | 5 | (is_string($selector) && substr(ltrim($selector), 0, 1) === '<') |
|
318 | 6 | ) { |
|
319 | 4 | $result->push($this->build()->getContentNodes($selector)); |
|
320 | 4 | } else { |
|
321 | 2 | $result->push($this->find($selector)); |
|
322 | } |
||
323 | 7 | $result->_nodes = $result->unique($result->_nodes); |
|
324 | 7 | return $result; |
|
325 | } |
||
326 | |||
327 | /** |
||
328 | * Add the previous selection to the current selection. |
||
329 | * |
||
330 | * @return Query |
||
331 | */ |
||
332 | 1 | public function addBack() { |
|
333 | 1 | $result = $this->spawn(); |
|
334 | 1 | $result->push($this->_nodes); |
|
335 | 1 | $result->push($this->_parent); |
|
336 | 1 | return $result; |
|
337 | } |
||
338 | |||
339 | /** |
||
340 | * Alias for addBack() |
||
341 | * |
||
342 | * @deprecated |
||
343 | * @return Query |
||
344 | */ |
||
345 | public function andSelf() { |
||
346 | return $this->addBack(); |
||
347 | } |
||
348 | |||
349 | /** |
||
350 | * Get a set of elements containing of the unique immediate |
||
351 | * child nodes including only elements (not text nodes) of each |
||
352 | * of the matched set of elements. |
||
353 | * |
||
354 | * @example children.php Usage Examples: FluentDOM\Query::children() |
||
355 | * @param string $selector selector |
||
356 | * @return Query |
||
357 | */ |
||
358 | 2 | public function children($selector = NULL) { |
|
359 | 2 | return $this->fetch( |
|
360 | 2 | '*', |
|
361 | 2 | $selector, |
|
362 | 2 | NULL, |
|
363 | Nodes\Fetcher::UNIQUE |
||
364 | 2 | ); |
|
365 | } |
||
366 | |||
367 | /** |
||
368 | * Get a set of elements containing the closest parent element that matches the specified |
||
369 | * selector, the starting element included. |
||
370 | * |
||
371 | * @example closest.php Usage Example: FluentDOM\Query::closest() |
||
372 | * @param string $selector selector |
||
373 | * @param array|\Traversable $context |
||
374 | * @return Query |
||
375 | */ |
||
376 | 4 | public function closest($selector, $context = NULL) { |
|
377 | 4 | $context = $context ? $this->spawn($context) : $this; |
|
378 | 4 | return $context->fetch( |
|
379 | 4 | 'ancestor-or-self::*', |
|
380 | 4 | $selector, |
|
381 | 4 | $selector, |
|
382 | 4 | Fetcher::REVERSE |Fetcher::INCLUDE_STOP |
|
383 | 4 | ); |
|
384 | } |
||
385 | |||
386 | /** |
||
387 | * Get a set of elements containing all of the unique immediate |
||
388 | * child nodes including elements and text nodes of each of the matched set of elements. |
||
389 | * |
||
390 | * @return Query |
||
391 | */ |
||
392 | 1 | public function contents() { |
|
393 | 1 | return $this->fetch( |
|
394 | 1 | '*|text()[normalize-space(.) != ""]', |
|
395 | 1 | NULL, |
|
396 | 1 | NULL, |
|
397 | Fetcher::UNIQUE |
||
398 | 1 | ); |
|
399 | } |
||
400 | |||
401 | /** |
||
402 | * Reduce the set of matched elements to a single element. |
||
403 | * |
||
404 | * @example eq.php Usage Example: FluentDOM\Query::eq() |
||
405 | * @param integer $position Element index (start with 0) |
||
406 | * @return Query |
||
407 | */ |
||
408 | 2 | public function eq($position) { |
|
409 | 2 | $result = $this->spawn(); |
|
410 | 2 | if ($position < 0) { |
|
411 | 1 | $position = count($this->_nodes) + $position; |
|
412 | 1 | } |
|
413 | 2 | if (isset($this->_nodes[$position])) { |
|
414 | 2 | $result->push($this->_nodes[$position]); |
|
415 | 2 | } |
|
416 | 2 | return $result; |
|
417 | } |
||
418 | |||
419 | /** |
||
420 | * Removes all elements from the set of matched elements that do not match |
||
421 | * the specified expression(s). |
||
422 | * |
||
423 | * @example filter-expr.php Usage Example: FluentDOM\Query::filter() with selector |
||
424 | * @example filter-fn.php Usage Example: FluentDOM\Query::filter() with Closure |
||
425 | * @param string|callable $selector selector or callback function |
||
426 | * @return Query |
||
427 | */ |
||
428 | 2 | public function filter($selector) { |
|
429 | 2 | $callback = $this->getSelectorCallback($selector); |
|
430 | 2 | $result = $this->spawn(); |
|
431 | 2 | foreach ($this->_nodes as $index => $node) { |
|
432 | 2 | if ($callback($node, $index)) { |
|
433 | 2 | $result->push($node); |
|
434 | 2 | } |
|
435 | 2 | } |
|
436 | 2 | return $result; |
|
437 | } |
||
438 | |||
439 | /** |
||
440 | * Get a set of elements containing only the first of the currently selected elements. |
||
441 | * |
||
442 | * @return Query |
||
443 | */ |
||
444 | 1 | public function first() { |
|
445 | 1 | return $this->eq(0); |
|
446 | } |
||
447 | |||
448 | /** |
||
449 | * Retrieve the matched DOM elements in an array. A negative position will be counted from the end. |
||
450 | * |
||
451 | * @param integer|NULL optional offset of a single element to get. |
||
452 | * @return array |
||
453 | */ |
||
454 | 4 | public function get($position = NULL) { |
|
455 | 4 | if (!isset($position)) { |
|
456 | 1 | return $this->_nodes; |
|
457 | } |
||
458 | 3 | if ($position < 0) { |
|
459 | 1 | $position = count($this->_nodes) + $position; |
|
460 | 1 | } |
|
461 | 3 | if (isset($this->_nodes[$position])) { |
|
462 | 2 | return array($this->_nodes[$position]); |
|
463 | } else { |
||
464 | 1 | return array(); |
|
465 | } |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Reduce the set of matched elements to those that have |
||
470 | * a descendant that matches the selector or DOM element. |
||
471 | * |
||
472 | * @param string|\DOMNode $selector selector or DOMNode |
||
473 | * @return Query |
||
474 | */ |
||
475 | 2 | public function has($selector) { |
|
476 | 2 | $callback = $this->getSelectorCallback($selector); |
|
477 | 2 | $result = $this->spawn(); |
|
478 | 2 | foreach ($this->_nodes as $node) { |
|
479 | 2 | if ($selector instanceof \DOMElement) { |
|
480 | 1 | $expression = './/*'; |
|
481 | 1 | } else { |
|
482 | 1 | $expression = './/node()'; |
|
483 | } |
||
484 | 2 | foreach ($this->xpath($expression, $node) as $has) { |
|
485 | 2 | if ($callback($has)) { |
|
486 | 2 | $result->push($node); |
|
487 | 2 | break; |
|
488 | } |
||
489 | 2 | } |
|
490 | 2 | } |
|
491 | 2 | return $result; |
|
492 | } |
||
493 | |||
494 | /** |
||
495 | * Checks the current selection against an expression and returns true, |
||
496 | * if at least one element of the selection fits the given expression. |
||
497 | * |
||
498 | * @example is.php Usage Example: FluentDOM\Query::is() |
||
499 | * @param string $selector selector |
||
500 | * @return boolean |
||
501 | */ |
||
502 | 2 | public function is($selector) { |
|
503 | 2 | foreach ($this->_nodes as $node) { |
|
504 | 1 | if ($this->matches($selector, $node)) { |
|
505 | 1 | return TRUE; |
|
506 | } |
||
507 | 2 | } |
|
508 | 2 | return FALSE; |
|
509 | } |
||
510 | |||
511 | /** |
||
512 | * Get a set of elements containing only the last of the currently selected elements. |
||
513 | * |
||
514 | * @return Query |
||
515 | */ |
||
516 | 1 | public function last() { |
|
517 | 1 | return $this->eq(-1); |
|
518 | } |
||
519 | |||
520 | /** |
||
521 | * Translate a set of elements in the FluentDOM\Query object into |
||
522 | * another set of values in an array (which may, or may not contain elements). |
||
523 | * |
||
524 | * If the callback function returns an array each element of the array will be added to the |
||
525 | * result array. All other variable types are put directly into the result array. |
||
526 | * |
||
527 | * @example map.php Usage Example: FluentDOM\Query::map() |
||
528 | * @param callable $function |
||
529 | * @return array |
||
530 | */ |
||
531 | 2 | public function map(callable $function) { |
|
532 | 2 | $result = array(); |
|
533 | 2 | foreach ($this->_nodes as $index => $node) { |
|
534 | 2 | $mapped = $function($node, $index); |
|
535 | 2 | if ($mapped === NULL) { |
|
536 | 1 | continue; |
|
537 | 2 | } elseif ($mapped instanceof \Traversable || is_array($mapped)) { |
|
538 | 1 | foreach ($mapped as $element) { |
|
539 | 1 | if ($element !== NULL) { |
|
540 | 1 | $result[] = $element; |
|
541 | 1 | } |
|
542 | 1 | } |
|
543 | 1 | } else { |
|
544 | 2 | $result[] = $mapped; |
|
545 | } |
||
546 | 2 | } |
|
547 | 2 | return $result; |
|
548 | } |
||
549 | |||
550 | /** |
||
551 | * Removes elements matching the specified expression from the set of matched elements. |
||
552 | * |
||
553 | * @example not.php Usage Example: FluentDOM\Query::not() |
||
554 | * @param string|callback $selector selector or callback function |
||
555 | * @return Query |
||
556 | */ |
||
557 | 2 | public function not($selector) { |
|
558 | 2 | $callback = $this->getSelectorCallback($selector); |
|
559 | 2 | return $this->filter( |
|
560 | function (\DOMNode $node, $index) use ($callback) { |
||
561 | 2 | return !$callback($node, $index); |
|
562 | } |
||
563 | 2 | ); |
|
564 | } |
||
565 | |||
566 | /** |
||
567 | * Get a set of elements containing the unique next siblings of each of the |
||
568 | * given set of elements. |
||
569 | * |
||
570 | * @example next.php Usage Example: FluentDOM\Query::next() |
||
571 | * @param string $selector |
||
572 | * @return Query |
||
573 | */ |
||
574 | 1 | public function next($selector = NULL) { |
|
575 | 1 | return $this->fetch( |
|
576 | 'following-sibling::node()[ |
||
577 | self::* or (self::text() and normalize-space(.) != "") |
||
578 | 1 | ][1]', |
|
579 | 1 | $selector, |
|
580 | 1 | NULL, |
|
581 | Nodes\Fetcher::UNIQUE |
||
582 | 1 | ); |
|
583 | } |
||
584 | |||
585 | /** |
||
586 | * Find all sibling elements after the current element. |
||
587 | * |
||
588 | * @example nextAll.php Usage Example: FluentDOM\Query::nextAll() |
||
589 | * @param string $selector selector |
||
590 | * @return Query |
||
591 | */ |
||
592 | 1 | public function nextAll($selector = NULL) { |
|
593 | 1 | return $this->fetch( |
|
594 | 1 | 'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]', |
|
595 | $selector |
||
596 | 1 | ); |
|
597 | } |
||
598 | |||
599 | /** |
||
600 | * Get all following siblings of each element up to but |
||
601 | * not including the element matched by the selector. |
||
602 | * |
||
603 | * @param string $selector selector |
||
604 | * @param string $filter selector |
||
605 | * @return Query |
||
606 | */ |
||
607 | 1 | public function nextUntil($selector = NULL, $filter = NULL) { |
|
608 | 1 | return $this->fetch( |
|
609 | 1 | 'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]', |
|
610 | 1 | $filter, |
|
611 | $selector |
||
612 | 1 | ); |
|
613 | } |
||
614 | |||
615 | /** |
||
616 | * Get a set of elements containing the unique parents of the matched set of elements. |
||
617 | * |
||
618 | * @example parent.php Usage Example: FluentDOM\Query::parent() |
||
619 | * @return Query |
||
620 | */ |
||
621 | 1 | public function parent() { |
|
622 | 1 | return $this->fetch( |
|
623 | 1 | 'parent::*', |
|
624 | 1 | NULL, |
|
625 | 1 | NULL, |
|
626 | Fetcher::UNIQUE |
||
627 | 1 | ); |
|
628 | } |
||
629 | |||
630 | /** |
||
631 | * Get the ancestors of each element in the current set of matched elements, |
||
632 | * optionally filtered by a selector. |
||
633 | * |
||
634 | * @example parents.php Usage Example: FluentDOM\Query::parents() |
||
635 | * @param string $selector selector |
||
636 | * @return Query |
||
637 | */ |
||
638 | 1 | public function parents($selector = NULL) { |
|
639 | 1 | return $this->fetch( |
|
640 | 1 | 'ancestor::*', |
|
641 | 1 | $selector, |
|
642 | 1 | NULL, |
|
643 | Fetcher::REVERSE |
||
644 | 1 | ); |
|
645 | } |
||
646 | |||
647 | /** |
||
648 | * Get the ancestors of each element in the current set of matched elements, |
||
649 | * up to but not including the element matched by the selector. |
||
650 | * |
||
651 | * @param string $stopAt selector |
||
652 | * @param string $filter selector |
||
653 | * @return Query |
||
654 | */ |
||
655 | 1 | public function parentsUntil($stopAt = NULL, $filter = NULL) { |
|
656 | 1 | return $this->fetch( |
|
657 | 1 | 'ancestor::*', |
|
658 | 1 | $filter, |
|
659 | 1 | $stopAt, |
|
660 | Nodes\Fetcher::REVERSE |
||
661 | 1 | ); |
|
662 | } |
||
663 | |||
664 | /** |
||
665 | * Get a set of elements containing the unique previous siblings of each of the |
||
666 | * matched set of elements. |
||
667 | * |
||
668 | * @example prev.php Usage Example: FluentDOM\Query::prev() |
||
669 | * @param string $selector selector |
||
670 | * @return Query |
||
671 | */ |
||
672 | 2 | public function prev($selector = NULL) { |
|
673 | 2 | return $this->fetch( |
|
674 | 'preceding-sibling::node()[ |
||
675 | self::* or (self::text() and normalize-space(.) != "") |
||
676 | 2 | ][1]', |
|
677 | 2 | $selector, |
|
678 | 2 | NULL, |
|
679 | Nodes\Fetcher::UNIQUE |
||
680 | 2 | ); |
|
681 | } |
||
682 | |||
683 | /** |
||
684 | * Find all sibling elements in front of the current element. |
||
685 | * |
||
686 | * @example prevAll.php Usage Example: FluentDOM\Query::prevAll() |
||
687 | * @param string $selector selector |
||
688 | * @return Query |
||
689 | */ |
||
690 | 1 | public function prevAll($selector = NULL) { |
|
691 | 1 | return $this->fetch( |
|
692 | 1 | 'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]', |
|
693 | 1 | $selector, |
|
694 | 1 | NULL, |
|
695 | Nodes\Fetcher::REVERSE |
||
696 | 1 | ); |
|
697 | } |
||
698 | |||
699 | /** |
||
700 | * Get all preceding siblings of each element up to but not including |
||
701 | * the element matched by the selector. |
||
702 | * |
||
703 | * @param string $selector selector |
||
704 | * @param string $filter selector |
||
705 | * @return Query |
||
706 | */ |
||
707 | 1 | public function prevUntil($selector = NULL, $filter = NULL) { |
|
708 | 1 | return $this->fetch( |
|
709 | 1 | 'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]', |
|
710 | 1 | $filter, |
|
711 | 1 | $selector, |
|
712 | Nodes\Fetcher::REVERSE |
||
713 | 1 | ); |
|
714 | } |
||
715 | |||
716 | |||
717 | /** |
||
718 | * Reverse the order of the matched elements. |
||
719 | * |
||
720 | * @return Query |
||
721 | */ |
||
722 | 1 | public function reverse() { |
|
723 | 1 | $result = $this->spawn(); |
|
724 | 1 | $result->push(array_reverse($this->_nodes)); |
|
725 | 1 | return $result; |
|
726 | } |
||
727 | |||
728 | /** |
||
729 | * Get a set of elements containing all of the unique siblings of each of the |
||
730 | * matched set of elements. |
||
731 | * |
||
732 | * @example siblings.php Usage Example: FluentDOM\Query::siblings() |
||
733 | * @param string $selector selector |
||
734 | * @return Query |
||
735 | */ |
||
736 | 1 | public function siblings($selector = NULL) { |
|
737 | 1 | return $this->fetch( |
|
738 | 'preceding-sibling::*| |
||
739 | preceding-sibling::text()[normalize-space(.) != ""]| |
||
740 | following-sibling::*| |
||
741 | 1 | following-sibling::text()[normalize-space(.) != ""]', |
|
742 | 1 | $selector, |
|
743 | 1 | NULL, |
|
744 | Nodes\Fetcher::REVERSE |
||
745 | 1 | ); |
|
746 | } |
||
747 | |||
748 | /** |
||
749 | * Selects a subset of the matched elements. |
||
750 | * |
||
751 | * @example slice.php Usage Example: FluentDOM\Query::slice() |
||
752 | * @param integer $start |
||
753 | * @param integer $end |
||
754 | * @return Query |
||
755 | */ |
||
756 | 4 | public function slice($start, $end = NULL) { |
|
757 | 4 | $result = $this->spawn(); |
|
758 | 4 | if ($end === NULL) { |
|
759 | 1 | $result->push(array_slice($this->_nodes, $start)); |
|
760 | 4 | } elseif ($end < 0) { |
|
761 | 1 | $result->push(array_slice($this->_nodes, $start, $end)); |
|
762 | 3 | } elseif ($end > $start) { |
|
763 | 1 | $result->push(array_slice($this->_nodes, $start, $end - $start)); |
|
764 | 1 | } else { |
|
765 | 1 | $result->push(array_slice($this->_nodes, $end, $start - $end)); |
|
766 | } |
||
767 | 4 | return $result; |
|
768 | } |
||
769 | |||
770 | /********************* |
||
771 | * Manipulation |
||
772 | ********************/ |
||
773 | |||
774 | /** |
||
775 | * Insert content after each of the matched elements. |
||
776 | * |
||
777 | * @example after.php Usage Example: FluentDOM\Query::after() |
||
778 | * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content |
||
779 | * @return Query |
||
780 | */ |
||
781 | 2 | public function after($content) { |
|
782 | 2 | return $this->applyToSpawn( |
|
783 | 2 | $this->_nodes, |
|
784 | 2 | $content, |
|
785 | function($targetNode, $contentNodes) { |
||
786 | 2 | return $this->modify($targetNode)->insertNodesAfter($contentNodes); |
|
787 | } |
||
788 | 2 | ); |
|
789 | } |
||
790 | |||
791 | /** |
||
792 | * Append content to the inside of every matched element. |
||
793 | * |
||
794 | * @example append.php Usage Example: FluentDOM\Query::append() |
||
795 | * @param string|array|\DOMNode|\Traversable|callable $content DOMNode or DOMNodeList or xml fragment string |
||
796 | * @return Query |
||
797 | */ |
||
798 | 14 | public function append($content) { |
|
799 | 14 | if (empty($this->_nodes) && |
|
800 | 14 | $this->_useDocumentContext && |
|
801 | 14 | !isset($this->getDocument()->documentElement)) { |
|
802 | 10 | if ($callback = Constraints::isCallable($content)) { |
|
803 | 1 | $contentNode = $this->build()->getContentElement($callback(NULL, 0, '')); |
|
804 | 1 | } else { |
|
805 | 9 | $contentNode = $this->build()->getContentElement($content); |
|
806 | } |
||
807 | 8 | return $this->spawn($this->getDocument()->appendChild($contentNode)); |
|
808 | } else { |
||
809 | 5 | return $this->applyToSpawn( |
|
810 | 5 | $this->_nodes, |
|
811 | 5 | $content, |
|
812 | function($targetNode, $contentNodes) { |
||
813 | 5 | return $this->modify($targetNode)->appendChildren($contentNodes); |
|
814 | } |
||
815 | 5 | ); |
|
816 | } |
||
817 | } |
||
818 | |||
819 | /** |
||
820 | * Append all of the matched elements to another, specified, set of elements. |
||
821 | * Returns all of the inserted elements. |
||
822 | * |
||
823 | * @example appendTo.php Usage Example: FluentDOM\Query::appendTo() |
||
824 | * @param string|array|\DOMNode|\DOMNodeList|Query $selector |
||
825 | * @return Query |
||
826 | */ |
||
827 | 2 | public function appendTo($selector) { |
|
828 | 2 | return $this->applyToSelector( |
|
829 | 2 | $selector, |
|
830 | function($targetNode, $contentNodes) { |
||
831 | 2 | return $this->modify($targetNode)->appendChildren($contentNodes); |
|
832 | 2 | }, |
|
833 | TRUE |
||
834 | 2 | ); |
|
835 | } |
||
836 | |||
837 | /** |
||
838 | * Insert content before each of the matched elements. |
||
839 | * |
||
840 | * @example before.php Usage Example: FluentDOM\Query::before() |
||
841 | * @param string|array|\DOMNode|\Traversable|callable $content |
||
842 | * @return Query |
||
843 | */ |
||
844 | 2 | public function before($content) { |
|
845 | 2 | return $this->applyToSpawn( |
|
846 | 2 | $this->_nodes, |
|
847 | 2 | $content, |
|
848 | function($targetNode, $contentNodes) { |
||
849 | 2 | return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
|
850 | } |
||
851 | 2 | ); |
|
852 | } |
||
853 | |||
854 | /** |
||
855 | * Clone matched DOM Elements and select the clones. |
||
856 | * |
||
857 | * This is the clone() method - but because clone |
||
858 | * is a reserved word we can no declare it directly |
||
859 | * @see __call |
||
860 | * |
||
861 | * @example clone.php Usage Example: FluentDOM\Query:clone() |
||
862 | * @return Query |
||
863 | */ |
||
864 | 1 | private function cloneNodes() { |
|
865 | 1 | $result = $this->spawn(); |
|
866 | 1 | foreach ($this->_nodes as $node) { |
|
867 | /** @var \DOMNode $node */ |
||
868 | 1 | $result->push($node->cloneNode(TRUE)); |
|
869 | 1 | } |
|
870 | 1 | return $result; |
|
871 | } |
||
872 | |||
873 | /** |
||
874 | * Remove all child nodes from the set of matched elements. |
||
875 | * |
||
876 | * This is the empty() method - but because empty |
||
877 | * is a reserved word we can no declare it directly |
||
878 | * @see __call |
||
879 | * |
||
880 | * @example empty.php Usage Example: FluentDOM\Query:empty() |
||
881 | * @return Query |
||
882 | */ |
||
883 | 2 | private function emptyNodes() { |
|
884 | 2 | $this->each( |
|
885 | function (\DOMNode $node) { |
||
886 | 2 | $node->nodeValue = ''; |
|
887 | 2 | } |
|
888 | 2 | ); |
|
889 | 2 | $this->_useDocumentContext = TRUE; |
|
890 | 2 | return $this; |
|
891 | } |
||
892 | |||
893 | /** |
||
894 | * Insert all of the matched elements after another, specified, set of elements. |
||
895 | * |
||
896 | * @example insertAfter.php Usage Example: FluentDOM\Query::insertAfter() |
||
897 | * @param string|array|\DOMNode|\Traversable $selector |
||
898 | * @return Query |
||
899 | */ |
||
900 | 1 | public function insertAfter($selector) { |
|
901 | 1 | return $this->applyToSpawn( |
|
902 | 1 | $this->build()->getTargetNodes($selector), |
|
903 | 1 | $this->_nodes, |
|
904 | function($targetNode, $contentNodes) { |
||
905 | 1 | return $this->modify($targetNode)->insertNodesAfter($contentNodes); |
|
906 | 1 | }, |
|
907 | TRUE |
||
908 | 1 | ); |
|
909 | } |
||
910 | |||
911 | /** |
||
912 | * Insert all of the matched elements before another, specified, set of elements. |
||
913 | * |
||
914 | * @example insertBefore.php Usage Example: FluentDOM\Query::insertBefore() |
||
915 | * @param string|array|\DOMNode|\Traversable $selector |
||
916 | * @return Query |
||
917 | */ |
||
918 | 1 | public function insertBefore($selector) { |
|
919 | 1 | return $this->applyToSelector( |
|
920 | 1 | $selector, |
|
921 | function($targetNode, $contentNodes) { |
||
922 | 1 | return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
|
923 | 1 | }, |
|
924 | TRUE |
||
925 | 1 | ); |
|
926 | } |
||
927 | |||
928 | /** |
||
929 | * Prepend content to the inside of every matched element. |
||
930 | * |
||
931 | * @example prepend.php Usage Example: FluentDOM\Query::prepend() |
||
932 | * @param string|array|\DOMNode|\Traversable $content |
||
933 | * @return Query |
||
934 | */ |
||
935 | 3 | public function prepend($content) { |
|
936 | 3 | return $this->applyToSpawn( |
|
937 | 3 | $this->_nodes, |
|
938 | 3 | $content, |
|
939 | function($targetNode, $contentNodes) { |
||
940 | 3 | return $this->modify($targetNode)->insertChildrenBefore($contentNodes); |
|
941 | } |
||
942 | 3 | ); |
|
943 | } |
||
944 | |||
945 | /** |
||
946 | * Prepend all of the matched elements to another, specified, set of elements. |
||
947 | * Returns all of the inserted elements. |
||
948 | * |
||
949 | * @example prependTo.php Usage Example: FluentDOM\Query::prependTo() |
||
950 | * @param string|array|\DOMNode|\DOMNodeList|Query $selector |
||
951 | * @return Query list of all new elements |
||
952 | */ |
||
953 | 1 | public function prependTo($selector) { |
|
954 | 1 | return $this->applyToSelector( |
|
955 | 1 | $selector, |
|
956 | function($targetNode, $contentNodes) { |
||
957 | 1 | return $this->modify($targetNode)->insertChildrenBefore($contentNodes); |
|
958 | 1 | }, |
|
959 | TRUE |
||
960 | 1 | ); |
|
961 | } |
||
962 | |||
963 | /** |
||
964 | * Replaces the elements matched by the specified selector with the matched elements. |
||
965 | * |
||
966 | * @example replaceAll.php Usage Example: FluentDOM\Query::replaceAll() |
||
967 | * @param string|array|\DOMNode|\Traversable $selector |
||
968 | * @return Query |
||
969 | */ |
||
970 | 3 | public function replaceAll($selector) { |
|
971 | 3 | $result = $this->applyToSpawn( |
|
972 | 3 | $targetNodes = $this->build()->getTargetNodes($selector), |
|
973 | 2 | $this->_nodes, |
|
974 | function($targetNode, $contentNodes) { |
||
975 | 2 | return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
|
976 | 2 | }, |
|
977 | TRUE |
||
978 | 2 | ); |
|
979 | 2 | $target = $this->spawn($targetNodes); |
|
980 | 2 | $target->remove(); |
|
981 | 2 | return $result; |
|
982 | } |
||
983 | |||
984 | /** |
||
985 | * Replaces all matched elements with the specified HTML or DOM elements. |
||
986 | * This returns the element that was just replaced, |
||
987 | * which has been removed from the DOM. |
||
988 | * |
||
989 | * @example replaceWith.php Usage Example: FluentDOM\Query::replaceWith() |
||
990 | * @param string|array|\DOMNode|\Traversable|callable $content |
||
991 | * @return Query |
||
992 | */ |
||
993 | 2 | public function replaceWith($content) { |
|
994 | 2 | $this->apply( |
|
995 | 2 | $this->_nodes, |
|
996 | 2 | $content, |
|
997 | function($targetNode, $contentNodes) { |
||
998 | 2 | return $this->modify($targetNode)->insertNodesBefore($contentNodes); |
|
999 | } |
||
1000 | 2 | ); |
|
1001 | 2 | $this->remove(); |
|
1002 | 2 | return $this; |
|
1003 | } |
||
1004 | |||
1005 | /** |
||
1006 | * Removes all matched elements from the DOM. |
||
1007 | * |
||
1008 | * @example remove.php Usage Example: FluentDOM\Query::remove() |
||
1009 | * @param string $selector selector |
||
1010 | * @return Query removed elements |
||
1011 | */ |
||
1012 | 11 | public function remove($selector = NULL) { |
|
1013 | 11 | $result = $this->spawn(); |
|
1014 | 11 | foreach ($this->_nodes as $node) { |
|
1015 | 11 | if ($node->parentNode instanceof \DOMNode) { |
|
1016 | 11 | if (empty($selector) || $this->matches($selector, $node)) { |
|
1017 | 11 | $result->push($node->parentNode->removeChild($node)); |
|
1018 | 11 | } |
|
1019 | 11 | } |
|
1020 | 11 | } |
|
1021 | 11 | return $result; |
|
1022 | } |
||
1023 | |||
1024 | /** |
||
1025 | * Get the combined text contents of all matched elements or |
||
1026 | * set the text contents of all matched elements. |
||
1027 | * |
||
1028 | * @example text.php Usage Example: FluentDOM\Query::text() |
||
1029 | * @param string|callable $text |
||
1030 | * @return string|Query |
||
1031 | */ |
||
1032 | 3 | public function text($text = NULL) { |
|
1033 | 3 | if (isset($text)) { |
|
1034 | 2 | $callback = Constraints::isCallable($text, FALSE, TRUE); |
|
1035 | 2 | foreach ($this->_nodes as $index => $node) { |
|
1036 | 2 | if ($callback) { |
|
1037 | 1 | $node->nodeValue = $callback($node, $index, $node->nodeValue); |
|
1038 | 1 | } else { |
|
1039 | 1 | $node->nodeValue = $text; |
|
1040 | } |
||
1041 | 2 | } |
|
1042 | 2 | return $this; |
|
1043 | } else { |
||
1044 | 1 | $result = ''; |
|
1045 | 1 | foreach ($this->_nodes as $node) { |
|
1046 | 1 | $result .= $node->textContent; |
|
1047 | 1 | } |
|
1048 | 1 | return $result; |
|
1049 | } |
||
1050 | } |
||
1051 | |||
1052 | /** |
||
1053 | * Wrap each matched element with the specified content. |
||
1054 | * |
||
1055 | * If $content contains several elements the first one is used |
||
1056 | * |
||
1057 | * @example wrap.php Usage Example: FluentDOM\Query::wrap() |
||
1058 | * @param string|array|\DOMNode|\Traversable|callable $content |
||
1059 | * @return Query |
||
1060 | */ |
||
1061 | 6 | public function wrap($content) { |
|
1062 | 6 | $result = $this->spawn($this->wrapNodes($this->_nodes, $content)); |
|
1063 | 5 | return $result; |
|
1064 | } |
||
1065 | |||
1066 | /** |
||
1067 | * Wrap al matched elements with the specified content |
||
1068 | * |
||
1069 | * If the matched elements are not siblings, wrap each group of siblings. |
||
1070 | * |
||
1071 | * @example wrapAll.php Usage Example: FluentDOM::wrapAll() |
||
1072 | * @param string|array|\DOMNode|\Traversable $content |
||
1073 | * @return Query |
||
1074 | */ |
||
1075 | 2 | public function wrapAll($content) { |
|
1076 | 2 | $result = $this->spawn(); |
|
1077 | 2 | if ($groups = $this->getGroupedNodes()) { |
|
1078 | 2 | $result->push( |
|
1079 | 2 | $this->wrapGroupedNodes( |
|
1080 | 2 | $groups, $this->build()->getContentElement($content) |
|
1081 | 2 | ) |
|
1082 | 2 | ); |
|
1083 | 2 | } |
|
1084 | 2 | return $result; |
|
1085 | } |
||
1086 | |||
1087 | /** |
||
1088 | * group selected elements by previous node - ignore whitespace text nodes |
||
1089 | * |
||
1090 | * @return array|bool |
||
1091 | */ |
||
1092 | 2 | private function getGroupedNodes() { |
|
1093 | 2 | $current = NULL; |
|
1094 | 2 | $counter = 0; |
|
1095 | 2 | $groups = array(); |
|
1096 | 2 | foreach ($this->_nodes as $node) { |
|
1097 | 2 | $previous = $node->previousSibling; |
|
1098 | 2 | while ($previous instanceof \DOMText && $previous->isWhitespaceInElementContent()) { |
|
1099 | 2 | $previous = $previous->previousSibling; |
|
1100 | 2 | } |
|
1101 | 2 | if ($previous !== $current) { |
|
1102 | 2 | $counter++; |
|
1103 | 2 | } |
|
1104 | 2 | $groups[$counter][] = $node; |
|
1105 | 2 | $current = $node; |
|
1106 | 2 | } |
|
1107 | 2 | return count($groups) > 0 ? $groups : FALSE; |
|
1108 | } |
||
1109 | |||
1110 | /** |
||
1111 | * Wrap grouped nodes |
||
1112 | * |
||
1113 | * @param array $groups |
||
1114 | * @param \DOMElement $template |
||
1115 | * @return array |
||
1116 | */ |
||
1117 | 2 | private function wrapGroupedNodes(array $groups, \DOMElement $template) { |
|
1118 | 2 | $result = []; |
|
1119 | 2 | $simple = FALSE; |
|
1120 | 2 | foreach ($groups as $group) { |
|
1121 | 2 | if (isset($group[0])) { |
|
1122 | 2 | $node = $group[0]; |
|
1123 | /** |
||
1124 | * @var \DOMElement $target |
||
1125 | * @var \DOMElement $wrapper |
||
1126 | */ |
||
1127 | 2 | list($target, $wrapper) = $this->build()->getWrapperNodes( |
|
1128 | 2 | $template, |
|
1129 | $simple |
||
1130 | 2 | ); |
|
1131 | 2 | if ($node->parentNode instanceof \DOMNode) { |
|
1132 | 2 | $node->parentNode->insertBefore($wrapper, $node); |
|
1133 | 2 | } |
|
1134 | 2 | foreach ($group as $node) { |
|
1135 | 2 | $target->appendChild($node); |
|
1136 | 2 | } |
|
1137 | 2 | $result[] = $node; |
|
1138 | 2 | } |
|
1139 | 2 | } |
|
1140 | 2 | return $result; |
|
1141 | } |
||
1142 | |||
1143 | /** |
||
1144 | * Wrap the inner child contents of each matched element |
||
1145 | * (including text nodes) with an XML structure. |
||
1146 | * |
||
1147 | * @example wrapInner.php Usage Example: FluentDOM\Query::wrapInner() |
||
1148 | * @param string|array|\DOMNode|\Traversable $content |
||
1149 | * @return Query |
||
1150 | */ |
||
1151 | 2 | public function wrapInner($content) { |
|
1152 | 2 | $elements = array(); |
|
1153 | 2 | foreach ($this->_nodes as $node) { |
|
1154 | 2 | foreach ($node->childNodes as $childNode) { |
|
1155 | 2 | if (Constraints::isNode($childNode)) { |
|
1156 | 2 | $elements[] = $childNode; |
|
1157 | 2 | } |
|
1158 | 2 | } |
|
1159 | 2 | } |
|
1160 | 2 | return $this->spawn($this->wrapNodes($elements, $content)); |
|
1161 | } |
||
1162 | |||
1163 | /** |
||
1164 | * Get xml contents of the first matched element or set the |
||
1165 | * xml contents of all selected element nodes. |
||
1166 | * |
||
1167 | * @example xml.php Usage Example: FluentDOM::xml() |
||
1168 | * @param string|callable|NULL $xml XML fragment |
||
1169 | * @return string|self |
||
1170 | */ |
||
1171 | 7 | public function xml($xml = NULL) { |
|
1172 | 7 | return $this->content( |
|
1173 | 7 | $xml, |
|
1174 | function($node) { |
||
1175 | 3 | return $this->build()->getInnerXml($node); |
|
1176 | 7 | }, |
|
1177 | function($node) { |
||
1178 | 4 | return $this->build()->getFragment($node, 'text/xml', TRUE); |
|
1179 | 7 | }, |
|
1180 | function($node, $fragment) { |
||
1181 | 3 | $this->modify($node)->replaceChildren($fragment); |
|
1182 | 3 | } |
|
1183 | 7 | ); |
|
1184 | } |
||
1185 | |||
1186 | /** |
||
1187 | * Get the first matched node as XML or replace each |
||
1188 | * matched nodes with the provided fragment. |
||
1189 | * |
||
1190 | * @param string|callable|NULL $xml |
||
1191 | * @return string|self |
||
1192 | */ |
||
1193 | 7 | View Code Duplication | public function outerXml($xml = NULL) { |
1 ignored issue
–
show
|
|||
1194 | 7 | return $this->outerContent( |
|
1195 | 7 | $xml, |
|
1196 | function($node) { |
||
1197 | 3 | return $this->getDocument()->saveXML($node); |
|
1198 | 7 | }, |
|
1199 | function($xml) { |
||
1200 | 4 | return $this->build()->getFragment($xml, 'text/xml', TRUE); |
|
1201 | } |
||
1202 | 7 | ); |
|
1203 | } |
||
1204 | |||
1205 | /** |
||
1206 | * Get the first matched node as HTML or replace each |
||
1207 | * matched nodes with the provided fragment. |
||
1208 | * |
||
1209 | * @param string|callable|NULL $html |
||
1210 | * @return string|self |
||
1211 | */ |
||
1212 | 7 | View Code Duplication | public function outerHtml($html = NULL) { |
1 ignored issue
–
show
This method seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
1213 | 7 | return $this->outerContent( |
|
1214 | 7 | $html, |
|
1215 | function($node) { |
||
1216 | 3 | return $this->getDocument()->saveHTML($node); |
|
1217 | 7 | }, |
|
1218 | function($html) { |
||
1219 | 4 | return $this->build()->getFragment($html, 'text/html'); |
|
1220 | } |
||
1221 | 7 | ); |
|
1222 | } |
||
1223 | |||
1224 | /** |
||
1225 | * Get html contents of the first matched element or set the |
||
1226 | * html contents of all selected element nodes. |
||
1227 | * |
||
1228 | * @param string|callable|NULL $html |
||
1229 | * @return string|self |
||
1230 | */ |
||
1231 | 4 | public function html($html = NULL) { |
|
1232 | 4 | return $this->content( |
|
1233 | 4 | $html, |
|
1234 | function($node) { |
||
1235 | 2 | $result = ''; |
|
1236 | 2 | foreach ($node->childNodes as $node) { |
|
1237 | 2 | $result .= $this->getDocument()->saveHTML($node); |
|
1238 | 2 | } |
|
1239 | 2 | return $result; |
|
1240 | 4 | }, |
|
1241 | function($html) { |
||
1242 | 2 | return $this->build()->getFragment($html, 'text/html'); |
|
1243 | 4 | }, |
|
1244 | function($node, $fragment) { |
||
1245 | 2 | $this->modify($node)->replaceChildren($fragment); |
|
1246 | 2 | } |
|
1247 | 4 | ); |
|
1248 | } |
||
1249 | |||
1250 | /** |
||
1251 | * @param string|callable|NULL $content |
||
1252 | * @param callable $export |
||
1253 | * @param callable $import |
||
1254 | * @param callable $insert |
||
1255 | * @return $this|string |
||
1256 | */ |
||
1257 | 25 | private function content($content, callable $export, callable $import, callable $insert) { |
|
1258 | 25 | if (isset($content)) { |
|
1259 | 14 | $callback = Constraints::isCallable($content, FALSE, TRUE); |
|
1260 | 14 | if ($callback) { |
|
1261 | 4 | foreach ($this->_nodes as $index => $node) { |
|
1262 | 4 | $contentString = $callback($node, $index, $export($node)); |
|
1263 | 4 | $insert($node, $import($contentString)); |
|
1264 | 4 | } |
|
1265 | 4 | } else { |
|
1266 | 10 | $fragment = $import($content); |
|
1267 | 7 | foreach ($this->_nodes as $node) { |
|
1268 | 7 | $insert($node, $fragment); |
|
1269 | 7 | } |
|
1270 | } |
||
1271 | 11 | return $this; |
|
1272 | 11 | } elseif (isset($this->_nodes[0])) { |
|
1273 | 7 | return $export($this->_nodes[0]); |
|
1274 | } |
||
1275 | 4 | return ''; |
|
1276 | } |
||
1277 | |||
1278 | /** |
||
1279 | * @param string|callable|NULL $content |
||
1280 | * @param callable $export |
||
1281 | * @param callable $import |
||
1282 | * @return $this|string |
||
1283 | */ |
||
1284 | 14 | private function outerContent($content, callable $export, callable $import) { |
|
1285 | 14 | return $this->content( |
|
1286 | 14 | $content, |
|
1287 | 14 | $export, |
|
1288 | 14 | $import, |
|
1289 | function($node, $fragment) { |
||
1290 | 6 | $this->modify($node)->replaceNode($fragment); |
|
1291 | 6 | } |
|
1292 | 14 | ); |
|
1293 | } |
||
1294 | |||
1295 | /**************************** |
||
1296 | * Manipulation - Attributes |
||
1297 | ***************************/ |
||
1298 | |||
1299 | /** |
||
1300 | * @param string|array|NULL $names |
||
1301 | * @throws \InvalidArgumentException |
||
1302 | * @return array |
||
1303 | */ |
||
1304 | 8 | private function getNamesList($names) { |
|
1305 | 8 | $attributes = NULL; |
|
1306 | 8 | if (is_array($names)) { |
|
1307 | 2 | $attributes = $names; |
|
1308 | 8 | } elseif (is_string($names) && $names !== '*' && $names !== '') { |
|
1309 | 2 | $attributes = array($names); |
|
1310 | 6 | } elseif (isset($names) && $names !== '*') { |
|
1311 | 2 | throw new \InvalidArgumentException(); |
|
1312 | } |
||
1313 | 6 | return $attributes; |
|
1314 | } |
||
1315 | |||
1316 | /** |
||
1317 | * @param string|array|\Traversable $name |
||
1318 | * @param string|float|int|NULL|callable $value |
||
1319 | * @return array|\Traversable |
||
1320 | * @throws \InvalidArgumentException |
||
1321 | */ |
||
1322 | 20 | private function getSetterValues($name, $value) { |
|
1323 | 20 | if (is_string($name)) { |
|
1324 | 15 | return array((string)$name => $value); |
|
1325 | 5 | } elseif (is_array($name) || $name instanceOf \Traversable) { |
|
1326 | 4 | return $name; |
|
1327 | } |
||
1328 | 1 | throw new \InvalidArgumentException('Invalid css property name argument type.'); |
|
1329 | } |
||
1330 | |||
1331 | /** |
||
1332 | * Access a property on the first matched element or set the attribute(s) of all matched elements |
||
1333 | * |
||
1334 | * @example attr.php Usage Example: FluentDOM\Query::attr() Read an attribute value. |
||
1335 | * @param string|array $attribute attribute name or attribute list |
||
1336 | * @param [callable|string] $arguments |
||
0 ignored issues
–
show
The doc-type
[callable|string] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.
Loading history...
|
|||
1337 | * @return Query|string attribute value or $this |
||
1338 | */ |
||
1339 | 18 | public function attr($attribute, ...$arguments) { |
|
1340 | 18 | if (count($arguments) == 0 && is_string($attribute)) { |
|
1341 | //empty value - read attribute from first element in list |
||
1342 | 10 | $attribute = (new QualifiedName($attribute))->name; |
|
1343 | 9 | $node = $this->getFirstElement(); |
|
1344 | 9 | if ($node && $node->hasAttribute($attribute)) { |
|
1345 | 6 | return $node->getAttribute($attribute); |
|
1346 | } |
||
1347 | 3 | return NULL; |
|
1348 | } else { |
||
1349 | 12 | $attributes = $this->getSetterValues($attribute, isset($arguments[0]) ? $arguments[0] : NULL); |
|
1350 | // set attributes on each element |
||
1351 | 12 | foreach ($attributes as $key => $value) { |
|
1352 | 12 | $name = (new QualifiedName($key))->name; |
|
1353 | 6 | $callback = Constraints::isCallable($value); |
|
1354 | 6 | $this->each( |
|
1355 | function(\DOMElement $node, $index) use ($name, $value, $callback) { |
||
1356 | 6 | $node->setAttribute( |
|
1357 | 6 | $name, |
|
1358 | $callback |
||
1359 | 6 | ? (string)$callback($node, $index, $node->getAttribute($name)) |
|
1360 | 1 | : (string)$value |
|
1361 | 6 | ); |
|
1362 | 6 | }, |
|
1363 | TRUE |
||
1364 | 6 | ); |
|
1365 | 6 | } |
|
1366 | } |
||
1367 | 6 | return $this; |
|
1368 | } |
||
1369 | |||
1370 | /** |
||
1371 | * Returns true if the specified attribute is present on at least one of |
||
1372 | * the set of matched elements. |
||
1373 | * |
||
1374 | * @param string $name |
||
1375 | * @return bool |
||
1376 | */ |
||
1377 | 3 | public function hasAttr($name) { |
|
1378 | 3 | foreach ($this->_nodes as $node) { |
|
1379 | 3 | if ($node instanceof \DOMElement && $node->hasAttribute($name)) { |
|
1380 | 2 | return TRUE; |
|
1381 | } |
||
1382 | 2 | } |
|
1383 | 1 | return FALSE; |
|
1384 | } |
||
1385 | |||
1386 | /** |
||
1387 | * Remove an attribute from each of the matched elements. If $name is NULL or *, |
||
1388 | * all attributes will be deleted. |
||
1389 | * |
||
1390 | * @example removeAttr.php Usage Example: FluentDOM\Query::removeAttr() |
||
1391 | * @param string|array $name |
||
1392 | * @throws \InvalidArgumentException |
||
1393 | * @return Query |
||
1394 | */ |
||
1395 | 4 | public function removeAttr($name) { |
|
1396 | 4 | $names = $this->getNamesList($name); |
|
1397 | 3 | $this->each( |
|
1398 | function(\DOMElement $node) use ($names) { |
||
1399 | /** @noinspection PhpParamsInspection */ |
||
1400 | 3 | $attributes = NULL === $names |
|
1401 | 3 | ? array_keys(iterator_to_array($node->attributes)) |
|
1402 | 3 | : $names; |
|
1403 | 3 | foreach ($attributes as $attribute) { |
|
1404 | 3 | if ($node->hasAttribute($attribute)) { |
|
1405 | 3 | $node->removeAttribute($attribute); |
|
1406 | 3 | } |
|
1407 | 3 | } |
|
1408 | 3 | }, |
|
1409 | TRUE |
||
1410 | 3 | ); |
|
1411 | 3 | return $this; |
|
1412 | } |
||
1413 | |||
1414 | /************************* |
||
1415 | * Manipulation - Classes |
||
1416 | ************************/ |
||
1417 | |||
1418 | /** |
||
1419 | * Adds the specified class(es) to each of the set of matched elements. |
||
1420 | * |
||
1421 | * @param string|callable $class |
||
1422 | * @return Query |
||
1423 | */ |
||
1424 | 1 | public function addClass($class) { |
|
1425 | 1 | return $this->toggleClass($class, TRUE); |
|
1426 | } |
||
1427 | |||
1428 | /** |
||
1429 | * Returns true if the specified class is present on at least one of the set of matched elements. |
||
1430 | * |
||
1431 | * @param string $class |
||
1432 | * @return boolean |
||
1433 | */ |
||
1434 | 2 | public function hasClass($class) { |
|
1435 | 2 | foreach ($this->_nodes as $node) { |
|
1436 | 2 | if ($node instanceof \DOMElement && $node->hasAttribute('class')) { |
|
1437 | 2 | $classes = preg_split('(\s+)', trim($node->getAttribute('class'))); |
|
1438 | 2 | if (in_array($class, $classes)) { |
|
1439 | 1 | return TRUE; |
|
1440 | } |
||
1441 | 1 | } |
|
1442 | 1 | } |
|
1443 | 1 | return FALSE; |
|
1444 | } |
||
1445 | |||
1446 | /** |
||
1447 | * Removes all or the specified class(es) from the set of matched elements. |
||
1448 | * |
||
1449 | * @param string|callable $class |
||
1450 | * @return Query |
||
1451 | */ |
||
1452 | 2 | public function removeClass($class = '') { |
|
1453 | 2 | return $this->toggleClass($class, FALSE); |
|
1454 | } |
||
1455 | |||
1456 | /** |
||
1457 | * Adds the specified classes if the switch is TRUE, |
||
1458 | * removes the specified classes if the switch is FALSE, |
||
1459 | * toggles the specified classes if the switch is NULL. |
||
1460 | * |
||
1461 | * @example toggleClass.php Usage Example: FluentDOM\Query::toggleClass() |
||
1462 | * @param string|callable $class |
||
1463 | * @param NULL|boolean $switch toggle if NULL, add if TRUE, remove if FALSE |
||
1464 | * @return Query |
||
1465 | */ |
||
1466 | 6 | public function toggleClass($class, $switch = NULL) { |
|
1467 | 6 | $callback = Constraints::isCallable($class); |
|
1468 | 6 | $this->each( |
|
1469 | function(\DOMElement $node, $index) use ($class, $switch, $callback) { |
||
1470 | 6 | if ($callback) { |
|
1471 | 1 | $classString = $callback($node, $index, $node->getAttribute('class')); |
|
1472 | 1 | } else { |
|
1473 | 5 | $classString = $class; |
|
1474 | } |
||
1475 | 6 | if (empty($classString) && !(bool)$switch) { |
|
1476 | 1 | if ($node->hasAttribute('class')) { |
|
1477 | 1 | $node->removeAttribute('class'); |
|
1478 | 1 | } |
|
1479 | 1 | } else { |
|
1480 | 5 | $modified = $this->changeClassString( |
|
1481 | 5 | $node->getAttribute('class'), |
|
1482 | 5 | $classString, |
|
1483 | $switch |
||
1484 | 5 | ); |
|
1485 | 5 | if (FALSE !== $modified) { |
|
1486 | 5 | if (empty($modified)) { |
|
1487 | 1 | $node->removeAttribute('class'); |
|
1488 | 1 | } else { |
|
1489 | 5 | $node->setAttribute('class', $modified); |
|
1490 | } |
||
1491 | 5 | } |
|
1492 | } |
||
1493 | 6 | }, |
|
1494 | TRUE |
||
1495 | 6 | ); |
|
1496 | 6 | return $this; |
|
1497 | } |
||
1498 | |||
1499 | /** |
||
1500 | * Change a class string |
||
1501 | * |
||
1502 | * Adds the specified classes if the switch is TRUE, |
||
1503 | * removes the specified classes if the switch is FALSE, |
||
1504 | * toggles the specified classes if the switch is NULL. |
||
1505 | * |
||
1506 | * @param string $current |
||
1507 | * @param string $toggle |
||
1508 | * @param bool|NULL $switch |
||
1509 | * @return FALSE|string |
||
1510 | */ |
||
1511 | 5 | private function changeClassString($current, $toggle, $switch) { |
|
1512 | 5 | $currentClasses = array_flip( |
|
1513 | 5 | preg_split('(\s+)', trim($current), 0, PREG_SPLIT_NO_EMPTY) |
|
1514 | 5 | ); |
|
1515 | 5 | $toggleClasses = array_unique( |
|
1516 | 5 | preg_split('(\s+)', trim($toggle), 0, PREG_SPLIT_NO_EMPTY) |
|
1517 | 5 | ); |
|
1518 | 5 | $modified = FALSE; |
|
1519 | 5 | foreach ($toggleClasses as $class) { |
|
1520 | if ( |
||
1521 | 5 | isset($currentClasses[$class]) && |
|
1522 | 4 | (NULL === $switch || FALSE === $switch) |
|
1523 | 5 | ) { |
|
1524 | 4 | unset($currentClasses[$class]); |
|
1525 | 4 | $modified = TRUE; |
|
1526 | 5 | } elseif (NULL === $switch || TRUE === $switch) { |
|
1527 | 4 | $currentClasses[$class] = TRUE; |
|
1528 | 4 | $modified = TRUE; |
|
1529 | 4 | } |
|
1530 | 5 | } |
|
1531 | return $modified |
||
1532 | 5 | ? implode(' ', array_keys($currentClasses)) |
|
1533 | 5 | : FALSE; |
|
1534 | } |
||
1535 | |||
1536 | /************************************* |
||
1537 | * Manipulation - CSS Style Attribute |
||
1538 | ************************************/ |
||
1539 | |||
1540 | /** |
||
1541 | * get or set CSS values in style attributes |
||
1542 | * |
||
1543 | * @param string|array $property |
||
1544 | * @param [string|object|callable] $arguments |
||
0 ignored issues
–
show
The doc-type
[string|object|callable] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.
Loading history...
|
|||
1545 | * @throws \InvalidArgumentException |
||
1546 | * @return string|NULL|$this |
||
1547 | */ |
||
1548 | 16 | public function css($property, ...$arguments) { |
|
1549 | 16 | if (count($arguments) == 0 && is_string($property)) { |
|
1550 | 4 | $properties = new Query\Css\Properties((string)$this->attr('style')); |
|
1551 | 4 | if (isset($properties[$property])) { |
|
1552 | 2 | return $properties[$property]; |
|
1553 | } |
||
1554 | 2 | return NULL; |
|
1555 | } |
||
1556 | 12 | $values = $this->getSetterValues($property, isset($arguments[0]) ? $arguments[0] : NULL); |
|
1557 | //set list of properties to all elements |
||
1558 | 11 | $this->each( |
|
1559 | function(\DOMElement $node, $index) use ($values) { |
||
1560 | 11 | $properties = new Query\Css\Properties($node->getAttribute('style')); |
|
1561 | 11 | foreach ($values as $name => $value) { |
|
1562 | 11 | $properties[$name] = $properties->compileValue( |
|
1563 | 11 | $value, $node, $index, isset($properties[$name]) ? $properties[$name] : NULL |
|
1564 | 11 | ); |
|
1565 | 9 | } |
|
1566 | 9 | if (count($properties) > 0) { |
|
1567 | 6 | $node->setAttribute('style', (string)$properties); |
|
1568 | 9 | } elseif ($node->hasAttribute('style')) { |
|
1569 | 3 | $node->removeAttribute('style'); |
|
1570 | 3 | } |
|
1571 | 11 | }, |
|
1572 | TRUE |
||
1573 | 11 | ); |
|
1574 | 9 | return $this; |
|
1575 | } |
||
1576 | |||
1577 | /********************************* |
||
1578 | * Manipulation - Data Attributes |
||
1579 | ********************************/ |
||
1580 | |||
1581 | /** |
||
1582 | * Read a data attribute from the first node or set data attributes on all selected nodes. |
||
1583 | * |
||
1584 | * @example data.php Usage Example: FluentDOM\Query::data() |
||
1585 | * @param string|array $name data attribute identifier or array of data attributes to set |
||
1586 | * @param [mixed] ...$arguments |
||
1587 | * @return mixed |
||
1588 | */ |
||
1589 | 5 | public function data($name, ...$arguments) { |
|
1590 | 5 | if (count($arguments) == 0 && !is_array($name)) { |
|
1591 | //reading |
||
1592 | 2 | if ($node = $this->getFirstElement()) { |
|
1593 | 1 | $data = new Query\Data($node); |
|
1594 | 1 | return $data->$name; |
|
1595 | } |
||
1596 | 1 | return NULL; |
|
1597 | } |
||
1598 | 3 | $values = $this->getSetterValues($name, isset($arguments[0]) ? $arguments[0] : NULL); |
|
1599 | 3 | $this->each( |
|
1600 | function(\DOMElement $node) use ($values) { |
||
1601 | 3 | $data = new Query\Data($node); |
|
1602 | 3 | foreach ($values as $dataName => $dataValue) { |
|
1603 | 3 | $data->$dataName = $dataValue; |
|
1604 | 3 | } |
|
1605 | 3 | }, |
|
1606 | TRUE |
||
1607 | 3 | ); |
|
1608 | 3 | return $this; |
|
1609 | } |
||
1610 | |||
1611 | /** |
||
1612 | * Remove an data - attribute from each of the matched elements. If $name is NULL or *, |
||
1613 | * all data attributes will be deleted. |
||
1614 | * |
||
1615 | * @example removeData.php Usage Example: FluentDOM\Query::removeData() |
||
1616 | * @param string|array|NULL $name |
||
1617 | * @throws \InvalidArgumentException |
||
1618 | * @return Query |
||
1619 | */ |
||
1620 | 4 | public function removeData($name = NULL) { |
|
1621 | 4 | $names = $this->getNamesList($name); |
|
1622 | 3 | $this->each( |
|
1623 | 3 | function ($node) use ($names) { |
|
1624 | 3 | $data = new Query\Data($node); |
|
1625 | 3 | if (is_array($names)) { |
|
1626 | 2 | foreach ($names as $dataName) { |
|
1627 | 2 | unset($data->$dataName); |
|
1628 | 2 | } |
|
1629 | 2 | } else { |
|
1630 | 1 | foreach ($data as $dataName => $dataValue) { |
|
1631 | 1 | unset($data->$dataName); |
|
1632 | 1 | } |
|
1633 | } |
||
1634 | 3 | }, |
|
1635 | TRUE |
||
1636 | 3 | ); |
|
1637 | 3 | } |
|
1638 | |||
1639 | /** |
||
1640 | * Validate if the element has an data attributes attached. If it is called without an |
||
1641 | * actual $element parameter, it will check the first matched node. |
||
1642 | * |
||
1643 | * @param \DOMElement $element |
||
1644 | * @return boolean |
||
1645 | */ |
||
1646 | 5 | public function hasData(\DOMElement $element = NULL) { |
|
1647 | 5 | if ($element || ($element = $this->getFirstElement())) { |
|
1648 | 4 | $data = new Query\Data($element); |
|
1649 | 4 | return count($data) > 0; |
|
1650 | } |
||
1651 | 1 | return FALSE; |
|
1652 | } |
||
1653 | } |
||
1654 | } |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.