• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / simple_html_dom / 24699745740

21 Apr 2026 01:46AM UTC coverage: 93.156% (+2.2%) from 91.002%
24699745740

push

github

voku
Tighten DOM wrapper type metadata

Clean up wrapper helper return metadata, blank-node aliases, and token list normalization so the remaining static-analysis-oriented changes stay separate from the runtime fixes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

17 of 17 new or added lines in 5 files covered. (100.0%)

70 existing lines in 4 files now uncovered.

2028 of 2177 relevant lines covered (93.16%)

83.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.63
/src/voku/helper/SimpleHtmlDom.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace voku\helper;
6

7
/**
8
 * {@inheritdoc}
9
 *
10
 * @implements \IteratorAggregate<int, \DOMNode>
11
 */
12
class SimpleHtmlDom extends AbstractSimpleHtmlDom implements \IteratorAggregate, SimpleHtmlDomInterface
13
{
14
    /**
15
     * @var HtmlDomParser|null
16
     */
17
    private $queryHtmlDomParser;
18

19
    /**
20
     * Create a wrapper around an existing DOM node.
21
     *
22
     * @param \DOMElement|\DOMNode $node
23
     * @param HtmlDomParser|null   $queryHtmlDomParser Internal parser context
24
     *                                                 used for wrappers created
25
     *                                                 by HtmlDomParser.
26
     */
27
    public function __construct(\DOMNode $node, ?HtmlDomParser $queryHtmlDomParser = null)
28
    {
29
        $this->node = $node;
490✔
30
        $this->queryHtmlDomParser = $queryHtmlDomParser;
490✔
31
    }
32

33
    /**
34
     * @param string $name
35
     * @param array  $arguments
36
     *
37
     * @throws \BadMethodCallException
38
     *
39
     * @return SimpleHtmlDomInterface|string|null
40
     */
41
    public function __call($name, $arguments)
42
    {
43
        $name = \strtolower($name);
42✔
44

45
        if (isset(self::$functionAliases[$name])) {
42✔
46
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
42✔
47
        }
48

49
        throw new \BadMethodCallException('Method does not exist');
×
50
    }
51

52
    /**
53
     * Find list of nodes with a CSS selector.
54
     *
55
     * @param string   $selector
56
     * @param int|null $idx
57
     *
58
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
59
     */
60
    public function find(string $selector, $idx = null)
61
    {
62
        $document = $this->node instanceof \DOMDocument ? $this->node : $this->node->ownerDocument;
108✔
63

64
        if (!$document instanceof \DOMDocument) {
108✔
65
            if ($idx === null) {
2✔
66
                return new SimpleHtmlDomNodeBlank();
2✔
67
            }
68

69
            return new SimpleHtmlDomBlank();
2✔
70
        }
71

72
        if ($this->queryHtmlDomParser !== null) {
108✔
73
            return $this->queryHtmlDomParser->findInNodeContext($selector, $this->node, $idx);
80✔
74
        }
75

76
        return HtmlDomParser::findInDocumentContext(
28✔
77
            $selector,
28✔
78
            $document,
28✔
79
            $this->node,
28✔
80
            $idx,
28✔
81
            null,
28✔
82
            null
28✔
83
        );
28✔
84
    }
85

86
    public function getTag(): string
87
    {
88
        return $this->tag;
2✔
89
    }
90

91
    /**
92
     * Returns an array of attributes.
93
     *
94
     * @return string[]|null
95
     */
96
    public function getAllAttributes()
97
    {
98
        if (
99
            $this->node
16✔
100
            &&
101
            $this->node->hasAttributes()
16✔
102
        ) {
103
            $attributes = [];
16✔
104
            foreach ($this->node->attributes ?? [] as $attr) {
16✔
105
                $attributes[$attr->name] = HtmlDomParser::putReplacedBackToPreserveHtmlEntities($attr->value);
16✔
106
            }
107

108
            return $attributes;
16✔
109
        }
110

111
        return null;
4✔
112
    }
113

114
    /**
115
     * @return bool
116
     */
117
    public function hasAttributes(): bool
118
    {
119
        return $this->node && $this->node->hasAttributes();
2✔
120
    }
121

122
    /**
123
     * Return attribute value.
124
     *
125
     * @param string $name
126
     *
127
     * @return string
128
     */
129
    public function getAttribute(string $name): string
130
    {
131
        if ($this->node instanceof \DOMElement) {
74✔
132
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities(
74✔
133
                $this->node->getAttribute($name)
74✔
134
            );
74✔
135
        }
136

137
        return '';
2✔
138
    }
139

140
    /**
141
     * Determine if an attribute exists on the element.
142
     *
143
     * @param string $name
144
     *
145
     * @return bool
146
     */
147
    public function hasAttribute(string $name): bool
148
    {
149
        if (!$this->node instanceof \DOMElement) {
14✔
150
            return false;
2✔
151
        }
152

153
        return $this->node->hasAttribute($name);
12✔
154
    }
155

156
    /**
157
     * Get dom node's outer html.
158
     *
159
     * @param bool $multiDecodeNewHtmlEntity
160
     *
161
     * @return string
162
     */
163
    public function html(bool $multiDecodeNewHtmlEntity = false): string
164
    {
165
        return $this->getHtmlDomParser()->html($multiDecodeNewHtmlEntity);
104✔
166
    }
167

168
    /**
169
     * Get dom node's inner html.
170
     *
171
     * @param bool $multiDecodeNewHtmlEntity
172
     * @param bool $putBrokenReplacedBack
173
     *
174
     * @return string
175
     */
176
    public function innerHtml(bool $multiDecodeNewHtmlEntity = false, bool $putBrokenReplacedBack = true): string
177
    {
178
        return $this->getHtmlDomParser()->innerHtml($multiDecodeNewHtmlEntity, $putBrokenReplacedBack);
62✔
179
    }
180

181
    /**
182
     * Remove attribute.
183
     *
184
     * @param string $name <p>The name of the html-attribute.</p>
185
     *
186
     * @return SimpleHtmlDomInterface
187
     */
188
    public function removeAttribute(string $name): SimpleHtmlDomInterface
189
    {
190
        if (\method_exists($this->node, 'removeAttribute')) {
10✔
191
            $this->node->removeAttribute($name);
10✔
192
        }
193

194
        return $this;
10✔
195
    }
196

197
    /**
198
     * Remove all attributes
199
     *
200
     * @return SimpleHtmlDomInterface
201
     */
202
    public function removeAttributes(): SimpleHtmlDomInterface
203
    {
204
        if ($this->hasAttributes()) {
2✔
205
            foreach (array_keys((array)$this->getAllAttributes()) as $attribute) {
2✔
206
                $this->removeAttribute($attribute);
2✔
207
            }
208
        }
209
        return $this;
2✔
210
    }
211

212
    /**
213
     * Replace child node.
214
     *
215
     * @param string $string
216
     * @param bool   $putBrokenReplacedBack
217
     *
218
     * @return SimpleHtmlDomInterface
219
     */
220
    protected function replaceChildWithString(string $string, bool $putBrokenReplacedBack = true): SimpleHtmlDomInterface
221
    {
222
        if (!empty($string)) {
26✔
223
            $newDocument = new HtmlDomParser($string);
24✔
224

225
            $tmpDomString = $this->normalizeStringForComparison($newDocument);
24✔
226
            $tmpStr = $this->normalizeStringForComparison($string);
24✔
227

228
            if ($tmpDomString !== $tmpStr) {
24✔
229
                throw new \RuntimeException(
2✔
230
                    'Not valid HTML fragment!' . "\n" .
2✔
231
                    $tmpDomString . "\n" .
2✔
232
                    $tmpStr
2✔
233
                );
2✔
234
            }
235
        }
236

237
        /** @var \DOMNode[] $remove_nodes */
238
        $remove_nodes = [];
26✔
239
        if ($this->node->childNodes->length > 0) {
26✔
240
            // INFO: We need to fetch the nodes first, before we can delete them, because of missing references in the dom,
241
            // if we delete the elements on the fly.
242
            foreach ($this->node->childNodes as $node) {
22✔
243
                $remove_nodes[] = $node;
22✔
244
            }
245
        }
246
        foreach ($remove_nodes as $remove_node) {
26✔
247
            $this->node->removeChild($remove_node);
22✔
248
        }
249

250
        if (!empty($newDocument)) {
26✔
251
            $newDocument = $this->cleanHtmlWrapper($newDocument);
24✔
252
            $ownerDocument = $this->node->ownerDocument;
24✔
253
            if (
254
                $ownerDocument
24✔
255
                &&
256
                $newDocument->getDocument()->documentElement
24✔
257
            ) {
258
                $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
22✔
259
                $this->node->appendChild($newNode);
22✔
260
            }
261
        }
262

263
        return $this;
26✔
264
    }
265

266
    /**
267
     * Replace this node.
268
     *
269
     * @param string $string
270
     *
271
     * @return SimpleHtmlDomInterface
272
     */
273
    protected function replaceNodeWithString(string $string): SimpleHtmlDomInterface
274
    {
275
        if (empty($string)) {
56✔
276
            if ($this->node->parentNode) {
34✔
277
                $this->node->parentNode->removeChild($this->node);
34✔
278
            }
279
            $this->node = new \DOMText();
34✔
280

281
            return $this;
34✔
282
        }
283

284
        $newDocument = new HtmlDomParser($string);
28✔
285

286
        $tmpDomOuterTextString = $this->normalizeStringForComparison($newDocument);
28✔
287
        $tmpStr = $this->normalizeStringForComparison($string);
28✔
288

289
        if ($tmpDomOuterTextString !== $tmpStr) {
28✔
290
            throw new \RuntimeException(
2✔
291
                'Not valid HTML fragment!' . "\n"
2✔
292
                . $tmpDomOuterTextString . "\n" .
2✔
293
                $tmpStr
2✔
294
            );
2✔
295
        }
296

297
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
28✔
298
        $ownerDocument = $this->node->ownerDocument;
28✔
299
        if (
300
            $ownerDocument === null
28✔
301
            ||
302
            $newDocument->getDocument()->documentElement === null
28✔
303
        ) {
304
            return $this;
2✔
305
        }
306

307
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
28✔
308

309
        if ($this->node->parentNode !== null) {
28✔
310
            $this->node->parentNode->replaceChild($newNode, $this->node);
28✔
311
        }
312
        $this->node = $newNode;
28✔
313

314
        // Remove head element, preserving child nodes. (again)
315
        if (
316
            $this->node->parentNode instanceof \DOMElement
28✔
317
            &&
318
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
28✔
319
        ) {
320
            $html = $this->node->parentNode->getElementsByTagName('head')[0];
18✔
321

322
            if (
323
                $html !== null
18✔
324
                &&
325
                $this->node->parentNode->ownerDocument
18✔
326
            ) {
327
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
2✔
328
                /** @var \DOMNode $html */
329
                while ($html->childNodes->length > 0) {
2✔
330
                    $tmpNode = $html->childNodes->item(0);
2✔
331
                    if ($tmpNode !== null) {
2✔
332
                        /** @noinspection UnusedFunctionResultInspection */
333
                        $fragment->appendChild($tmpNode);
2✔
334
                    }
335
                }
336
                $html->parentNode->replaceChild($fragment, $html);
2✔
337
            }
338
        }
339

340
        return $this;
28✔
341
    }
342

343
    /**
344
     * Replace this node with text
345
     *
346
     * @param string $string
347
     *
348
     * @return SimpleHtmlDomInterface
349
     */
350
    protected function replaceTextWithString($string): SimpleHtmlDomInterface
351
    {
352
        if (empty($string)) {
2✔
353
            if ($this->node->parentNode) {
2✔
354
                $this->node->parentNode->removeChild($this->node);
2✔
355
            }
356
            $this->node = new \DOMText();
2✔
357

358
            return $this;
2✔
359
        }
360

361
        $ownerDocument = $this->node->ownerDocument;
2✔
362
        if ($ownerDocument) {
2✔
363
            $newElement = $ownerDocument->createTextNode($string);
2✔
364
            $newNode = $ownerDocument->importNode($newElement, true);
2✔
365
            if ($this->node->parentNode !== null) {
2✔
366
                $this->node->parentNode->replaceChild($newNode, $this->node);
2✔
367
            }
368
            $this->node = $newNode;
2✔
369
        }
370

371
        return $this;
2✔
372
    }
373

374
    /**
375
     * Set attribute value.
376
     *
377
     * @param string      $name                     <p>The name of the html-attribute.</p>
378
     * @param string|null $value                    <p>Set to NULL or empty string, to remove the attribute.</p>
379
     * @param bool        $strictEmptyValueCheck <p>
380
     *                                $value must be NULL, to remove the attribute,
381
     *                                so that you can set an empty string as attribute-value e.g. autofocus=""
382
     *                                </p>
383
     *
384
     * @return SimpleHtmlDomInterface
385
     */
386
    public function setAttribute(string $name, $value = null, bool $strictEmptyValueCheck = false): SimpleHtmlDomInterface
387
    {
388
        if (
389
            ($strictEmptyValueCheck && $value === null)
46✔
390
            ||
391
            (!$strictEmptyValueCheck && empty($value))
46✔
392
        ) {
393
            /** @noinspection UnusedFunctionResultInspection */
394
            $this->removeAttribute($name);
4✔
395
        } elseif (\method_exists($this->node, 'setAttribute')) {
46✔
396
            /** @noinspection UnusedFunctionResultInspection */
397
            $this->node->setAttribute($name, HtmlDomParser::replaceToPreserveHtmlEntities((string) $value));
46✔
398
        }
399

400
        return $this;
46✔
401
    }
402

403
    /**
404
     * Get dom node's plain text.
405
     *
406
     * @return string
407
     */
408
    public function text(): string
409
    {
410
        if ($this->node instanceof \DOMCharacterData) {
128✔
411
            return HtmlDomParser::putReplacedBackToPreserveHtmlEntities($this->node->nodeValue ?? '');
52✔
412
        }
413

414
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
76✔
415
    }
416

417
    /**
418
     * Change the name of a tag in a "DOMNode".
419
     *
420
     * @param \DOMNode $node
421
     * @param string   $name
422
     *
423
     * @return \DOMElement|false
424
     *                          <p>DOMElement a new instance of class DOMElement or false
425
     *                          if an error occurred.</p>
426
     */
427
    protected function changeElementName(\DOMNode $node, string $name)
428
    {
429
        $ownerDocument = $node->ownerDocument;
36✔
430
        if (!$ownerDocument) {
36✔
431
            return false;
2✔
432
        }
433

434
        $newNode = $ownerDocument->createElement($name);
36✔
435

436
        foreach ($node->childNodes as $child) {
36✔
437
            $child = $ownerDocument->importNode($child, true);
36✔
438
            $newNode->appendChild($child);
36✔
439
        }
440

441
        foreach ($node->attributes ?? [] as $attrNode) {
36✔
442
            /** @noinspection UnusedFunctionResultInspection */
443
            $newNode->setAttribute($attrNode->nodeName, $attrNode->value);
2✔
444
        }
445

446
        if ($node->parentNode) {
36✔
447
            /** @noinspection UnusedFunctionResultInspection */
448
            $node->parentNode->replaceChild($newNode, $node);
36✔
449
        }
450

451
        return $newNode;
36✔
452
    }
453

454
    /**
455
     * Returns children of node.
456
     *
457
     * @param int $idx
458
     *
459
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface|null
460
     */
461
    public function childNodes(int $idx = -1)
462
    {
463
        $nodeList = $this->getIterator();
4✔
464

465
        if ($idx === -1) {
4✔
466
            return $nodeList;
4✔
467
        }
468

469
        return $nodeList[$idx] ?? null;
4✔
470
    }
471

472
    /**
473
     * Find nodes with a CSS selector.
474
     *
475
     * @param string $selector
476
     *
477
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
478
     */
479
    public function findMulti(string $selector): SimpleHtmlDomNodeInterface
480
    {
481
        /** @var SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface> $return */
482
        $return = $this->find($selector, null);
6✔
483

484
        return $return;
6✔
485
    }
486

487
    /**
488
     * Find nodes with a CSS selector or false, if no element is found.
489
     *
490
     * @param string $selector
491
     *
492
     * @return false|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
493
     */
494
    public function findMultiOrFalse(string $selector)
495
    {
496
        /** @var SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface> $return */
497
        $return = $this->find($selector, null);
4✔
498

499
        if ($return instanceof SimpleHtmlDomNodeBlank) {
4✔
500
            return false;
2✔
501
        }
502

503
        return $return;
2✔
504
    }
505

506
    /**
507
     * Find nodes with a CSS selector or null, if no element is found.
508
     *
509
     * @param string $selector
510
     *
511
     * @return null|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
512
     */
513
    public function findMultiOrNull(string $selector)
514
    {
515
        /** @var SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface> $return */
516
        $return = $this->find($selector, null);
6✔
517

518
        if ($return instanceof SimpleHtmlDomNodeBlank) {
6✔
519
            return null;
2✔
520
        }
521

522
        return $return;
4✔
523
    }
524

525
    /**
526
     * Find one node with a CSS selector.
527
     *
528
     * @param string $selector
529
     *
530
     * @return SimpleHtmlDomInterface
531
     */
532
    public function findOne(string $selector): SimpleHtmlDomInterface
533
    {
534
        /** @var SimpleHtmlDomInterface $return */
535
        $return = $this->find($selector, 0);
22✔
536

537
        return $return;
22✔
538
    }
539

540
    /**
541
     * Find one node with a CSS selector or false, if no element is found.
542
     *
543
     * @param string $selector
544
     *
545
     * @return false|SimpleHtmlDomInterface
546
     */
547
    public function findOneOrFalse(string $selector)
548
    {
549
        /** @var SimpleHtmlDomInterface $return */
550
        $return = $this->find($selector, 0);
4✔
551

552
        if ($return instanceof SimpleHtmlDomBlank) {
4✔
553
            return false;
2✔
554
        }
555

556
        return $return;
2✔
557
    }
558

559
    /**
560
     * Find one node with a CSS selector or null, if no element is found.
561
     *
562
     * @param string $selector
563
     *
564
     * @return null|SimpleHtmlDomInterface
565
     */
566
    public function findOneOrNull(string $selector)
567
    {
568
        /** @var SimpleHtmlDomInterface $return */
569
        $return = $this->find($selector, 0);
6✔
570

571
        if ($return instanceof SimpleHtmlDomBlank) {
6✔
572
            return null;
4✔
573
        }
574

575
        return $return;
6✔
576
    }
577

578
    /**
579
     * Returns the first child of node.
580
     *
581
     * @return SimpleHtmlDomInterface|null
582
     */
583
    public function firstChild()
584
    {
585
        /** @var \DOMNode|null $node */
586
        $node = $this->node->firstChild;
8✔
587

588
        if ($node === null) {
8✔
589
            return null;
2✔
590
        }
591

592
        return $this->createWrapper($node);
8✔
593
    }
594

595
    /**
596
     * Return elements by ".class".
597
     *
598
     * @param string $class
599
     *
600
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
601
     */
602
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
603
    {
604
        return $this->findMulti(".{$class}");
2✔
605
    }
606

607
    /**
608
     * Return element by #id.
609
     *
610
     * @param string $id
611
     *
612
     * @return SimpleHtmlDomInterface
613
     */
614
    public function getElementById(string $id): SimpleHtmlDomInterface
615
    {
616
        return $this->findOne("#{$id}");
6✔
617
    }
618

619
    /**
620
     * Return element by tag name.
621
     *
622
     * @param string $name
623
     *
624
     * @return SimpleHtmlDomInterface
625
     */
626
    public function getElementByTagName(string $name): SimpleHtmlDomInterface
627
    {
628
        if ($this->node instanceof \DOMElement) {
6✔
629
            $node = $this->node->getElementsByTagName($name)->item(0);
6✔
630
        } else {
631
            $node = null;
2✔
632
        }
633

634
        if ($node === null) {
6✔
635
            return new SimpleHtmlDomBlank();
2✔
636
        }
637

638
        return $this->createWrapper($node);
6✔
639
    }
640

641
    /**
642
     * Returns elements by "#id".
643
     *
644
     * @param string   $id
645
     * @param int|null $idx
646
     *
647
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
648
     */
649
    public function getElementsById(string $id, $idx = null)
650
    {
651
        return $this->find("#{$id}", $idx);
2✔
652
    }
653

654
    /**
655
     * Returns elements by tag name.
656
     *
657
     * @param string   $name
658
     * @param int|null $idx
659
     *
660
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
661
     */
662
    public function getElementsByTagName(string $name, $idx = null)
663
    {
664
        if ($this->node instanceof \DOMElement) {
10✔
665
            $nodesList = $this->node->getElementsByTagName($name);
8✔
666
        } else {
667
            $nodesList = false;
2✔
668
        }
669

670
        return $this->createFindResultFromNodeList($nodesList, $idx);
10✔
671
    }
672

673
    /**
674
     * Create a new "HtmlDomParser"-object from the current context.
675
     *
676
     * @return HtmlDomParser
677
     */
678
    public function getHtmlDomParser(): HtmlDomParser
679
    {
680
        return new HtmlDomParser($this);
220✔
681
    }
682

683
    /**
684
     * @return \DOMNode
685
     */
686
    public function getNode(): \DOMNode
687
    {
688
        return $this->node;
232✔
689
    }
690

691
    /**
692
     * Nodes can get partially destroyed in which they're still an
693
     * actual DOM node (such as \DOMElement) but almost their entire
694
     * body is gone, including the `nodeType` attribute.
695
     *
696
     * @return bool true if node has been destroyed
697
     */
698
    public function isRemoved(): bool
699
    {
700
        return !isset($this->node->nodeType);
2✔
701
    }
702

703
    /**
704
     * Returns the last child of node.
705
     *
706
     * @return SimpleHtmlDomInterface|null
707
     */
708
    public function lastChild()
709
    {
710
        /** @var \DOMNode|null $node */
711
        $node = $this->node->lastChild;
8✔
712

713
        if ($node === null) {
8✔
714
            return null;
2✔
715
        }
716

717
        return $this->createWrapper($node);
8✔
718
    }
719

720
    /**
721
     * Returns the next sibling of node.
722
     *
723
     * @return SimpleHtmlDomInterface|null
724
     */
725
    public function nextSibling()
726
    {
727
        /** @var \DOMNode|null $node */
728
        $node = $this->node->nextSibling;
2✔
729

730
        if ($node === null) {
2✔
731
            return null;
2✔
732
        }
733

734
        return $this->createWrapper($node);
2✔
735
    }
736

737
    /**
738
     * Returns the next sibling of node.
739
     *
740
     * @return SimpleHtmlDomInterface|null
741
     */
742
    public function nextNonWhitespaceSibling()
743
    {
744
        /** @var \DOMNode|null $node */
745
        $node = $this->node->nextSibling;
4✔
746

747
        while ($node && !\trim($node->textContent)) {
4✔
748
            /** @var \DOMNode|null $node */
749
            $node = $node->nextSibling;
2✔
750
        }
751

752
        if ($node === null) {
4✔
753
            return null;
2✔
754
        }
755

756
        return $this->createWrapper($node);
2✔
757
    }
758

759
    /**
760
     * Returns the parent of node.
761
     *
762
     * @return SimpleHtmlDomInterface|null
763
     */
764
    public function parentNode(): ?SimpleHtmlDomInterface
765
    {
766
        if (
767
            ($node = $this->node->parentNode)
8✔
768
            &&
769
            !$node instanceof \DOMDocument
8✔
770
        ) {
771
            return $this->createWrapper($node);
4✔
772
        }
773

774
        return null;
4✔
775
    }
776

777
    /**
778
     * Returns the previous sibling of node.
779
     *
780
     * @return SimpleHtmlDomInterface|null
781
     */
782
    public function previousSibling()
783
    {
784
        /** @var \DOMNode|null $node */
785
        $node = $this->node->previousSibling;
2✔
786

787
        if ($node === null) {
2✔
788
            return null;
2✔
789
        }
790

791
        return $this->createWrapper($node);
2✔
792
    }
793

794
    /**
795
     * Returns the previous sibling of node.
796
     *
797
     * @return SimpleHtmlDomInterface|null
798
     */
799
    public function previousNonWhitespaceSibling()
800
    {
801
        /** @var \DOMNode|null $node */
802
        $node = $this->node->previousSibling;
4✔
803

804
        while ($node && !\trim($node->textContent)) {
4✔
805
            /** @var \DOMNode|null $node */
806
            $node = $node->previousSibling;
2✔
807
        }
808

809
        if ($node === null) {
4✔
810
            return null;
2✔
811
        }
812

813
        return $this->createWrapper($node);
2✔
814
    }
815

816
    /**
817
     * @param string|string[]|null $value <p>
818
     *                                    null === get the current input value
819
     *                                    text === set a new input value
820
     *                                    </p>
821
     *
822
     * @return string|string[]|null
823
     */
824
    public function val($value = null)
825
    {
826
        if ($value === null) {
10✔
827
            if (
828
                $this->tag === 'input'
8✔
829
                &&
830
                (
831
                    $this->getAttribute('type') === 'hidden'
8✔
832
                    ||
8✔
833
                    $this->getAttribute('type') === 'text'
8✔
834
                    ||
8✔
835
                    !$this->hasAttribute('type')
8✔
836
                )
837
            ) {
838
                return $this->getAttribute('value');
4✔
839
            }
840

841
            if (
842
                $this->hasAttribute('checked')
8✔
843
                &&
844
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
8✔
845
            ) {
846
                return $this->getAttribute('value');
4✔
847
            }
848

849
            if ($this->node->nodeName === 'select') {
8✔
850
                $valuesFromDom = [];
6✔
851
                $options = $this->getElementsByTagName('option');
6✔
852
                if ($options instanceof SimpleHtmlDomNode) {
6✔
853
                    foreach ($options as $option) {
6✔
854
                        if ($option->hasAttribute('selected')) {
6✔
855
                            $valuesFromDom[] = (string) $option->getAttribute('value');
4✔
856
                        }
857
                    }
858
                }
859

860
                if (\count($valuesFromDom) === 0) {
6✔
861
                    return null;
2✔
862
                }
863

864
                return $valuesFromDom;
4✔
865
            }
866

867
            if ($this->node->nodeName === 'textarea') {
4✔
868
                return $this->node->nodeValue;
4✔
869
            }
870
        } else {
871
            /** @noinspection NestedPositiveIfStatementsInspection */
872
            if (\in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)) {
8✔
873
                $selectedValues = \is_array($value) ? $value : [$value];
6✔
874
                if (\in_array($this->getAttribute('value'), $selectedValues, true)) {
6✔
875
                    /** @noinspection UnusedFunctionResultInspection */
876
                    $this->setAttribute('checked', 'checked');
6✔
877
                } else {
878
                    /** @noinspection UnusedFunctionResultInspection */
879
                    $this->removeAttribute('checked');
4✔
880
                }
881
            } elseif ($this->node instanceof \DOMElement && $this->node->nodeName === 'select') {
6✔
882
                $selectedValues = \is_array($value) ? $value : [$value];
4✔
883
                foreach ($this->node->getElementsByTagName('option') as $option) {
4✔
884
                    /** @var \DOMElement $option */
885
                    if (\in_array($option->getAttribute('value'), $selectedValues, true)) {
4✔
886
                        /** @noinspection UnusedFunctionResultInspection */
887
                        $option->setAttribute('selected', 'selected');
4✔
888
                    } else {
889
                        $option->removeAttribute('selected');
4✔
890
                    }
891
                }
892
            } elseif ($this->node->nodeName === 'input' && \is_string($value)) {
4✔
893
                // Set value for input elements
894
                /** @noinspection UnusedFunctionResultInspection */
895
                $this->setAttribute('value', $value);
4✔
896
            } elseif ($this->node->nodeName === 'textarea' && \is_string($value)) {
4✔
897
                $this->node->nodeValue = $value;
4✔
898
            }
899
        }
900

901
        return null;
8✔
902
    }
903

904
    /**
905
     * @param HtmlDomParser $newDocument
906
     * @param bool          $removeExtraHeadTag
907
     *
908
     * @return HtmlDomParser
909
     */
910
    protected function cleanHtmlWrapper(
911
        HtmlDomParser $newDocument,
912
        $removeExtraHeadTag = false
913
    ): HtmlDomParser {
914
        if (
915
            $newDocument->getIsDOMDocumentCreatedWithoutHtml()
52✔
916
            ||
917
            $newDocument->getIsDOMDocumentCreatedWithoutHtmlWrapper()
52✔
918
        ) {
919
            // Remove doc-type node.
920
            if ($newDocument->getDocument()->doctype !== null) {
52✔
921
                $newDocument->getDocument()->doctype->parentNode->removeChild($newDocument->getDocument()->doctype);
2✔
922
            }
923

924
            // Replace html element, preserving child nodes -> but keep the html wrapper, otherwise we got other problems ...
925
            // so we replace it with "<simpleHtmlDomHtml>" and delete this at the ending.
926
            $item = $newDocument->getDocument()->getElementsByTagName('html')->item(0);
52✔
927
            if ($item !== null) {
52✔
928
                /** @noinspection UnusedFunctionResultInspection */
929
                $this->changeElementName($item, 'simpleHtmlDomHtml');
32✔
930
            }
931

932
            if ($newDocument->getIsDOMDocumentCreatedWithoutPTagWrapper()) {
52✔
933
                // Remove <p>-element, preserving child nodes.
934
                $pElement = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
38✔
935
                if ($pElement instanceof \DOMElement) {
38✔
UNCOV
936
                    $fragment = $newDocument->getDocument()->createDocumentFragment();
×
937

UNCOV
938
                    while ($pElement->childNodes->length > 0) {
×
UNCOV
939
                        $tmpNode = $pElement->childNodes->item(0);
×
UNCOV
940
                        if ($tmpNode !== null) {
×
941
                            /** @noinspection UnusedFunctionResultInspection */
UNCOV
942
                            $fragment->appendChild($tmpNode);
×
943
                        }
944
                    }
945

UNCOV
946
                    if ($pElement->parentNode !== null) {
×
947
                        $pElement->parentNode->replaceChild($fragment, $pElement);
×
948
                    }
949
                }
950
            }
951

952
            // Remove <body>-element, preserving child nodes.
953
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
52✔
954
            if ($body instanceof \DOMElement) {
52✔
955
                $fragment = $newDocument->getDocument()->createDocumentFragment();
22✔
956

957
                while ($body->childNodes->length > 0) {
22✔
958
                    $tmpNode = $body->childNodes->item(0);
22✔
959
                    if ($tmpNode !== null) {
22✔
960
                        /** @noinspection UnusedFunctionResultInspection */
961
                        $fragment->appendChild($tmpNode);
22✔
962
                    }
963
                }
964

965
                if ($body->parentNode !== null) {
22✔
966
                    $body->parentNode->replaceChild($fragment, $body);
22✔
967
                }
968
            }
969
        }
970

971
        // Remove head element, preserving child nodes.
972
        if (
973
            $removeExtraHeadTag
52✔
974
            &&
975
            $this->node->parentNode instanceof \DOMElement
52✔
976
            &&
977
            $newDocument->getIsDOMDocumentCreatedWithoutHeadWrapper()
52✔
978
        ) {
979
            $html = $this->node->parentNode->getElementsByTagName('head')[0] ?? null;
20✔
980

981
            if (
982
                $html !== null
20✔
983
                &&
984
                $this->node->parentNode->ownerDocument
20✔
985
            ) {
986
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
2✔
987

988
                /** @var \DOMNode $html */
989
                while ($html->childNodes->length > 0) {
2✔
990
                    $tmpNode = $html->childNodes->item(0);
2✔
991
                    if ($tmpNode !== null) {
2✔
992
                        /** @noinspection UnusedFunctionResultInspection */
993
                        $fragment->appendChild($tmpNode);
2✔
994
                    }
995
                }
996

997
                $html->parentNode->replaceChild($fragment, $html);
2✔
998
            }
999
        }
1000

1001
        return $newDocument;
52✔
1002
    }
1003

1004
    /**
1005
     * Retrieve an external iterator.
1006
     *
1007
     * @see  http://php.net/manual/en/iteratoraggregate.getiterator.php
1008
     *
1009
     * @return SimpleHtmlDomNode
1010
     *                           <p>
1011
     *                              An instance of an object implementing <b>Iterator</b> or
1012
     *                              <b>Traversable</b>
1013
     *                           </p>
1014
     */
1015
    public function getIterator(): SimpleHtmlDomNodeInterface
1016
    {
1017
        $elements = new SimpleHtmlDomNode();
6✔
1018
        if ($this->node->hasChildNodes()) {
6✔
1019
            foreach ($this->node->childNodes as $node) {
6✔
1020
                $elements[] = $this->createWrapper($node);
6✔
1021
            }
1022
        }
1023

1024
        return $elements;
6✔
1025
    }
1026

1027
    /**
1028
     * @param \DOMNode $node
1029
     *
1030
     * @return static
1031
     */
1032
    private function createWrapper(\DOMNode $node)
1033
    {
1034
        return new static($node, $this->queryHtmlDomParser);
34✔
1035
    }
1036

1037
    /**
1038
     * @param \DOMNodeList<\DOMNode>|false $nodesList
1039
     * @param int|null                     $idx
1040
     *
1041
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
1042
     */
1043
    private function createFindResultFromNodeList($nodesList, $idx)
1044
    {
1045
        $elements = new SimpleHtmlDomNode();
10✔
1046

1047
        if ($nodesList) {
10✔
1048
            foreach ($nodesList as $node) {
10✔
1049
                $elements[] = $this->createWrapper($node);
10✔
1050
            }
1051
        }
1052

1053
        if ($idx === null) {
10✔
1054
            if (\count($elements) === 0) {
10✔
1055
                return new SimpleHtmlDomNodeBlank();
4✔
1056
            }
1057

1058
            return $elements;
10✔
1059
        }
1060

1061
        if ($idx < 0) {
6✔
1062
            $idx = \count($elements) + $idx;
4✔
1063
        }
1064

1065
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
6✔
1066
    }
1067

1068
    /**
1069
     * Get dom node's inner html.
1070
     *
1071
     * @param bool $multiDecodeNewHtmlEntity
1072
     *
1073
     * @return string
1074
     */
1075
    public function innerXml(bool $multiDecodeNewHtmlEntity = false): string
1076
    {
1077
        return $this->getHtmlDomParser()->innerXml($multiDecodeNewHtmlEntity);
2✔
1078
    }
1079

1080
    /**
1081
     * Normalize the given input for comparison.
1082
     *
1083
     * @param HtmlDomParser|string $input
1084
     *
1085
     * @return string
1086
     */
1087
    private function normalizeStringForComparison($input): string
1088
    {
1089
        if ($input instanceof HtmlDomParser) {
48✔
1090
            $string = $input->html(false, true);
48✔
1091

1092
            if ($input->getIsDOMDocumentCreatedWithoutHeadWrapper()) {
48✔
1093
                /** @noinspection HtmlRequiredTitleElement */
1094
                $string = \str_replace(['<head>', '</head>'], '', $string);
48✔
1095
            }
1096
        } else {
1097
            // Also restore any broken-HTML placeholders that may already be
1098
            // present in the raw string (e.g. when innerhtmlKeep concatenates
1099
            // the current innerHTML — which still contains placeholders — with
1100
            // new content before passing the combined string back as the new
1101
            // innerHTML).  This keeps both sides of the comparison at the same
1102
            // level of substitution.
1103
            $string = HtmlDomParser::putReplacedBackToPreserveHtmlEntities((string) $input, true);
48✔
1104
        }
1105

1106
        return
48✔
1107
            \urlencode(
48✔
1108
                \urldecode(
48✔
1109
                    \trim(
48✔
1110
                        \str_replace(
48✔
1111
                            [
48✔
1112
                                ' ',
48✔
1113
                                "\n",
48✔
1114
                                "\r",
48✔
1115
                                '/>',
48✔
1116
                            ],
48✔
1117
                            [
48✔
1118
                                '',
48✔
1119
                                '',
48✔
1120
                                '',
48✔
1121
                                '>',
48✔
1122
                            ],
48✔
1123
                            \strtolower($string)
48✔
1124
                        )
48✔
1125
                    )
48✔
1126
                )
48✔
1127
            );
48✔
1128
    }
1129

1130
    /**
1131
     * Remove this node from the DOM.
1132
     *
1133
     * @return void
1134
     */
1135
    public function delete()
1136
    {
1137
        $this->outertext = '';
24✔
1138
    }
1139

1140
    /**
1141
     * Remove this node from the DOM (alias for delete).
1142
     *
1143
     * @return void
1144
     */
1145
    public function remove()
1146
    {
1147
        $this->delete();
2✔
1148
    }
1149
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc