Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,20 @@ The following options are supported:
assigning the HTML5 namespace to the DOM document. This is for
non-namespace aware DOM tools.
* `target_document` (\DOMDocument): A DOM document that will be used as the
destination for the parsed nodes.
destination for the parsed nodes. When parsing `<template>` contents into a
caller-supplied document, use `HTML5::save()` or `HTML5::saveHTML()` for
serialization, since native `DOMDocument::saveHTML()` cannot preserve the
detached template subtree on a plain `DOMDocument`. The detached subtree is
preserved for documents parsed by HTML5-PHP, including `cloneNode(true)` on
caller-supplied target documents and `importNode(true)` on HTML5-PHP-created
documents. When a parsed document contains `<template>` elements, HTML5-PHP
registers template-aware DOM node wrappers on that document if it is still
using the native `DOMElement` and `DOMDocumentFragment` classes, so detached
contents can follow `cloneNode(true)`. If a caller-supplied target document
already uses custom node classes, HTML5-PHP leaves them in place and detached
template contents are still preserved by `HTML5::save()` / `HTML5::saveHTML()`.
Native `importNode()` into a plain external `DOMDocument` cannot carry
detached template contents.
* `implicit_namespaces` (array): An assoc array of namespaces that should be
used by the parser. Name is tag prefix, value is NS URI.

Expand Down
128 changes: 128 additions & 0 deletions src/HTML5/HTML5DOMDocument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace Masterminds\HTML5;

use Masterminds\HTML5\Serializer\OutputRules;
use Masterminds\HTML5\Serializer\Traverser;

/**
* Shared serializer logic for template-aware DOMDocument implementations.
*/
class HTML5DOMDocumentBase extends \DOMDocument
{
/**
* @var \SplObjectStorage|null
*/
protected $templateContents;

/**
* @param bool $create
*
* @return \SplObjectStorage|null
*/
public function html5PhpTemplateContentsStorage($create = true)
{
if (null === $this->templateContents && $create) {
$this->templateContents = new \SplObjectStorage();
}

return $this->templateContents;
}

protected function html5PhpSaveHTML($node = null)
{
$target = null === $node ? $this : $node;
if (!$this->containsDetachedTemplateContents($target)) {
return parent::saveHTML($node);
}

$stream = fopen('php://temp', 'wb');
$rules = new OutputRules($stream);
$traverser = new Traverser($target, $stream, $rules);
$traverser->walk();
$rules->unsetTraverser();

$html = stream_get_contents($stream, -1, 0);
fclose($stream);

return $html;
}

protected function html5PhpImportNode($node, $deep = false)
{
$imported = parent::importNode($node, $deep);
TemplateContents::copySubtree($node, $imported, $deep);

return $imported;
}

protected function html5PhpCloneNode($deep = false)
{
$class = get_class($this);
$clone = new $class($this->xmlVersion, $this->encoding);
TemplateContents::registerNodeClasses($clone);

$clone->encoding = $this->encoding;
$clone->formatOutput = $this->formatOutput;
$clone->preserveWhiteSpace = $this->preserveWhiteSpace;
$clone->recover = $this->recover;
$clone->resolveExternals = $this->resolveExternals;
$clone->strictErrorChecking = $this->strictErrorChecking;
$clone->substituteEntities = $this->substituteEntities;
$clone->validateOnParse = $this->validateOnParse;

if (!$deep) {
return $clone;
}

foreach ($this->childNodes as $child) {
$clone->appendChild($clone->importNode($child, true));
}

return $clone;
}

/**
* @param \DOMNode $node
*
* @return bool
*/
protected function containsDetachedTemplateContents(\DOMNode $node)
{
if ($node instanceof \DOMElement && 'template' === strtolower($node->tagName)) {
return null !== TemplateContents::find($node);
}

if ($node->hasChildNodes()) {
foreach ($node->childNodes as $child) {
if ($this->containsDetachedTemplateContents($child)) {
return true;
}
}
}

return false;
}
}

if (PHP_VERSION_ID >= 80100) {
require __DIR__ . '/HTML5DOMDocument81.php';
} else {
class HTML5DOMDocument extends HTML5DOMDocumentBase
{
public function saveHTML($node = null)
{
return $this->html5PhpSaveHTML($node);
}

public function importNode($node, $deep = false)
{
return $this->html5PhpImportNode($node, $deep);
}

public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}
}
}
24 changes: 24 additions & 0 deletions src/HTML5/HTML5DOMDocument81.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Masterminds\HTML5;

class HTML5DOMDocument extends HTML5DOMDocumentBase
{
#[\ReturnTypeWillChange]
public function saveHTML($node = null)
{
return $this->html5PhpSaveHTML($node);
}

#[\ReturnTypeWillChange]
public function importNode($node, $deep = false)
{
return $this->html5PhpImportNode($node, $deep);
}

#[\ReturnTypeWillChange]
public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}
}
47 changes: 47 additions & 0 deletions src/HTML5/HTML5DOMDocumentFragment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace Masterminds\HTML5;

/**
* Keep the template-aware owner document alive for detached fragments.
*/
class HTML5DOMDocumentFragmentBase extends \DOMDocumentFragment
{
/**
* @var \DOMDocument|null
*/
protected $templateOwnerDocument;

/**
* @param \DOMDocument $document
*/
public function html5PhpKeepTemplateOwnerDocument(\DOMDocument $document)
{
$this->templateOwnerDocument = $document;
}

protected function html5PhpCloneNode($deep = false)
{
$clone = parent::cloneNode($deep);

if ($this->templateOwnerDocument instanceof \DOMDocument && method_exists($clone, 'html5PhpKeepTemplateOwnerDocument')) {
$clone->html5PhpKeepTemplateOwnerDocument($this->templateOwnerDocument);
}

TemplateContents::copySubtree($this, $clone, $deep);

return $clone;
}
}

if (PHP_VERSION_ID >= 80100) {
require __DIR__ . '/HTML5DOMDocumentFragment81.php';
} else {
class HTML5DOMDocumentFragment extends HTML5DOMDocumentFragmentBase
{
public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}
}
}
12 changes: 12 additions & 0 deletions src/HTML5/HTML5DOMDocumentFragment81.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Masterminds\HTML5;

class HTML5DOMDocumentFragment extends HTML5DOMDocumentFragmentBase
{
#[\ReturnTypeWillChange]
public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}
}
115 changes: 115 additions & 0 deletions src/HTML5/HTML5DOMElement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace Masterminds\HTML5;

/**
* Shared template-aware behavior for DOMElement implementations.
*/
class HTML5DOMElementBase extends \DOMElement
{
/**
* @var \DOMDocumentFragment|null
*/
protected $templateContents;

/**
* @param \DOMDocumentFragment|null $fragment
*/
public function html5PhpSetTemplateContents($fragment)
{
$this->templateContents = $fragment;
}

/**
* @return \DOMDocumentFragment|null
*/
public function html5PhpTemplateContents()
{
return $this->templateContents;
}

protected function html5PhpHasDetachedTemplateContents()
{
return 'template' === strtolower($this->tagName) && $this->templateContents instanceof \DOMDocumentFragment;
}

protected function html5PhpCloneNode($deep = false)
{
$clone = parent::cloneNode($deep);
TemplateContents::copySubtree($this, $clone, $deep);

return $clone;
}

protected function html5PhpAppendChild($node)
{
if ($this->html5PhpHasDetachedTemplateContents()) {
return $this->templateContents->appendChild($node);
}

return parent::appendChild($node);
}

protected function html5PhpInsertBefore($newnode, $refnode = null)
{
if ($this->html5PhpHasDetachedTemplateContents()) {
if (null === $refnode) {
return $this->templateContents->appendChild($newnode);
}

return $this->templateContents->insertBefore($newnode, $refnode);
}

return parent::insertBefore($newnode, $refnode);
}

protected function html5PhpReplaceChild($newnode, $oldnode)
{
if ($this->html5PhpHasDetachedTemplateContents()) {
return $this->templateContents->replaceChild($newnode, $oldnode);
}

return parent::replaceChild($newnode, $oldnode);
}

protected function html5PhpRemoveChild($oldnode)
{
if ($this->html5PhpHasDetachedTemplateContents()) {
return $this->templateContents->removeChild($oldnode);
}

return parent::removeChild($oldnode);
}
}

if (PHP_VERSION_ID >= 80100) {
require __DIR__ . '/HTML5DOMElement81.php';
} else {
class HTML5DOMElement extends HTML5DOMElementBase
{
public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}

public function appendChild($node)
{
return $this->html5PhpAppendChild($node);
}

public function insertBefore($newnode, $refnode = null)
{
return $this->html5PhpInsertBefore($newnode, $refnode);
}

public function replaceChild($newnode, $oldnode)
{
return $this->html5PhpReplaceChild($newnode, $oldnode);
}

public function removeChild($oldnode)
{
return $this->html5PhpRemoveChild($oldnode);
}
}
}
36 changes: 36 additions & 0 deletions src/HTML5/HTML5DOMElement81.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Masterminds\HTML5;

class HTML5DOMElement extends HTML5DOMElementBase
{
#[\ReturnTypeWillChange]
public function cloneNode($deep = false)
{
return $this->html5PhpCloneNode($deep);
}

#[\ReturnTypeWillChange]
public function appendChild($node)
{
return $this->html5PhpAppendChild($node);
}

#[\ReturnTypeWillChange]
public function insertBefore($newnode, $refnode = null)
{
return $this->html5PhpInsertBefore($newnode, $refnode);
}

#[\ReturnTypeWillChange]
public function replaceChild($newnode, $oldnode)
{
return $this->html5PhpReplaceChild($newnode, $oldnode);
}

#[\ReturnTypeWillChange]
public function removeChild($oldnode)
{
return $this->html5PhpRemoveChild($oldnode);
}
}
Loading
Loading