Completed
Push — master ( 39b072...2fa9c0 )
by Thomas
02:12
created

src/FluentDOM/Query.php (2 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/**
3
 * 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 12
      foreach ($this->_nodes as $node) {
156 4
        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
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...
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
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
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
}