Merge pull request #4036 from dokuwiki/issue4033
[dokuwiki.git] / _test / vendor / scotteh / php-dom-wrapper / src / Traits / ManipulationTrait.php
blobc463392ce725a764f9d8848ba89149e6df81f824
1 <?php declare(strict_types=1);
3 namespace DOMWrap\Traits;
5 use DOMWrap\{
6 Text,
7 Element,
8 NodeList
9 };
11 /**
12 * Manipulation Trait
14 * @package DOMWrap\Traits
15 * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
17 trait ManipulationTrait
19 /**
20 * Magic method - Trap function names using reserved keyword (empty, clone, etc..)
22 * @param string $name
23 * @param array $arguments
25 * @return mixed
27 public function __call(string $name, array $arguments) {
28 if (!method_exists($this, '_' . $name)) {
29 throw new \BadMethodCallException("Call to undefined method " . get_class($this) . '::' . $name . "()");
32 return call_user_func_array([$this, '_' . $name], $arguments);
35 /**
36 * @return string
38 public function __toString(): string {
39 return $this->getOuterHtml(true);
42 /**
43 * @param string|NodeList|\DOMNode $input
45 * @return iterable
47 protected function inputPrepareAsTraversable($input): iterable {
48 if ($input instanceof \DOMNode) {
49 // Handle raw \DOMNode elements and 'convert' them into their DOMWrap/* counterpart
50 if (!method_exists($input, 'inputPrepareAsTraversable')) {
51 $input = $this->document()->importNode($input, true);
54 $nodes = [$input];
55 } else if (is_string($input)) {
56 $nodes = $this->nodesFromHtml($input);
57 } else if (is_iterable($input)) {
58 $nodes = $input;
59 } else {
60 throw new \InvalidArgumentException();
63 return $nodes;
66 /**
67 * @param string|NodeList|\DOMNode $input
68 * @param bool $cloneForManipulate
70 * @return NodeList
72 protected function inputAsNodeList($input, $cloneForManipulate = true): NodeList {
73 $nodes = $this->inputPrepareAsTraversable($input);
75 $newNodes = $this->newNodeList();
77 foreach ($nodes as $node) {
78 if ($node->document() !== $this->document()) {
79 $node = $this->document()->importNode($node, true);
82 if ($cloneForManipulate && $node->parentNode !== null) {
83 $node = $node->cloneNode(true);
86 $newNodes[] = $node;
89 return $newNodes;
92 /**
93 * @param string|NodeList|\DOMNode $input
95 * @return \DOMNode|null
97 protected function inputAsFirstNode($input): ?\DOMNode {
98 $nodes = $this->inputAsNodeList($input);
100 return $nodes->findXPath('self::*')->first();
104 * @param string $html
106 * @return NodeList
108 protected function nodesFromHtml($html): NodeList {
109 $class = get_class($this->document());
110 $doc = new $class();
111 $doc->setEncoding($this->document()->getEncoding());
112 $nodes = $doc->html($html)->find('body > *');
114 return $nodes;
118 * @param string|NodeList|\DOMNode|callable $input
119 * @param callable $callback
121 * @return self
123 protected function manipulateNodesWithInput($input, callable $callback): self {
124 $this->collection()->each(function($node, $index) use ($input, $callback) {
125 $html = $input;
127 /*if ($input instanceof \DOMNode) {
128 if ($input->parentNode !== null) {
129 $html = $input->cloneNode(true);
131 } else*/if (is_callable($input)) {
132 $html = $input($node, $index);
135 $newNodes = $this->inputAsNodeList($html);
137 $callback($node, $newNodes);
140 return $this;
144 * @param string|null $selector
146 * @return NodeList
148 public function detach(string $selector = null): NodeList {
149 if (!is_null($selector)) {
150 $nodes = $this->find($selector, 'self::');
151 } else {
152 $nodes = $this->collection();
155 $nodeList = $this->newNodeList();
157 $nodes->each(function($node) use($nodeList) {
158 if ($node->parent() instanceof \DOMNode) {
159 $nodeList[] = $node->parent()->removeChild($node);
163 $nodes->fromArray([]);
165 return $nodeList;
169 * @param string|null $selector
171 * @return self
173 public function destroy(string $selector = null): self {
174 $this->detach($selector);
176 return $this;
180 * @param string|NodeList|\DOMNode|callable $input
182 * @return self
184 public function substituteWith($input): self {
185 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
186 foreach ($newNodes as $newNode) {
187 $node->parent()->replaceChild($newNode, $node);
191 return $this;
195 * @param string|NodeList|\DOMNode|callable $input
197 * @return string|self
199 public function text($input = null) {
200 if (is_null($input)) {
201 return $this->getText();
202 } else {
203 return $this->setText($input);
208 * @return string
210 public function getText(): string {
211 return (string)$this->collection()->reduce(function($carry, $node) {
212 return $carry . $node->textContent;
213 }, '');
217 * @param string|NodeList|\DOMNode|callable $input
219 * @return self
221 public function setText($input): self {
222 if (is_string($input)) {
223 $input = new Text($input);
226 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
227 // Remove old contents from the current node.
228 $node->contents()->destroy();
230 // Add new contents in it's place.
231 $node->appendWith(new Text($newNodes->getText()));
234 return $this;
238 * @param string|NodeList|\DOMNode|callable $input
240 * @return self
242 public function precede($input): self {
243 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
244 foreach ($newNodes as $newNode) {
245 $node->parent()->insertBefore($newNode, $node);
249 return $this;
253 * @param string|NodeList|\DOMNode|callable $input
255 * @return self
257 public function follow($input): self {
258 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
259 foreach ($newNodes as $newNode) {
260 if (is_null($node->following())) {
261 $node->parent()->appendChild($newNode);
262 } else {
263 $node->parent()->insertBefore($newNode, $node->following());
268 return $this;
272 * @param string|NodeList|\DOMNode|callable $input
274 * @return self
276 public function prependWith($input): self {
277 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
278 foreach ($newNodes as $newNode) {
279 $node->insertBefore($newNode, $node->contents()->first());
283 return $this;
287 * @param string|NodeList|\DOMNode|callable $input
289 * @return self
291 public function appendWith($input): self {
292 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
293 foreach ($newNodes as $newNode) {
294 $node->appendChild($newNode);
298 return $this;
302 * @param string|NodeList|\DOMNode $selector
304 * @return self
306 public function prependTo($selector): self {
307 if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
308 $nodes = $this->inputAsNodeList($selector);
309 } else {
310 $nodes = $this->document()->find($selector);
313 $nodes->prependWith($this);
315 return $this;
319 * @param string|NodeList|\DOMNode $selector
321 * @return self
323 public function appendTo($selector): self {
324 if ($selector instanceof \DOMNode || $selector instanceof NodeList) {
325 $nodes = $this->inputAsNodeList($selector);
326 } else {
327 $nodes = $this->document()->find($selector);
330 $nodes->appendWith($this);
332 return $this;
336 * @return self
338 public function _empty(): self {
339 $this->collection()->each(function($node) {
340 $node->contents()->destroy();
343 return $this;
347 * @return NodeList|\DOMNode
349 public function _clone() {
350 $clonedNodes = $this->newNodeList();
352 $this->collection()->each(function($node) use($clonedNodes) {
353 $clonedNodes[] = $node->cloneNode(true);
356 return $this->result($clonedNodes);
360 * @param string $name
362 * @return self
364 public function removeAttr(string $name): self {
365 $this->collection()->each(function($node) use($name) {
366 if ($node instanceof \DOMElement) {
367 $node->removeAttribute($name);
371 return $this;
375 * @param string $name
377 * @return bool
379 public function hasAttr(string $name): bool {
380 return (bool)$this->collection()->reduce(function($carry, $node) use ($name) {
381 if ($node->hasAttribute($name)) {
382 return true;
385 return $carry;
386 }, false);
390 * @internal
392 * @param string $name
394 * @return string
396 public function getAttr(string $name): string {
397 $node = $this->collection()->first();
399 if (!($node instanceof \DOMElement)) {
400 return '';
403 return $node->getAttribute($name);
407 * @internal
409 * @param string $name
410 * @param mixed $value
412 * @return self
414 public function setAttr(string $name, $value): self {
415 $this->collection()->each(function($node) use($name, $value) {
416 if ($node instanceof \DOMElement) {
417 $node->setAttribute($name, (string)$value);
421 return $this;
425 * @param string $name
426 * @param mixed $value
428 * @return self|string
430 public function attr(string $name, $value = null) {
431 if (is_null($value)) {
432 return $this->getAttr($name);
433 } else {
434 return $this->setAttr($name, $value);
439 * @internal
441 * @param string $name
442 * @param string|callable $value
443 * @param bool $addValue
445 protected function _pushAttrValue(string $name, $value, bool $addValue = false): void {
446 $this->collection()->each(function($node, $index) use($name, $value, $addValue) {
447 if ($node instanceof \DOMElement) {
448 $attr = $node->getAttribute($name);
450 if (is_callable($value)) {
451 $value = $value($node, $index, $attr);
454 // Remove any existing instances of the value, or empty values.
455 $values = array_filter(explode(' ', $attr), function($_value) use($value) {
456 if (strcasecmp($_value, $value) == 0 || empty($_value)) {
457 return false;
460 return true;
463 // If required add attr value to array
464 if ($addValue) {
465 $values[] = $value;
468 // Set the attr if we either have values, or the attr already
469 // existed (we might be removing classes).
471 // Don't set the attr if it doesn't already exist.
472 if (!empty($values) || $node->hasAttribute($name)) {
473 $node->setAttribute($name, implode(' ', $values));
480 * @param string|callable $class
482 * @return self
484 public function addClass($class): self {
485 $this->_pushAttrValue('class', $class, true);
487 return $this;
491 * @param string|callable $class
493 * @return self
495 public function removeClass($class): self {
496 $this->_pushAttrValue('class', $class);
498 return $this;
502 * @param string $class
504 * @return bool
506 public function hasClass(string $class): bool {
507 return (bool)$this->collection()->reduce(function($carry, $node) use ($class) {
508 $attr = $node->getAttr('class');
510 return array_reduce(explode(' ', (string)$attr), function($carry, $item) use ($class) {
511 if (strcasecmp($item, $class) == 0) {
512 return true;
515 return $carry;
516 }, false);
517 }, false);
521 * @param Element $node
523 * @return \SplStack
525 protected function _getFirstChildWrapStack(Element $node): \SplStack {
526 $stack = new \SplStack;
528 do {
529 // Push our current node onto the stack
530 $stack->push($node);
532 // Get the first element child node
533 $node = $node->children()->first();
534 } while ($node instanceof Element);
536 // Get the top most node.
537 return $stack;
541 * @param Element $node
543 * @return \SplStack
545 protected function _prepareWrapStack(Element $node): \SplStack {
546 // Generate a stack (root to leaf) of the wrapper.
547 // Includes only first element nodes / first element children.
548 $stackNodes = $this->_getFirstChildWrapStack($node);
550 // Only using the first element, remove any siblings.
551 foreach ($stackNodes as $stackNode) {
552 $stackNode->siblings()->destroy();
555 return $stackNodes;
559 * @param string|NodeList|\DOMNode|callable $input
560 * @param callable $callback
562 protected function wrapWithInputByCallback($input, callable $callback): void {
563 $this->collection()->each(function($node, $index) use ($input, $callback) {
564 $html = $input;
566 if (is_callable($input)) {
567 $html = $input($node, $index);
570 $inputNode = $this->inputAsFirstNode($html);
572 if ($inputNode instanceof Element) {
573 // Pre-process wrapper into a stack of first element nodes.
574 $stackNodes = $this->_prepareWrapStack($inputNode);
576 $callback($node, $stackNodes);
582 * @param string|NodeList|\DOMNode|callable $input
584 * @return self
586 public function wrapInner($input): self {
587 $this->wrapWithInputByCallback($input, function($node, $stackNodes) {
588 foreach ($node->contents() as $child) {
589 // Remove child from the current node
590 $oldChild = $child->detach()->first();
592 // Add it back as a child of the top (leaf) node on the stack
593 $stackNodes->top()->appendWith($oldChild);
596 // Add the bottom (root) node on the stack
597 $node->appendWith($stackNodes->bottom());
600 return $this;
604 * @param string|NodeList|\DOMNode|callable $input
606 * @return self
608 public function wrap($input): self {
609 $this->wrapWithInputByCallback($input, function($node, $stackNodes) {
610 // Add the new bottom (root) node after the current node
611 $node->follow($stackNodes->bottom());
613 // Remove the current node
614 $oldNode = $node->detach()->first();
616 // Add the 'current node' back inside the new top (leaf) node.
617 $stackNodes->top()->appendWith($oldNode);
620 return $this;
624 * @param string|NodeList|\DOMNode|callable $input
626 * @return self
628 public function wrapAll($input): self {
629 if (!$this->collection()->count()) {
630 return $this;
633 if (is_callable($input)) {
634 $input = $input($this->collection()->first());
637 $inputNode = $this->inputAsFirstNode($input);
639 if (!($inputNode instanceof Element)) {
640 return $this;
643 $stackNodes = $this->_prepareWrapStack($inputNode);
645 // Add the new bottom (root) node before the first matched node
646 $this->collection()->first()->precede($stackNodes->bottom());
648 $this->collection()->each(function($node) use ($stackNodes) {
649 // Detach and add node back inside the new wrappers top (leaf) node.
650 $stackNodes->top()->appendWith($node->detach());
653 return $this;
657 * @return self
659 public function unwrap(): self {
660 $this->collection()->each(function($node) {
661 $parent = $node->parent();
663 // Replace parent node (the one we're unwrapping) with it's children.
664 $parent->contents()->each(function($childNode) use($parent) {
665 $oldChildNode = $childNode->detach()->first();
667 $parent->precede($oldChildNode);
670 $parent->destroy();
673 return $this;
677 * @param int $isIncludeAll
679 * @return string
681 public function getOuterHtml(bool $isIncludeAll = false): string {
682 $nodes = $this->collection();
684 if (!$isIncludeAll) {
685 $nodes = $this->newNodeList([$nodes->first()]);
688 return $nodes->reduce(function($carry, $node) {
689 return $carry . $this->document()->saveHTML($node);
690 }, '');
694 * @param int $isIncludeAll
696 * @return string
698 public function getHtml(bool $isIncludeAll = false): string {
699 $nodes = $this->collection();
701 if (!$isIncludeAll) {
702 $nodes = $this->newNodeList([$nodes->first()]);
705 return $nodes->contents()->reduce(function($carry, $node) {
706 return $carry . $this->document()->saveHTML($node);
707 }, '');
711 * @param string|NodeList|\DOMNode|callable $input
713 * @return self
715 public function setHtml($input): self {
716 $this->manipulateNodesWithInput($input, function($node, $newNodes) {
717 // Remove old contents from the current node.
718 $node->contents()->destroy();
720 // Add new contents in it's place.
721 $node->appendWith($newNodes);
724 return $this;
728 * @param string|NodeList|\DOMNode|callable $input
730 * @return string|self
732 public function html($input = null) {
733 if (is_null($input)) {
734 return $this->getHtml();
735 } else {
736 return $this->setHtml($input);
741 * @param string|NodeList|\DOMNode $input
743 * @return NodeList
745 public function create($input): NodeList {
746 return $this->inputAsNodeList($input);