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

Query   D

Complexity

Total Complexity 218

Size/Duplication

Total Lines 1660
Duplicated Lines 1.33 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
dl 22
loc 1660
ccs 591
cts 591
cp 1
rs 4.4102
c 0
b 0
f 0
wmc 218
lcom 1
cbo 9

76 Methods

Rating   Name   Duplication   Size   Complexity  
A __isset() 0 9 4
B __get() 0 17 5
B __set() 0 18 6
A __unset() 0 16 4
A getFirstElement() 0 8 3
C wrapNodes() 0 34 7
A modify() 0 3 1
A build() 0 6 2
C apply() 0 24 7
A applyToSpawn() 0 9 2
A applyToSelector() 0 8 1
B add() 0 15 5
A addBack() 0 3 1
A children() 0 3 1
A closest() 0 9 2
A contents() 0 8 1
A eq() 0 10 3
A filter() 0 10 3
A first() 0 3 1
A get() 0 12 4
B has() 0 17 5
A is() 0 8 3
A last() 0 3 1
B map() 0 19 7
A not() 0 8 1
A next() 0 10 1
A nextAll() 0 6 1
A nextUntil() 0 7 1
A parent() 0 3 1
A parents() 0 3 1
A parentsUntil() 0 3 1
A prev() 0 10 1
A prevAll() 0 8 1
A prevUntil() 0 8 1
A reverse() 0 3 1
A siblings() 0 11 1
A slice() 0 13 4
A after() 0 9 1
B append() 0 20 5
A appendTo() 0 9 1
A before() 0 9 1
A clone() 0 8 2
A empty() 0 9 1
A insertAfter() 0 10 1
A insertBefore() 0 9 1
A prepend() 0 9 1
A prependTo() 0 9 1
A replaceAll() 0 13 1
A replaceWith() 0 11 1
B remove() 0 11 5
B text() 0 14 5
A wrap() 0 3 1
A wrapAll() 0 11 2
B getGroupedNodes() 0 17 6
B wrapGroupedNodes() 0 25 5
A wrapInner() 0 11 4
A xml() 0 14 1
A outerXml() 11 11 1
A outerHtml() 11 11 1
A html() 0 18 2
B content() 0 21 6
A outerContent() 0 10 1
B getNamesList() 0 12 7
A getSetterValues() 0 9 4
C attr() 0 30 7
A hasAttr() 0 8 4
A removeAttr() 0 16 3
A addClass() 0 3 1
B hasClass() 0 11 5
A removeClass() 0 3 1
C toggleClass() 0 28 7
C changeClassString() 0 24 8
C css() 0 28 7
B data() 0 19 5
A removeData() 0 19 4
A hasData() 0 7 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

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
1186
     * @return string|$this
1187
     * @throws \InvalidArgumentException
1188
     * @throws \FluentDOM\Exceptions\InvalidFragmentLoader
1189
     * @throws \UnexpectedValueException
1190
     * @throws \LogicException
1191
     */
1192 7
    public function xml($xml = NULL) {
1193 7
      return $this->content(
1194 7
        $xml,
1195
        function($node) {
1196 3
          return $this->build()->getInnerXml($node);
1197 7
        },
1198
        function($node) {
1199 4
          return $this->build()->getFragment($node);
1200 7
        },
1201
        function($node, $fragment) {
1202 3
          $this->modify($node)->replaceChildren($fragment);
1203 7
        }
1204
      );
1205
    }
1206
1207
    /**
1208
     * Get the first matched node as XML or replace each
1209
     * matched nodes with the provided fragment.
1210
     *
1211
     * @param string|callable|NULL $xml
1212
     * @return string|$this
1213
     * @throws \LogicException
1214
     * @throws \UnexpectedValueException
1215
     * @throws \InvalidArgumentException
1216
     * @throws \FluentDOM\Exceptions\InvalidFragmentLoader
1217
     */
1218 7 View Code Duplication
    public function outerXml($xml = NULL) {
1219 7
      return $this->outerContent(
1220 7
        $xml,
1221
        function($node) {
1222 3
          return $this->getDocument()->saveXML($node);
1223 7
        },
1224
        function($xml) {
1225 4
          return $this->build()->getFragment($xml);
1226 7
        }
1227
      );
1228
    }
1229
1230
    /**
1231
     * Get the first matched node as HTML or replace each
1232
     * matched nodes with the provided fragment.
1233
     *
1234
     * @param string|callable|NULL $html
1235
     * @return string|$this
1236
     * @throws \LogicException
1237
     * @throws \UnexpectedValueException
1238
     * @throws \InvalidArgumentException
1239
     * @throws \FluentDOM\Exceptions\InvalidFragmentLoader
1240
     */
1241 7 View Code Duplication
    public function outerHtml($html = NULL) {
1242 7
      return $this->outerContent(
1243 7
        $html,
1244
        function($node) {
1245 3
          return $this->getDocument()->saveHTML($node);
1246 7
        },
1247
        function($html) {
1248 4
          return $this->build()->getFragment($html, 'text/html');
1249 7
        }
1250
      );
1251
    }
1252
1253
    /**
1254
     * Get html contents of the first matched element or set the
1255
     * html contents of all selected element nodes.
1256
     *
1257
     * @param string|callable|NULL $html
1258
     * @return string|$this
1259
     * @throws \LogicException
1260
     * @throws \UnexpectedValueException
1261
     * @throws \InvalidArgumentException
1262
     * @throws \FluentDOM\Exceptions\InvalidFragmentLoader
1263
     */
1264 4
    public function html($html = NULL) {
1265 4
      return $this->content(
1266 4
        $html,
1267
        function(\DOMNode $parent) {
1268 2
          $result = '';
1269 2
          foreach ($parent->childNodes as $node) {
1270 2
            $result .= $this->getDocument()->saveHTML($node);
1271
          }
1272 2
          return $result;
1273 4
        },
1274
        function($html) {
1275 2
          return $this->build()->getFragment($html, 'text/html');
1276 4
        },
1277
        function($node, $fragment) {
1278 2
          $this->modify($node)->replaceChildren($fragment);
1279 4
        }
1280
      );
1281
    }
1282
1283
    /**
1284
     * @param string|callable|NULL $content
1285
     * @param callable $export
1286
     * @param callable $import
1287
     * @param callable $insert
1288
     * @return string|$this
1289
     * @throws \InvalidArgumentException
1290
     */
1291 25
    private function content($content, callable $export, callable $import, callable $insert) {
1292 25
      if (NULL !== $content) {
1293 14
        $callback = Constraints::filterCallable($content);
1294 14
        if ($callback) {
1295 4
          foreach ($this->_nodes as $index => $node) {
1296 4
            $contentString = $callback($node, $index, $export($node));
1297 4
            $insert($node, $import($contentString));
1298
          }
1299
        } else {
1300 10
          $fragment = $import($content);
1301 7
          foreach ($this->_nodes as $node) {
1302 7
            $insert($node, $fragment);
1303
          }
1304
        }
1305 11
        return $this;
1306
      }
1307 11
      if (isset($this->_nodes[0])) {
1308 7
        return $export($this->_nodes[0]);
1309
      }
1310 4
      return '';
1311
    }
1312
1313
    /**
1314
     * @param string|callable|NULL $content
1315
     * @param callable $export
1316
     * @param callable $import
1317
     * @return $this|string
1318
     * @throws \InvalidArgumentException
1319
     */
1320 14
    private function outerContent($content, callable $export, callable $import) {
1321 14
      return $this->content(
1322 14
        $content,
1323 14
        $export,
1324 14
        $import,
1325
        function(\DOMNode $node, $fragment) {
1326 6
          $this->modify($node)->replaceNode($fragment);
1327 14
        }
1328
      );
1329
    }
1330
1331
    /****************************
1332
     * Manipulation - Attributes
1333
     ***************************/
1334
1335
    /**
1336
     * @param string|array|NULL $names
1337
     * @throws \InvalidArgumentException
1338
     * @return array|NULL
1339
     */
1340 8
    private function getNamesList($names) {
1341 8
      if (is_array($names)) {
1342 2
        return $names;
1343
      }
1344 6
      if (is_string($names) && $names !== '*' && $names !== '') {
1345 2
        return [$names];
1346
      }
1347 4
      if (NULL !== $names && $names !== '*') {
1348 2
        throw new \InvalidArgumentException('Invalid names argument provided.');
1349
      }
1350 2
      return NULL;
1351
    }
1352
1353
    /**
1354
     * @param string|array|\Traversable $name
1355
     * @param string|float|int|NULL|callable $value
1356
     * @return array|\Traversable
1357
     * @throws \InvalidArgumentException
1358
     */
1359 20
    private function getSetterValues($name, $value) {
1360 20
      if (is_string($name)) {
1361 15
        return [(string)$name => $value];
1362
      }
1363 5
      if (is_array($name) || $name instanceOf \Traversable) {
1364 4
        return $name;
1365
      }
1366 1
      throw new \InvalidArgumentException('Invalid css property name argument type.');
1367
    }
1368
1369
    /**
1370
     * Access a property on the first matched element or set the attribute(s) of all matched elements
1371
     *
1372
     * @example attr.php Usage Example: FluentDOM\Query::attr() Read an attribute value.
1373
     * @param string|array $attribute attribute name or attribute list
1374
     * @param array ...$arguments
1375
     * @return $this|string attribute value or $this
1376
     * @throws \InvalidArgumentException
1377
     */
1378 18
    public function attr($attribute, ...$arguments) {
1379 18
      if (is_string($attribute) && count($arguments) === 0) {
1380
        //empty value - read attribute from first element in list
1381 10
        $attribute = (new QualifiedName($attribute))->name;
1382 9
        $node = $this->getFirstElement();
1383 9
        if ($node && $node->hasAttribute($attribute)) {
1384 6
          return $node->getAttribute($attribute);
1385
        }
1386 3
        return NULL;
1387
      } else {
1388 12
        $attributes = $this->getSetterValues($attribute, $arguments[0] ?? NULL);
1389
        // set attributes on each element
1390 12
        foreach ($attributes as $key => $value) {
1391 12
          $name = (new QualifiedName($key))->name;
1392 6
          $callback = Constraints::filterCallable($value);
1393 6
          $this->each(
1394
            function(\DOMElement $node, $index) use ($name, $value, $callback) {
1395 6
              $node->setAttribute(
1396 6
                $name,
1397 6
                $callback
1398 1
                  ? (string)$callback($node, $index, $node->getAttribute($name))
1399 6
                  : (string)$value
1400
              );
1401 6
            },
1402 6
            TRUE
1403
          );
1404
        }
1405
      }
1406 6
      return $this;
1407
    }
1408
1409
    /**
1410
     * Returns TRUE if the specified attribute is present on at least one of
1411
     * the set of matched elements.
1412
     *
1413
     * @param string $name
1414
     * @return bool
1415
     */
1416 3
    public function hasAttr($name) {
1417 3
      foreach ($this->_nodes as $node) {
1418 3
        if ($node instanceof \DOMElement && $node->hasAttribute($name)) {
1419 2
          return TRUE;
1420
        }
1421
      }
1422 1
      return FALSE;
1423
    }
1424
1425
    /**
1426
     * Remove an attribute from each of the matched elements. If $name is NULL or *,
1427
     * all attributes will be deleted.
1428
     *
1429
     * @example removeAttr.php Usage Example: FluentDOM\Query::removeAttr()
1430
     * @param string|array $name
1431
     * @throws \InvalidArgumentException
1432
     * @return $this|Query
1433
     */
1434 4
    public function removeAttr($name): Query {
1435 4
      $names = $this->getNamesList($name);
1436 3
      $this->each(
1437
        function(\DOMElement $node) use ($names) {
1438
          /** @noinspection PhpParamsInspection */
1439 3
          $attributes = $names ?? array_keys(iterator_to_array($node->attributes));
1440 3
          foreach ($attributes as $attribute) {
1441 3
            if ($node->hasAttribute($attribute)) {
1442 3
              $node->removeAttribute($attribute);
1443
            }
1444
          }
1445 3
        },
1446 3
        TRUE
1447
      );
1448 3
      return $this;
1449
    }
1450
1451
    /*************************
1452
     * Manipulation - Classes
1453
     ************************/
1454
1455
    /**
1456
     * Adds the specified class(es) to each of the set of matched elements.
1457
     *
1458
     * @param string|callable $class
1459
     * @return Query
1460
     */
1461 1
    public function addClass($class) {
1462 1
      return $this->toggleClass($class, TRUE);
1463
    }
1464
1465
    /**
1466
     * Returns TRUE if the specified class is present on at least one of the set of matched elements.
1467
     *
1468
     * @param string $class
1469
     * @return bool
1470
     */
1471 2
    public function hasClass(string $class): bool {
1472 2
      foreach ($this->_nodes as $node) {
1473 2
        if ($node instanceof \DOMElement && $node->hasAttribute('class')) {
1474 2
          $classes = preg_split('(\s+)', trim($node->getAttribute('class')));
1475 2
          if (in_array($class, $classes, TRUE)) {
1476 1
            return TRUE;
1477
          }
1478
        }
1479
      }
1480 1
      return FALSE;
1481
    }
1482
1483
    /**
1484
     * Removes all or the specified class(es) from the set of matched elements.
1485
     *
1486
     * @param string|callable $class
1487
     * @return $this|Query
1488
     * @throws \InvalidArgumentException
1489
     */
1490 2
    public function removeClass($class = ''): Query {
1491 2
      return $this->toggleClass($class, FALSE);
1492
    }
1493
1494
    /**
1495
     * Adds the specified classes if the switch is TRUE,
1496
     * removes the specified classes if the switch is FALSE,
1497
     * toggles the specified classes if the switch is NULL.
1498
     *
1499
     * @example toggleClass.php Usage Example: FluentDOM\Query::toggleClass()
1500
     * @param string|callable $class
1501
     * @param NULL|bool $switch toggle if NULL, add if TRUE, remove if FALSE
1502
     * @return $this|Query
1503
     * @throws \InvalidArgumentException
1504
     */
1505 6
    public function toggleClass($class, bool $switch = NULL): Query {
1506 6
      $callback = Constraints::filterCallable($class);
1507 6
      $this->each(
1508
        function(\DOMElement $node, $index) use ($class, $switch, $callback) {
1509 6
          $classString = $callback ? $callback($node, $index, $node->getAttribute('class')) : $class;
1510 6
          if (empty($classString) && !(bool)$switch) {
1511 1
            if ($node->hasAttribute('class')) {
1512 1
              $node->removeAttribute('class');
1513
            }
1514
          } else {
1515 5
            $modified = $this->changeClassString(
1516 5
              $node->getAttribute('class'),
1517 5
              $classString,
1518 5
              $switch
1519
            );
1520 5
            if (FALSE !== $modified) {
1521 5
              if (empty($modified)) {
1522 1
                $node->removeAttribute('class');
1523
              } else {
1524 5
                $node->setAttribute('class', $modified);
1525
              }
1526
            }
1527
          }
1528 6
        },
1529 6
        TRUE
1530
      );
1531 6
      return $this;
1532
    }
1533
1534
    /**
1535
     * Change a class string
1536
     *
1537
     * Adds the specified classes if the switch is TRUE,
1538
     * removes the specified classes if the switch is FALSE,
1539
     * toggles the specified classes if the switch is NULL.
1540
     *
1541
     * @param string $current
1542
     * @param string $toggle
1543
     * @param bool|NULL $switch
1544
     * @return FALSE|string
1545
     */
1546 5
    private function changeClassString(string $current, string $toggle, bool $switch = NULL) {
1547 5
      $currentClasses = array_flip(
1548 5
        preg_split('(\s+)', trim($current), 0, PREG_SPLIT_NO_EMPTY)
1549
      );
1550 5
      $toggleClasses = array_unique(
1551 5
        preg_split('(\s+)', trim($toggle), 0, PREG_SPLIT_NO_EMPTY)
1552
      );
1553 5
      $modified = FALSE;
1554 5
      foreach ($toggleClasses as $class) {
1555
        if (
1556 5
          isset($currentClasses[$class]) &&
1557 4
          (NULL === $switch || FALSE === $switch)
1558
        ) {
1559 4
          unset($currentClasses[$class]);
1560 4
          $modified = TRUE;
1561 5
        } elseif (NULL === $switch || TRUE === $switch) {
1562 4
          $currentClasses[$class] = TRUE;
1563 4
          $modified = TRUE;
1564
        }
1565
      }
1566 5
      return $modified
1567 5
        ? implode(' ', array_keys($currentClasses))
1568 5
        : FALSE;
1569
    }
1570
1571
    /*************************************
1572
     * Manipulation - CSS Style Attribute
1573
     ************************************/
1574
1575
    /**
1576
     * get or set CSS values in style attributes
1577
     *
1578
     * @param string|array $property
1579
     * @param array ...$arguments
1580
     * @throws \InvalidArgumentException
1581
     * @return string|NULL|$this
1582
     */
1583 16
    public function css($property, ...$arguments) {
1584 16
      if (is_string($property) && count($arguments) === 0) {
1585 4
        $properties = new Query\Css\Properties((string)$this->attr('style'));
1586 4
        if (isset($properties[$property])) {
1587 2
          return $properties[$property];
1588
        }
1589 2
        return NULL;
1590
      }
1591 12
      $values = $this->getSetterValues($property, $arguments[0] ?? NULL);
1592
      //set list of properties to all elements
1593 11
      $this->each(
1594
        function(\DOMElement $node, $index) use ($values) {
1595 11
          $properties = new Query\Css\Properties($node->getAttribute('style'));
1596 11
          foreach ($values as $name => $value) {
1597 11
            $properties[$name] = $properties->compileValue(
1598 11
              $value, $node, $index, $properties[$name] ?? NULL
1599
            );
1600
          }
1601 9
          if (count($properties) > 0) {
1602 6
            $node->setAttribute('style', (string)$properties);
1603 3
          } elseif ($node->hasAttribute('style')) {
1604 3
            $node->removeAttribute('style');
1605
          }
1606 11
        },
1607 11
        TRUE
1608
      );
1609 9
      return $this;
1610
    }
1611
1612
    /*********************************
1613
     * Manipulation - Data Attributes
1614
     ********************************/
1615
1616
    /**
1617
     * Read a data attribute from the first node or set data attributes on all selected nodes.
1618
     *
1619
     * @example data.php Usage Example: FluentDOM\Query::data()
1620
     * @param string|array $name data attribute identifier or array of data attributes to set
1621
     * @param [mixed] ...$arguments
1622
     * @return mixed
1623
     * @throws \InvalidArgumentException
1624
     */
1625 5
    public function data($name, ...$arguments) {
1626 5
      if (is_string($name) && count($arguments) === 0) {
1627 2
        if ($node = $this->getFirstElement()) {
1628 1
          return (new Query\Data($node))->$name;
1629
        }
1630 1
        return NULL;
1631
      }
1632 3
      $values = $this->getSetterValues($name, $arguments[0] ?? NULL);
1633 3
      $this->each(
1634
        function(\DOMElement $node) use ($values) {
1635 3
          $data = new Query\Data($node);
1636 3
          foreach ($values as $dataName => $dataValue) {
1637 3
            $data->$dataName = $dataValue;
1638
          }
1639 3
        },
1640 3
        TRUE
1641
      );
1642 3
      return $this;
1643
    }
1644
1645
    /**
1646
     * Remove an data - attribute from each of the matched elements. If $name is NULL or *,
1647
     * all data attributes will be deleted.
1648
     *
1649
     * @example removeData.php Usage Example: FluentDOM\Query::removeData()
1650
     * @param string|array|NULL $name
1651
     * @throws \InvalidArgumentException
1652
     * @return Query
1653
     */
1654 4
    public function removeData($name = NULL): Query {
1655 4
      $names = $this->getNamesList($name);
1656 3
      $this->each(
1657 3
        function ($node) use ($names) {
1658 3
          $data = new Query\Data($node);
1659 3
          if (is_array($names)) {
1660 2
            foreach ($names as $dataName) {
1661 2
              unset($data->$dataName);
1662
            }
1663
          } else {
1664 1
            foreach ($data as $dataName => $dataValue) {
1665 1
              unset($data->$dataName);
1666
            }
1667
          }
1668 3
        },
1669 3
        TRUE
1670
      );
1671 3
      return $this;
1672
    }
1673
1674
    /**
1675
     * Validate if the element has an data attributes attached. If it is called without an
1676
     * actual $element parameter, it will check the first matched node.
1677
     *
1678
     * @param \DOMElement|NULL $element
1679
     * @return bool
1680
     */
1681 5
    public function hasData(\DOMElement $element = NULL): bool {
1682 5
      if ($element || ($element = $this->getFirstElement())) {
1683 4
        $data = new Query\Data($element);
1684 4
        return count($data) > 0;
1685
      }
1686 1
      return FALSE;
1687
    }
1688
  }
1689
}