Completed
Push — master ( 941dea...fac52c )
by Thomas
06:01
created

HtmlQuery::removeCss()   B

Complexity

Conditions 6
Paths 1

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 21
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 30
rs 8.9617
1
<?php
2
3
namespace Sulao\HtmlQuery;
4
5
use DOMDocument, DOMElement, DOMNode, DOMNodeList;
6
use Traversable;
7
8
/**
9
 * Class HtmlQuery
10
 *
11
 * @package Sulao\HtmlQuery
12
 */
13
class HtmlQuery extends Selection
14
{
15
    const VERSION = '1.0.0';
16
17
    /**
18
     * @var DOMDocument
19
     */
20
    protected $doc;
21
22
    /**
23
     * @var DOMNode[]
24
     */
25
    protected $nodes;
26
27
    /**
28
     * HtmlQuery constructor.
29
     *
30
     * @param DOMDocument                   $doc
31
     * @param DOMNode|DOMNode[]|DOMNodeList $nodes
32
     *
33
     * @throws Exception
34
     */
35
    public function __construct(DOMDocument $doc, $nodes)
36
    {
37
        $this->doc = $doc;
38
        $this->nodes = $this->validateNodes($nodes);
39
    }
40
41
    /**
42
     * Get the outer HTML contents of the first matched node.
43
     *
44
     * @return string|null
45
     */
46
    public function outerHtml()
47
    {
48
        return $this->mapFirst(function (DOMNode $node) {
49
            return $this->doc->saveHTML($node);
50
        });
51
    }
52
53
    /**
54
     * Get the inner HTML contents of the first matched node or
55
     * set the inner HTML contents of every matched node.
56
     *
57
     * @param string|null $html
58
     *
59
     * @return string|null|static
60
     */
61
    public function html(?string $html = null)
62
    {
63
        if (!is_null($html)) {
64
            return $this->setHtml($html);
65
        }
66
67
        return $this->getHtml();
68
    }
69
70
    /**
71
     * Get the inner HTML contents of the first matched node.
72
     *
73
     * @return string|null
74
     */
75
    public function getHtml()
76
    {
77
        return $this->mapFirst(function (DOMNode $node) {
78
            $content = '';
79
            foreach (iterator_to_array($node->childNodes) as $childNode) {
80
                $content .= $this->doc->saveHTML($childNode);
81
            }
82
83
            return $content;
84
        });
85
    }
86
87
    /**
88
     * Set the inner HTML contents of every matched node.
89
     *
90
     * @param string $html
91
     *
92
     * @return static
93
     */
94
    public function setHtml(string $html)
95
    {
96
        $this->empty();
97
98
        if ($html !== '') {
99
            $this->append($html);
100
        }
101
102
        return $this;
103
    }
104
105
    /**
106
     * Get the combined text contents of the first matched node, including
107
     * it's descendants, or set the text contents of every matched node.
108
     *
109
     * @param string|null $text
110
     *
111
     * @return string|null|static
112
     */
113
    public function text(?string $text = null)
114
    {
115
        if (!is_null($text)) {
116
            return $this->setText($text);
117
        }
118
119
        return $this->getText();
120
    }
121
122
    /**
123
     * Get the combined text contents of the first matched node,
124
     * including it's descendants.
125
     *
126
     * @return string|null
127
     */
128
    public function getText()
129
    {
130
        return $this->mapFirst(function (DOMNode $node) {
131
            return $node->textContent;
132
        });
133
    }
134
135
    /**
136
     * set the text contents of every matched node.
137
     *
138
     * @param string $text
139
     *
140
     * @return static
141
     */
142
    public function setText(string $text)
143
    {
144
        return $this->each(function (DOMNode $node) use ($text) {
145
            return $node->nodeValue = $text;
146
        });
147
    }
148
149
    /**
150
     * Get the value of an attribute for the first matched node
151
     * or set one or more attributes for every matched node.
152
     *
153
     * @param string|array $name
154
     * @param string|null  $value
155
     *
156
     * @return static|mixed|null
157
     */
158
    public function attr($name, $value = null)
159
    {
160
        if (is_array($name)) {
161
            foreach ($name as $key => $val) {
162
                $this->setAttr($key, $val);
163
            }
164
165
            return $this;
166
        }
167
168
        if (!is_null($value)) {
169
            return $this->setAttr($name, $value);
170
        }
171
172
        return $this->getAttr($name);
173
    }
174
175
    /**
176
     * Get the value of an attribute for the first matched node
177
     *
178
     * @param string $name
179
     *
180
     * @return string|null
181
     */
182
    public function getAttr(string $name)
183
    {
184
        return $this->mapFirst(function (DOMNode $node) use ($name) {
185
            if (!($node instanceof DOMElement)) {
186
                return null;
187
            }
188
189
            return $node->getAttribute($name);
190
        });
191
    }
192
193
    /**
194
     * Set one or more attributes for every matched node.
195
     *
196
     * @param string $name
197
     * @param string $value
198
     *
199
     * @return static
200
     */
201
    public function setAttr(string $name, string $value)
202
    {
203
        return $this->each(function (DOMNode $node) use ($name, $value) {
204
            if ($node instanceof DOMElement) {
205
                $node->setAttribute($name, $value);
206
            }
207
        });
208
    }
209
210
    /**
211
     * Remove an attribute from every matched nodes.
212
     *
213
     * @param string $attributeName
214
     *
215
     * @return static
216
     */
217
    public function removeAttr(string $attributeName)
218
    {
219
        return $this->each(function (DOMNode $node) use ($attributeName) {
220
            if ($node instanceof DOMElement) {
221
                $node->removeAttribute($attributeName);
222
            }
223
        });
224
    }
225
226
    /**
227
     * Remove all attributes from every matched nodes except the specified ones.
228
     *
229
     * @param string|array $except The attribute name(s) that won't be removed
230
     *
231
     * @return static
232
     */
233
    public function removeAllAttrs($except = [])
234
    {
235
        return $this->each(function (DOMNode $node) use ($except) {
236
            $names = [];
237
            foreach (iterator_to_array($node->attributes) as $attribute) {
238
                $names[] = $attribute->name;
239
            }
240
241
            foreach (array_diff($names, (array) $except) as $name) {
242
                if ($node instanceof DOMElement) {
243
                    $node->removeAttribute($name);
244
                }
245
            }
246
        });
247
    }
248
249
    /**
250
     * Determine whether any of the nodes have the given attribute.
251
     *
252
     * @param string $attributeName
253
     *
254
     * @return bool
255
     */
256
    public function hasAttr(string $attributeName)
257
    {
258
        return $this->mapAnyTrue(
259
            function (DOMNode $node) use ($attributeName) {
260
                if (!($node instanceof DOMElement)) {
261
                    return false;
262
                }
263
264
                return $node->hasAttribute($attributeName);
265
            }
266
        );
267
    }
268
269
    /**
270
     * Alias of attr
271
     *
272
     * @param string|array $name
273
     * @param string|null  $value
274
     *
275
     * @return static|mixed|null
276
     */
277
    public function prop($name, $value = null)
278
    {
279
        return $this->attr($name, $value);
280
    }
281
282
    /**
283
     * Alias of removeAttr
284
     *
285
     * @param string $attributeName
286
     *
287
     * @return static
288
     */
289
    public function removeProp(string $attributeName)
290
    {
291
        return $this->removeAttr($attributeName);
292
    }
293
294
    /**
295
     * Get the value of an attribute with prefix data- for the first matched
296
     * node, if the value is valid json string, returns the value encoded in
297
     * json in appropriate PHP type
298
     *
299
     * or set one or more attributes with prefix data- for every matched node.
300
     *
301
     * @param string|array $name
302
     * @param string|null  $value
303
     *
304
     * @return static|mixed|null
305
     */
306
    public function data($name, $value = null)
307
    {
308
        if (is_array($name)) {
309
            $keys = array_keys($name);
310
            $keys = array_map(function ($value) {
311
                return 'data-' . $value;
312
            }, $keys);
313
314
            $name = array_combine($keys, $name);
315
        } else {
316
            $name = 'data-' . $name;
317
        }
318
319
        if (!is_null($value) && !is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
320
            $value = (string) json_encode($value);
321
        }
322
323
        $result = $this->attr($name, $value);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type false; however, parameter $name of Sulao\HtmlQuery\HtmlQuery::attr() does only seem to accept array|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

323
        $result = $this->attr(/** @scrutinizer ignore-type */ $name, $value);
Loading history...
324
325
        if (is_string($result)) {
326
            $json = json_decode($result);
327
            if (json_last_error() === JSON_ERROR_NONE) {
328
                return $json;
329
            }
330
        }
331
332
        return $result;
333
    }
334
335
    /**
336
     * Determine whether any of the nodes have the given attribute
337
     * prefix with data-.
338
     *
339
     * @param string $name
340
     *
341
     * @return bool
342
     */
343
    public function hasData(string $name)
344
    {
345
        return $this->hasAttr('data-' . $name);
346
    }
347
348
    /**
349
     * Remove an attribute prefix with data- from every matched nodes.
350
     *
351
     * @param string $name
352
     *
353
     * @return static
354
     */
355
    public function removeData(string $name)
356
    {
357
        return $this->removeAttr('data-' . $name);
358
    }
359
360
    /**
361
     * Remove all child nodes of all matched nodes from the DOM.
362
     *
363
     * @return static
364
     */
365
    public function empty()
366
    {
367
        return $this->each(function (DOMNode $node) {
368
            $node->nodeValue = '';
369
        });
370
    }
371
372
    /**
373
     * Remove the matched nodes from the DOM.
374
     * optionally filtered by a selector.
375
     *
376
     * @param string|null $selector
377
     *
378
     * @return static
379
     */
380
    public function remove(?string $selector = null)
381
    {
382
        if (!is_null($selector)) {
383
            $this->filter($selector)->remove();
384
        } else {
385
            $this->each(function (DOMNode $node) {
386
                if ($node->parentNode) {
387
                    $node->parentNode->removeChild($node);
388
                }
389
            });
390
        }
391
392
        return $this;
393
    }
394
395
    /**
396
     * Get the current value of the first matched node
397
     * or set the value of every matched node.
398
     *
399
     * @param string|null $value
400
     *
401
     * @return string|null|static
402
     */
403
    public function val(?string $value = null)
404
    {
405
        if (is_null($value)) {
406
            return $this->getVal();
407
        }
408
409
        return $this->setVal($value);
410
    }
411
412
    /**
413
     * Get the current value of the first matched node
414
     *
415
     * @return string|null
416
     */
417
    public function getVal()
418
    {
419
        return $this->mapFirst(function (DOMNode $node) {
420
            if (!($node instanceof DOMElement)) {
421
                return null;
422
            }
423
424
            switch ($node->tagName) {
425
                case 'input':
426
                    return $node->getAttribute('value');
427
                case 'textarea':
428
                    return $node->nodeValue;
429
                case 'select':
430
                    $ht = $this->resolve($node);
431
432
                    $selected = $ht->children('option:selected');
433
                    if ($selected->count()) {
434
                        return $selected->getAttr('value');
435
                    }
436
437
                    $fistChild = $ht->xpathFind('child::*[1]');
438
                    if ($fistChild->count()) {
439
                        return $fistChild->getAttr('value');
440
                    }
441
                    break;
442
            }
443
444
            return null;
445
        });
446
    }
447
448
    /**
449
     * Set the value of every matched node.
450
     *
451
     * @param string $value
452
     *
453
     * @return static
454
     */
455
    public function setVal(string $value)
456
    {
457
        return $this->each(function (DOMNode $node) use ($value) {
458
            if (!($node instanceof DOMElement)) {
459
                return;
460
            }
461
462
            switch ($node->tagName) {
463
                case 'input':
464
                    $node->setAttribute('value', $value);
465
                    break;
466
                case 'textarea':
467
                    $node->nodeValue = $value;
468
                    break;
469
                case 'select':
470
                    $ht = $this->resolve($node);
471
472
                    $selected = $ht->children('option:selected');
473
                    if ($selected->count()) {
474
                        $selected->removeAttr('selected');
475
                    }
476
477
                    $options = $ht->children("option[value='{$value}']");
478
                    if ($options->count()) {
479
                        $options->first()->setAttr('selected', 'selected');
480
                    }
481
                    break;
482
            }
483
        });
484
    }
485
486
    /**
487
     * Adds the specified class(es) to each node in the matched nodes.
488
     *
489
     * @param string $className
490
     *
491
     * @return static
492
     */
493
    public function addClass(string $className)
494
    {
495
        return $this->each(function (HtmlQuery $node) use ($className) {
496
            if (!$node->hasAttr('class')) {
497
                $node->setAttr('class', $className);
498
                return;
499
            }
500
501
            $classNames = Helper::splitClass($className);
502
            $class = (string) $node->getAttr('class');
503
            $classes = Helper::splitClass($class);
504
505
            $classArr = array_diff($classNames, $classes);
506
            if (empty($classArr)) {
507
                return;
508
            }
509
510
            $class .= ' ' . implode(' ', $classArr);
511
            $this->setAttr('class', $class);
512
        });
513
    }
514
515
    /**
516
     * Determine whether any of the matched nodes are assigned the given class.
517
     *
518
     * @param string $className
519
     *
520
     * @return bool
521
     */
522
    public function hasClass(string $className)
523
    {
524
        return $this->mapAnyTrue(
525
            function (HtmlQuery $node) use ($className) {
526
                if (!$node->hasAttr('class')) {
527
                    return false;
528
                }
529
530
                $class = (string) $node->getAttr('class');
531
                $classes = Helper::splitClass($class);
532
533
                return in_array($className, $classes);
534
            }
535
        );
536
    }
537
538
    /**
539
     * Remove a single class, multiple classes, or all classes
540
     * from each matched node.
541
     *
542
     * @param string|null $className
543
     *
544
     * @return static
545
     */
546
    public function removeClass(?string $className = null)
547
    {
548
        return $this->each(function (HtmlQuery $node) use ($className) {
549
            if (!$node->hasAttr('class')) {
550
                return;
551
            }
552
553
            if (is_null($className)) {
554
                $node->removeAttr('class');
555
                return;
556
            }
557
558
            $classNames = Helper::splitClass($className);
559
            $class = (string) $node->getAttr('class');
560
            $classes = Helper::splitClass($class);
561
562
            $classArr = array_diff($classes, $classNames);
563
            if (empty($classArr)) {
564
                $node->removeAttr('class');
565
                return;
566
            }
567
568
            $class = implode(' ', $classArr);
569
            $node->setAttr('class', $class);
570
        });
571
    }
572
573
    /**
574
     * Add or remove class(es) from each matched node, depending on
575
     * either the class's presence or the value of the state argument.
576
     *
577
     * @param string $className
578
     * @param bool|null   $state
579
     *
580
     * @return static
581
     */
582
    public function toggleClass(string $className, ?bool $state = null)
583
    {
584
        return $this->each(function (HtmlQuery $node) use ($className, $state) {
585
            if (!is_null($state)) {
586
                if ($state) {
587
                    $node->addClass($className);
588
                } else {
589
                    $node->removeClass($className);
590
                }
591
                return;
592
            }
593
594
            if (!$this->hasAttr('class')) {
595
                $node->setAttr('class', $className);
596
                return;
597
            }
598
599
            $classNames = Helper::splitClass($className);
600
            $classes = Helper::splitClass($this->getAttr('class'));
0 ignored issues
show
Bug introduced by
It seems like $this->getAttr('class') can also be of type null; however, parameter $className of Sulao\HtmlQuery\Helper::splitClass() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

600
            $classes = Helper::splitClass(/** @scrutinizer ignore-type */ $this->getAttr('class'));
Loading history...
601
602
            $classArr = array_diff($classes, $classNames);
603
            $classArr = array_merge(
604
                $classArr,
605
                array_diff($classNames, $classes)
606
            );
607
            if (empty($classArr)) {
608
                $node->removeClass($className);
609
                return;
610
            }
611
612
            $node->setAttr('class', implode(' ', $classArr));
613
        });
614
    }
615
616
    /**
617
     * Get the value of a computed style property for the first matched node
618
     * or set one or more CSS properties for every matched node.
619
     *
620
     * @param string|array $name
621
     * @param string|null  $value
622
     *
623
     * @return static|string|null
624
     */
625
    public function css($name, $value = null)
626
    {
627
        if (is_null($value) && !is_array($name)) {
628
            return $this->getCss($name);
629
        }
630
631
        if (is_array($name)) {
632
            foreach ($name as $key => $val) {
633
                $this->setCss($key, $val);
634
            }
635
        } else {
636
            $this->setCss($name, $value);
637
        }
638
639
        return $this;
640
    }
641
642
643
    /**
644
     * Get the value of a computed style property for the first matched node
645
     *
646
     * @param string $name
647
     *
648
     * @return string|null
649
     */
650
    public function getCss(string $name)
651
    {
652
        return $this->mapFirst(function (HtmlQuery $node) use ($name) {
653
            $style = (string) $node->attr('style');
654
            $css = Helper::splitCss($style);
655
            if (!$css) {
656
                return null;
657
            }
658
659
            if (array_key_exists($name, $css)) {
660
                return $css[$name];
661
            }
662
663
            $arr = array_change_key_case($css, CASE_LOWER);
664
            $key = strtolower($name);
665
            if (array_key_exists($key, $arr)) {
666
                return $arr[$key];
667
            }
668
669
            return null;
670
        });
671
    }
672
673
    /**
674
     * Set or Remove one CSS property for every matched node.
675
     *
676
     * @param string      $name
677
     * @param string|null $value
678
     *
679
     * @return static
680
     */
681
    public function setCss(string $name, ?string $value)
682
    {
683
        return $this->each(function (HtmlQuery $node) use ($name, $value) {
684
            if ((string) $value === '') {
685
                $node->removeCss($name);
686
                return;
687
            }
688
689
            $style = (string) $node->attr('style');
690
            if (!$style) {
691
                $node->setAttr('style', $name . ': ' . $value . ';');
692
                return;
693
            }
694
695
            $css = Helper::splitCss($style);
696
            if (!array_key_exists($name, $css)) {
697
                $allKeys = array_keys($css);
698
                $arr = array_combine(
699
                    $allKeys,
700
                    array_map('strtolower', $allKeys)
701
                ) ?: [];
702
703
                $keys = array_keys($arr, strtolower($name));
704
                foreach ($keys as $key) {
705
                    unset($css[$key]);
706
                }
707
            }
708
709
            $css[$name] = $value;
710
            $style = Helper::implodeCss($css);
711
            $this->setAttr('style', $style);
712
        });
713
    }
714
715
    /**
716
     * Remove one CSS property for every matched node.
717
     *
718
     * @param string $name
719
     *
720
     * @return static
721
     */
722
    public function removeCss(string $name)
723
    {
724
        return $this->each(function (HtmlQuery $node) use ($name) {
725
            $style = (string) $node->attr('style');
726
            if (!$style) {
727
                return;
728
            }
729
730
            $css = Helper::splitCss($style);
731
            $removed = false;
732
            if (array_key_exists($name, $css)) {
733
                unset($css[$name]);
734
                $removed = true;
735
            } else {
736
                $allKeys = array_keys($css);
737
                $arr = array_combine(
738
                    $allKeys,
739
                    array_map('strtolower', $allKeys)
740
                ) ?: [];
741
742
                $keys = array_keys($arr, strtolower($name));
743
                foreach ($keys as $key) {
744
                    unset($css[$key]);
745
                    $removed = true;
746
                }
747
            }
748
749
            if ($removed) {
750
                $style = Helper::implodeCss($css);
751
                $this->setAttr('style', $style);
752
            }
753
        });
754
    }
755
756
    /**
757
     * Insert content or node(s) before each matched node.
758
     *
759
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
760
     *
761
     * @return static
762
     */
763
    public function before($content)
764
    {
765
        $content = $this->contentResolve($content);
766
767
        return $this->each(function (DOMNode $node, $index) use ($content) {
768
            $content->each(function (DOMNode $newNode) use ($node, $index) {
769
                if ($node->parentNode) {
770
                    $newNode = $index !== $this->count() - 1
771
                        ? $newNode->cloneNode(true)
772
                        : $newNode;
773
774
                    $node->parentNode->insertBefore($newNode, $node);
775
                }
776
            });
777
        });
778
    }
779
780
    /**
781
     * Insert every matched node before the target.
782
     *
783
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $selector
784
     *
785
     * @return static
786
     */
787
    public function insertBefore($selector)
788
    {
789
        $target = $this->targetResolve($selector);
790
791
        return $target->before($this);
792
    }
793
794
    /**
795
     * Insert content or node(s) after each matched node.
796
     *
797
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
798
     *
799
     * @return static
800
     */
801
    public function after($content)
802
    {
803
        $content = $this->contentResolve($content);
804
805
        return $this->each(function (HtmlQuery $node, $index) use ($content) {
806
            $content->each(function (DOMNode $newNode) use ($node, $index) {
807
                $newNode = $index !== $this->count() - 1
808
                    ? $newNode->cloneNode(true)
809
                    : $newNode;
810
811
                if ($node->next()->count()) {
812
                    $node->next()->before($newNode);
813
                } else {
814
                    $node->parent()->append($newNode);
815
                }
816
            }, true);
817
        });
818
    }
819
820
    /**
821
     * Insert every matched node after the target.
822
     *
823
     * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
824
     *
825
     * @return static
826
     */
827
    public function insertAfter($selector)
828
    {
829
        $target = $this->targetResolve($selector);
830
831
        return $target->after($this);
832
    }
833
834
    /**
835
     * Insert content or node(s) to the end of every matched node.
836
     *
837
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
838
     *
839
     * @return static
840
     */
841
    public function append($content)
842
    {
843
        $content = $this->contentResolve($content);
844
845
        return $this->each(function (DOMNode $node, $index) use ($content) {
846
            $content->each(function (DOMNode $newNode) use ($node, $index) {
847
                $newNode = $index !== $this->count() - 1
848
                    ? $newNode->cloneNode(true)
849
                    : $newNode;
850
851
                $node->appendChild($newNode);
852
            });
853
        });
854
    }
855
856
    /**
857
     * Insert every matched node to the end of the target.
858
     *
859
     * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
860
     *
861
     * @return static
862
     */
863
    public function appendTo($selector)
864
    {
865
        $target = $this->targetResolve($selector);
866
867
        return $target->append($this);
868
    }
869
870
    /**
871
     * Insert content or node(s) to the beginning of each matched node.
872
     *
873
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
874
     *
875
     * @return static
876
     */
877
    public function prepend($content)
878
    {
879
        $content = $this->contentResolve($content);
880
881
        return $this->each(function (DOMNode $node, $index) use ($content) {
882
            $content->each(function (DOMNode $newNode) use ($node, $index) {
883
                $newNode = $index !== $this->count() - 1
884
                    ? $newNode->cloneNode(true)
885
                    : $newNode;
886
887
                if ($node->firstChild) {
888
                    $node->insertBefore($newNode, $node->firstChild);
889
                } else {
890
                    $node->appendChild($newNode);
891
                }
892
            }, true);
893
        });
894
    }
895
896
    /**
897
     * Insert every matched node to the beginning of the target.
898
     *
899
     * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
900
     *
901
     * @return static
902
     */
903
    public function prependTo($selector)
904
    {
905
        $target = $this->targetResolve($selector);
906
907
        return $target->prepend($this);
908
    }
909
910
    /**
911
     * Replace each matched node with the provided new content or node(s)
912
     *
913
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
914
     *
915
     * @return static
916
     */
917
    public function replaceWith($content)
918
    {
919
        $content = $this->contentResolve($content);
920
        return $this->each(function (DOMNode $node, $index) use ($content) {
921
            if (!$node->parentNode) {
922
                return;
923
            }
924
925
            $len = $content->count();
926
            $content->each(
927
                function (DOMNode $newNode) use ($node, $index, $len) {
928
                    $newNode = $index !== $this->count() - 1
929
                        ? $newNode->cloneNode(true)
930
                        : $newNode;
931
932
                    if ($len === 1) {
933
                        $node->parentNode->replaceChild($newNode, $node);
934
                    } else {
935
                        $this->resolve($newNode)->insertAfter($node);
936
                    }
937
                },
938
                true
939
            );
940
941
            if ($len !== 1) {
942
                $node->parentNode->removeChild($node);
943
            }
944
        });
945
    }
946
947
    /**
948
     * Replace each target node with the matched node(s)
949
     *
950
     * @param string|DOMNode|DOMNode[]DOMNodeList|static $selector
951
     *
952
     * @return static
953
     */
954
    public function replaceAll($selector)
955
    {
956
        $target = $this->targetResolve($selector);
957
958
        return $target->replaceWith($this);
959
    }
960
961
    /**
962
     * Wrap an HTML structure around each matched node.
963
     *
964
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
965
     *
966
     * @return static
967
     */
968
    public function wrap($content)
969
    {
970
        $content = $this->contentResolve($content);
971
        $newNode = $content[0];
972
973
        if (empty($newNode)) {
974
            return $this;
975
        }
976
977
        $newNode = $content[0];
978
979
        return $this->each(function (DOMNode $node, $index) use ($newNode) {
980
            $newNode = $index !== $this->count() - 1
981
                ? $newNode->cloneNode(true)
0 ignored issues
show
Bug introduced by
The method cloneNode() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

981
                ? $newNode->/** @scrutinizer ignore-call */ cloneNode(true)

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
982
                : $newNode;
983
984
            $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
985
            if (!$nodes) {
986
                throw new Exception('Invalid wrap html format.');
987
            }
988
989
            $deepestNode = end($nodes);
990
            $node->parentNode->replaceChild($newNode, $node);
0 ignored issues
show
Bug introduced by
It seems like $newNode can also be of type null; however, parameter $newnode of DOMNode::replaceChild() does only seem to accept DOMNode, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

990
            $node->parentNode->replaceChild(/** @scrutinizer ignore-type */ $newNode, $node);
Loading history...
991
            $deepestNode->appendChild($node);
992
        });
993
    }
994
995
    /**
996
     * Wrap an HTML structure around the content of each matched node.
997
     *
998
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
999
     *
1000
     * @return static
1001
     */
1002
    public function wrapInner($content)
1003
    {
1004
        $content = $this->contentResolve($content);
1005
        $newNode = $content[0];
1006
1007
        if (empty($newNode)) {
1008
            return $this;
1009
        }
1010
1011
        return $this->each(function (DOMNode $node, $index) use ($newNode) {
1012
            $newNode = $index !== $this->count() - 1
1013
                ? $newNode->cloneNode(true)
1014
                : $newNode;
1015
1016
            $nodes = $this->xpathQuery('descendant::*[last()]', $newNode);
1017
            if (!$nodes) {
1018
                throw new Exception('Invalid wrap html format.');
1019
            }
1020
1021
            $deepestNode = end($nodes);
1022
1023
            // Can't loop $this->node->childNodes directly to append child,
1024
            // Because childNodes will change once appending child.
1025
            foreach (iterator_to_array($node->childNodes) as $childNode) {
1026
                $deepestNode->appendChild($childNode);
1027
            }
1028
1029
            $node->appendChild($newNode);
1030
        });
1031
    }
1032
1033
    /**
1034
     * Wrap an HTML structure around all matched nodes.
1035
     *
1036
     * @param string|DOMNode|DOMNode[]|DOMNodeList|static $content
1037
     *
1038
     * @return static
1039
     */
1040
    public function wrapAll($content)
1041
    {
1042
        $content = $this->contentResolve($content);
1043
        if (!$content->count()) {
1044
            return $this;
1045
        }
1046
1047
        $newNode = $content[0];
1048
        $this->each(function (DOMNode $node, $index) use ($newNode) {
1049
            if ($index === 0) {
1050
                $this->resolve($node)->wrap($newNode);
1051
            } else {
1052
                $this->nodes[0]->parentNode->appendChild($node);
1053
            }
1054
        });
1055
1056
        return $this;
1057
    }
1058
1059
    /**
1060
     * Remove the parents of the matched nodes from the DOM.
1061
     * A optional selector to check the parent node against.
1062
     *
1063
     * @param string|null $selector
1064
     *
1065
     * @return static
1066
     */
1067
    public function unwrap(?string $selector = null)
1068
    {
1069
        return $this->parent($selector)->unwrapSelf();
1070
    }
1071
1072
    /**
1073
     * Remove the HTML tag of the matched nodes from the DOM.
1074
     * Leaving the child nodes in their place.
1075
     *
1076
     * @return static
1077
     */
1078
    public function unwrapSelf()
1079
    {
1080
        return $this->each(function (DOMNode $node) {
1081
            if (!$node->parentNode) {
1082
                return;
1083
            }
1084
1085
            foreach (iterator_to_array($node->childNodes) as $childNode) {
1086
                $node->parentNode->insertBefore($childNode, $node);
1087
            }
1088
1089
            $node->parentNode->removeChild($node);
1090
        });
1091
    }
1092
1093
    /**
1094
     * Validate the nodes
1095
     *
1096
     * @param DOMNode|DOMNode[]|DOMNodeList|static $nodes
1097
     *
1098
     * @return DOMNode[]
1099
     * @throws Exception
1100
     */
1101
    protected function validateNodes($nodes)
1102
    {
1103
        // Handle false, null
1104
        if (!$nodes) {
1105
            $nodes = [];
1106
        }
1107
1108
        if (!is_array($nodes) && !($nodes instanceof Traversable)) {
1109
            $nodes = [$nodes];
1110
        }
1111
1112
        $nodes = Helper::strictArrayUnique($nodes);
0 ignored issues
show
Bug introduced by
It seems like $nodes can also be of type DOMNode; however, parameter $arr of Sulao\HtmlQuery\Helper::strictArrayUnique() does only seem to accept Traversable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1112
        $nodes = Helper::strictArrayUnique(/** @scrutinizer ignore-type */ $nodes);
Loading history...
1113
        foreach ($nodes as $node) {
1114
            if (!($node instanceof DOMNode)) {
1115
                throw new Exception(
1116
                    'Expect an instance of DOMNode, '
1117
                        . gettype($node) . ' given.'
1118
                );
1119
            }
1120
1121
            if ((!$node->ownerDocument && $node !== $this->doc)
1122
                || ($node->ownerDocument && $node->ownerDocument !== $this->doc)
1123
            ) {
1124
                throw new Exception(
1125
                    'The DOMNode does not belong to the DOMDocument.'
1126
                );
1127
            }
1128
        }
1129
1130
        return $nodes;
1131
    }
1132
}
1133