php-dom-wrapper 3.0

Simple DOM wrapper library to manipulate and traverse HTML documents similar to jQuery (php8+)
This commit is contained in:
gtbu 2024-06-07 13:28:57 +02:00
parent 142c292a2f
commit 4ad157b14b
14 changed files with 3385 additions and 0 deletions

View file

@ -0,0 +1,169 @@
<?php declare(strict_types=1);
namespace DOMWrap\Collections;
/**
* Node List
*
* @package DOMWrap\Collections
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class NodeCollection implements \Countable, \ArrayAccess, \RecursiveIterator
{
/** @var array */
protected array $nodes = [];
/**
* @param iterable $nodes
*/
public function __construct(?iterable $nodes = null) {
if (!is_iterable($nodes)) {
$nodes = [];
}
foreach ($nodes as $node) {
$this->nodes[] = $node;
}
}
/**
* @see \Countable::count()
*
* @return int
*/
public function count(): int {
return count($this->nodes);
}
/**
* @see \ArrayAccess::offsetExists()
*
* @param mixed $offset
*
* @return bool
*/
public function offsetExists(mixed $offset): bool {
return isset($this->nodes[$offset]);
}
/**
* @see \ArrayAccess::offsetGet()
*
* @param mixed $offset
*
* @return mixed
*/
public function offsetGet(mixed $offset): mixed {
return isset($this->nodes[$offset]) ? $this->nodes[$offset] : null;
}
/**
* @see \ArrayAccess::offsetSet()
*
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet(mixed $offset, mixed $value): void {
if (is_null($offset)) {
$this->nodes[] = $value;
} else {
$this->nodes[$offset] = $value;
}
}
/**
* @see \ArrayAccess::offsetUnset()
*
* @param mixed $offset
*/
public function offsetUnset(mixed $offset): void {
unset($this->nodes[$offset]);
}
/**
* @see \RecursiveIterator::RecursiveIteratorIterator()
*
* @return \RecursiveIteratorIterator
*/
public function getRecursiveIterator(): \RecursiveIteratorIterator {
return new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
}
/**
* @see \RecursiveIterator::getChildren()
*
* @return \RecursiveIterator
*/
public function getChildren(): \RecursiveIterator {
$nodes = [];
if ($this->valid()) {
$nodes = $this->current()->childNodes;
}
return new static($nodes);
}
/**
* @see \RecursiveIterator::hasChildren()
*
* @return bool
*/
public function hasChildren(): bool {
if ($this->valid()) {
return $this->current()->hasChildNodes();
}
return false;
}
/**
* @see \RecursiveIterator::current()
* @see \Iterator::current()
*
* @return mixed
*/
public function current(): mixed {
return current($this->nodes);
}
/**
* @see \RecursiveIterator::key()
* @see \Iterator::key()
*
* @return mixed
*/
public function key(): mixed {
return key($this->nodes);
}
/**
* @see \RecursiveIterator::next()
* @see \Iterator::next()
*
* @return void
*/
public function next(): void {
next($this->nodes);
}
/**
* @see \RecursiveIterator::rewind()
* @see \Iterator::rewind()
*
* @return void
*/
public function rewind(): void {
reset($this->nodes);
}
/**
* @see \RecursiveIterator::valid()
* @see \Iterator::valid()
*
* @return bool
*/
public function valid(): bool {
return key($this->nodes) !== null;
}
}

24
include/thirdparty/dom/Comment.php vendored Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Comment Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Comment extends \DOMComment
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

346
include/thirdparty/dom/Document.php vendored Normal file
View file

@ -0,0 +1,346 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Document Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Document extends \DOMDocument
{
use CommonTrait;
use TraversalTrait;
use ManipulationTrait;
/** @var int */
protected $libxmlOptions = LIBXML_NONET | LIBXML_HTML_NODEFDTD;
/** @var string|null */
protected $documentEncoding = null;
public function __construct(string $version = '1.0', string $encoding = 'UTF-8') {
parent::__construct($version, $encoding);
$this->registerNodeClass('DOMText', 'DOMWrap\\Text');
$this->registerNodeClass('DOMElement', 'DOMWrap\\Element');
$this->registerNodeClass('DOMComment', 'DOMWrap\\Comment');
$this->registerNodeClass('DOMDocument', 'DOMWrap\\Document');
$this->registerNodeClass('DOMDocumentType', 'DOMWrap\\DocumentType');
$this->registerNodeClass('DOMProcessingInstruction', 'DOMWrap\\ProcessingInstruction');
}
/**
* Set libxml options.
*
* Multiple values must use bitwise OR.
* eg: LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
*
* @link http://php.net/manual/en/libxml.constants.php
*
* @param int $libxmlOptions
*/
public function setLibxmlOptions(int $libxmlOptions): void {
$this->libxmlOptions = $libxmlOptions;
}
/**
* {@inheritdoc}
*/
public function document(): ?\DOMDocument {
return $this;
}
/**
* {@inheritdoc}
*/
public function collection(): NodeList {
return $this->newNodeList([$this]);
}
/**
* {@inheritdoc}
*/
public function result(NodeList $nodeList): NodeList|\DOMNode|null {
if ($nodeList->count()) {
return $nodeList->first();
}
return null;
}
/**
* {@inheritdoc}
*/
public function parent(string|NodeList|\DOMNode|callable|null $selector = null): Document|Element|NodeList|null {
return null;
}
/**
* {@inheritdoc}
*/
public function parents(?string $selector = null): NodeList {
return $this->newNodeList();
}
/**
* {@inheritdoc}
*/
public function substituteWith(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->replaceChild($newNode, $node);
}
});
return $this;
}
/**
* {@inheritdoc}
*/
public function _clone(): void {
return;
}
/**
* {@inheritdoc}
*/
public function getHtml(bool $isIncludeAll = false): string {
return $this->getOuterHtml($isIncludeAll);
}
/**
* {@inheritdoc}
*/
public function setHtml(string|NodeList|\DOMNode|callable $input): self {
if (!is_string($input) || trim($input) == '') {
return $this;
}
$internalErrors = libxml_use_internal_errors(true);
if (\PHP_VERSION_ID < 80000) {
$disableEntities = libxml_disable_entity_loader(true);
$this->composeXmlNode($input);
libxml_use_internal_errors($internalErrors);
libxml_disable_entity_loader($disableEntities);
} else {
$this->composeXmlNode($input);
libxml_use_internal_errors($internalErrors);
}
return $this;
}
/**
* @param string $html
* @param int $options
*
* @return bool
*/
public function loadHTML(string $html, int $options = 0): bool {
// Fix LibXML's crazy-ness RE root nodes
// While importing HTML using the LIBXML_HTML_NOIMPLIED option LibXML insists
// on having one root node. All subsequent nodes are appended to this first node.
// To counter this we will create a fake element, allow LibXML to 'do its thing'
// then undo it by taking the contents of the fake element, placing it back into
// the root and then remove our fake element.
if ($options & LIBXML_HTML_NOIMPLIED) {
$html = '<domwrap></domwrap>' . $html;
}
$html = '<?xml encoding="' . ($this->getEncoding() ?? 'UTF-8') . '">' . $html;
$result = parent::loadHTML($html, $options);
// Do our re-shuffling of nodes.
if ($this->libxmlOptions & LIBXML_HTML_NOIMPLIED) {
$this->children()->first()->contents()->each(function($node){
$this->appendWith($node);
});
$this->removeChild($this->children()->first());
}
return $result;
}
/**
* @param \DOMNode $node
*
* @return string|bool
*/
public function saveHTML(?\DOMNode $node = null): string|false {
$target = $node ?: $this;
// Undo any url encoding of attributes automatically applied by LibXML.
// See htmlAttrDumpOutput() in:
// https://github.com/GNOME/libxml2/blob/master/HTMLtree.c
$i = 0;
$search = [];
$replace = [];
$escapes = [
['attr' => 'src'],
['attr' => 'href'],
['attr' => 'action'],
['attr' => 'name', 'tag' => 'a'],
];
$nodes = $target->find('*[src],*[href],*[action],a[name]', 'descendant-or-self::');
foreach ($nodes as $node) {
foreach ($escapes as $escape) {
if (
(!array_key_exists('tag', $escape) || strcasecmp($node->tagName, $escape['tag']) === 0)
&& $node->hasAttribute($escape['attr'])
) {
$value = $node->getAttribute($escape['attr']);
$newName = 'DOMWRAP--ATTR-' . $i . '--' . $escape['attr'];
$node->setAttribute($newName, $value);
$node->removeAttribute($escape['attr']);
// Determine if the attribute will be wrapped in single
// or double quotes and further encodings to apply.
//
// See xmlBufWriteQuotedString() in:
// https://github.com/GNOME/libxml2/blob/master/buf.c
$hasQuot = strstr($value, '"');
$hasApos = strstr($value, "'");
if ($hasQuot && $hasApos) {
$value = str_replace('"', '&quot;', $value);
}
$char = '"';
if ($hasQuot && !$hasApos) {
$char = "'";
}
// See xmlEscapeEntities() in:
// https://github.com/GNOME/libxml2/blob/master/xmlsave.c
$searchValue = str_replace(['<', '>', '&'], ['&lt;', '&gt;', '&amp;'], $value);
$search[] = $newName. '=' . $char . $searchValue . $char;
$replace[] = $escape['attr']. '=' . $char . $value . $char;
$i++;
}
}
}
$html = parent::saveHTML($target);
$html = str_replace($search, $replace, $html);
return $html;
}
/*
* @param $encoding string|null
*/
public function setEncoding(?string $encoding = null): void {
$this->documentEncoding = $encoding;
}
/*
* @return string|null
*/
public function getEncoding(): ?string {
return $this->documentEncoding;
}
/*
* @param $html string
*
* @return string|null
*/
private function getCharset(string $html): ?string {
$charset = null;
if (preg_match('@<meta[^>]*?charset=["\']?([^"\'\s>]+)@im', $html, $matches)) {
$charset = mb_strtoupper($matches[1]);
}
return $charset;
}
/*
* @param $html string
*/
private function detectEncoding(string $html): void {
$charset = $this->getEncoding();
if (is_null($charset)) {
$charset = $this->getCharset($html);
}
$detectedCharset = mb_detect_encoding($html, mb_detect_order(), true);
if ($charset === null && $detectedCharset == 'UTF-8') {
$charset = $detectedCharset;
}
$this->setEncoding($charset);
}
/*
* @param $html string
*
* @return string
*/
private function convertToUtf8(string $html): string {
$charset = $this->getEncoding();
if ($charset !== null) {
$html = preg_replace('@(charset=["]?)([^"\s]+)([^"]*["]?)@im', '$1UTF-8$3', $html);
$mbHasCharset = in_array($charset, array_map('mb_strtoupper', mb_list_encodings()));
if ($mbHasCharset) {
$html = mb_convert_encoding($html, 'UTF-8', $charset);
// Fallback to iconv if available.
} elseif (extension_loaded('iconv')) {
$htmlIconv = iconv($charset, 'UTF-8', $html);
if ($htmlIconv !== false) {
$html = $htmlIconv;
} else {
$charset = null;
}
}
}
if ($charset === null) {
$html = htmlspecialchars_decode(mb_encode_numericentity(htmlentities($html, ENT_QUOTES, 'UTF-8'), [0x80, 0x10FFFF, 0, ~0], 'UTF-8'));
}
return $html;
}
/**
* @param $html string
*/
private function composeXmlNode(string $html): void {
$this->detectEncoding($html);
$html = $this->convertToUtf8($html);
$this->loadHTML($html, $this->libxmlOptions);
// Remove <?xml ...> processing instruction.
$this->contents()->each(function($node) {
if ($node instanceof ProcessingInstruction && $node->nodeName == 'xml') {
$node->destroy();
}
});
}
}

24
include/thirdparty/dom/DocumentType.php vendored Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* DocumentType Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class DocumentType extends \DOMDocumentType
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

24
include/thirdparty/dom/Element.php vendored Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Element Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Element extends \DOMElement
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

30
include/thirdparty/dom/LICENSE vendored Normal file
View file

@ -0,0 +1,30 @@
https://github.com/scotteh/php-dom-wrapper/releases/tag/3.0.0
Copyright (c) 2015, Andrew Scott
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of php-dom-wrapper nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

297
include/thirdparty/dom/NodeList.php vendored Normal file
View file

@ -0,0 +1,297 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
CommonTrait,
TraversalTrait,
ManipulationTrait
};
use DOMWrap\Collections\NodeCollection;
/**
* Node List
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class NodeList extends NodeCollection
{
use CommonTrait;
use TraversalTrait;
use ManipulationTrait {
ManipulationTrait::__call as __manipulationCall;
}
/** @var Document */
protected Document $document;
/**
* @param Document $document
* @param iterable $nodes
*/
public function __construct(Document $document = null, ?iterable $nodes = null) {
parent::__construct($nodes);
$this->document = $document;
}
/**
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call(string $name, array $arguments) {
try {
$result = $this->__manipulationCall($name, $arguments);
} catch (\BadMethodCallException $e) {
if (!$this->first() || !method_exists($this->first(), $name)) {
throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
}
$result = call_user_func_array([$this->first(), $name], $arguments);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function collection(): NodeList {
return $this;
}
/**
* {@inheritdoc}
*/
public function document(): ?\DOMDocument {
return $this->document;
}
/**
* {@inheritdoc}
*/
public function result(NodeList $nodeList): NodeList|\DOMNode|null {
return $nodeList;
}
/**
* @return NodeList
*/
public function reverse(): NodeList {
array_reverse($this->nodes);
return $this;
}
/**
* @return mixed
*/
public function first(): mixed {
if (!empty($this->nodes)) {
$this->rewind();
return $this->current();
}
return null;
}
/**
* @return mixed
*/
public function last(): mixed {
return $this->end();
}
/**
* @return mixed
*/
public function end(): mixed {
return !empty($this->nodes) ? end($this->nodes) : null;
}
/**
* @param int $key
*
* @return mixed
*/
public function get(int $key): mixed {
if (isset($this->nodes[$key])) {
return $this->nodes[$key];
}
return null;
}
/**
* @param int $key
* @param mixed $value
*
* @return self
*/
public function set(int $key, mixed $value): self {
$this->nodes[$key] = $value;
return $this;
}
/**
* @param callable $function
*
* @return self
*/
public function each(callable $function): self {
foreach ($this->nodes as $index => $node) {
$result = $function($node, $index);
if ($result === false) {
break;
}
}
return $this;
}
/**
* @param callable $function
*
* @return NodeList
*/
public function map(callable $function): NodeList {
$nodes = $this->newNodeList();
foreach ($this->nodes as $node) {
$result = $function($node);
if (!is_null($result) && $result !== false) {
$nodes[] = $result;
}
}
return $nodes;
}
/**
* @param callable $function
* @param mixed|null $initial
*
* @return iterable
*/
public function reduce(callable $function, mixed $initial = null) {
return array_reduce($this->nodes, $function, $initial);
}
/**
* @return array
*/
public function toArray(): iterable {
return $this->nodes;
}
/**
* @param iterable $nodes
*/
public function fromArray(?iterable $nodes = null) {
$this->nodes = [];
if (is_iterable($nodes)) {
foreach ($nodes as $node) {
$this->nodes[] = $node;
}
}
}
/**
* @param NodeList|array $elements
*
* @return NodeList
*/
public function merge(NodeList|array $elements = []): NodeList {
if (!is_array($elements)) {
$elements = $elements->toArray();
}
return $this->newNodeList(array_merge($this->toArray(), $elements));
}
/**
* @param int $start
* @param int $end
*
* @return NodeList
*/
public function slice(int $start, ?int $end = null): NodeList {
$nodeList = array_slice($this->toArray(), $start, $end);
return $this->newNodeList($nodeList);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function push(\DOMNode $node): self {
$this->nodes[] = $node;
return $this;
}
/**
* @return \DOMNode
*/
public function pop(): \DOMNode {
return array_pop($this->nodes);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function unshift(\DOMNode $node): self {
array_unshift($this->nodes, $node);
return $this;
}
/**
* @return \DOMNode
*/
public function shift(): \DOMNode {
return array_shift($this->nodes);
}
/**
* @param \DOMNode $node
*
* @return bool
*/
public function exists(\DOMNode $node): bool {
return in_array($node, $this->nodes, true);
}
/**
* @param \DOMNode $node
*
* @return self
*/
public function delete(\DOMNode $node): self {
$index = array_search($node, $this->nodes, true);
if ($index !== false) {
unset($this->nodes[$index]);
}
return $this;
}
/**
* @return bool
*/
public function isRemoved(): bool {
return false;
}
}

View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* ProcessingInstruction Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class ProcessingInstruction extends \DOMProcessingInstruction
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

1123
include/thirdparty/dom/README.md vendored Normal file

File diff suppressed because it is too large Load diff

24
include/thirdparty/dom/Text.php vendored Normal file
View file

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace DOMWrap;
use DOMWrap\Traits\{
NodeTrait,
CommonTrait,
TraversalTrait,
ManipulationTrait
};
/**
* Text Node
*
* @package DOMWrap
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
class Text extends \DOMText
{
use CommonTrait;
use NodeTrait;
use TraversalTrait;
use ManipulationTrait;
}

View file

@ -0,0 +1,38 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\NodeList;
/**
* Common Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait CommonTrait
{
/**
* @return NodeList
*/
abstract public function collection(): NodeList;
/**
* @return \DOMDocument
*/
abstract public function document(): ?\DOMDocument;
/**
* @param NodeList $nodeList
*
* @return NodeList|\DOMNode|null
*/
abstract public function result(NodeList $nodeList): NodeList|\DOMNode|null;
/**
* @return bool
*/
public function isRemoved(): bool {
return !isset($this->nodeType);
}
}

View file

@ -0,0 +1,748 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\{
Text,
Element,
NodeList
};
/**
* Manipulation Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait ManipulationTrait
{
/**
* Magic method - Trap function names using reserved keyword (empty, clone, etc..)
*
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public function __call(string $name, array $arguments): mixed {
if (!method_exists($this, '_' . $name)) {
throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
}
return call_user_func_array([$this, '_' . $name], $arguments);
}
/**
* @return string
*/
public function __toString(): string {
return $this->getOuterHtml(true);
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return iterable
*/
protected function inputPrepareAsTraversable(string|NodeList|\DOMNode $input): iterable {
if ($input instanceof \DOMNode) {
// Handle raw \DOMNode elements and 'convert' them into their DOMWrap/* counterpart
if (!method_exists($input, 'inputPrepareAsTraversable')) {
$input = $this->document()->importNode($input, true);
}
$nodes = [$input];
} else if (is_string($input)) {
$nodes = $this->nodesFromHtml($input);
} else if (is_iterable($input)) {
$nodes = $input;
} else {
throw new \InvalidArgumentException();
}
return $nodes;
}
/**
* @param string|NodeList|\DOMNode $input
* @param bool $cloneForManipulate
*
* @return NodeList
*/
protected function inputAsNodeList(string|NodeList|\DOMNode $input, bool $cloneForManipulate = true): NodeList {
$nodes = $this->inputPrepareAsTraversable($input);
$newNodes = $this->newNodeList();
foreach ($nodes as $node) {
if ($node->document() !== $this->document()) {
$node = $this->document()->importNode($node, true);
}
if ($cloneForManipulate && $node->parentNode !== null) {
$node = $node->cloneNode(true);
}
$newNodes[] = $node;
}
return $newNodes;
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return \DOMNode|null
*/
protected function inputAsFirstNode(string|NodeList|\DOMNode $input): ?\DOMNode {
$nodes = $this->inputAsNodeList($input);
return $nodes->findXPath('self::*')->first();
}
/**
* @param string $html
*
* @return NodeList
*/
protected function nodesFromHtml(string $html): NodeList {
$class = get_class($this->document());
$doc = new $class();
$doc->setEncoding($this->document()->getEncoding());
$nodes = $doc->html($html)->find('body')->contents();
return $nodes;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param callable $callback
*
* @return self
*/
protected function manipulateNodesWithInput(string|NodeList|\DOMNode|callable $input, callable $callback): self {
$this->collection()->each(function($node, $index) use ($input, $callback) {
$html = $input;
/*if ($input instanceof \DOMNode) {
if ($input->parentNode !== null) {
$html = $input->cloneNode(true);
}
} else*/if (is_callable($input)) {
$html = $input($node, $index);
}
$newNodes = $this->inputAsNodeList($html);
$callback($node, $newNodes);
});
return $this;
}
/**
* @param string|null $selector
*
* @return NodeList
*/
public function detach(?string $selector = null): NodeList {
if (!is_null($selector)) {
$nodes = $this->find($selector, 'self::');
} else {
$nodes = $this->collection();
}
$nodeList = $this->newNodeList();
$nodes->each(function($node) use($nodeList) {
if ($node->parent() instanceof \DOMNode) {
$nodeList[] = $node->parent()->removeChild($node);
}
});
$nodes->fromArray([]);
return $nodeList;
}
/**
* @param string|null $selector
*
* @return self
*/
public function destroy(?string $selector = null): self {
$this->detach($selector);
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function substituteWith(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->parent()->replaceChild($newNode, $node);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return string|self
*/
public function text(string|NodeList|\DOMNode|callable|null $input = null): string|self {
if (is_null($input)) {
return $this->getText();
} else {
return $this->setText($input);
}
}
/**
* @return string
*/
public function getText(): string {
return (string)$this->collection()->reduce(function($carry, $node) {
return $carry . $node->textContent;
}, '');
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function setText(string|NodeList|\DOMNode|callable $input): self {
if (is_string($input)) {
$input = new Text($input);
}
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
// Remove old contents from the current node.
$node->contents()->destroy();
// Add new contents in it's place.
$node->appendWith(new Text($newNodes->getText()));
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function precede(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->parent()->insertBefore($newNode, $node);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function follow(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
if (is_null($node->following())) {
$node->parent()->appendChild($newNode);
} else {
$node->parent()->insertBefore($newNode, $node->following());
}
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function prependWith(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->insertBefore($newNode, $node->contents()->first());
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function appendWith(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
foreach ($newNodes as $newNode) {
$node->appendChild($newNode);
}
});
return $this;
}
/**
* @param string|NodeList|\DOMNode $selector
*
* @return self
*/
public function prependTo(string|NodeList|\DOMNode $selector): self {
if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
$nodes = $this->inputAsNodeList($selector);
} else {
$nodes = $this->document()->find($selector);
}
$nodes->prependWith($this);
return $this;
}
/**
* @param string|NodeList|\DOMNode $selector
*
* @return self
*/
public function appendTo(string|NodeList|\DOMNode $selector): self {
if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
$nodes = $this->inputAsNodeList($selector);
} else {
$nodes = $this->document()->find($selector);
}
$nodes->appendWith($this);
return $this;
}
/**
* @return self
*/
public function _empty(): self {
$this->collection()->each(function($node) {
$node->contents()->destroy();
});
return $this;
}
/**
* @return NodeList|\DOMNode
*/
public function _clone(): NodeList|\DOMNode {
$clonedNodes = $this->newNodeList();
$this->collection()->each(function($node) use($clonedNodes) {
$clonedNodes[] = $node->cloneNode(true);
});
return $this->result($clonedNodes);
}
/**
* @param string $name
*
* @return self
*/
public function removeAttr(string $name): self {
$this->collection()->each(function($node) use($name) {
if ($node instanceof \DOMElement) {
$node->removeAttribute($name);
}
});
return $this;
}
/**
* @param string $name
*
* @return bool
*/
public function hasAttr(string $name): bool {
return (bool)$this->collection()->reduce(function($carry, $node) use ($name) {
if ($node->hasAttribute($name)) {
return true;
}
return $carry;
}, false);
}
/**
* @internal
*
* @param string $name
*
* @return string
*/
public function getAttr(string $name): string {
$node = $this->collection()->first();
if (!($node instanceof \DOMElement)) {
return '';
}
return $node->getAttribute($name);
}
/**
* @internal
*
* @param string $name
* @param mixed $value
*
* @return self
*/
public function setAttr(string $name, mixed $value): self {
$this->collection()->each(function($node) use($name, $value) {
if ($node instanceof \DOMElement) {
$node->setAttribute($name, (string)$value);
}
});
return $this;
}
/**
* @param string $name
* @param mixed $value
*
* @return self|string
*/
public function attr(string $name, mixed $value = null): string|self {
if (is_null($value)) {
return $this->getAttr($name);
} else {
return $this->setAttr($name, $value);
}
}
/**
* @internal
*
* @param string $name
* @param string|callable $value
* @param bool $addValue
*/
protected function _pushAttrValue(string $name, string|callable $value, bool $addValue = false): void {
$this->collection()->each(function($node, $index) use($name, $value, $addValue) {
if ($node instanceof \DOMElement) {
$attr = $node->getAttribute($name);
if (is_callable($value)) {
$value = $value($node, $index, $attr);
}
// Remove any existing instances of the value, or empty values.
$values = array_filter(explode(' ', $attr), function($_value) use($value) {
if (strcasecmp($_value, $value) == 0 || empty($_value)) {
return false;
}
return true;
});
// If required add attr value to array
if ($addValue) {
$values[] = $value;
}
// Set the attr if we either have values, or the attr already
// existed (we might be removing classes).
//
// Don't set the attr if it doesn't already exist.
if (!empty($values) || $node->hasAttribute($name)) {
$node->setAttribute($name, implode(' ', $values));
}
}
});
}
/**
* @param string|callable $class
*
* @return self
*/
public function addClass(string|callable $class): self {
$this->_pushAttrValue('class', $class, true);
return $this;
}
/**
* @param string|callable $class
*
* @return self
*/
public function removeClass(string|callable $class): self {
$this->_pushAttrValue('class', $class);
return $this;
}
/**
* @param string $class
*
* @return bool
*/
public function hasClass(string $class): bool {
return (bool)$this->collection()->reduce(function($carry, $node) use ($class) {
$attr = $node->getAttr('class');
return array_reduce(explode(' ', (string)$attr), function($carry, $item) use ($class) {
if (strcasecmp($item, $class) == 0) {
return true;
}
return $carry;
}, false);
}, false);
}
/**
* @param Element $node
*
* @return \SplStack
*/
protected function _getFirstChildWrapStack(Element $node): \SplStack {
$stack = new \SplStack;
do {
// Push our current node onto the stack
$stack->push($node);
// Get the first element child node
$node = $node->children()->first();
} while ($node instanceof Element);
// Get the top most node.
return $stack;
}
/**
* @param Element $node
*
* @return \SplStack
*/
protected function _prepareWrapStack(Element $node): \SplStack {
// Generate a stack (root to leaf) of the wrapper.
// Includes only first element nodes / first element children.
$stackNodes = $this->_getFirstChildWrapStack($node);
// Only using the first element, remove any siblings.
foreach ($stackNodes as $stackNode) {
$stackNode->siblings()->destroy();
}
return $stackNodes;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param callable $callback
*/
protected function wrapWithInputByCallback(string|NodeList|\DOMNode|callable $input, callable $callback): void {
$this->collection()->each(function($node, $index) use ($input, $callback) {
$html = $input;
if (is_callable($input)) {
$html = $input($node, $index);
}
$inputNode = $this->inputAsFirstNode($html);
if ($inputNode instanceof Element) {
// Pre-process wrapper into a stack of first element nodes.
$stackNodes = $this->_prepareWrapStack($inputNode);
$callback($node, $stackNodes);
}
});
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrapInner(string|NodeList|\DOMNode|callable $input): self {
$this->wrapWithInputByCallback($input, function($node, $stackNodes) {
foreach ($node->contents() as $child) {
// Remove child from the current node
$oldChild = $child->detach()->first();
// Add it back as a child of the top (leaf) node on the stack
$stackNodes->top()->appendWith($oldChild);
}
// Add the bottom (root) node on the stack
$node->appendWith($stackNodes->bottom());
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrap(string|NodeList|\DOMNode|callable $input): self {
$this->wrapWithInputByCallback($input, function($node, $stackNodes) {
// Add the new bottom (root) node after the current node
$node->follow($stackNodes->bottom());
// Remove the current node
$oldNode = $node->detach()->first();
// Add the 'current node' back inside the new top (leaf) node.
$stackNodes->top()->appendWith($oldNode);
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function wrapAll(string|NodeList|\DOMNode|callable $input): self {
if (!$this->collection()->count()) {
return $this;
}
if (is_callable($input)) {
$input = $input($this->collection()->first());
}
$inputNode = $this->inputAsFirstNode($input);
if (!($inputNode instanceof Element)) {
return $this;
}
$stackNodes = $this->_prepareWrapStack($inputNode);
// Add the new bottom (root) node before the first matched node
$this->collection()->first()->precede($stackNodes->bottom());
$this->collection()->each(function($node) use ($stackNodes) {
// Detach and add node back inside the new wrappers top (leaf) node.
$stackNodes->top()->appendWith($node->detach());
});
return $this;
}
/**
* @return self
*/
public function unwrap(): self {
$this->collection()->each(function($node) {
$parent = $node->parent();
// Replace parent node (the one we're unwrapping) with it's children.
$parent->contents()->each(function($childNode) use($parent) {
$oldChildNode = $childNode->detach()->first();
$parent->precede($oldChildNode);
});
$parent->destroy();
});
return $this;
}
/**
* @param bool $isIncludeAll
*
* @return string
*/
public function getOuterHtml(bool $isIncludeAll = false): string {
$nodes = $this->collection();
if (!$isIncludeAll) {
$nodes = $this->newNodeList([$nodes->first()]);
}
return $nodes->reduce(function($carry, $node) {
return $carry . $this->document()->saveHTML($node);
}, '');
}
/**
* @param bool $isIncludeAll
*
* @return string
*/
public function getHtml(bool $isIncludeAll = false): string {
$nodes = $this->collection();
if (!$isIncludeAll) {
$nodes = $this->newNodeList([$nodes->first()]);
}
return $nodes->contents()->reduce(function($carry, $node) {
return $carry . $this->document()->saveHTML($node);
}, '');
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return self
*/
public function setHtml(string|NodeList|\DOMNode|callable $input): self {
$this->manipulateNodesWithInput($input, function($node, $newNodes) {
// Remove old contents from the current node.
$node->contents()->destroy();
// Add new contents in it's place.
$node->appendWith($newNodes);
});
return $this;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return string|self
*/
public function html(string|NodeList|\DOMNode|callable|null $input = null): string|self {
if (is_null($input)) {
return $this->getHtml();
} else {
return $this->setHtml($input);
}
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return NodeList
*/
public function create(string|NodeList|\DOMNode $input): NodeList {
return $this->inputAsNodeList($input);
}
}

View file

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\NodeList;
/**
* Node Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
* @property \DOMDocument $ownerDocument
*/
trait NodeTrait
{
/**
* @return NodeList
*/
public function collection(): NodeList {
return $this->newNodeList([$this]);
}
/**
* @return \DOMDocument
*/
public function document(): ?\DOMDocument {
if ($this->isRemoved()) {
return null;
}
return $this->ownerDocument;
}
/**
* @param NodeList $nodeList
*
* @return NodeList|\DOMNode|null
*/
public function result(NodeList $nodeList): NodeList|\DOMNode|null {
if ($nodeList->count()) {
return $nodeList->first();
}
return null;
}
}

View file

@ -0,0 +1,468 @@
<?php declare(strict_types=1);
namespace DOMWrap\Traits;
use DOMWrap\{
Document,
Element,
NodeList
};
use Symfony\Component\CssSelector\CssSelectorConverter;
/**
* Traversal Trait
*
* @package DOMWrap\Traits
* @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
*/
trait TraversalTrait
{
protected static ?CssSelectorConverter $cssSelectorConverter = null;
/**
* @param iterable $nodes
*
* @return NodeList
*/
public function newNodeList(?iterable $nodes = null): NodeList {
if (!is_iterable($nodes)) {
if (!is_null($nodes)) {
$nodes = [$nodes];
} else {
$nodes = [];
}
}
return new NodeList($this->document(), $nodes);
}
/**
* @param string $selector
* @param string $prefix
*
* @return NodeList
*/
public function find(string $selector, string $prefix = 'descendant::'): NodeList {
if (!self::$cssSelectorConverter) {
self::$cssSelectorConverter = new CssSelectorConverter();
}
return $this->findXPath(self::$cssSelectorConverter->toXPath($selector, $prefix));
}
/**
* @param string $xpath
*
* @return NodeList
*/
public function findXPath(string $xpath): NodeList {
$results = $this->newNodeList();
if ($this->isRemoved()) {
return $results;
}
$domxpath = new \DOMXPath($this->document());
foreach ($this->collection() as $node) {
$results = $results->merge(
$node->newNodeList($domxpath->query($xpath, $node))
);
}
return $results;
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param bool $matchType
*
* @return NodeList
*/
protected function getNodesMatchingInput(string|NodeList|\DOMNode|callable $input, bool $matchType = true): NodeList {
if ($input instanceof NodeList || $input instanceof \DOMNode) {
$inputNodes = $this->inputAsNodeList($input, false);
$fn = function($node) use ($inputNodes) {
return $inputNodes->exists($node);
};
} elseif (is_callable($input)) {
// Since we're at the behest of the input callable, the 'matched'
// return value is always true.
$matchType = true;
$fn = $input;
} elseif (is_string($input)) {
$fn = function($node) use ($input) {
return $node->find($input, 'self::')->count() != 0;
};
} else {
throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
}
// Build a list of matching nodes.
return $this->collection()->map(function($node) use ($fn, $matchType) {
if ($fn($node) !== $matchType) {
return null;
}
return $node;
});
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return bool
*/
public function is(string|NodeList|\DOMNode|callable $input): bool {
return $this->getNodesMatchingInput($input)->count() != 0;
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function not(string|NodeList|\DOMNode|callable $input): NodeList {
return $this->getNodesMatchingInput($input, false);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function filter(string|NodeList|\DOMNode|callable $input): NodeList {
return $this->getNodesMatchingInput($input);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return NodeList
*/
public function has(string|NodeList|\DOMNode|callable $input): NodeList {
if ($input instanceof NodeList || $input instanceof \DOMNode) {
$inputNodes = $this->inputAsNodeList($input, false);
$fn = function($node) use ($inputNodes) {
$descendantNodes = $node->find('*', 'descendant::');
// Determine if we have a descendant match.
return $inputNodes->reduce(function($carry, $inputNode) use ($descendantNodes) {
// Match descendant nodes against input nodes.
if ($descendantNodes->exists($inputNode)) {
return true;
}
return $carry;
}, false);
};
} elseif (is_string($input)) {
$fn = function($node) use ($input) {
return $node->find($input, 'descendant::')->count() != 0;
};
} elseif (is_callable($input)) {
$fn = $input;
} else {
throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
}
return $this->getNodesMatchingInput($fn);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return \DOMNode|null
*/
public function preceding(string|NodeList|\DOMNode|callable|null $selector = null): ?\DOMNode {
return $this->precedingUntil(null, $selector)->first();
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function precedingAll(string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
return $this->precedingUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function precedingUntil(string|NodeList|\DOMNode|callable|null $input = null, string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
return $this->_walkPathUntil('previousSibling', $input, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return \DOMNode|null
*/
public function following(string|NodeList|\DOMNode|callable|null $selector = null): ?\DOMNode {
return $this->followingUntil(null, $selector)->first();
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function followingAll(string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
return $this->followingUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function followingUntil(string|NodeList|\DOMNode|callable|null $input = null, string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
return $this->_walkPathUntil('nextSibling', $input, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function siblings(string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
$results = $this->collection()->reduce(function($carry, $node) use ($selector) {
return $carry->merge(
$node->precedingAll($selector)->merge(
$node->followingAll($selector)
)
);
}, $this->newNodeList());
return $results;
}
/**
* NodeList is only array like. Removing items using foreach() has undesired results.
*
* @return NodeList
*/
public function children(): NodeList {
$results = $this->collection()->reduce(function($carry, $node) {
return $carry->merge(
$node->findXPath('child::*')
);
}, $this->newNodeList());
return $results;
}
/**
* @param string|NodeList|\DOMNode|callable $selector
*
* @return Document|Element|NodeList|null
*/
public function parent(string|NodeList|\DOMNode|callable|null $selector = null): Document|Element|NodeList|null {
$results = $this->_walkPathUntil('parentNode', null, $selector, self::$MATCH_TYPE_FIRST);
return $this->result($results);
}
/**
* @param int $index
*
* @return \DOMNode|null
*/
public function eq(int $index): ?\DOMNode {
if ($index < 0) {
$index = $this->collection()->count() + $index;
}
return $this->collection()->offsetGet($index);
}
/**
* @param string $selector
*
* @return NodeList
*/
public function parents(string $selector = null): NodeList {
return $this->parentsUntil(null, $selector);
}
/**
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
*
* @return NodeList
*/
public function parentsUntil(string|NodeList|\DOMNode|callable|null $input = null, string|NodeList|\DOMNode|callable|null $selector = null): NodeList {
return $this->_walkPathUntil('parentNode', $input, $selector);
}
/**
* @return \DOMNode
*/
public function intersect(): \DOMNode {
if ($this->collection()->count() < 2) {
return $this->collection()->first();
}
$nodeParents = [];
// Build a multi-dimensional array of the collection nodes parent elements
$this->collection()->each(function($node) use(&$nodeParents) {
$nodeParents[] = $node->parents()->unshift($node)->toArray();
});
// Find the common parent
$diff = call_user_func_array('array_uintersect', array_merge($nodeParents, [function($a, $b) {
return strcmp(spl_object_hash($a), spl_object_hash($b));
}]));
return array_shift($diff);
}
/**
* @param string|NodeList|\DOMNode|callable $input
*
* @return Document|Element|NodeList|null
*/
public function closest(string|NodeList|\DOMNode|callable|null $input): Document|Element|NodeList|null {
$results = $this->_walkPathUntil('parentNode', $input, null, self::$MATCH_TYPE_LAST);
return $this->result($results);
}
/**
* NodeList is only array like. Removing items using foreach() has undesired results.
*
* @return NodeList
*/
public function contents(): NodeList {
$results = $this->collection()->reduce(function($carry, $node) {
if ($node->isRemoved()) {
return $carry;
}
return $carry->merge(
$node->newNodeList($node->childNodes)
);
}, $this->newNodeList());
return $results;
}
/**
* @param string|NodeList|\DOMNode $input
*
* @return NodeList
*/
public function add(string|NodeList|\DOMNode $input): NodeList {
$nodes = $this->inputAsNodeList($input);
$results = $this->collection()->merge(
$nodes
);
return $results;
}
/** @var int */
private static $MATCH_TYPE_FIRST = 1;
/** @var int */
private static $MATCH_TYPE_LAST = 2;
/**
* @param \DOMNode $baseNode
* @param string $property
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
* @param int $matchType
*
* @return NodeList
*/
protected function _buildNodeListUntil(\DOMNode $baseNode, string $property, string|NodeList|\DOMNode|callable|null $input = null, string|NodeList|\DOMNode|callable|null $selector = null, ?int $matchType = null): NodeList {
$resultNodes = $this->newNodeList();
// Get our first node
$node = $baseNode->$property;
// Keep looping until we are out of nodes.
// Allow either FIRST to reach \DOMDocument. Others that return multiple should ignore it.
while ($node instanceof \DOMNode && ($matchType === self::$MATCH_TYPE_FIRST || !($node instanceof \DOMDocument))) {
// Filter nodes if not matching last
if ($matchType != self::$MATCH_TYPE_LAST && (is_null($selector) || $node->is($selector))) {
$resultNodes[] = $node;
}
// 'Until' check or first match only
if ($matchType == self::$MATCH_TYPE_FIRST || (!is_null($input) && $node->is($input))) {
// Set last match
if ($matchType == self::$MATCH_TYPE_LAST) {
$resultNodes[] = $node;
}
break;
}
// Find the next node
$node = $node->{$property};
}
return $resultNodes;
}
/**
* @param iterable $nodeLists
*
* @return NodeList
*/
protected function _uniqueNodes(iterable $nodeLists): NodeList {
$resultNodes = $this->newNodeList();
// Loop through our array of NodeLists
foreach ($nodeLists as $nodeList) {
// Each node in the NodeList
foreach ($nodeList as $node) {
// We're only interested in unique nodes
if (!$resultNodes->exists($node)) {
$resultNodes[] = $node;
}
}
}
// Sort resulting NodeList: outer-most => inner-most.
return $resultNodes->reverse();
}
/**
* @param string $property
* @param string|NodeList|\DOMNode|callable $input
* @param string|NodeList|\DOMNode|callable $selector
* @param int $matchType
*
* @return NodeList
*/
protected function _walkPathUntil(string $property, string|NodeList|\DOMNode|callable|null $input = null, string|NodeList|\DOMNode|callable|null $selector = null, ?int $matchType = null): NodeList {
$nodeLists = [];
$this->collection()->each(function($node) use($property, $input, $selector, $matchType, &$nodeLists) {
$nodeLists[] = $this->_buildNodeListUntil($node, $property, $input, $selector, $matchType);
});
return $this->_uniqueNodes($nodeLists);
}
}