Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.10% covered (warning)
76.10%
121 / 159
67.86% covered (warning)
67.86%
19 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
DOMCompat
76.10% covered (warning)
76.10%
121 / 159
67.86% covered (warning)
67.86%
19 / 28
114.47
0.00% covered (danger)
0.00%
0 / 1
 newDocument
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nodeName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBody
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 getHead
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
4.05
 getTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setTitle
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getParentElement
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getElementById
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 setIdAttribute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getElementsByTagName
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getFirstElementChild
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getLastElementChild
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 querySelector
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 querySelectorAll
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getPreviousElementSibling
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getNextElementSibling
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 append
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 remove
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getInnerHTML
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInnerHTML
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getOuterHTML
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttribute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getClassList
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stripAndCollapseASCIIWhitespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 stripEmptyTextNodes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 normalize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 replaceChildren
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getTemplateElementContent
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Utils;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\Parsoid\DOM\CharacterData;
8use Wikimedia\Parsoid\DOM\Document;
9use Wikimedia\Parsoid\DOM\DocumentFragment;
10use Wikimedia\Parsoid\DOM\Element;
11use Wikimedia\Parsoid\DOM\Node;
12use Wikimedia\Parsoid\DOM\Text;
13use Wikimedia\Parsoid\Utils\DOMCompat\TokenList;
14use Wikimedia\Parsoid\Wt2Html\XMLSerializer;
15use Wikimedia\RemexHtml\DOM\DOMBuilder;
16use Wikimedia\RemexHtml\HTMLData;
17use Wikimedia\RemexHtml\Tokenizer\Tokenizer;
18use Wikimedia\RemexHtml\TreeBuilder\Dispatcher;
19use Wikimedia\RemexHtml\TreeBuilder\TreeBuilder;
20use Wikimedia\Zest\Zest;
21
22/**
23 * Helper class that provides missing DOM level 3 methods for the PHP DOM classes.
24 * For a DOM method $node->foo( $bar) the equivalent helper is DOMCompat::foo( $node, $bar ).
25 * For a DOM property $node->foo there is a DOMCompat::getFoo( $node ) and
26 * DOMCompat::setFoo( $node, $value ).
27 *
28 * Only implements the methods that are actually used by Parsoid.
29 *
30 * Because this class may be used by code outside Parsoid it tries to
31 * be relatively tolerant of object types: you can call it either with
32 * PHP's DOM* types or with a "proper" DOM implementation, and it will
33 * attempt to Do The Right Thing regardless.  As a result there are
34 * generally not parameter type hints for DOM object types, and the
35 * return types will be broad enough to accomodate the value a "real"
36 * DOM implementation would return, as well as the values our
37 * thunk will return. (For instance, we can't create a "real" NodeList
38 * in our compatibility thunk.)
39 */
40class DOMCompat {
41
42    /**
43     * Tab, LF, FF, CR, space
44     * @see https://infra.spec.whatwg.org/#ascii-whitespace
45     */
46    private const ASCII_WHITESPACE = "\t\r\f\n ";
47
48    /**
49     * Create a new empty document.
50     * This is abstracted because the process is a little different depending
51     * on whether we're using Dodo or DOMDocument, and phan gets a little
52     * confused by this.
53     * @param bool $isHtml
54     * @return Document
55     */
56    public static function newDocument( bool $isHtml ) {
57        // @phan-suppress-next-line PhanParamTooMany,PhanTypeInstantiateInterface
58        return new Document( "1.0", "UTF-8" );
59    }
60
61    /**
62     * Return the lower-case version of the node name (HTML says this should
63     * be capitalized).
64     * @param Node $node
65     * @return string
66     */
67    public static function nodeName( Node $node ): string {
68        return strtolower( $node->nodeName );
69    }
70
71    /**
72     * Get document body.
73     * Unlike the spec we return it as a native PHP DOM object.
74     * @param Document $document
75     * @return Element|null
76     * @see https://html.spec.whatwg.org/multipage/dom.html#dom-document-body
77     */
78    public static function getBody( $document ) {
79        // WARNING: this will not be updated if (for some reason) the
80        // document body changes.
81        if ( $document->body !== null ) {
82            return $document->body;
83        }
84        foreach ( $document->documentElement->childNodes as $element ) {
85            /** @var Element $element */
86            $nodeName = self::nodeName( $element );
87            if ( $nodeName === 'body' || $nodeName === 'frameset' ) {
88                // Caching!
89                $document->body = $element;
90                // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
91                return $element;
92            }
93        }
94        return null;
95    }
96
97    /**
98     * Get document head.
99     * Unlike the spec we return it as a native PHP DOM object.
100     * @param Document $document
101     * @return Element|null
102     * @see https://html.spec.whatwg.org/multipage/dom.html#dom-document-head
103     */
104    public static function getHead( $document ) {
105        // Use an undeclared dynamic property as a cache.
106        // WARNING: this will not be updated if (for some reason) the
107        // document head changes.
108        if ( isset( $document->head ) ) {
109            return $document->head;
110        }
111        foreach ( $document->documentElement->childNodes as $element ) {
112            /** @var Element $element */
113            if ( self::nodeName( $element ) === 'head' ) {
114                $document->head = $element; // Caching!
115                // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
116                return $element;
117            }
118        }
119        return null;
120    }
121
122    /**
123     * Get document title.
124     * @param Document $document
125     * @return string
126     * @see https://html.spec.whatwg.org/multipage/dom.html#document.title
127     */
128    public static function getTitle( $document ): string {
129        $titleElement = self::querySelector( $document, 'title' );
130        return $titleElement ? self::stripAndCollapseASCIIWhitespace( $titleElement->textContent ) : '';
131    }
132
133    /**
134     * Set document title.
135     * @param Document $document
136     * @param string $title
137     * @see https://html.spec.whatwg.org/multipage/dom.html#document.title
138     */
139    public static function setTitle( $document, string $title ): void {
140        $titleElement = self::querySelector( $document, 'title' );
141        if ( !$titleElement ) {
142            $headElement = self::getHead( $document );
143            if ( $headElement ) {
144                $titleElement = DOMUtils::appendToHead( $document, 'title' );
145            }
146        }
147        if ( $titleElement ) {
148            $titleElement->textContent = $title;
149        }
150    }
151
152    /**
153     * Return the parent element, or null if the parent is not an element.
154     * @param Node $node
155     * @return Element|null
156     * @see https://dom.spec.whatwg.org/#dom-node-parentelement
157     */
158    public static function getParentElement( $node ) {
159        $parent = $node->parentNode;
160        if ( $parent && $parent->nodeType === XML_ELEMENT_NODE ) {
161            /** @var Element $parent */
162            // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
163            return $parent;
164        }
165        return null;
166    }
167
168    /**
169     * Return the descendant with the specified ID.
170     * Workaround for https://bugs.php.net/bug.php?id=77686 and other issues related to
171     * inconsistent indexing behavior.
172     * XXX: 77686 is fixed in php 8.1.21
173     * @param Document|DocumentFragment $node
174     * @param string $id
175     * @return Element|null
176     * @see https://dom.spec.whatwg.org/#dom-nonelementparentnode-getelementbyid
177     */
178    public static function getElementById( $node, string $id ) {
179        Assert::parameterType( [
180                Document::class, DocumentFragment::class,
181                // For compatibility with code which might call this from
182                // outside Parsoid.
183                \DOMDocument::class, \DOMDocumentFragment::class
184            ],
185            $node, '$node' );
186        // @phan-suppress-next-line PhanTypeMismatchArgument Zest is declared to take DOMDocument\DOMElement
187        $elements = Zest::getElementsById( $node, $id );
188        // @phan-suppress-next-line PhanTypeMismatchReturn
189        return $elements[0] ?? null;
190    }
191
192    /**
193     * Workaround bug in PHP's Document::getElementById() which doesn't
194     * actually index the 'id' attribute unless you use the non-standard
195     * `Element::setIdAttribute` method after the attribute is set;
196     * see https://www.php.net/manual/en/domdocument.getelementbyid.php
197     * for more details.
198     *
199     * @param Element $element
200     * @param string $id The desired value for the `id` attribute on $element.
201     * @see https://phabricator.wikimedia.org/T232390
202     */
203    public static function setIdAttribute( $element, string $id ): void {
204        $element->setAttribute( 'id', $id );
205        $element->setIdAttribute( 'id', true );// phab:T232390
206    }
207
208    /**
209     * Return all descendants with the specified tag name.
210     * Workaround for PHP's getElementsByTagName being inexplicably slow in some situations
211     * and the lack of Element::getElementsByTagName().
212     * @param Document|Element $node
213     * @param string $tagName
214     * @return (iterable<Element>&\Countable)|array<Element> Either an array or an HTMLCollection object
215     * @see https://dom.spec.whatwg.org/#dom-document-getelementsbytagname
216     * @see https://dom.spec.whatwg.org/#dom-element-getelementsbytagname
217     * @note Note that unlike the spec this method is not guaranteed to return a NodeList
218     *   (which cannot be freely constructed in PHP), just a traversable containing Elements.
219     */
220    public static function getElementsByTagName( $node, string $tagName ): iterable {
221        Assert::parameterType( [
222                Document::class, Element::class,
223                // For compatibility with code which might call this from
224                // outside Parsoid.
225                \DOMDocument::class, \DOMElement::class
226            ],
227            $node, '$node' );
228        // @phan-suppress-next-line PhanTypeMismatchArgument Zest is declared to take DOMDocument\DOMElement
229        $result = Zest::getElementsByTagName( $node, $tagName );
230        '@phan-var array<Element> $result'; // @var array<Element> $result
231        return $result;
232    }
233
234    /**
235     * Return the first child of the node that is an Element, or null
236     * otherwise.
237     * @param Document|DocumentFragment|Element $node
238     * @return Element|null
239     * @see https://dom.spec.whatwg.org/#dom-parentnode-firstelementchild
240     * @note This property was added to PHP in 8.0.0, and won't be needed
241     *  once our minimum required version >= 8.0.0
242     */
243    public static function getFirstElementChild( $node ) {
244        Assert::parameterType( [
245                Document::class, DocumentFragment::class, Element::class,
246                // For compatibility with code which might call this from
247                // outside Parsoid.
248                \DOMDocument::class, \DOMDocumentFragment::class, \DOMElement::class
249            ],
250            $node, '$node' );
251        $firstChild = $node->firstChild;
252        while ( $firstChild && $firstChild->nodeType !== XML_ELEMENT_NODE ) {
253            $firstChild = $firstChild->nextSibling;
254        }
255        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
256        return $firstChild;
257    }
258
259    /**
260     * Return the last child of the node that is an Element, or null otherwise.
261     * @param Document|DocumentFragment|Element $node
262     * @return Element|null
263     * @see https://dom.spec.whatwg.org/#dom-parentnode-lastelementchild
264     * @note This property was added to PHP in 8.0.0, and won't be needed
265     *  once our minimum required version >= 8.0.0
266     */
267    public static function getLastElementChild( $node ) {
268        Assert::parameterType( [
269                Document::class, DocumentFragment::class, Element::class,
270                // For compatibility with code which might call this from
271                // outside Parsoid.
272                \DOMDocument::class, \DOMDocumentFragment::class, \DOMElement::class
273            ],
274            $node, '$node' );
275        $lastChild = $node->lastChild;
276        while ( $lastChild && $lastChild->nodeType !== XML_ELEMENT_NODE ) {
277            $lastChild = $lastChild->previousSibling;
278        }
279        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
280        return $lastChild;
281    }
282
283    /**
284     * @param Document|DocumentFragment|Element $node
285     * @param string $selector
286     * @return Element|null
287     * @see https://dom.spec.whatwg.org/#dom-parentnode-queryselector
288     */
289    public static function querySelector( $node, string $selector ) {
290        foreach ( self::querySelectorAll( $node, $selector ) as $el ) {
291            return $el;
292        }
293        return null;
294    }
295
296    /**
297     * @param Document|DocumentFragment|Element $node
298     * @param string $selector
299     * @return (iterable<Element>&\Countable)|array<Element> Either a NodeList or an array
300     * @see https://dom.spec.whatwg.org/#dom-parentnode-queryselectorall
301     * @note Note that unlike the spec this method is not guaranteed to return a NodeList
302     *   (which cannot be freely constructed in PHP), just a traversable containing Elements.
303     */
304    public static function querySelectorAll( $node, string $selector ): iterable {
305        Assert::parameterType( [
306                Document::class, DocumentFragment::class, Element::class,
307                // For compatibility with code which might call this from
308                // outside Parsoid.
309                \DOMDocument::class, \DOMDocumentFragment::class, \DOMElement::class
310            ],
311            $node, '$node' );
312        // @phan-suppress-next-line PhanTypeMismatchArgument DOMNode
313        return Zest::find( $selector, $node );
314    }
315
316    /**
317     * Return the last preceding sibling of the node that is an element, or null otherwise.
318     * @param Node $node
319     * @return Element|null
320     * @see https://dom.spec.whatwg.org/#dom-nondocumenttypechildnode-previouselementsibling
321     */
322    public static function getPreviousElementSibling( $node ) {
323        Assert::parameterType( [
324                Element::class, CharacterData::class,
325                // For compatibility with code which might call this from
326                // outside Parsoid.
327                \DOMElement::class, \DOMCharacterData::class
328            ],
329            $node, '$node' );
330        $previousSibling = $node->previousSibling;
331        while ( $previousSibling && $previousSibling->nodeType !== XML_ELEMENT_NODE ) {
332            $previousSibling = $previousSibling->previousSibling;
333        }
334        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
335        return $previousSibling;
336    }
337
338    /**
339     * Return the first following sibling of the node that is an element, or null otherwise.
340     * @param Node $node
341     * @return Element|null
342     * @see https://dom.spec.whatwg.org/#dom-nondocumenttypechildnode-nextelementsibling
343     */
344    public static function getNextElementSibling( $node ) {
345        Assert::parameterType( [
346                Element::class, CharacterData::class,
347                // For compatibility with code which might call this from
348                // outside Parsoid.
349                \DOMElement::class, \DOMCharacterData::class
350            ],
351            $node, '$node' );
352        $nextSibling = $node->nextSibling;
353        while ( $nextSibling && $nextSibling->nodeType !== XML_ELEMENT_NODE ) {
354            $nextSibling = $nextSibling->nextSibling;
355        }
356        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
357        return $nextSibling;
358    }
359
360    /**
361     * Append the node to the parent node.
362     * @param Document|DocumentFragment|Element $parentNode
363     * @param Node|string ...$nodes
364     * @note This method was added in PHP 8.0.0
365     */
366    public static function append( $parentNode, ...$nodes ): void {
367        Assert::parameterType( [
368                Document::class, DocumentFragment::class, Element::class,
369                // For compatibility with code which might call this from
370                // outside Parsoid.
371                \DOMDocument::class, \DOMDocumentFragment::class, \DOMElement::class
372            ],
373            $parentNode, '$parentNode'
374        );
375        foreach ( $nodes as $node ) {
376            if ( is_string( $node ) ) {
377                $node = $parentNode->ownerDocument->createTextNode( $node );
378            }
379            $parentNode->appendChild( $node );
380        }
381    }
382
383    /**
384     * Removes the node from the document.
385     * @param Element|CharacterData $node
386     * @see https://dom.spec.whatwg.org/#dom-childnode-remove
387     */
388    public static function remove( $node ): void {
389        Assert::parameterType( [
390                Element::class, CharacterData::class,
391                // For compatibility with code which might call this from
392                // outside Parsoid.
393                \DOMElement::class, \DOMCharacterData::class
394            ],
395            $node, '$node' );
396        if ( $node->parentNode ) {
397            $node->parentNode->removeChild( $node );
398        }
399    }
400
401    /**
402     * Get innerHTML.
403     * @see DOMUtils::getFragmentInnerHTML() for the fragment version
404     * @param Element $element
405     * @return string
406     * @see https://w3c.github.io/DOM-Parsing/#dom-innerhtml-innerhtml
407     */
408    public static function getInnerHTML( $element ): string {
409        return XMLSerializer::serialize( $element, [ 'innerXML' => true ] )['html'];
410    }
411
412    /**
413     * Set innerHTML.
414     * @see https://w3c.github.io/DOM-Parsing/#dom-innerhtml-innerhtml
415     * @see DOMUtils::setFragmentInnerHTML() for the fragment version
416     * @param Element $element
417     * @param string $html
418     */
419    public static function setInnerHTML( $element, string $html ): void {
420        $domBuilder = new class( [
421            'suppressHtmlNamespace' => true,
422        ] ) extends DOMBuilder {
423            /** @inheritDoc */
424            protected function createDocument(
425                ?string $doctypeName = null,
426                ?string $public = null,
427                ?string $system = null
428            ) {
429                // @phan-suppress-next-line PhanTypeMismatchReturn
430                return DOMCompat::newDocument( $doctypeName === 'html' );
431            }
432        };
433        $treeBuilder = new TreeBuilder( $domBuilder );
434        $dispatcher = new Dispatcher( $treeBuilder );
435        $tokenizer = new Tokenizer( $dispatcher, $html, [ 'ignoreErrors' => true ] );
436
437        $tokenizer->execute( [
438            'fragmentNamespace' => HTMLData::NS_HTML,
439            'fragmentName' => self::nodeName( $element ),
440        ] );
441
442        // Empty the element
443        self::replaceChildren( $element );
444
445        $frag = $domBuilder->getFragment();
446        '@phan-var Node $frag'; // @var Node $frag
447        DOMUtils::migrateChildrenBetweenDocs(
448            $frag, $element
449        );
450    }
451
452    /**
453     * Get outerHTML.
454     * @param Element $element
455     * @return string
456     * @see https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml
457     */
458    public static function getOuterHTML( $element ): string {
459        return XMLSerializer::serialize( $element, [ 'addDoctype' => false ] )['html'];
460    }
461
462    /**
463     * Return the value of an element attribute.
464     *
465     * Unlike PHP's version, this is spec-compliant and returns `null` if
466     * the attribute is not present, allowing the caller to distinguish
467     * between "the attribute exists but has the empty string as its value"
468     * and "the attribute does not exist".
469     *
470     * @param Element $element
471     * @param string $attributeName
472     * @return ?string The attribute value, or `null` if the attribute does
473     *   not exist on the element.
474     * @see https://dom.spec.whatwg.org/#dom-element-getattribute
475     */
476    public static function getAttribute( $element, string $attributeName ): ?string {
477        if ( !$element->hasAttribute( $attributeName ) ) {
478            return null;
479        }
480        return $element->getAttribute( $attributeName );
481    }
482
483    /**
484     * Return the class list of this element.
485     * @param Element $node
486     * @return TokenList
487     * @see https://dom.spec.whatwg.org/#dom-element-classlist
488     */
489    public static function getClassList( $node ): TokenList {
490        return new TokenList( $node );
491    }
492
493    /**
494     * @param string $text
495     * @return string
496     * @see https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace
497     */
498    private static function stripAndCollapseASCIIWhitespace( string $text ): string {
499        $ws = self::ASCII_WHITESPACE;
500        return preg_replace( "/[$ws]+/", ' ', trim( $text, $ws ) );
501    }
502
503    /**
504     * @param Element|DocumentFragment $e
505     */
506    private static function stripEmptyTextNodes( $e ): void {
507        $c = $e->firstChild;
508        while ( $c ) {
509            $next = $c->nextSibling;
510            if ( $c instanceof Text ) {
511                if ( $c->nodeValue === '' ) {
512                    $e->removeChild( $c );
513                }
514            } elseif ( $c instanceof Element ) {
515                self::stripEmptyTextNodes( $c );
516            }
517            $c = $next;
518        }
519    }
520
521    /**
522     * @param Element|DocumentFragment $elt root of the DOM tree that
523     *   needs to be normalized
524     */
525    public static function normalize( $elt ): void {
526        $elt->normalize();
527
528        // Now traverse the tree rooted at $elt and remove any stray empty text nodes
529        // Unlike what https://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-normalize says,
530        // the PHP DOM's normalization leaves behind up to 1 empty text node.
531        // See https://bugs.php.net/bug.php?id=78221
532        self::stripEmptyTextNodes( $elt );
533    }
534
535    /**
536     * ParentNode.replaceChildren()
537     * https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/replaceChildren
538     *
539     * @param Document|DocumentFragment|Element $parentNode
540     * @param string|Node ...$nodes
541     */
542    public static function replaceChildren(
543        $parentNode, ...$nodes
544    ): void {
545        Assert::parameterType( [
546                Document::class, DocumentFragment::class, Element::class,
547                // For compatibility with code which might call this from
548                // outside Parsoid.
549                \DOMDocument::class, \DOMDocumentFragment::class, \DOMElement::class
550            ],
551            $parentNode, '$parentNode'
552        );
553        while ( $parentNode->firstChild ) {
554            $parentNode->removeChild( $parentNode->firstChild );
555        }
556        foreach ( $nodes as $node ) {
557            if ( is_string( $node ) ) {
558                $node = $parentNode->ownerDocument->createTextNode( $node );
559            }
560            $parentNode->insertBefore( $node, null );
561        }
562    }
563
564    /**
565     * Return HTMLTemplateElement#content
566     *
567     * In the PHP DOM, <template> elements do not have a dedicated
568     * DocumentFragment and children are stored directly under the
569     * Element.  In the HTML5 spec, the contents are stored in a
570     * DocumentFragment with a unique owner document.
571     *
572     * Bridge this gap by returning the <template> element for
573     * PHP's DOM, or the DocumentFragment for an HTML5-compliant DOM.
574     *
575     * @param Element $node A <template> element
576     * @return Element|DocumentFragment Either the element (for PHP compat)
577     *  or the DocumentFragment which is the template's "content"
578     */
579    public static function getTemplateElementContent( $node ) {
580        // @phan-suppress-next-line PhanUndeclaredProperty only in IDLeDOM
581        if ( isset( $node->content ) ) {
582            // @phan-suppress-next-line PhanUndeclaredProperty only in IDLeDOM
583            return $node->content;
584        }
585        return $node;
586    }
587}