mirror of
https://github.com/gtbu/Typesetter-5.3-p8.git
synced 2024-11-21 13:59:11 +01:00
php-dom-wrapper 3.0
Simple DOM wrapper library to manipulate and traverse HTML documents similar to jQuery (php8+)
This commit is contained in:
parent
142c292a2f
commit
4ad157b14b
14 changed files with 3385 additions and 0 deletions
169
include/thirdparty/dom/Collections/NodeCollection.php
vendored
Normal file
169
include/thirdparty/dom/Collections/NodeCollection.php
vendored
Normal 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
24
include/thirdparty/dom/Comment.php
vendored
Normal 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
346
include/thirdparty/dom/Document.php
vendored
Normal 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('"', '"', $value);
|
||||
}
|
||||
|
||||
$char = '"';
|
||||
|
||||
if ($hasQuot && !$hasApos) {
|
||||
$char = "'";
|
||||
}
|
||||
|
||||
// See xmlEscapeEntities() in:
|
||||
// https://github.com/GNOME/libxml2/blob/master/xmlsave.c
|
||||
$searchValue = str_replace(['<', '>', '&'], ['<', '>', '&'], $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
24
include/thirdparty/dom/DocumentType.php
vendored
Normal 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
24
include/thirdparty/dom/Element.php
vendored
Normal 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
30
include/thirdparty/dom/LICENSE
vendored
Normal 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
297
include/thirdparty/dom/NodeList.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
24
include/thirdparty/dom/ProcessingInstruction.php
vendored
Normal file
24
include/thirdparty/dom/ProcessingInstruction.php
vendored
Normal 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
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
24
include/thirdparty/dom/Text.php
vendored
Normal 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;
|
||||
}
|
38
include/thirdparty/dom/Traits/CommonTrait.php
vendored
Normal file
38
include/thirdparty/dom/Traits/CommonTrait.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
748
include/thirdparty/dom/Traits/ManipulationTrait.php
vendored
Normal file
748
include/thirdparty/dom/Traits/ManipulationTrait.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
46
include/thirdparty/dom/Traits/NodeTrait.php
vendored
Normal file
46
include/thirdparty/dom/Traits/NodeTrait.php
vendored
Normal 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;
|
||||
}
|
||||
}
|
468
include/thirdparty/dom/Traits/TraversalTrait.php
vendored
Normal file
468
include/thirdparty/dom/Traits/TraversalTrait.php
vendored
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue