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

voku / simple_html_dom / 24632839975

19 Apr 2026 03:42PM UTC coverage: 77.11% (+6.3%) from 70.769%
24632839975

push

github

web-flow
Merge pull request #135 from voku/copilot/fix-html-parsing-newline-issue

Preserve node HTML formatting when serializing nested elements

4 of 24 new or added lines in 1 file covered. (16.67%)

51 existing lines in 6 files now uncovered.

1654 of 2145 relevant lines covered (77.11%)

262.05 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace voku\helper;
6

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

21
    /**
22
     * @param \DOMElement|\DOMNode $node
23
     * @param HtmlDomParser|null   $queryHtmlDomParser
24
     */
25
    public function __construct(\DOMNode $node, ?HtmlDomParser $queryHtmlDomParser = null)
26
    {
27
        $this->node = $node;
1,603✔
28
        $this->queryHtmlDomParser = $queryHtmlDomParser;
1,603✔
29
    }
30

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

43
        if (isset(self::$functionAliases[$name])) {
140✔
44
            return \call_user_func_array([$this, self::$functionAliases[$name]], $arguments);
140✔
45
        }
46

47
        throw new \BadMethodCallException('Method does not exist');
×
48
    }
49

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

62
        if (!$document instanceof \DOMDocument) {
350✔
63
            if ($idx === null) {
×
64
                return new SimpleHtmlDomNodeBlank();
×
65
            }
66

67
            return new SimpleHtmlDomBlank();
×
68
        }
69

70
        if ($this->queryHtmlDomParser !== null) {
350✔
71
            return $this->queryHtmlDomParser->findInNodeContext($selector, $this->node, $idx);
252✔
72
        }
73

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

84
    public function getTag(): string
85
    {
86
        return $this->tag;
7✔
87
    }
88

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

106
            return $attributes;
49✔
107
        }
108

109
        return null;
7✔
110
    }
111

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

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

135
        return '';
×
136
    }
137

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

151
        return $this->node->hasAttribute($name);
14✔
152
    }
153

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

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

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

192
        return $this;
21✔
193
    }
194

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

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

223
            $tmpDomString = $this->normalizeStringForComparison($newDocument);
70✔
224
            $tmpStr = $this->normalizeStringForComparison($string);
70✔
225

226
            if ($tmpDomString !== $tmpStr) {
70✔
227
                throw new \RuntimeException(
×
228
                    'Not valid HTML fragment!' . "\n" .
×
229
                    $tmpDomString . "\n" .
×
230
                    $tmpStr
×
231
                );
×
232
            }
233
        }
234

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

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

261
        return $this;
77✔
262
    }
263

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

279
            return $this;
119✔
280
        }
281

282
        $newDocument = new HtmlDomParser($string);
84✔
283

284
        $tmpDomOuterTextString = $this->normalizeStringForComparison($newDocument);
84✔
285
        $tmpStr = $this->normalizeStringForComparison($string);
84✔
286

287
        if ($tmpDomOuterTextString !== $tmpStr) {
84✔
288
            throw new \RuntimeException(
×
289
                'Not valid HTML fragment!' . "\n"
×
290
                . $tmpDomOuterTextString . "\n" .
×
291
                $tmpStr
×
292
            );
×
293
        }
294

295
        $newDocument = $this->cleanHtmlWrapper($newDocument, true);
84✔
296
        $ownerDocument = $this->node->ownerDocument;
84✔
297
        if (
298
            $ownerDocument === null
84✔
299
            ||
300
            $newDocument->getDocument()->documentElement === null
84✔
301
        ) {
302
            return $this;
×
303
        }
304

305
        $newNode = $ownerDocument->importNode($newDocument->getDocument()->documentElement, true);
84✔
306

307
        if ($this->node->parentNode !== null) {
84✔
308
            $this->node->parentNode->replaceChild($newNode, $this->node);
84✔
309
        }
310
        $this->node = $newNode;
84✔
311

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

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

338
        return $this;
84✔
339
    }
340

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

356
            return $this;
7✔
357
        }
358

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

369
        return $this;
7✔
370
    }
371

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

398
        return $this;
140✔
399
    }
400

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

412
        return $this->getHtmlDomParser()->fixHtmlOutput($this->node->textContent);
231✔
413
    }
414

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

432
        $newNode = $ownerDocument->createElement($name);
82✔
433

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

439
        foreach ($node->attributes ?? [] as $attrName => $attrNode) {
82✔
440
            /** @noinspection UnusedFunctionResultInspection */
441
            $newNode->setAttribute($attrName, $attrNode);
×
442
        }
443

444
        if ($newNode->ownerDocument) {
82✔
445
            /** @noinspection UnusedFunctionResultInspection */
446
            $newNode->ownerDocument->replaceChild($newNode, $node);
82✔
447
        }
448

449
        return $newNode;
82✔
450
    }
451

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

463
        if ($idx === -1) {
14✔
464
            return $nodeList;
14✔
465
        }
466

467
        return $nodeList[$idx] ?? null;
14✔
468
    }
469

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

482
    /**
483
     * Find nodes with a CSS selector or false, if no element is found.
484
     *
485
     * @param string $selector
486
     *
487
     * @return false|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
488
     */
489
    public function findMultiOrFalse(string $selector)
490
    {
491
        $return = $this->find($selector, null);
14✔
492

493
        if ($return instanceof SimpleHtmlDomNodeBlank) {
14✔
494
            return false;
7✔
495
        }
496

497
        return $return;
7✔
498
    }
499

500
    /**
501
     * Find nodes with a CSS selector or null, if no element is found.
502
     *
503
     * @param string $selector
504
     *
505
     * @return null|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
506
     */
507
    public function findMultiOrNull(string $selector)
508
    {
509
        $return = $this->find($selector, null);
14✔
510

511
        if ($return instanceof SimpleHtmlDomNodeBlank) {
14✔
512
            return null;
×
513
        }
514

515
        return $return;
14✔
516
    }
517

518
    /**
519
     * Find one node with a CSS selector.
520
     *
521
     * @param string $selector
522
     *
523
     * @return SimpleHtmlDomInterface
524
     */
525
    public function findOne(string $selector): SimpleHtmlDomInterface
526
    {
527
        return $this->find($selector, 0);
56✔
528
    }
529

530
    /**
531
     * Find one node with a CSS selector or false, if no element is found.
532
     *
533
     * @param string $selector
534
     *
535
     * @return false|SimpleHtmlDomInterface
536
     */
537
    public function findOneOrFalse(string $selector)
538
    {
539
        $return = $this->find($selector, 0);
14✔
540

541
        if ($return instanceof SimpleHtmlDomBlank) {
14✔
542
            return false;
7✔
543
        }
544

545
        return $return;
7✔
546
    }
547

548
    /**
549
     * Find one node with a CSS selector or null, if no element is found.
550
     *
551
     * @param string $selector
552
     *
553
     * @return null|SimpleHtmlDomInterface
554
     */
555
    public function findOneOrNull(string $selector)
556
    {
557
        $return = $this->find($selector, 0);
21✔
558

559
        if ($return instanceof SimpleHtmlDomBlank) {
21✔
560
            return null;
12✔
561
        }
562

563
        return $return;
21✔
564
    }
565

566
    /**
567
     * Returns the first child of node.
568
     *
569
     * @return SimpleHtmlDomInterface|null
570
     */
571
    public function firstChild()
572
    {
573
        /** @var \DOMNode|null $node */
574
        $node = $this->node->firstChild;
28✔
575

576
        if ($node === null) {
28✔
577
            return null;
7✔
578
        }
579

580
        return $this->createWrapper($node);
28✔
581
    }
582

583
    /**
584
     * Return elements by ".class".
585
     *
586
     * @param string $class
587
     *
588
     * @return SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
589
     */
590
    public function getElementByClass(string $class): SimpleHtmlDomNodeInterface
591
    {
592
        return $this->findMulti(".{$class}");
×
593
    }
594

595
    /**
596
     * Return element by #id.
597
     *
598
     * @param string $id
599
     *
600
     * @return SimpleHtmlDomInterface
601
     */
602
    public function getElementById(string $id): SimpleHtmlDomInterface
603
    {
604
        return $this->findOne("#{$id}");
7✔
605
    }
606

607
    /**
608
     * Return element by tag name.
609
     *
610
     * @param string $name
611
     *
612
     * @return SimpleHtmlDomInterface
613
     */
614
    public function getElementByTagName(string $name): SimpleHtmlDomInterface
615
    {
616
        if ($this->node instanceof \DOMElement) {
7✔
617
            $node = $this->node->getElementsByTagName($name)->item(0);
7✔
618
        } else {
619
            $node = null;
×
620
        }
621

622
        if ($node === null) {
7✔
623
            return new SimpleHtmlDomBlank();
×
624
        }
625

626
        return $this->createWrapper($node);
7✔
627
    }
628

629
    /**
630
     * Returns elements by "#id".
631
     *
632
     * @param string   $id
633
     * @param int|null $idx
634
     *
635
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
636
     */
637
    public function getElementsById(string $id, $idx = null)
638
    {
639
        return $this->find("#{$id}", $idx);
×
640
    }
641

642
    /**
643
     * Returns elements by tag name.
644
     *
645
     * @param string   $name
646
     * @param int|null $idx
647
     *
648
     * @return SimpleHtmlDomInterface|SimpleHtmlDomInterface[]|SimpleHtmlDomNodeInterface<SimpleHtmlDomInterface>
649
     */
650
    public function getElementsByTagName(string $name, $idx = null)
651
    {
652
        if ($this->node instanceof \DOMElement) {
7✔
653
            $nodesList = $this->node->getElementsByTagName($name);
7✔
654
        } else {
655
            $nodesList = [];
×
656
        }
657

658
        $elements = new SimpleHtmlDomNode();
7✔
659

660
        foreach ($nodesList as $node) {
7✔
661
            $elements[] = $this->createWrapper($node);
7✔
662
        }
663

664
        // return all elements
665
        if ($idx === null) {
7✔
666
            if (\count($elements) === 0) {
7✔
667
                return new SimpleHtmlDomNodeBlank();
×
668
            }
669

670
            return $elements;
7✔
671
        }
672

673
        // handle negative values
674
        if ($idx < 0) {
×
675
            $idx = \count($elements) + $idx;
×
676
        }
677

678
        // return one element
679
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
×
680
    }
681

682
    /**
683
     * Create a new "HtmlDomParser"-object from the current context.
684
     *
685
     * @return HtmlDomParser
686
     */
687
    public function getHtmlDomParser(): HtmlDomParser
688
    {
689
        return new HtmlDomParser($this);
721✔
690
    }
691

692
    /**
693
     * @return \DOMNode
694
     */
695
    public function getNode(): \DOMNode
696
    {
697
        return $this->node;
756✔
698
    }
699

700
    /**
701
     * Nodes can get partially destroyed in which they're still an
702
     * actual DOM node (such as \DOMElement) but almost their entire
703
     * body is gone, including the `nodeType` attribute.
704
     *
705
     * @return bool true if node has been destroyed
706
     */
707
    public function isRemoved(): bool
708
    {
709
        return !isset($this->node->nodeType);
×
710
    }
711

712
    /**
713
     * Returns the last child of node.
714
     *
715
     * @return SimpleHtmlDomInterface|null
716
     */
717
    public function lastChild()
718
    {
719
        /** @var \DOMNode|null $node */
720
        $node = $this->node->lastChild;
28✔
721

722
        if ($node === null) {
28✔
723
            return null;
7✔
724
        }
725

726
        return $this->createWrapper($node);
28✔
727
    }
728

729
    /**
730
     * Returns the next sibling of node.
731
     *
732
     * @return SimpleHtmlDomInterface|null
733
     */
734
    public function nextSibling()
735
    {
736
        /** @var \DOMNode|null $node */
737
        $node = $this->node->nextSibling;
7✔
738

739
        if ($node === null) {
7✔
740
            return null;
7✔
741
        }
742

743
        return $this->createWrapper($node);
7✔
744
    }
745

746
    /**
747
     * Returns the next sibling of node.
748
     *
749
     * @return SimpleHtmlDomInterface|null
750
     */
751
    public function nextNonWhitespaceSibling()
752
    {
753
        /** @var \DOMNode|null $node */
754
        $node = $this->node->nextSibling;
7✔
755

756
        while ($node && !\trim($node->textContent)) {
7✔
757
            /** @var \DOMNode|null $node */
758
            $node = $node->nextSibling;
7✔
759
        }
760

761
        if ($node === null) {
7✔
762
            return null;
×
763
        }
764

765
        return $this->createWrapper($node);
7✔
766
    }
767

768
    /**
769
     * Returns the parent of node.
770
     *
771
     * @return SimpleHtmlDomInterface|null
772
     */
773
    public function parentNode(): ?SimpleHtmlDomInterface
774
    {
775
        if ($node = $this->node->parentNode) {
14✔
776
            return $this->createWrapper($node);
14✔
777
        }
778

779
        return null;
×
780
    }
781

782
    /**
783
     * Returns the previous sibling of node.
784
     *
785
     * @return SimpleHtmlDomInterface|null
786
     */
787
    public function previousSibling()
788
    {
789
        /** @var \DOMNode|null $node */
790
        $node = $this->node->previousSibling;
7✔
791

792
        if ($node === null) {
7✔
793
            return null;
7✔
794
        }
795

796
        return $this->createWrapper($node);
7✔
797
    }
798

799
    /**
800
     * Returns the previous sibling of node.
801
     *
802
     * @return SimpleHtmlDomInterface|null
803
     */
804
    public function previousNonWhitespaceSibling()
805
    {
806
        /** @var \DOMNode|null $node */
807
        $node = $this->node->previousSibling;
7✔
808

809
        while ($node && !\trim($node->textContent)) {
7✔
810
            /** @var \DOMNode|null $node */
811
            $node = $node->previousSibling;
7✔
812
        }
813

814
        if ($node === null) {
7✔
815
            return null;
×
816
        }
817

818
        return $this->createWrapper($node);
7✔
819
    }
820

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

846
            if (
847
                $this->hasAttribute('checked')
7✔
848
                &&
849
                \in_array($this->getAttribute('type'), ['checkbox', 'radio'], true)
7✔
850
            ) {
851
                return $this->getAttribute('value');
7✔
852
            }
853

854
            if ($this->node->nodeName === 'select') {
7✔
855
                $valuesFromDom = [];
×
856
                $options = $this->getElementsByTagName('option');
×
857
                if ($options instanceof SimpleHtmlDomNode) {
×
858
                    foreach ($options as $option) {
×
859
                        if ($this->hasAttribute('checked')) {
×
860
                            $valuesFromDom[] = (string) $option->getAttribute('value');
×
861
                        }
862
                    }
863
                }
864

865
                if (\count($valuesFromDom) === 0) {
×
866
                    return null;
×
867
                }
868

869
                return $valuesFromDom;
×
870
            }
871

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

904
        return null;
7✔
905
    }
906

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

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

935
            if ($newDocument->getIsDOMDocumentCreatedWithoutPTagWrapper()) {
154✔
936
                // Remove <p>-element, preserving child nodes.
937
                $pElement = $newDocument->getDocument()->getElementsByTagName('p')->item(0);
105✔
938
                if ($pElement instanceof \DOMElement) {
105✔
939
                    $fragment = $newDocument->getDocument()->createDocumentFragment();
×
940

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

949
                    if ($pElement->parentNode !== null) {
×
950
                        $pElement->parentNode->replaceChild($fragment, $pElement);
×
951
                    }
952
                }
953
            }
954

955
            // Remove <body>-element, preserving child nodes.
956
            $body = $newDocument->getDocument()->getElementsByTagName('body')->item(0);
154✔
957
            if ($body instanceof \DOMElement) {
154✔
958
                $fragment = $newDocument->getDocument()->createDocumentFragment();
70✔
959

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

968
                if ($body->parentNode !== null) {
70✔
969
                    $body->parentNode->replaceChild($fragment, $body);
70✔
970
                }
971
            }
972
        }
973

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

984
            if (
985
                $html !== null
56✔
986
                &&
987
                $this->node->parentNode->ownerDocument
56✔
988
            ) {
989
                $fragment = $this->node->parentNode->ownerDocument->createDocumentFragment();
×
990

991
                /** @var \DOMNode $html */
992
                while ($html->childNodes->length > 0) {
×
993
                    $tmpNode = $html->childNodes->item(0);
×
994
                    if ($tmpNode !== null) {
×
995
                        /** @noinspection UnusedFunctionResultInspection */
996
                        $fragment->appendChild($tmpNode);
×
997
                    }
998
                }
999

1000
                $html->parentNode->replaceChild($fragment, $html);
×
1001
            }
1002
        }
1003

1004
        return $newDocument;
154✔
1005
    }
1006

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

1027
        return $elements;
21✔
1028
    }
1029

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

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

1050
        if ($nodesList) {
×
1051
            foreach ($nodesList as $node) {
×
1052
                $elements[] = $this->createWrapper($node);
×
1053
            }
1054
        }
1055

1056
        if ($idx === null) {
×
1057
            if (\count($elements) === 0) {
×
1058
                return new SimpleHtmlDomNodeBlank();
×
1059
            }
1060

1061
            return $elements;
×
1062
        }
1063

1064
        if ($idx < 0) {
×
1065
            $idx = \count($elements) + $idx;
×
1066
        }
1067

1068
        return $elements[$idx] ?? new SimpleHtmlDomBlank();
×
1069
    }
1070

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

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

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

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

1133
    /**
1134
     * Delete
1135
     *
1136
     * @return void
1137
     */
1138
    public function delete()
1139
    {
1140
        $this->outertext = '';
84✔
1141
    }
1142

1143
    /**
1144
     * Remove this node from the DOM (alias for delete).
1145
     *
1146
     * @return mixed
1147
     */
1148
    public function remove()
1149
    {
1150
        return $this->delete();
7✔
1151
    }
1152
}
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