Issues (14)

src/HtmlPageCrawler.php (1 issue)

Labels
1
<?php
2
3
namespace Wa72\HtmlPageDom;
4
5
use Symfony\Component\DomCrawler\Crawler;
6
7
/**
8
 * Extends \Symfony\Component\DomCrawler\Crawler by adding tree manipulation functions
9
 * for HTML documents inspired by jQuery such as setInnerHtml(), css(), append(), prepend(), before(),
10
 * addClass(), removeClass()
11
 *
12
 * @author Christoph Singer
13
 * @license MIT
14
 *
15
 */
16
class HtmlPageCrawler extends Crawler
17
{
18
    /**
19
     * the (internal) root element name used when importing html fragments
20
     * */
21
    public const FRAGMENT_ROOT_TAGNAME = '_root';
22
23
    /**
24
     * Get an HtmlPageCrawler object from a HTML string, DOMNode, DOMNodeList or HtmlPageCrawler
25
     *
26
     * This is the equivalent to jQuery's $() function when used for wrapping DOMNodes or creating DOMElements from HTML code.
27
     *
28
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList|array $content
29
     * @return HtmlPageCrawler
30
     * @api
31
     */
32 136
    public static function create($content)
33
    {
34 136
        if ($content instanceof HtmlPageCrawler) {
35 24
            return $content;
36
        }
37
38 136
        return new HtmlPageCrawler($content);
39
    }
40
41
    /**
42
     * Adds the specified class(es) to each element in the set of matched elements.
43
     *
44
     * @param string $name One or more space-separated classes to be added to the class attribute of each matched element.
45
     * @return HtmlPageCrawler $this for chaining
46
     * @api
47
     */
48 8
    public function addClass($name)
49
    {
50 8
        foreach ($this as $node) {
51 8
            if ($node instanceof \DOMElement) {
52
                /** @var \DOMElement $node */
53 8
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
54 8
                $found = false;
55 8
                $count = count($classes);
56 8
                for ($i = 0; $i < $count; $i++) {
57 8
                    if ($classes[$i] == $name) {
58 8
                        $found = true;
59
                    }
60
                }
61 8
                if (!$found) {
62 8
                    $classes[] = $name;
63 8
                    $node->setAttribute('class', trim(join(' ', $classes)));
64
                }
65
            }
66
        }
67
68 8
        return $this;
69
    }
70
71
    /**
72
     * Insert content, specified by the parameter, after each element in the set of matched elements.
73
     *
74
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
75
     * @return HtmlPageCrawler $this for chaining
76
     * @api
77
     */
78 24
    public function after($content)
79
    {
80 24
        $content = self::create($content);
81 24
        $newnodes = [];
82 24
        foreach ($this as $i => $node) {
83
            /** @var \DOMNode $node */
84 24
            $refnode = $node->nextSibling;
85 24
            foreach ($content as $newnode) {
86
                /** @var \DOMNode $newnode */
87 24
                $newnode = static::importNewnode($newnode, $node, $i);
88 24
                if ($refnode === null) {
89 24
                    $node->parentNode->appendChild($newnode);
90
                } else {
91 8
                    $node->parentNode->insertBefore($newnode, $refnode);
92
                }
93 24
                $newnodes[] = $newnode;
94
            }
95
        }
96 24
        $content->clear();
97 24
        $content->add($newnodes);
98
99 24
        return $this;
100
    }
101
102
    /**
103
     * Insert HTML content as child nodes of each element after existing children
104
     *
105
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment or DOMNode to append
106
     * @return HtmlPageCrawler $this for chaining
107
     * @api
108
     */
109 16
    public function append($content)
110
    {
111 16
        $content = self::create($content);
112 16
        $newnodes = [];
113 16
        foreach ($this as $i => $node) {
114
            /** @var \DOMNode $node */
115 16
            foreach ($content as $newnode) {
116
                /** @var \DOMNode $newnode */
117 16
                $newnode = static::importNewnode($newnode, $node, $i);
118 16
                $node->appendChild($newnode);
119 16
                $newnodes[] = $newnode;
120
            }
121
        }
122 16
        $content->clear();
123 16
        $content->add($newnodes);
124
125 16
        return $this;
126
    }
127
128
    /**
129
     * Insert every element in the set of matched elements to the end of the target.
130
     *
131
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
132
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
133
     * @api
134
     */
135 16
    public function appendTo($element)
136
    {
137 16
        $e = self::create($element);
138 16
        $newnodes = [];
139 16
        foreach ($e as $i => $node) {
140
            /** @var \DOMNode $node */
141 16
            foreach ($this as $newnode) {
142
                /** @var \DOMNode $newnode */
143 16
                if ($node !== $newnode) {
144 16
                    $newnode = static::importNewnode($newnode, $node, $i);
145 16
                    $node->appendChild($newnode);
146
                }
147 16
                $newnodes[] = $newnode;
148
            }
149
        }
150
151 16
        return self::create($newnodes);
152
    }
153
154
    /**
155
     * Sets an attribute on each element
156
     *
157
     * @param string $name
158
     * @param string $value
159
     * @return HtmlPageCrawler $this for chaining
160
     * @api
161
     */
162 24
    public function setAttribute($name, $value)
163
    {
164 24
        foreach ($this as $node) {
165 24
            if ($node instanceof \DOMElement) {
166
                /** @var \DOMElement $node */
167 24
                $node->setAttribute($name, $value);
168
            }
169
        }
170
171 24
        return $this;
172
    }
173
174
    /**
175
     * Returns the attribute value of the first node of the list.
176
     * This is just an alias for attr() for naming consistency with setAttribute()
177
     *
178
     * @param string $name The attribute name
179
     * @throws \InvalidArgumentException When current node is empty
180
     * @return string|null The attribute value or null if the attribute does not exist
181
     */
182 8
    public function getAttribute($name)
183
    {
184 8
        return parent::attr($name);
185
    }
186
187
    /**
188
     * Insert content, specified by the parameter, before each element in the set of matched elements.
189
     *
190
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
191
     * @return HtmlPageCrawler $this for chaining
192
     * @api
193
     */
194 16
    public function before($content)
195
    {
196 16
        $content = self::create($content);
197 16
        $newnodes = [];
198 16
        foreach ($this as $i => $node) {
199
            /** @var \DOMNode $node */
200 16
            foreach ($content as $newnode) {
201
                /** @var \DOMNode $newnode */
202 16
                if ($node !== $newnode) {
203 16
                    $newnode = static::importNewnode($newnode, $node, $i);
204 16
                    $node->parentNode->insertBefore($newnode, $node);
205 16
                    $newnodes[] = $newnode;
206
                }
207
            }
208
        }
209 16
        $content->clear();
210 16
        $content->add($newnodes);
211
212 16
        return $this;
213
    }
214
215
    /**
216
     * Create a deep copy of the set of matched elements.
217
     *
218
     * Equivalent to clone() in jQuery (clone is not a valid PHP function name)
219
     *
220
     * @return HtmlPageCrawler
221
     * @api
222
     */
223 8
    public function makeClone()
224
    {
225 8
        return clone $this;
226
    }
227
228 8
    public function __clone()
229
    {
230 8
        $newnodes = [];
231 8
        foreach ($this as $node) {
232
            /** @var \DOMNode $node */
233 8
            $newnodes[] = $node->cloneNode(true);
234
        }
235 8
        $this->clear();
236 8
        $this->add($newnodes);
237 8
    }
238
239
    /**
240
     * Get one CSS style property of the first element or set it for all elements in the list
241
     *
242
     * Function is here for compatibility with jQuery; it is the same as getStyle() and setStyle()
243
     *
244
     * @see HtmlPageCrawler::getStyle()
245
     * @see HtmlPageCrawler::setStyle()
246
     *
247
     * @param string $key The name of the style property
248
     * @param null|string $value The CSS value to set, or NULL to get the current value
249
     * @return HtmlPageCrawler|string If no param is provided, returns the CSS styles of the first element
250
     * @api
251
     */
252 8
    public function css($key, $value = null)
253
    {
254 8
        if (null === $value) {
255 8
            return $this->getStyle($key);
256
        }
257
258 8
        return $this->setStyle($key, $value);
259
    }
260
261
    /**
262
     * get one CSS style property of the first element
263
     *
264
     * @param string $key name of the property
265
     * @return string|null value of the property
266
     */
267 8
    public function getStyle($key)
268
    {
269 8
        $styles = Helpers::cssStringToArray($this->getAttribute('style'));
270
271 8
        return (isset($styles[$key]) ? $styles[$key] : null);
272
    }
273
274
    /**
275
     * set one CSS style property for all elements in the list
276
     *
277
     * @param string $key name of the property
278
     * @param string $value value of the property
279
     * @return HtmlPageCrawler $this for chaining
280
     */
281 8
    public function setStyle($key, $value)
282
    {
283 8
        foreach ($this as $node) {
284 8
            if ($node instanceof \DOMElement) {
285
                /** @var \DOMElement $node */
286 8
                $styles = Helpers::cssStringToArray($node->getAttribute('style'));
287 8
                if ($value != '') {
288 8
                    $styles[$key] = $value;
289 8
                } elseif (isset($styles[$key])) {
290 8
                    unset($styles[$key]);
291
                }
292 8
                $node->setAttribute('style', Helpers::cssArrayToString($styles));
293
            }
294
        }
295
296 8
        return $this;
297
    }
298
299
    /**
300
     * Removes all child nodes and text from all nodes in set
301
     *
302
     * Equivalent to jQuery's empty() function which is not a valid function name in PHP
303
     * @return HtmlPageCrawler $this
304
     * @api
305
     */
306 8
    public function makeEmpty()
307
    {
308 8
        foreach ($this as $node) {
309 8
            $node->nodeValue = '';
310
        }
311
312 8
        return $this;
313
    }
314
315
    /**
316
     * Determine whether any of the matched elements are assigned the given class.
317
     *
318
     * @param string $name
319
     * @return bool
320
     * @api
321
     */
322 16
    public function hasClass($name)
323
    {
324 16
        foreach ($this as $node) {
325 16
            if ($node instanceof \DOMElement && $class = $node->getAttribute('class')) {
326 16
                $classes = preg_split('/\s+/s', $class);
327 16
                if (in_array($name, $classes)) {
328 16
                    return true;
329
                }
330
            }
331
        }
332
333 16
        return false;
334
    }
335
336
    /**
337
     * Set the HTML contents of each element
338
     *
339
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
340
     * @return HtmlPageCrawler $this for chaining
341
     * @api
342
     */
343 24
    public function setInnerHtml($content)
344
    {
345 24
        $content = self::create($content);
346 24
        foreach ($this as $node) {
347 24
            $node->nodeValue = '';
348 24
            foreach ($content as $newnode) {
349
                /** @var \DOMNode $node */
350
                /** @var \DOMNode $newnode */
351 24
                $newnode = static::importNewnode($newnode, $node);
352 24
                $node->appendChild($newnode);
353
            }
354
        }
355
356 24
        return $this;
357
    }
358
359
    /**
360
     * Alias for Crawler::html() for naming consistency with setInnerHtml()
361
     *
362
     * @return string
363
     * @api
364
     */
365 8
    public function getInnerHtml()
366
    {
367 8
        return parent::html();
368
    }
369
370
    /**
371
     * Insert every element in the set of matched elements after the target.
372
     *
373
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
374
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
375
     * @api
376
     */
377 16
    public function insertAfter($element)
378
    {
379 16
        $e = self::create($element);
380 16
        $newnodes = [];
381 16
        foreach ($e as $i => $node) {
382
            /** @var \DOMNode $node */
383 16
            $refnode = $node->nextSibling;
384 16
            foreach ($this as $newnode) {
385
                /** @var \DOMNode $newnode */
386 16
                $newnode = static::importNewnode($newnode, $node, $i);
387 16
                if ($refnode === null) {
388 16
                    $node->parentNode->appendChild($newnode);
389
                } else {
390 8
                    $node->parentNode->insertBefore($newnode, $refnode);
391
                }
392 16
                $newnodes[] = $newnode;
393
            }
394
        }
395
396 16
        return self::create($newnodes);
397
    }
398
399
    /**
400
     * Insert every element in the set of matched elements before the target.
401
     *
402
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
403
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
404
     * @api
405
     */
406 16
    public function insertBefore($element)
407
    {
408 16
        $e = self::create($element);
409 16
        $newnodes = [];
410 16
        foreach ($e as $i => $node) {
411
            /** @var \DOMNode $node */
412 16
            foreach ($this as $newnode) {
413
                /** @var \DOMNode $newnode */
414 16
                $newnode = static::importNewnode($newnode, $node, $i);
415 16
                if ($newnode !== $node) {
416 16
                    $node->parentNode->insertBefore($newnode, $node);
417
                }
418 16
                $newnodes[] = $newnode;
419
            }
420
        }
421
422 16
        return self::create($newnodes);
423
    }
424
425
    /**
426
     * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements.
427
     *
428
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content HTML code fragment
429
     * @return HtmlPageCrawler $this for chaining
430
     * @api
431
     */
432 16
    public function prepend($content)
433
    {
434 16
        $content = self::create($content);
435 16
        $newnodes = [];
436 16
        foreach ($this as $i => $node) {
437 16
            $refnode = $node->firstChild;
438
            /** @var \DOMNode $node */
439 16
            foreach ($content as $newnode) {
440
                /** @var \DOMNode $newnode */
441 16
                $newnode = static::importNewnode($newnode, $node, $i);
442 16
                if ($refnode === null) {
443 8
                    $node->appendChild($newnode);
444 16
                } elseif ($refnode !== $newnode) {
445 16
                    $node->insertBefore($newnode, $refnode);
446
                }
447 16
                $newnodes[] = $newnode;
448
            }
449
        }
450 16
        $content->clear();
451 16
        $content->add($newnodes);
452
453 16
        return $this;
454
    }
455
456
    /**
457
     * Insert every element in the set of matched elements to the beginning of the target.
458
     *
459
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
460
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements prepended to the target elements
461
     * @api
462
     */
463 8
    public function prependTo($element)
464
    {
465 8
        $e = self::create($element);
466 8
        $newnodes = [];
467 8
        foreach ($e as $i => $node) {
468 8
            $refnode = $node->firstChild;
469
            /** @var \DOMNode $node */
470 8
            foreach ($this as $newnode) {
471
                /** @var \DOMNode $newnode */
472 8
                $newnode = static::importNewnode($newnode, $node, $i);
473 8
                if ($newnode !== $node) {
474 8
                    if ($refnode === null) {
475 8
                        $node->appendChild($newnode);
476
                    } else {
477 8
                        $node->insertBefore($newnode, $refnode);
478
                    }
479
                }
480 8
                $newnodes[] = $newnode;
481
            }
482
        }
483
484 8
        return self::create($newnodes);
485
    }
486
487
    /**
488
     * Remove the set of matched elements from the DOM.
489
     *
490
     * (as opposed to Crawler::clear() which detaches the nodes only from Crawler
491
     * but leaves them in the DOM)
492
     *
493
     * @api
494
     */
495 16
    public function remove()
496
    {
497 16
        foreach ($this as $node) {
498
            /**
499
             * @var \DOMNode $node
500
             */
501 16
            if ($node->parentNode instanceof \DOMElement) {
502 16
                $node->parentNode->removeChild($node);
503
            }
504
        }
505 16
        $this->clear();
506 16
    }
507
508
    /**
509
     * Remove an attribute from each element in the set of matched elements.
510
     *
511
     * Alias for removeAttribute for compatibility with jQuery
512
     *
513
     * @param string $name
514
     * @return HtmlPageCrawler
515
     * @api
516
     */
517 8
    public function removeAttr($name)
518
    {
519 8
        return $this->removeAttribute($name);
520
    }
521
522
    /**
523
     * Remove an attribute from each element in the set of matched elements.
524
     *
525
     * @param string $name
526
     * @return HtmlPageCrawler
527
     */
528 8
    public function removeAttribute($name)
529
    {
530 8
        foreach ($this as $node) {
531 8
            if ($node instanceof \DOMElement) {
532
                /** @var \DOMElement $node */
533 8
                if ($node->hasAttribute($name)) {
534 8
                    $node->removeAttribute($name);
535
                }
536
            }
537
        }
538
539 8
        return $this;
540
    }
541
542
    /**
543
     * Remove a class from each element in the list
544
     *
545
     * @param string $name
546
     * @return HtmlPageCrawler $this for chaining
547
     * @api
548
     */
549 16
    public function removeClass($name)
550
    {
551 16
        foreach ($this as $node) {
552 16
            if ($node instanceof \DOMElement) {
553
                /** @var \DOMElement $node */
554 16
                $classes = preg_split('/\s+/s', $node->getAttribute('class'));
555 16
                $count = count($classes);
556 16
                for ($i = 0; $i < $count; $i++) {
557 16
                    if ($classes[$i] == $name) {
558 16
                        unset($classes[$i]);
559
                    }
560
                }
561 16
                $node->setAttribute('class', trim(join(' ', $classes)));
562
            }
563
        }
564
565 16
        return $this;
566
    }
567
568
    /**
569
     * Replace each target element with the set of matched elements.
570
     *
571
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $element
572
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler A new Crawler object containing all elements appended to the target elements
573
     * @api
574
     */
575 16
    public function replaceAll($element)
576
    {
577 16
        $e = self::create($element);
578 16
        $newnodes = [];
579 16
        foreach ($e as $i => $node) {
580
            /** @var \DOMNode $node */
581 16
            $parent = $node->parentNode;
582 16
            $refnode = $node->nextSibling;
583 16
            foreach ($this as $j => $newnode) {
584
                /** @var \DOMNode $newnode */
585 16
                $newnode = static::importNewnode($newnode, $node, $i);
586 16
                if ($j == 0) {
587 16
                    $parent->replaceChild($newnode, $node);
588
                } else {
589 8
                    $parent->insertBefore($newnode, $refnode);
590
                }
591 16
                $newnodes[] = $newnode;
592
            }
593
        }
594
595 16
        return self::create($newnodes);
596
    }
597
598
    /**
599
     * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed.
600
     *
601
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
602
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
603
     * @api
604
     */
605 16
    public function replaceWith($content)
606
    {
607 16
        $content = self::create($content);
608 16
        $newnodes = [];
609 16
        foreach ($this as $i => $node) {
610
            /** @var \DOMNode $node */
611 16
            $parent = $node->parentNode;
612 16
            $refnode = $node->nextSibling;
613 16
            foreach ($content as $j => $newnode) {
614
                /** @var \DOMNode $newnode */
615 16
                $newnode = static::importNewnode($newnode, $node, $i);
616 16
                if ($j == 0) {
617 16
                    $parent->replaceChild($newnode, $node);
618
                } else {
619 8
                    $parent->insertBefore($newnode, $refnode);
620
                }
621 16
                $newnodes[] = $newnode;
622
            }
623
        }
624 16
        $content->clear();
625 16
        $content->add($newnodes);
626
627 16
        return $this;
628
    }
629
630
    /**
631
     * Get the combined text contents of each element in the set of matched elements, including their descendants.
632
     * This is what the jQuery text() function does, contrary to the Crawler::text() method that returns only
633
     * the text of the first node.
634
     *
635
     * @return string
636
     * @api
637
     */
638 8
    public function getCombinedText()
639
    {
640 8
        $text = '';
641 8
        foreach ($this as $node) {
642
            /** @var \DOMNode $node */
643 8
            $text .= $node->nodeValue;
644
        }
645
646 8
        return $text;
647
    }
648
649
    /**
650
     * Set the text contents of the matched elements.
651
     *
652
     * @param string $text
653
     * @return HtmlPageCrawler
654
     * @api
655
     */
656 16
    public function setText($text)
657
    {
658 16
        $text = htmlspecialchars($text);
659 16
        foreach ($this as $node) {
660
            /** @var \DOMNode $node */
661 16
            $node->nodeValue = $text;
662
        }
663
664 16
        return $this;
665
    }
666
667
    /**
668
     * Add or remove one or more classes from each element in the set of matched elements, depending the class’s presence.
669
     *
670
     * @param string $classname One or more classnames separated by spaces
671
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
672
     * @api
673
     */
674 8
    public function toggleClass($classname)
675
    {
676 8
        $classes = explode(' ', $classname);
677 8
        foreach ($this as $i => $node) {
678 8
            $c = self::create($node);
679
            /** @var \DOMNode $node */
680 8
            foreach ($classes as $class) {
681 8
                if ($c->hasClass($class)) {
682 8
                    $c->removeClass($class);
683
                } else {
684 8
                    $c->addClass($class);
685
                }
686
            }
687
        }
688
689 8
        return $this;
690
    }
691
692
    /**
693
     * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
694
     *
695
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
696
     * @api
697
     */
698 8
    public function unwrap()
699
    {
700 8
        $parents = [];
701 8
        foreach ($this as $i => $node) {
702 8
            $parents[] = $node->parentNode;
703
        }
704
705 8
        self::create($parents)->unwrapInner();
706
707 8
        return $this;
708
    }
709
710
    /**
711
     * Remove the matched elements, but promote the children to take their place.
712
     *
713
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
714
     * @api
715
     */
716 16
    public function unwrapInner()
717
    {
718 16
        foreach ($this as $i => $node) {
719 16
            if (!$node->parentNode instanceof \DOMElement) {
720 8
                throw new \InvalidArgumentException('DOMElement does not have a parent DOMElement node.');
721
            }
722
723
            /** @var \DOMNode[] $children */
724 16
            $children = iterator_to_array($node->childNodes);
725 16
            foreach ($children as $child) {
726 8
                $node->parentNode->insertBefore($child, $node);
727
            }
728
729 16
            $node->parentNode->removeChild($node);
730
        }
731 16
    }
732
733
734
    /**
735
     * Wrap an HTML structure around each element in the set of matched elements
736
     *
737
     * The HTML structure must contain only one root node, e.g.:
738
     * Works: <div><div></div></div>
739
     * Does not work: <div></div><div></div>
740
     *
741
     * @param string|HtmlPageCrawler|\DOMNode $wrappingElement
742
     * @return HtmlPageCrawler $this for chaining
743
     * @api
744
     */
745 8
    public function wrap($wrappingElement)
746
    {
747 8
        $content = self::create($wrappingElement);
748 8
        $newnodes = [];
749 8
        foreach ($this as $i => $node) {
750
            /** @var \DOMNode $node */
751 8
            $newnode = $content->getNode(0);
752
            /** @var \DOMNode $newnode */
753
//            $newnode = static::importNewnode($newnode, $node, $i);
754 8
            if ($newnode->ownerDocument !== $node->ownerDocument) {
755 8
                $newnode = $node->ownerDocument->importNode($newnode, true);
756
            } else {
757
                if ($i > 0) {
758
                    $newnode = $newnode->cloneNode(true);
759
                }
760
            }
761 8
            $oldnode = $node->parentNode->replaceChild($newnode, $node);
762 8
            while ($newnode->hasChildNodes()) {
763 8
                $elementFound = false;
764 8
                foreach ($newnode->childNodes as $child) {
765 8
                    if ($child instanceof \DOMElement) {
766 8
                        $newnode = $child;
767 8
                        $elementFound = true;
768
769 8
                        break;
770
                    }
771
                }
772 8
                if (!$elementFound) {
773 8
                    break;
774
                }
775
            }
776 8
            $newnode->appendChild($oldnode);
777 8
            $newnodes[] = $newnode;
778
        }
779 8
        $content->clear();
780 8
        $content->add($newnodes);
781
782 8
        return $this;
783
    }
784
785
    /**
786
     * Wrap an HTML structure around all elements in the set of matched elements.
787
     *
788
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
789
     * @throws \LogicException
790
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
791
     * @api
792
     */
793 8
    public function wrapAll($content)
794
    {
795 8
        $content = self::create($content);
796 8
        $parent = $this->getNode(0)->parentNode;
797 8
        foreach ($this as $i => $node) {
798
            /** @var \DOMNode $node */
799 8
            if ($node->parentNode !== $parent) {
800
                throw new \LogicException('Nodes to be wrapped with wrapAll() must all have the same parent');
801
            }
802
        }
803
804 8
        $newnode = $content->getNode(0);
805
        /** @var \DOMNode $newnode */
806 8
        $newnode = static::importNewnode($newnode, $parent);
0 ignored issues
show
It seems like $parent can also be of type null; however, parameter $referencenode of Wa72\HtmlPageDom\HtmlPageCrawler::importNewnode() 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

806
        $newnode = static::importNewnode($newnode, /** @scrutinizer ignore-type */ $parent);
Loading history...
807
808 8
        $newnode = $parent->insertBefore($newnode, $this->getNode(0));
809 8
        $content->clear();
810 8
        $content->add($newnode);
811
812 8
        while ($newnode->hasChildNodes()) {
813 8
            $elementFound = false;
814 8
            foreach ($newnode->childNodes as $child) {
815 8
                if ($child instanceof \DOMElement) {
816 8
                    $newnode = $child;
817 8
                    $elementFound = true;
818
819 8
                    break;
820
                }
821
            }
822 8
            if (!$elementFound) {
823
                break;
824
            }
825
        }
826 8
        foreach ($this as $i => $node) {
827
            /** @var \DOMNode $node */
828 8
            $newnode->appendChild($node);
829
        }
830
831 8
        return $this;
832
    }
833
834
    /**
835
     * Wrap an HTML structure around the content of each element in the set of matched elements.
836
     *
837
     * @param string|HtmlPageCrawler|\DOMNode|\DOMNodeList $content
838
     * @return \Wa72\HtmlPageDom\HtmlPageCrawler $this for chaining
839
     * @api
840
     */
841 8
    public function wrapInner($content)
842
    {
843 8
        foreach ($this as $i => $node) {
844
            /** @var \DOMNode $node */
845 8
            self::create($node->childNodes)->wrapAll($content);
846
        }
847
848 8
        return $this;
849
    }
850
851
    /**
852
     * Get the HTML code fragment of all elements and their contents.
853
     *
854
     * If the first node contains a complete HTML document return only
855
     * the full code of this document.
856
     *
857
     * @return string HTML code (fragment)
858
     * @api
859
     */
860 64
    public function saveHTML()
861
    {
862 64
        if ($this->isHtmlDocument()) {
863 8
            return $this->getDOMDocument()->saveHTML();
864
        }
865 64
        $doc = new \DOMDocument('1.0', 'UTF-8');
866 64
        $root = $doc->appendChild($doc->createElement('_root'));
867 64
        foreach ($this as $node) {
868 64
            $root->appendChild($doc->importNode($node, true));
869
        }
870 64
        $html = trim($doc->saveHTML());
871
872 64
        return preg_replace('@^<' . self::FRAGMENT_ROOT_TAGNAME . '[^>]*>|</' . self::FRAGMENT_ROOT_TAGNAME . '>$@', '', $html);
873
    }
874
875 32
    public function __toString()
876
    {
877 32
        return $this->saveHTML();
878
    }
879
880
    /**
881
     * checks whether the first node contains a complete html document
882
     * (as opposed to a document fragment)
883
     *
884
     * @return boolean
885
     */
886 64
    public function isHtmlDocument()
887
    {
888 64
        $node = $this->getNode(0);
889 64
        if ($node instanceof \DOMElement
890 64
            && $node->ownerDocument instanceof \DOMDocument
891 64
            && $node->ownerDocument->documentElement === $node
892 64
            && $node->nodeName == 'html'
893
        ) {
894 8
            return true;
895
        }
896
897 64
        return false;
898
    }
899
900
    /**
901
     * get ownerDocument of the first element
902
     *
903
     * @return \DOMDocument|null
904
     */
905 8
    public function getDOMDocument()
906
    {
907 8
        $node = $this->getNode(0);
908 8
        $r = null;
909 8
        if ($node instanceof \DOMElement
910 8
            && $node->ownerDocument instanceof \DOMDocument
911
        ) {
912 8
            $r = $node->ownerDocument;
913
        }
914
915 8
        return $r;
916
    }
917
918
    /**
919
     * Filters the list of nodes with a CSS selector.
920
     *
921
     * @param string $selector
922
     * @return HtmlPageCrawler
923
     */
924 56
    public function filter($selector)
925
    {
926 56
        return parent::filter($selector);
927
    }
928
929
    /**
930
     * Filters the list of nodes with an XPath expression.
931
     *
932
     * @param string $xpath An XPath expression
933
     *
934
     * @return HtmlPageCrawler A new instance of Crawler with the filtered list of nodes
935
     *
936
     * @api
937
     */
938 16
    public function filterXPath($xpath)
939
    {
940 16
        return parent::filterXPath($xpath);
941
    }
942
943
    /**
944
     * Adds HTML/XML content to the HtmlPageCrawler object (but not to the DOM of an already attached node).
945
     *
946
     * Function overriden from Crawler because HTML fragments are always added as complete documents there
947
     *
948
     *
949
     * @param string      $content A string to parse as HTML/XML
950
     * @param null|string $type    The content type of the string
951
     *
952
     * @return null|void
953
     */
954 136
    public function addContent($content, $type = null)
955
    {
956 136
        if (empty($type)) {
957 136
            $type = 'text/html;charset=UTF-8';
958
        }
959 136
        if (substr($type, 0, 9) == 'text/html' && !preg_match('/<html\b[^>]*>/i', $content)) {
960
            // string contains no <html> Tag => no complete document but an HTML fragment!
961 128
            $this->addHtmlFragment($content);
962
        } else {
963 16
            parent::addContent($content, $type);
964
        }
965 136
    }
966
967 120
    public function addHtmlFragment($content, $charset = 'UTF-8')
968
    {
969 120
        $d = new \DOMDocument('1.0', $charset);
970 120
        $d->preserveWhiteSpace = false;
971 120
        $root = $d->appendChild($d->createElement(self::FRAGMENT_ROOT_TAGNAME));
972 120
        $bodynode = Helpers::getBodyNodeFromHtmlFragment($content, $charset);
973 120
        foreach ($bodynode->childNodes as $child) {
974 120
            $inode = $root->appendChild($d->importNode($child, true));
975 120
            if ($inode) {
976 120
                $this->addNode($inode);
977
            }
978
        }
979 120
    }
980
981
    /**
982
     * Adds a node to the current list of nodes.
983
     *
984
     * This method uses the appropriate specialized add*() method based
985
     * on the type of the argument.
986
     *
987
     * Overwritten from parent to allow Crawler to be added
988
     *
989
     * @param null|\DOMNodeList|array|\DOMNode|Crawler $node A node
990
     *
991
     * @api
992
     */
993 224
    public function add($node)
994
    {
995 224
        if ($node instanceof Crawler) {
996 8
            foreach ($node as $childnode) {
997 8
                $this->addNode($childnode);
998
            }
999
        } else {
1000 224
            parent::add($node);
1001
        }
1002 224
    }
1003
1004
    /**
1005
     * @param \DOMNode $newnode
1006
     * @param \DOMNode $referencenode
1007
     * @param int $clone
1008
     * @return \DOMNode
1009
     */
1010 48
    protected static function importNewnode(\DOMNode $newnode, \DOMNode $referencenode, $clone = 0)
1011
    {
1012 48
        if ($newnode->ownerDocument !== $referencenode->ownerDocument) {
1013 40
            $referencenode->ownerDocument->preserveWhiteSpace = false;
1014 40
            $newnode = $referencenode->ownerDocument->importNode($newnode, true);
1015
        } else {
1016 16
            if ($clone > 0) {
1017
                $newnode = $newnode->cloneNode(true);
1018
            }
1019
        }
1020
1021 48
        return $newnode;
1022
    }
1023
1024
//    /**
1025
//     * Checks whether the first node in the set is disconnected (has no parent node)
1026
//     *
1027
//     * @return bool
1028
//     */
1029
//    public function isDisconnected()
1030
//    {
1031
//        $parent = $this->getNode(0)->parentNode;
1032
//        return ($parent == null || $parent->tagName == self::FRAGMENT_ROOT_TAGNAME);
1033
//    }
1034
1035 8
    public function __get($name)
1036
    {
1037 8
        switch ($name) {
1038 8
            case 'count':
1039 8
            case 'length':
1040 8
                return count($this);
1041
        }
1042
1043 8
        throw new \Exception('No such property ' . $name);
1044
    }
1045
}
1046