Skip to content

Commit 0f76fad

Browse files
committed
Fix template contents participating in the DOM tree
1 parent a2633d9 commit 0f76fad

11 files changed

Lines changed: 1034 additions & 10 deletions

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,20 @@ The following options are supported:
122122
assigning the HTML5 namespace to the DOM document. This is for
123123
non-namespace aware DOM tools.
124124
* `target_document` (\DOMDocument): A DOM document that will be used as the
125-
destination for the parsed nodes.
125+
destination for the parsed nodes. When parsing `<template>` contents into a
126+
caller-supplied document, use `HTML5::save()` or `HTML5::saveHTML()` for
127+
serialization, since native `DOMDocument::saveHTML()` cannot preserve the
128+
detached template subtree on a plain `DOMDocument`. The detached subtree is
129+
preserved for documents parsed by HTML5-PHP, including `cloneNode(true)` on
130+
caller-supplied target documents and `importNode(true)` on HTML5-PHP-created
131+
documents. When a parsed document contains `<template>` elements, HTML5-PHP
132+
registers template-aware DOM node wrappers on that document if it is still
133+
using the native `DOMElement` and `DOMDocumentFragment` classes, so detached
134+
contents can follow `cloneNode(true)`. If a caller-supplied target document
135+
already uses custom node classes, HTML5-PHP leaves them in place and detached
136+
template contents are still preserved by `HTML5::save()` / `HTML5::saveHTML()`.
137+
Native `importNode()` into a plain external `DOMDocument` cannot carry
138+
detached template contents.
126139
* `implicit_namespaces` (array): An assoc array of namespaces that should be
127140
used by the parser. Name is tag prefix, value is NS URI.
128141

src/HTML5/HTML5DOMDocument.php

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
use Masterminds\HTML5\Serializer\OutputRules;
6+
use Masterminds\HTML5\Serializer\Traverser;
7+
8+
/**
9+
* Shared serializer logic for template-aware DOMDocument implementations.
10+
*/
11+
class HTML5DOMDocumentBase extends \DOMDocument
12+
{
13+
/**
14+
* @var \SplObjectStorage|null
15+
*/
16+
protected $templateContents;
17+
18+
/**
19+
* @param bool $create
20+
*
21+
* @return \SplObjectStorage|null
22+
*/
23+
public function html5PhpTemplateContentsStorage($create = true)
24+
{
25+
if (null === $this->templateContents && $create) {
26+
$this->templateContents = new \SplObjectStorage();
27+
}
28+
29+
return $this->templateContents;
30+
}
31+
32+
protected function html5PhpSaveHTML($node = null)
33+
{
34+
$target = null === $node ? $this : $node;
35+
if (!$this->containsDetachedTemplateContents($target)) {
36+
return parent::saveHTML($node);
37+
}
38+
39+
$stream = fopen('php://temp', 'wb');
40+
$rules = new OutputRules($stream);
41+
$traverser = new Traverser($target, $stream, $rules);
42+
$traverser->walk();
43+
$rules->unsetTraverser();
44+
45+
$html = stream_get_contents($stream, -1, 0);
46+
fclose($stream);
47+
48+
return $html;
49+
}
50+
51+
protected function html5PhpImportNode($node, $deep = false)
52+
{
53+
$imported = parent::importNode($node, $deep);
54+
TemplateContents::copySubtree($node, $imported, $deep);
55+
56+
return $imported;
57+
}
58+
59+
protected function html5PhpCloneNode($deep = false)
60+
{
61+
$class = get_class($this);
62+
$clone = new $class($this->xmlVersion, $this->encoding);
63+
TemplateContents::registerNodeClasses($clone);
64+
65+
$clone->encoding = $this->encoding;
66+
$clone->formatOutput = $this->formatOutput;
67+
$clone->preserveWhiteSpace = $this->preserveWhiteSpace;
68+
$clone->recover = $this->recover;
69+
$clone->resolveExternals = $this->resolveExternals;
70+
$clone->strictErrorChecking = $this->strictErrorChecking;
71+
$clone->substituteEntities = $this->substituteEntities;
72+
$clone->validateOnParse = $this->validateOnParse;
73+
74+
if (!$deep) {
75+
return $clone;
76+
}
77+
78+
foreach ($this->childNodes as $child) {
79+
$clone->appendChild($clone->importNode($child, true));
80+
}
81+
82+
return $clone;
83+
}
84+
85+
/**
86+
* @param \DOMNode $node
87+
*
88+
* @return bool
89+
*/
90+
protected function containsDetachedTemplateContents(\DOMNode $node)
91+
{
92+
if ($node instanceof \DOMElement && 'template' === strtolower($node->tagName)) {
93+
return null !== TemplateContents::find($node);
94+
}
95+
96+
if ($node->hasChildNodes()) {
97+
foreach ($node->childNodes as $child) {
98+
if ($this->containsDetachedTemplateContents($child)) {
99+
return true;
100+
}
101+
}
102+
}
103+
104+
return false;
105+
}
106+
}
107+
108+
if (PHP_VERSION_ID >= 80100) {
109+
require __DIR__ . '/HTML5DOMDocument81.php';
110+
} else {
111+
class HTML5DOMDocument extends HTML5DOMDocumentBase
112+
{
113+
public function saveHTML($node = null)
114+
{
115+
return $this->html5PhpSaveHTML($node);
116+
}
117+
118+
public function importNode($node, $deep = false)
119+
{
120+
return $this->html5PhpImportNode($node, $deep);
121+
}
122+
123+
public function cloneNode($deep = false)
124+
{
125+
return $this->html5PhpCloneNode($deep);
126+
}
127+
}
128+
}

src/HTML5/HTML5DOMDocument81.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
class HTML5DOMDocument extends HTML5DOMDocumentBase
6+
{
7+
#[\ReturnTypeWillChange]
8+
public function saveHTML($node = null)
9+
{
10+
return $this->html5PhpSaveHTML($node);
11+
}
12+
13+
#[\ReturnTypeWillChange]
14+
public function importNode($node, $deep = false)
15+
{
16+
return $this->html5PhpImportNode($node, $deep);
17+
}
18+
19+
#[\ReturnTypeWillChange]
20+
public function cloneNode($deep = false)
21+
{
22+
return $this->html5PhpCloneNode($deep);
23+
}
24+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
/**
6+
* Keep the template-aware owner document alive for detached fragments.
7+
*/
8+
class HTML5DOMDocumentFragmentBase extends \DOMDocumentFragment
9+
{
10+
/**
11+
* @var \DOMDocument|null
12+
*/
13+
protected $templateOwnerDocument;
14+
15+
/**
16+
* @param \DOMDocument $document
17+
*/
18+
public function html5PhpKeepTemplateOwnerDocument(\DOMDocument $document)
19+
{
20+
$this->templateOwnerDocument = $document;
21+
}
22+
23+
protected function html5PhpCloneNode($deep = false)
24+
{
25+
$clone = parent::cloneNode($deep);
26+
27+
if ($this->templateOwnerDocument instanceof \DOMDocument && method_exists($clone, 'html5PhpKeepTemplateOwnerDocument')) {
28+
$clone->html5PhpKeepTemplateOwnerDocument($this->templateOwnerDocument);
29+
}
30+
31+
TemplateContents::copySubtree($this, $clone, $deep);
32+
33+
return $clone;
34+
}
35+
}
36+
37+
if (PHP_VERSION_ID >= 80100) {
38+
require __DIR__ . '/HTML5DOMDocumentFragment81.php';
39+
} else {
40+
class HTML5DOMDocumentFragment extends HTML5DOMDocumentFragmentBase
41+
{
42+
public function cloneNode($deep = false)
43+
{
44+
return $this->html5PhpCloneNode($deep);
45+
}
46+
}
47+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
class HTML5DOMDocumentFragment extends HTML5DOMDocumentFragmentBase
6+
{
7+
#[\ReturnTypeWillChange]
8+
public function cloneNode($deep = false)
9+
{
10+
return $this->html5PhpCloneNode($deep);
11+
}
12+
}

src/HTML5/HTML5DOMElement.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
/**
6+
* Shared template-aware behavior for DOMElement implementations.
7+
*/
8+
class HTML5DOMElementBase extends \DOMElement
9+
{
10+
/**
11+
* @var \DOMDocumentFragment|null
12+
*/
13+
protected $templateContents;
14+
15+
/**
16+
* @param \DOMDocumentFragment|null $fragment
17+
*/
18+
public function html5PhpSetTemplateContents($fragment)
19+
{
20+
$this->templateContents = $fragment;
21+
}
22+
23+
/**
24+
* @return \DOMDocumentFragment|null
25+
*/
26+
public function html5PhpTemplateContents()
27+
{
28+
return $this->templateContents;
29+
}
30+
31+
protected function html5PhpHasDetachedTemplateContents()
32+
{
33+
return 'template' === strtolower($this->tagName) && $this->templateContents instanceof \DOMDocumentFragment;
34+
}
35+
36+
protected function html5PhpCloneNode($deep = false)
37+
{
38+
$clone = parent::cloneNode($deep);
39+
TemplateContents::copySubtree($this, $clone, $deep);
40+
41+
return $clone;
42+
}
43+
44+
protected function html5PhpAppendChild($node)
45+
{
46+
if ($this->html5PhpHasDetachedTemplateContents()) {
47+
return $this->templateContents->appendChild($node);
48+
}
49+
50+
return parent::appendChild($node);
51+
}
52+
53+
protected function html5PhpInsertBefore($newnode, $refnode = null)
54+
{
55+
if ($this->html5PhpHasDetachedTemplateContents()) {
56+
if (null === $refnode) {
57+
return $this->templateContents->appendChild($newnode);
58+
}
59+
60+
return $this->templateContents->insertBefore($newnode, $refnode);
61+
}
62+
63+
return parent::insertBefore($newnode, $refnode);
64+
}
65+
66+
protected function html5PhpReplaceChild($newnode, $oldnode)
67+
{
68+
if ($this->html5PhpHasDetachedTemplateContents()) {
69+
return $this->templateContents->replaceChild($newnode, $oldnode);
70+
}
71+
72+
return parent::replaceChild($newnode, $oldnode);
73+
}
74+
75+
protected function html5PhpRemoveChild($oldnode)
76+
{
77+
if ($this->html5PhpHasDetachedTemplateContents()) {
78+
return $this->templateContents->removeChild($oldnode);
79+
}
80+
81+
return parent::removeChild($oldnode);
82+
}
83+
}
84+
85+
if (PHP_VERSION_ID >= 80100) {
86+
require __DIR__ . '/HTML5DOMElement81.php';
87+
} else {
88+
class HTML5DOMElement extends HTML5DOMElementBase
89+
{
90+
public function cloneNode($deep = false)
91+
{
92+
return $this->html5PhpCloneNode($deep);
93+
}
94+
95+
public function appendChild($node)
96+
{
97+
return $this->html5PhpAppendChild($node);
98+
}
99+
100+
public function insertBefore($newnode, $refnode = null)
101+
{
102+
return $this->html5PhpInsertBefore($newnode, $refnode);
103+
}
104+
105+
public function replaceChild($newnode, $oldnode)
106+
{
107+
return $this->html5PhpReplaceChild($newnode, $oldnode);
108+
}
109+
110+
public function removeChild($oldnode)
111+
{
112+
return $this->html5PhpRemoveChild($oldnode);
113+
}
114+
}
115+
}

src/HTML5/HTML5DOMElement81.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Masterminds\HTML5;
4+
5+
class HTML5DOMElement extends HTML5DOMElementBase
6+
{
7+
#[\ReturnTypeWillChange]
8+
public function cloneNode($deep = false)
9+
{
10+
return $this->html5PhpCloneNode($deep);
11+
}
12+
13+
#[\ReturnTypeWillChange]
14+
public function appendChild($node)
15+
{
16+
return $this->html5PhpAppendChild($node);
17+
}
18+
19+
#[\ReturnTypeWillChange]
20+
public function insertBefore($newnode, $refnode = null)
21+
{
22+
return $this->html5PhpInsertBefore($newnode, $refnode);
23+
}
24+
25+
#[\ReturnTypeWillChange]
26+
public function replaceChild($newnode, $oldnode)
27+
{
28+
return $this->html5PhpReplaceChild($newnode, $oldnode);
29+
}
30+
31+
#[\ReturnTypeWillChange]
32+
public function removeChild($oldnode)
33+
{
34+
return $this->html5PhpRemoveChild($oldnode);
35+
}
36+
}

0 commit comments

Comments
 (0)