Completed
Push — master ( 4f6354...e74d94 )
by Thomas
02:57
created

Query::css()   C

Complexity

Conditions 7
Paths 3

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 28
Code Lines 19

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 3
nop 2
dl 0
loc 28
ccs 19
cts 19
cp 1
crap 7
rs 6.7272
c 0
b 0
f 0
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-2017 FluentDOM Contributors
7
 */
8
9
namespace FluentDOM {
10
11
  use FluentDOM\Nodes\Fetcher;
12
  use FluentDOM\Utility\Constraints;
13
  use FluentDOM\Utility\QualifiedName;
14
15
  /**
16
   * FluentDOM\Query implements a jQuery like replacement for DOMNodeList
17
   *
18
   * @property Query\Attributes $attr
19
   * @property Query\Data $data
20
   * @property Query\Css $css
21
   *
22
   * Define methods from parent to provide information for code completion
23
   * @method Query load($source, string $contentType = NULL, $options = [])
24
   * @method Query spawn($elements = NULL)
25
   * @method Query find($selector, int $options = 0)
26
   * @method Query end()
27
   * @method Query formatOutput(string $contentType = NULL)
28
   */
29
  class Query extends Nodes {
30
31
    /**
32
     * @var Nodes\Builder
33
     */
34
    private $_builder;
35
36
    /**
37
     * Virtual properties, validate existence
38
     *
39
     * @param string $name
40
     * @return bool
41
     */
42 4
    public function __isset(string $name): bool {
43
      switch ($name) {
44 4
      case 'attr' :
45 3
      case 'css' :
46 2
      case 'data' :
47 3
        return TRUE;
48
      }
49 1
      return parent::__isset($name);
50
    }
51
52
    /**
53
     * Virtual properties, read property
54
     *
55
     * @param string $name
56
     * @throws \UnexpectedValueException
57
     * @return mixed
58
     * @throws \LogicException
59
     */
60 57
    public function __get(string $name) {
61
      switch ($name) {
62 57
      case 'attr' :
63 1
        return new Query\Attributes($this);
64 56
      case 'css' :
65 1
        return new Query\Css($this);
66 55
      case 'data' :
67 2
        if ($node = $this->getFirstElement()) {
68 1
          return new Query\Data($node);
69
        } else {
70 1
          throw new \UnexpectedValueException(
71 1
            'UnexpectedValueException: first selected node is no element.'
72
          );
73
        }
74
      }
75 53
      return parent::__get($name);
76
    }
77
78
    /**
79
     * Block changing the readonly dynamic property
80
     *
81
     * @param string $name
82
     * @param mixed $value
83
     * @throws \InvalidArgumentException
84
     * @throws \BadMethodCallException
85
     */
86 5
    public function __set(string $name, $value) {
87
      switch ($name) {
88 5
      case 'attr' :
89 2
        $this->attr(
90 2
          $value instanceOf Query\Attributes ? $value->toArray() : $value
91
        );
92 2
        break;
93 3
      case 'css' :
94 2
        $this->css($value);
95 2
        break;
96 1
      case 'data' :
97 1
        $this->data(
98 1
          $value instanceOf Query\Data ? $value->toArray() : $value
99
        );
100 1
        break;
101
      }
102 5
      parent::__set($name, $value);
103 5
    }
104
105
    /**
106
     * Throws an exception if somebody tries to unset one
107
     * of the dynamic properties
108
     *
109
     * @param string $name
110
     * @throws \BadMethodCallException
111
     */
112 4
    public function __unset(string $name) {
113
      switch ($name) {
114 4
      case 'attr' :
115 3
      case 'css' :
116 2
      case 'data' :
117 3
        throw new \BadMethodCallException(
118 3
          sprintf(
119 3
            'Can not unset property %s::$%s',
120 3
            get_class($this),
121 3
            $name
122
          )
123
        );
124
      }
125 1
      parent::__unset($name);
126
      // @codeCoverageIgnoreStart
127
    }
128
    // @codeCoverageIgnoreEnd
129
130
    /******************
131
     * Internal
132
     *****************/
133
134
    /**
135
     * Returns the item from the internal array if
136
     * if the index exists and is an DOMElement
137
     *
138
     * @param array|\Traversable
139
     * @return NULL|\DOMElement
140
     */
141 4
    private function getFirstElement() {
142 4
      foreach ($this->_nodes as $node) {
143 4
        if ($node instanceof \DOMElement) {
144 3
          return $node;
145
        }
146
      }
147 1
      return NULL;
148
    }
149
150
    /**
151
     * Wrap $content around a set of elements
152
     *
153
     * @param array $elements
154
     * @param string|array|\DOMNode|\Traversable|callable $content
155
     * @return array
156
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
157
     * @throws \InvalidArgumentException
158
     */
159 8
    private function wrapNodes(array $elements, $content): array {
160 8
      $result = [];
161 8
      $wrapperTemplate = NULL;
162 8
      $callback = Constraints::filterCallable($content);
163 8
      if (!$callback) {
164 6
        $wrapperTemplate = $this->build()->getContentElement($content);
165
      }
166 7
      $simple = FALSE;
167 7
      foreach ($elements as $index => $node) {
168 7
        if ($callback) {
169 2
          $wrapperTemplate = NULL;
170 2
          $wrapContent = $callback($node, $index);
171 2
          if (!empty($wrapContent)) {
172 2
            $wrapperTemplate = $this->build()->getContentElement($wrapContent);
173
          }
174
        }
175 7
        if ($wrapperTemplate instanceof \DOMElement) {
176
          /**
177
           * @var \DOMElement $target
178
           * @var \DOMElement $wrapper
179
           */
180 7
          list($target, $wrapper) = $this->build()->getWrapperNodes(
181 7
            $wrapperTemplate,
182 7
            $simple
183
          );
184 7
          if ($node->parentNode instanceof \DOMNode) {
185 7
            $node->parentNode->insertBefore($wrapper, $node);
186
          }
187 7
          $target->appendChild($node);
188 7
          $result[] = $node;
189
        }
190
      }
191 7
      return $result;
192
    }
193
194
    /*********************
195
     * Core
196
     ********************/
197
198
    /**
199
     * @param \DOMNode $node
200
     * @return Nodes\Modifier
201
     */
202 32
    private function modify(\DOMNode $node): Nodes\Modifier {
203 32
      return new Nodes\Modifier($node);
204
    }
205
206
    /**
207
     * @return Nodes\Builder
208
     */
209 57
    private function build(): Nodes\Builder {
210 57
      if (NULL === $this->_builder) {
211 57
        $this->_builder = new Nodes\Builder($this);
212
      }
213 57
      return $this->_builder;
214
    }
215
216
    /**
217
     * Use a handler callback to apply a content argument to each node $targetNodes. The content
218
     * argument can be an easy setter function
219
     *
220
     * @param array|\DOMNodeList $targetNodes
221
     * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content
222
     * @param callable $handler
223
     * @return array
224
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
225
     * @throws \InvalidArgumentException
226
     */
227 21
    private function apply($targetNodes, $content, callable $handler): array {
228 21
      $result = [];
229 21
      $isSetterFunction = FALSE;
230 21
      if ($callback = Constraints::filterCallable($content)) {
231 6
        $isSetterFunction = TRUE;
232
      } else {
233 15
        $contentNodes = $this->build()->getContentNodes($content);
234
      }
235 21
      foreach ($targetNodes as $index => $node) {
236 21
        if ($isSetterFunction) {
237 6
          $contentData = $callback($node, $index, $this->build()->getInnerXml($node));
238 6
          if (!empty($contentData)) {
239 6
            $contentNodes = $this->build()->getContentNodes($contentData);
240
          }
241
        }
242 21
        if (!empty($contentNodes)) {
243 21
          $resultNodes = $handler($node, $contentNodes);
244 21
          if (is_array($resultNodes)) {
245 21
            $result = array_merge($result, $resultNodes);
246
          }
247
        }
248
      }
249 21
      return $result;
250
    }
251
252
    /**
253
     * Apply the content to the target nodes using the handler callback
254
     * and push them into a spawned Query object.
255
     *
256
     * @param array|\DOMNodeList $targetNodes
257
     * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content
258
     * @param callable $handler
259
     * @param bool $remove Call remove() on $this, remove the current selection from the DOM
260
     * @return Query
261
     */
262 19
    private function applyToSpawn($targetNodes, $content, callable $handler, bool $remove = FALSE): Query {
263 19
      $result = $this->spawn(
264 19
        $this->apply($targetNodes, $content, $handler)
265
      );
266 19
      if ($remove) {
267 7
        $this->remove();
268
      }
269 19
      return $result;
270
    }
271
272
    /**
273
     * Apply the handler the $handler to nodes defined by selector, using
274
     * the currently selected nodes as context.
275
     *
276
     * @param string|array|\DOMNode|\Traversable $selector
277
     * @param callable $handler
278
     * @param bool $remove Call remove() on $this, remove the current selection from the DOM
279
     * @return Query
280
     */
281 4
    private function applyToSelector($selector, callable $handler, bool $remove = FALSE): Query {
282 4
      return $this->applyToSpawn(
283 4
        $this->build()->getTargetNodes($selector),
284 4
        $this->_nodes,
285 4
        $handler,
286 4
        $remove
287
      );
288
    }
289
290
    /*********************
291
     * Traversing
292
     ********************/
293
294
    /**
295
     * Adds more elements, matched by the given expression, to the set of matched elements.
296
     *
297
     * @example add.php Usage Examples: FluentDOM::add()
298
     * @param string|\Traversable|array $selector selector
299
     * @param array|\Traversable $context
300
     * @return Query
301
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
302
     * @throws \OutOfBoundsException
303
     * @throws \InvalidArgumentException
304
     */
305 7
    public function add($selector, $context = NULL) {
306 7
      $result = $this->spawn($this);
307 7
      if (NULL !== $context) {
308 1
        $result->push($this->spawn($context)->find($selector));
309
      } elseif (
310 6
        is_object($selector) ||
311 5
        (is_string($selector) && substr(ltrim($selector), 0, 1) === '<')
312
      ) {
313 4
        $result->push($this->build()->getContentNodes($selector));
314
      } else {
315 2
        $result->push($this->find($selector));
316
      }
317 7
      $result->_nodes = $result->unique($result->_nodes);
318 7
      return $result;
319
    }
320
321
    /**
322
     * Add the previous selection to the current selection.
323
     *
324
     * @return Query
325
     * @throws \OutOfBoundsException
326
     * @throws \InvalidArgumentException
327
     */
328 1
    public function addBack(): Query {
329 1
      return $this->spawn()->push($this->_nodes)->push($this->_parent);
330
    }
331
332
    /**
333
     * Get a set of elements containing of the unique immediate
334
     * child nodes including only elements (not text nodes) of each
335
     * of the matched set of elements.
336
     *
337
     * @example children.php Usage Examples: FluentDOM\Query::children()
338
     * @param string $selector selector
339
     * @return Query|Nodes
340
     * @throws \OutOfBoundsException
341
     * @throws \LogicException
342
     * @throws \InvalidArgumentException
343
     */
344 2
    public function children($selector = NULL): Query {
345 2
      return $this->fetch('*', $selector, NULL, Nodes\Fetcher::UNIQUE);
346
    }
347
348
    /**
349
     * Get a set of elements containing the closest parent element that matches the specified
350
     * selector, the starting element included.
351
     *
352
     * @example closest.php Usage Example: FluentDOM\Query::closest()
353
     * @param string $selector selector
354
     * @param array|\Traversable $context
355
     * @return Query|Nodes
356
     * @throws \OutOfBoundsException
357
     * @throws \LogicException
358
     * @throws \InvalidArgumentException
359
     */
360 4
    public function closest($selector, $context = NULL): Query {
361 4
      $context = $context ? $this->spawn($context) : $this;
362 4
      return $context->fetch(
363 4
        'ancestor-or-self::*',
364 4
        $selector,
365 4
        $selector,
366 4
        Fetcher::REVERSE |Fetcher::INCLUDE_STOP
367
      );
368
    }
369
370
    /**
371
     * Get a set of elements containing all of the unique immediate
372
     * child nodes including elements and text nodes of each of the matched set of elements.
373
     *
374
     * @return Query|Nodes
375
     * @throws \OutOfBoundsException
376
     * @throws \LogicException
377
     * @throws \InvalidArgumentException
378
     */
379 1
    public function contents(): Query {
380 1
      return $this->fetch(
381 1
        '*|text()[normalize-space(.) != ""]',
382 1
        NULL,
383 1
        NULL,
384 1
        Fetcher::UNIQUE
385
      );
386
    }
387
388
    /**
389
     * Reduce the set of matched elements to a single element.
390
     *
391
     * @example eq.php Usage Example: FluentDOM\Query::eq()
392
     * @param int $position Element index (start with 0)
393
     * @return Query
394
     * @throws \OutOfBoundsException
395
     * @throws \InvalidArgumentException
396
     */
397 2
    public function eq(int $position): Query {
398 2
      $result = $this->spawn();
399 2
      if ($position < 0) {
400 1
        $position = count($this->_nodes) + $position;
401
      }
402 2
      if (isset($this->_nodes[$position])) {
403 2
        $result->push($this->_nodes[$position]);
404
      }
405 2
      return $result;
406
    }
407
408
    /**
409
     * Removes all elements from the set of matched elements that do not match
410
     * the specified expression(s).
411
     *
412
     * @example filter-expr.php Usage Example: FluentDOM\Query::filter() with selector
413
     * @example filter-fn.php Usage Example: FluentDOM\Query::filter() with Closure
414
     * @param string|callable $selector selector or callback function
415
     * @return Query
416
     * @throws \OutOfBoundsException
417
     * @throws \InvalidArgumentException
418
     */
419 2
    public function filter($selector): Query {
420 2
      $callback = $this->getSelectorCallback($selector);
421 2
      $result = $this->spawn();
422 2
      foreach ($this->_nodes as $index => $node) {
423 2
        if ($callback($node, $index)) {
424 2
          $result->push($node);
425
        }
426
      }
427 2
      return $result;
428
    }
429
430
    /**
431
     * Get a set of elements containing only the first of the currently selected elements.
432
     *
433
     * @return Query
434
     * @throws \OutOfBoundsException
435
     * @throws \InvalidArgumentException
436
     */
437 1
    public function first(): Query {
438 1
      return $this->eq(0);
439
    }
440
441
    /**
442
     * Retrieve the matched DOM elements in an array. A negative position will be counted from the end.
443
     *
444
     * @param int|NULL optional offset of a single element to get.
445
     * @return array|\DOMNode|NULL
446
     */
447 4
    public function get(int $position = NULL) {
448 4
      if (NULL === $position) {
449 1
        return $this->_nodes;
450
      }
451 3
      if ($position < 0) {
452 1
        $position = count($this->_nodes) + $position;
453
      }
454 3
      if (isset($this->_nodes[$position])) {
455 2
        return $this->_nodes[$position];
456
      }
457 1
      return NULL;
458
    }
459
460
    /**
461
     * Reduce the set of matched elements to those that have
462
     * a descendant that matches the selector or DOM element.
463
     *
464
     * @param string|\DOMNode $selector selector or DOMNode
465
     * @return Query
466
     * @throws \OutOfBoundsException
467
     * @throws \InvalidArgumentException
468
     */
469 2
    public function has($selector): Query {
470 2
      $callback = $this->getSelectorCallback($selector);
471 2
      $result = $this->spawn();
472 2
      $expression = './/node()';
473 2
      if ($selector instanceof \DOMElement) {
474 1
        $expression = './/*';
475
      }
476 2
      foreach ($this->_nodes as $node) {
477 2
        foreach ($this->xpath($expression, $node) as $has) {
478 2
          if ($callback($has)) {
479 2
            $result->push($node);
480 2
            break;
481
          }
482
        }
483
      }
484 2
      return $result;
485
    }
486
487
    /**
488
     * Checks the current selection against an expression and returns TRUE,
489
     * if at least one element of the selection fits the given expression.
490
     *
491
     * @example is.php Usage Example: FluentDOM\Query::is()
492
     * @param string $selector selector
493
     * @return bool
494
     */
495 2
    public function is(string $selector): bool {
496 2
      foreach ($this->_nodes as $node) {
497 1
        if ($this->matches($selector, $node)) {
498 1
          return TRUE;
499
        }
500
      }
501 2
      return FALSE;
502
    }
503
504
    /**
505
     * Get a set of elements containing only the last of the currently selected elements.
506
     *
507
     * @return Query
508
     * @throws \OutOfBoundsException
509
     * @throws \InvalidArgumentException
510
     */
511 1
    public function last(): Query {
512 1
      return $this->eq(-1);
513
    }
514
515
    /**
516
     * Translate a set of elements in the FluentDOM\Query object into
517
     * another set of values in an array (which may, or may not contain elements).
518
     *
519
     * If the callback function returns an array each element of the array will be added to the
520
     * result array. All other variable types are put directly into the result array.
521
     *
522
     * @example map.php Usage Example: FluentDOM\Query::map()
523
     * @param callable $function
524
     * @return array
525
     */
526 2
    public function map(callable $function): array {
527 2
      $result = [];
528 2
      foreach ($this->_nodes as $index => $node) {
529 2
        $mapped = $function($node, $index);
530 2
        if ($mapped === NULL) {
531 1
          continue;
532
        }
533 2
        if ($mapped instanceof \Traversable || is_array($mapped)) {
534 1
          foreach ($mapped as $element) {
535 1
            if ($element !== NULL) {
536 1
              $result[] = $element;
537
            }
538
          }
539
        } else {
540 2
          $result[] = $mapped;
541
        }
542
      }
543 2
      return $result;
544
    }
545
546
    /**
547
     * Removes elements matching the specified expression from the set of matched elements.
548
     *
549
     * @example not.php Usage Example: FluentDOM\Query::not()
550
     * @param string|callback $selector selector or callback function
551
     * @return Query
552
     * @throws \OutOfBoundsException
553
     * @throws \InvalidArgumentException
554
     */
555 2
    public function not($selector): Query {
556 2
      $callback = $this->getSelectorCallback($selector);
557 2
      return $this->filter(
558
        function (\DOMNode $node, $index) use ($callback) {
559 2
          return !$callback($node, $index);
560 2
        }
561
      );
562
    }
563
564
    /**
565
     * Get a set of elements containing the unique next siblings of each of the
566
     * given set of elements.
567
     *
568
     * @example next.php Usage Example: FluentDOM\Query::next()
569
     * @param string $selector
570
     * @return Query|Nodes
571
     * @throws \OutOfBoundsException
572
     * @throws \LogicException
573
     * @throws \InvalidArgumentException
574
     */
575 1
    public function next($selector = NULL): Query {
576 1
      return $this->fetch(
577 1
        'following-sibling::node()[
578
          self::* or (self::text() and normalize-space(.) != "")
579
        ][1]',
580 1
        $selector,
581 1
        NULL,
582 1
        Nodes\Fetcher::UNIQUE
583
      );
584
    }
585
586
    /**
587
     * Find all sibling elements after the current element.
588
     *
589
     * @example nextAll.php Usage Example: FluentDOM\Query::nextAll()
590
     * @param string $selector selector
591
     * @return Query|Nodes
592
     * @throws \OutOfBoundsException
593
     * @throws \LogicException
594
     * @throws \InvalidArgumentException
595
     */
596 1
    public function nextAll($selector = NULL): Query {
597 1
      return $this->fetch(
598 1
        'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]',
599 1
        $selector
600
      );
601
    }
602
603
    /**
604
     * Get all following siblings of each element up to but
605
     * not including the element matched by the selector.
606
     *
607
     * @param string $selector selector
608
     * @param string $filter selector
609
     * @return Query|Nodes
610
     * @throws \OutOfBoundsException
611
     * @throws \LogicException
612
     * @throws \InvalidArgumentException
613
     */
614 1
    public function nextUntil($selector = NULL, $filter = NULL): Query {
615 1
      return $this->fetch(
616 1
        'following-sibling::*|following-sibling::text()[normalize-space(.) != ""]',
617 1
        $filter,
618 1
        $selector
619
      );
620
    }
621
622
    /**
623
     * Get a set of elements containing the unique parents of the matched set of elements.
624
     *
625
     * @example parent.php Usage Example: FluentDOM\Query::parent()
626
     * @return Query|Nodes
627
     * @throws \OutOfBoundsException
628
     * @throws \LogicException
629
     * @throws \InvalidArgumentException
630
     */
631 1
    public function parent(): Query {
632 1
      return $this->fetch('parent::*', NULL, NULL, Fetcher::UNIQUE);
633
    }
634
635
    /**
636
     * Get the ancestors of each element in the current set of matched elements,
637
     * optionally filtered by a selector.
638
     *
639
     * @example parents.php Usage Example: FluentDOM\Query::parents()
640
     * @param string $selector selector
641
     * @return Query|Nodes
642
     * @throws \LogicException
643
     * @throws \OutOfBoundsException
644
     * @throws \InvalidArgumentException
645
     */
646 1
    public function parents($selector = NULL): Query {
647 1
      return $this->fetch('ancestor::*', $selector, NULL, Fetcher::REVERSE);
648
    }
649
650
    /**
651
     * Get the ancestors of each element in the current set of matched elements,
652
     * up to but not including the element matched by the selector.
653
     *
654
     * @param string $stopAt selector
655
     * @param string $filter selector
656
     * @return Query|Nodes
657
     * @throws \OutOfBoundsException
658
     * @throws \LogicException
659
     * @throws \InvalidArgumentException
660
     */
661 1
    public function parentsUntil($stopAt = NULL, $filter = NULL): Query {
662 1
      return $this->fetch('ancestor::*', $filter, $stopAt,Nodes\Fetcher::REVERSE);
663
    }
664
665
    /**
666
     * Get a set of elements containing the unique previous siblings of each of the
667
     * matched set of elements.
668
     *
669
     * @example prev.php Usage Example: FluentDOM\Query::prev()
670
     * @param string $selector selector
671
     * @return Query|Nodes
672
     * @throws \OutOfBoundsException
673
     * @throws \LogicException
674
     * @throws \InvalidArgumentException
675
     */
676 2
    public function prev($selector = NULL): Query {
677 2
      return $this->fetch(
678 2
        'preceding-sibling::node()[
679
          self::* or (self::text() and normalize-space(.) != "")
680
        ][1]',
681 2
        $selector,
682 2
        NULL,
683 2
        Nodes\Fetcher::UNIQUE
684
      );
685
    }
686
687
    /**
688
     * Find all sibling elements in front of the current element.
689
     *
690
     * @example prevAll.php Usage Example: FluentDOM\Query::prevAll()
691
     * @param string $selector selector
692
     * @return Query|Nodes
693
     * @throws \OutOfBoundsException
694
     * @throws \LogicException
695
     * @throws \InvalidArgumentException
696
     */
697 1
    public function prevAll($selector = NULL): Query {
698 1
      return $this->fetch(
699 1
        'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]',
700 1
        $selector,
701 1
        NULL,
702 1
        Nodes\Fetcher::REVERSE
703
      );
704
    }
705
706
    /**
707
     * Get all preceding siblings of each element up to but not including
708
     * the element matched by the selector.
709
     *
710
     * @param string $selector selector
711
     * @param string $filter selector
712
     * @return Query|Nodes
713
     * @throws \OutOfBoundsException
714
     * @throws \LogicException
715
     * @throws \InvalidArgumentException
716
     */
717 1
    public function prevUntil($selector = NULL, $filter = NULL): Query {
718 1
      return $this->fetch(
719 1
        'preceding-sibling::*|preceding-sibling::text()[normalize-space(.) != ""]',
720 1
        $filter,
721 1
        $selector,
722 1
        Nodes\Fetcher::REVERSE
723
      );
724
    }
725
726
727
    /**
728
     * Reverse the order of the matched elements.
729
     *
730
     * @return Query
731
     * @throws \OutOfBoundsException
732
     * @throws \InvalidArgumentException
733
     */
734 1
    public function reverse(): Query {
735 1
      return $this->spawn()->push(array_reverse($this->_nodes));
736
    }
737
738
    /**
739
     * Get a set of elements containing all of the unique siblings of each of the
740
     * matched set of elements.
741
     *
742
     * @example siblings.php Usage Example: FluentDOM\Query::siblings()
743
     * @param string $selector selector
744
     * @return Query|Nodes
745
     * @throws \OutOfBoundsException
746
     * @throws \LogicException
747
     * @throws \InvalidArgumentException
748
     */
749 1
    public function siblings($selector = NULL): Query {
750 1
      return $this->fetch(
751 1
        'preceding-sibling::*|
752
         preceding-sibling::text()[normalize-space(.) != ""]|
753
         following-sibling::*|
754
         following-sibling::text()[normalize-space(.) != ""]',
755 1
        $selector,
756 1
        NULL,
757 1
        Nodes\Fetcher::REVERSE
758
      );
759
    }
760
761
    /**
762
     * Selects a subset of the matched elements.
763
     *
764
     * @example slice.php Usage Example: FluentDOM\Query::slice()
765
     * @param int $start
766
     * @param int $end
767
     * @return Query
768
     * @throws \OutOfBoundsException
769
     * @throws \InvalidArgumentException
770
     */
771 4
    public function slice(int $start, int $end = NULL): Query {
772 4
      $result = $this->spawn();
773 4
      if ($end === NULL) {
774 1
        $result->push(array_slice($this->_nodes, $start));
775 3
      } elseif ($end < 0) {
776 1
        $result->push(array_slice($this->_nodes, $start, $end));
777 2
      } elseif ($end > $start) {
778 1
        $result->push(array_slice($this->_nodes, $start, $end - $start));
779
      } else {
780 1
        $result->push(array_slice($this->_nodes, $end, $start - $end));
781
      }
782 4
      return $result;
783
    }
784
785
    /*********************
786
     * Manipulation
787
     ********************/
788
789
    /**
790
     * Insert content after each of the matched elements.
791
     *
792
     * @example after.php Usage Example: FluentDOM\Query::after()
793
     * @param string|array|\DOMNode|\DOMNodeList|\Traversable|callable $content
794
     * @return Query
795
     */
796 2
    public function after($content): Query {
797 2
      return $this->applyToSpawn(
798 2
        $this->_nodes,
799 2
        $content,
800
        function($targetNode, $contentNodes) {
801 2
          return $this->modify($targetNode)->insertNodesAfter($contentNodes);
802 2
        }
803
      );
804
    }
805
806
    /**
807
     * Append content to the inside of every matched element.
808
     *
809
     * @example append.php Usage Example: FluentDOM\Query::append()
810
     * @param string|array|\DOMNode|\Traversable|callable $content DOMNode or DOMNodeList or xml fragment string
811
     * @return Query
812
     * @throws \InvalidArgumentException
813
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
814
     * @throws \LogicException
815
     */
816 14
    public function append($content): Query {
817 14
      if (empty($this->_nodes) &&
818 10
        $this->_useDocumentContext &&
819 10
        !isset($this->getDocument()->documentElement)) {
820 10
        if ($callback = Constraints::filterCallable($content)) {
821 1
          $contentNode = $this->build()->getContentElement($callback(NULL, 0, ''));
822
        } else {
823 9
          $contentNode = $this->build()->getContentElement($content);
824
        }
825 8
        return $this->spawn($this->getDocument()->appendChild($contentNode));
826
      } else {
827 5
        return $this->applyToSpawn(
828 5
          $this->_nodes,
829 5
          $content,
830
          function($targetNode, $contentNodes) {
831 5
            return $this->modify($targetNode)->appendChildren($contentNodes);
832 5
          }
833
        );
834
      }
835
    }
836
837
    /**
838
     * Append all of the matched elements to another, specified, set of elements.
839
     * Returns all of the inserted elements.
840
     *
841
     * @example appendTo.php Usage Example: FluentDOM\Query::appendTo()
842
     * @param string|array|\DOMNode|\DOMNodeList|Query $selector
843
     * @return Query
844
     */
845 2
    public function appendTo($selector): Query {
846 2
      return $this->applyToSelector(
847 2
        $selector,
848
        function($targetNode, $contentNodes) {
849 2
          return $this->modify($targetNode)->appendChildren($contentNodes);
850 2
        },
851 2
        TRUE
852
      );
853
    }
854
855
    /**
856
     * Insert content before each of the matched elements.
857
     *
858
     * @example before.php Usage Example: FluentDOM\Query::before()
859
     * @param string|array|\DOMNode|\Traversable|callable $content
860
     * @return Query
861
     */
862 2
    public function before($content): Query {
863 2
      return $this->applyToSpawn(
864 2
        $this->_nodes,
865 2
        $content,
866
        function($targetNode, $contentNodes) {
867 2
          return $this->modify($targetNode)->insertNodesBefore($contentNodes);
868 2
        }
869
      );
870
    }
871
872
    /**
873
     * Clone matched DOM Elements and select the clones.
874
     *
875
     * @example clone.php Usage Example: FluentDOM\Query:clone()
876
     * @return Query
877
     * @throws \OutOfBoundsException
878
     * @throws \InvalidArgumentException
879
     */
880
    public function clone(): Query {
881 1
      $result = $this->spawn();
882 1
      foreach ($this->_nodes as $node) {
883
        /** @var \DOMNode $node */
884 1
        $result->push($node->cloneNode(TRUE));
885
      }
886 1
      return $result;
887
    }
888
889
    /**
890
     * Remove all child nodes from the set of matched elements.
891
     *
892
     * @example empty.php Usage Example: FluentDOM\Query:empty()
893
     * @return Query
894
     * @throws \InvalidArgumentException
895
     */
896
    public function empty(): Query {
897 2
      $this->each(
898
        function (\DOMNode $node) {
899 2
          $node->nodeValue = '';
900 2
        }
901
      );
902 2
      $this->_useDocumentContext = TRUE;
903 2
      return $this;
904
    }
905
906
    /**
907
     * Insert all of the matched elements after another, specified, set of elements.
908
     *
909
     * @example insertAfter.php Usage Example: FluentDOM\Query::insertAfter()
910
     * @param string|array|\DOMNode|\Traversable $selector
911
     * @return Query
912
     * @throws \InvalidArgumentException
913
     */
914 1
    public function insertAfter($selector): Query {
915 1
      return $this->applyToSpawn(
916 1
        $this->build()->getTargetNodes($selector),
917 1
        $this->_nodes,
918
        function($targetNode, $contentNodes) {
919 1
          return $this->modify($targetNode)->insertNodesAfter($contentNodes);
920 1
        },
921 1
        TRUE
922
      );
923
    }
924
925
    /**
926
     * Insert all of the matched elements before another, specified, set of elements.
927
     *
928
     * @example insertBefore.php Usage Example: FluentDOM\Query::insertBefore()
929
     * @param string|array|\DOMNode|\Traversable $selector
930
     * @return Query
931
     */
932 1
    public function insertBefore($selector): Query {
933 1
      return $this->applyToSelector(
934 1
        $selector,
935
        function($targetNode, $contentNodes) {
936 1
          return $this->modify($targetNode)->insertNodesBefore($contentNodes);
937 1
        },
938 1
        TRUE
939
      );
940
    }
941
942
    /**
943
     * Prepend content to the inside of every matched element.
944
     *
945
     * @example prepend.php Usage Example: FluentDOM\Query::prepend()
946
     * @param string|array|\DOMNode|\Traversable $content
947
     * @return Query
948
     */
949 3
    public function prepend($content): Query {
950 3
      return $this->applyToSpawn(
951 3
        $this->_nodes,
952 3
        $content,
953
        function($targetNode, $contentNodes) {
954 3
          return $this->modify($targetNode)->insertChildrenBefore($contentNodes);
955 3
        }
956
      );
957
    }
958
959
    /**
960
     * Prepend all of the matched elements to another, specified, set of elements.
961
     * Returns all of the inserted elements.
962
     *
963
     * @example prependTo.php Usage Example: FluentDOM\Query::prependTo()
964
     * @param string|array|\DOMNode|\DOMNodeList|Query $selector
965
     * @return Query list of all new elements
966
     */
967 1
    public function prependTo($selector): Query {
968 1
      return $this->applyToSelector(
969 1
        $selector,
970
        function($targetNode, $contentNodes) {
971 1
          return $this->modify($targetNode)->insertChildrenBefore($contentNodes);
972 1
        },
973 1
        TRUE
974
      );
975
    }
976
977
    /**
978
     * Replaces the elements matched by the specified selector with the matched elements.
979
     *
980
     * @example replaceAll.php Usage Example: FluentDOM\Query::replaceAll()
981
     * @param string|array|\DOMNode|\Traversable $selector
982
     * @return Query
983
     * @throws \InvalidArgumentException
984
     */
985 3
    public function replaceAll($selector): Query {
986 3
      $result = $this->applyToSpawn(
987 3
        $targetNodes = $this->build()->getTargetNodes($selector),
988 2
        $this->_nodes,
989
        function($targetNode, $contentNodes) {
990 2
          return $this->modify($targetNode)->insertNodesBefore($contentNodes);
991 2
        },
992 2
        TRUE
993
      );
994 2
      $target = $this->spawn($targetNodes);
995 2
      $target->remove();
996 2
      return $result;
997
    }
998
999
    /**
1000
     * Replaces all matched elements with the specified HTML or DOM elements.
1001
     * This returns the element that was just replaced,
1002
     * which has been removed from the DOM.
1003
     *
1004
     * @example replaceWith.php Usage Example: FluentDOM\Query::replaceWith()
1005
     * @param string|array|\DOMNode|\Traversable|callable $content
1006
     * @return Query
1007
     * @throws \InvalidArgumentException
1008
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
1009
     */
1010 2
    public function replaceWith($content): Query {
1011 2
      $this->apply(
1012 2
        $this->_nodes,
1013 2
        $content,
1014
        function($targetNode, $contentNodes) {
1015 2
          return $this->modify($targetNode)->insertNodesBefore($contentNodes);
1016 2
        }
1017
      );
1018 2
      $this->remove();
1019 2
      return $this;
1020
    }
1021
1022
    /**
1023
     * Removes all matched elements from the DOM.
1024
     *
1025
     * @example remove.php Usage Example: FluentDOM\Query::remove()
1026
     * @param string $selector selector
1027
     * @return Query removed elements
1028
     * @throws \OutOfBoundsException
1029
     * @throws \InvalidArgumentException
1030
     */
1031 11
    public function remove(string $selector = NULL): Query {
1032 11
      $result = $this->spawn();
1033 11
      foreach ($this->_nodes as $node) {
1034 11
        if ($node->parentNode instanceof \DOMNode) {
1035 11
          if (NULL === $selector || $this->matches($selector, $node)) {
1036 11
            $result->push($node->parentNode->removeChild($node));
1037
          }
1038
        }
1039
      }
1040 11
      return $result;
1041
    }
1042
1043
    /**
1044
     * Get the combined text contents of all matched elements or
1045
     * set the text contents of all matched elements.
1046
     *
1047
     * @example text.php Usage Example: FluentDOM\Query::text()
1048
     * @param string|callable $text
1049
     * @return string|Query
1050
     * @throws \InvalidArgumentException
1051
     */
1052 3
    public function text($text = NULL) {
1053 3
      if (NULL !== $text) {
1054 2
        $callback = Constraints::filterCallable($text);
1055 2
        foreach ($this->_nodes as $index => $node) {
1056 2
          $node->nodeValue = $callback ? $callback($node, $index, $node->nodeValue): $text;
1057
        }
1058 2
        return $this;
1059
      }
1060 1
      $result = '';
1061 1
      foreach ($this->_nodes as $node) {
1062 1
        $result .= $node->textContent;
1063
      }
1064 1
      return $result;
1065
    }
1066
1067
    /**
1068
     * Wrap each matched element with the specified content.
1069
     *
1070
     * If $content contains several elements the first one is used
1071
     *
1072
     * @example wrap.php Usage Example: FluentDOM\Query::wrap()
1073
     * @param string|array|\DOMNode|\Traversable|callable $content
1074
     * @return Query
1075
     */
1076 6
    public function wrap($content): Query {
1077 6
      return $this->spawn($this->wrapNodes($this->_nodes, $content));
1078
    }
1079
1080
    /**
1081
     * Wrap al matched elements with the specified content
1082
     *
1083
     * If the matched elements are not siblings, wrap each group of siblings.
1084
     *
1085
     * @example wrapAll.php Usage Example: FluentDOM::wrapAll()
1086
     * @param string|array|\DOMNode|\Traversable $content
1087
     * @return Query
1088
     * @throws \FluentDOM\Exceptions\LoadingError\EmptyResult
1089
     * @throws \OutOfBoundsException
1090
     * @throws \InvalidArgumentException
1091
     */
1092 2
    public function wrapAll($content): Query {
1093 2
      $result = $this->spawn();
1094 2
      if ($groups = $this->getGroupedNodes()) {
1095 2
        $result->push(
1096 2
          $this->wrapGroupedNodes(
1097 2
            $groups, $this->build()->getContentElement($content)
1098
          )
1099
        );
1100
      }
1101 2
      return $result;
1102
    }
1103
1104
    /**
1105
     * group selected elements by previous node - ignore whitespace text nodes
1106
     *
1107
     * @return array|bool
1108
     */
1109 2
    private function getGroupedNodes() {
1110 2
      $current = NULL;
1111 2
      $counter = 0;
1112 2
      $groups = [];
1113 2
      foreach ($this->_nodes as $node) {
1114 2
        $previous = $node->previousSibling;
1115 2
        while ($previous instanceof \DOMText && $previous->isWhitespaceInElementContent()) {
1116 2
          $previous = $previous->previousSibling;
1117
        }
1118 2
        if ($previous !== $current) {
1119 2
          $counter++;
1120
        }
1121 2
        $groups[$counter][] = $node;
1122 2
        $current = $node;
1123
      }
1124 2
      return count($groups) > 0 ? $groups : FALSE;
1125
    }
1126
1127
    /**
1128
     * Wrap grouped nodes
1129
     *
1130
     * @param array $groups
1131
     * @param \DOMElement $template
1132
     * @return array
1133
     */
1134 2
    private function wrapGroupedNodes(array $groups, \DOMElement $template): array {
1135 2
      $result = [];
1136 2
      $simple = FALSE;
1137 2
      foreach ($groups as $group) {
1138 2
        if (isset($group[0])) {
1139 2
          $node = $group[0];
1140
          /**
1141
           * @var \DOMElement $target
1142
           * @var \DOMElement $wrapper
1143
           */
1144 2
          list($target, $wrapper) = $this->build()->getWrapperNodes(
1145 2
            $template,
1146 2
            $simple
1147
          );
1148 2
          if ($node->parentNode instanceof \DOMNode) {
1149 2
            $node->parentNode->insertBefore($wrapper, $node);
1150
          }
1151 2
          foreach ($group as $node) {
1152 2
            $target->appendChild($node);
1153
          }
1154 2
          $result[] = $node;
1155
        }
1156
      }
1157 2
      return $result;
1158
    }
1159
1160
    /**
1161
     * Wrap the inner child contents of each matched element
1162
     * (including text nodes) with an XML structure.
1163
     *
1164
     * @example wrapInner.php Usage Example: FluentDOM\Query::wrapInner()
1165
     * @param string|array|\DOMNode|\Traversable $content
1166
     * @return Query
1167
     */
1168 2
    public function wrapInner($content): Query {
1169 2
      $elements = [];
1170 2
      foreach ($this->_nodes as $node) {
1171 2
        foreach ($node->childNodes as $childNode) {
1172 2
          if (Constraints::filterNode($childNode)) {
1173 2
            $elements[] = $childNode;
1174
          }
1175
        }
1176
      }
1177 2
      return $this->spawn($this->wrapNodes($elements, $content));
1178
    }
1179
1180
    /**
1181
     * Get xml contents of the first matched element or set the
1182
     * xml contents of all selected element nodes.
1183
     *
1184
     * @example xml.php Usage Example: FluentDOM::xml()
1185
     * @param string|callable|NULL $xml XML fragment