[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/_test/vendor/scotteh/php-dom-wrapper/src/Traits/ -> ManipulationTrait.php (source)

   1  <?php declare(strict_types=1);
   2  
   3  namespace DOMWrap\Traits;
   4  
   5  use DOMWrap\{
   6      Text,
   7      Element,
   8      NodeList
   9  };
  10  
  11  /**
  12   * Manipulation Trait
  13   *
  14   * @package DOMWrap\Traits
  15   * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
  16   */
  17  trait ManipulationTrait
  18  {
  19      /**
  20       * Magic method - Trap function names using reserved keyword (empty, clone, etc..)
  21       *
  22       * @param string $name
  23       * @param array $arguments
  24       *
  25       * @return mixed
  26       */
  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 . "()");
  30          }
  31  
  32          return call_user_func_array([$this, '_' . $name], $arguments);
  33      }
  34  
  35      /**
  36       * @return string
  37       */
  38      public function __toString(): string {
  39          return $this->getOuterHtml(true);
  40      }
  41  
  42      /**
  43       * @param string|NodeList|\DOMNode $input
  44       *
  45       * @return iterable
  46       */
  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);
  52              }
  53  
  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();
  61          }
  62  
  63          return $nodes;
  64      }
  65  
  66      /**
  67       * @param string|NodeList|\DOMNode $input
  68       * @param bool $cloneForManipulate
  69       *
  70       * @return NodeList
  71       */
  72      protected function inputAsNodeList($input, $cloneForManipulate = true): NodeList {
  73          $nodes = $this->inputPrepareAsTraversable($input);
  74  
  75          $newNodes = $this->newNodeList();
  76  
  77          foreach ($nodes as $node) {
  78              if ($node->document() !== $this->document()) {
  79                   $node = $this->document()->importNode($node, true);
  80              }
  81  
  82              if ($cloneForManipulate && $node->parentNode !== null) {
  83                  $node = $node->cloneNode(true);
  84              }
  85  
  86              $newNodes[] = $node;
  87          }
  88  
  89          return $newNodes;
  90      }
  91  
  92      /**
  93       * @param string|NodeList|\DOMNode $input
  94       *
  95       * @return \DOMNode|null
  96       */
  97      protected function inputAsFirstNode($input): ?\DOMNode {
  98          $nodes = $this->inputAsNodeList($input);
  99  
 100          return $nodes->findXPath('self::*')->first();
 101      }
 102  
 103      /**
 104       * @param string $html
 105       *
 106       * @return NodeList
 107       */
 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 > *');
 113  
 114          return $nodes;
 115      }
 116  
 117      /**
 118       * @param string|NodeList|\DOMNode|callable $input
 119       * @param callable $callback
 120       *
 121       * @return self
 122       */
 123      protected function manipulateNodesWithInput($input, callable $callback): self {
 124          $this->collection()->each(function($node, $index) use ($input, $callback) {
 125              $html = $input;
 126  
 127              /*if ($input instanceof \DOMNode) {
 128                  if ($input->parentNode !== null) {
 129                      $html = $input->cloneNode(true);
 130                  }
 131              } else*/if (is_callable($input)) {
 132                  $html = $input($node, $index);
 133              }
 134  
 135              $newNodes = $this->inputAsNodeList($html);
 136  
 137              $callback($node, $newNodes);
 138          });
 139  
 140          return $this;
 141      }
 142  
 143      /**
 144       * @param string|null $selector
 145       *
 146       * @return NodeList
 147       */
 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();
 153          }
 154  
 155          $nodeList = $this->newNodeList();
 156  
 157          $nodes->each(function($node) use($nodeList) {
 158              if ($node->parent() instanceof \DOMNode) {
 159                  $nodeList[] = $node->parent()->removeChild($node);
 160              }
 161          });
 162  
 163          $nodes->fromArray([]);
 164  
 165          return $nodeList;
 166      }
 167  
 168      /**
 169       * @param string|null $selector
 170       *
 171       * @return self
 172       */
 173      public function destroy(string $selector = null): self {
 174          $this->detach($selector);
 175  
 176          return $this;
 177      }
 178  
 179      /**
 180       * @param string|NodeList|\DOMNode|callable $input
 181       *
 182       * @return self
 183       */
 184      public function substituteWith($input): self {
 185          $this->manipulateNodesWithInput($input, function($node, $newNodes) {
 186              foreach ($newNodes as $newNode) {
 187                  $node->parent()->replaceChild($newNode, $node);
 188              }
 189          });
 190  
 191          return $this;
 192      }
 193  
 194      /**
 195       * @param string|NodeList|\DOMNode|callable $input
 196       *
 197       * @return string|self
 198       */
 199      public function text($input = null) {
 200          if (is_null($input)) {
 201              return $this->getText();
 202          } else {
 203              return $this->setText($input);
 204          }
 205      }
 206  
 207      /**
 208       * @return string
 209       */
 210      public function getText(): string {
 211          return (string)$this->collection()->reduce(function($carry, $node) {
 212              return $carry . $node->textContent;
 213          }, '');
 214      }
 215  
 216      /**
 217       * @param string|NodeList|\DOMNode|callable $input
 218       *
 219       * @return self
 220       */
 221      public function setText($input): self {
 222          if (is_string($input)) {
 223              $input = new Text($input);
 224          }
 225  
 226          $this->manipulateNodesWithInput($input, function($node, $newNodes) {
 227              // Remove old contents from the current node.
 228              $node->contents()->destroy();
 229  
 230              // Add new contents in it's place.
 231              $node->appendWith(new Text($newNodes->getText()));
 232          });
 233  
 234          return $this;
 235      }
 236  
 237      /**
 238       * @param string|NodeList|\DOMNode|callable $input
 239       *
 240       * @return self
 241       */
 242      public function precede($input): self {
 243          $this->manipulateNodesWithInput($input, function($node, $newNodes) {
 244              foreach ($newNodes as $newNode) {
 245                  $node->parent()->insertBefore($newNode, $node);
 246              }
 247          });
 248  
 249          return $this;
 250      }
 251  
 252      /**
 253       * @param string|NodeList|\DOMNode|callable $input
 254       *
 255       * @return self
 256       */
 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());
 264                  }
 265              }
 266          });
 267  
 268          return $this;
 269      }
 270  
 271      /**
 272       * @param string|NodeList|\DOMNode|callable $input
 273       *
 274       * @return self
 275       */
 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());
 280              }
 281          });
 282  
 283          return $this;
 284      }
 285  
 286      /**
 287       * @param string|NodeList|\DOMNode|callable $input
 288       *
 289       * @return self
 290       */
 291      public function appendWith($input): self {
 292          $this->manipulateNodesWithInput($input, function($node, $newNodes) {
 293              foreach ($newNodes as $newNode) {
 294                  $node->appendChild($newNode);
 295              }
 296          });
 297  
 298          return $this;
 299      }
 300  
 301      /**
 302       * @param string|NodeList|\DOMNode $selector
 303       *
 304       * @return self
 305       */
 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);
 311          }
 312  
 313          $nodes->prependWith($this);
 314  
 315          return $this;
 316      }
 317  
 318      /**
 319       * @param string|NodeList|\DOMNode $selector
 320       *
 321       * @return self
 322       */
 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);
 328          }
 329  
 330          $nodes->appendWith($this);
 331  
 332          return $this;
 333      }
 334  
 335      /**
 336       * @return self
 337       */
 338      public function _empty(): self {
 339          $this->collection()->each(function($node) {
 340              $node->contents()->destroy();
 341          });
 342  
 343          return $this;
 344      }
 345  
 346      /**
 347       * @return NodeList|\DOMNode
 348       */
 349      public function _clone() {
 350          $clonedNodes = $this->newNodeList();
 351  
 352          $this->collection()->each(function($node) use($clonedNodes) {
 353              $clonedNodes[] = $node->cloneNode(true);
 354          });
 355  
 356          return $this->result($clonedNodes);
 357      }
 358  
 359      /**
 360       * @param string $name
 361       *
 362       * @return self
 363       */
 364      public function removeAttr(string $name): self {
 365          $this->collection()->each(function($node) use($name) {
 366              if ($node instanceof \DOMElement) {
 367                  $node->removeAttribute($name);
 368              }
 369          });
 370  
 371          return $this;
 372      }
 373  
 374      /**
 375       * @param string $name
 376       *
 377       * @return bool
 378       */
 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;
 383              }
 384  
 385              return $carry;
 386          }, false);
 387      }
 388  
 389      /**
 390       * @internal
 391       *
 392       * @param string $name
 393       *
 394       * @return string
 395       */
 396      public function getAttr(string $name): string {
 397          $node = $this->collection()->first();
 398  
 399          if (!($node instanceof \DOMElement)) {
 400              return '';
 401          }
 402  
 403          return $node->getAttribute($name);
 404      }
 405  
 406      /**
 407       * @internal
 408       *
 409       * @param string $name
 410       * @param mixed $value
 411       *
 412       * @return self
 413       */
 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);
 418              }
 419          });
 420  
 421          return $this;
 422      }
 423  
 424      /**
 425       * @param string $name
 426       * @param mixed $value
 427       *
 428       * @return self|string
 429       */
 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);
 435          }
 436      }
 437  
 438      /**
 439       * @internal
 440       *
 441       * @param string $name
 442       * @param string|callable $value
 443       * @param bool $addValue
 444       */
 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);
 449  
 450                  if (is_callable($value)) {
 451                      $value = $value($node, $index, $attr);
 452                  }
 453  
 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;
 458                      }
 459  
 460                      return true;
 461                  });
 462  
 463                  // If required add attr value to array
 464                  if ($addValue) {
 465                      $values[] = $value;
 466                  }
 467  
 468                  // Set the attr if we either have values, or the attr already
 469                  //  existed (we might be removing classes).
 470                  //
 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));
 474                  }
 475              }
 476          });
 477      }
 478  
 479      /**
 480       * @param string|callable $class
 481       *
 482       * @return self
 483       */
 484      public function addClass($class): self {
 485          $this->_pushAttrValue('class', $class, true);
 486  
 487          return $this;
 488      }
 489  
 490      /**
 491       * @param string|callable $class
 492       *
 493       * @return self
 494       */
 495      public function removeClass($class): self {
 496          $this->_pushAttrValue('class', $class);
 497  
 498          return $this;
 499      }
 500  
 501      /**
 502       * @param string $class
 503       *
 504       * @return bool
 505       */
 506      public function hasClass(string $class): bool {
 507          return (bool)$this->collection()->reduce(function($carry, $node) use ($class) {
 508              $attr = $node->getAttr('class');
 509  
 510              return array_reduce(explode(' ', (string)$attr), function($carry, $item) use ($class) {
 511                  if (strcasecmp($item, $class) == 0) {
 512                      return true;
 513                  }
 514  
 515                  return $carry;
 516              }, false);
 517          }, false);
 518      }
 519  
 520      /**
 521       * @param Element $node
 522       *
 523       * @return \SplStack
 524       */
 525      protected function _getFirstChildWrapStack(Element $node): \SplStack {
 526          $stack = new \SplStack;
 527  
 528          do {
 529              // Push our current node onto the stack
 530              $stack->push($node);
 531  
 532              // Get the first element child node
 533              $node = $node->children()->first();
 534          } while ($node instanceof Element);
 535  
 536          // Get the top most node.
 537          return $stack;
 538      }
 539  
 540      /**
 541       * @param Element $node
 542       *
 543       * @return \SplStack
 544       */
 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);
 549  
 550          // Only using the first element, remove any siblings.
 551          foreach ($stackNodes as $stackNode) {
 552              $stackNode->siblings()->destroy();
 553          }
 554  
 555          return $stackNodes;
 556      }
 557  
 558      /**
 559       * @param string|NodeList|\DOMNode|callable $input
 560       * @param callable $callback
 561       */
 562      protected function wrapWithInputByCallback($input, callable $callback): void {
 563          $this->collection()->each(function($node, $index) use ($input, $callback) {
 564              $html = $input;
 565  
 566              if (is_callable($input)) {
 567                  $html = $input($node, $index);
 568              }
 569  
 570              $inputNode = $this->inputAsFirstNode($html);
 571  
 572              if ($inputNode instanceof Element) {
 573                  // Pre-process wrapper into a stack of first element nodes.
 574                  $stackNodes = $this->_prepareWrapStack($inputNode);
 575  
 576                  $callback($node, $stackNodes);
 577              }
 578          });
 579      }
 580  
 581      /**
 582       * @param string|NodeList|\DOMNode|callable $input
 583       *
 584       * @return self
 585       */
 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();
 591  
 592                  // Add it back as a child of the top (leaf) node on the stack
 593                  $stackNodes->top()->appendWith($oldChild);
 594              }
 595  
 596              // Add the bottom (root) node on the stack
 597              $node->appendWith($stackNodes->bottom());
 598          });
 599  
 600          return $this;
 601      }
 602  
 603      /**
 604       * @param string|NodeList|\DOMNode|callable $input
 605       *
 606       * @return self
 607       */
 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());
 612  
 613              // Remove the current node
 614              $oldNode = $node->detach()->first();
 615  
 616              // Add the 'current node' back inside the new top (leaf) node.
 617              $stackNodes->top()->appendWith($oldNode);
 618          });
 619  
 620          return $this;
 621      }
 622  
 623      /**
 624       * @param string|NodeList|\DOMNode|callable $input
 625       *
 626       * @return self
 627       */
 628      public function wrapAll($input): self {
 629          if (!$this->collection()->count()) {
 630              return $this;
 631          }
 632  
 633          if (is_callable($input)) {
 634              $input = $input($this->collection()->first());
 635          }
 636  
 637          $inputNode = $this->inputAsFirstNode($input);
 638  
 639          if (!($inputNode instanceof Element)) {
 640              return $this;
 641          }
 642  
 643          $stackNodes = $this->_prepareWrapStack($inputNode);
 644  
 645          // Add the new bottom (root) node before the first matched node
 646          $this->collection()->first()->precede($stackNodes->bottom());
 647  
 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());
 651          });
 652  
 653          return $this;
 654      }
 655  
 656      /**
 657       * @return self
 658       */
 659      public function unwrap(): self {
 660          $this->collection()->each(function($node) {
 661              $parent = $node->parent();
 662  
 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();
 666  
 667                  $parent->precede($oldChildNode);
 668              });
 669  
 670              $parent->destroy();
 671          });
 672  
 673          return $this;
 674      }
 675  
 676      /**
 677       * @param int $isIncludeAll
 678       *
 679       * @return string
 680       */
 681      public function getOuterHtml(bool $isIncludeAll = false): string {
 682          $nodes = $this->collection();
 683  
 684          if (!$isIncludeAll) {
 685              $nodes = $this->newNodeList([$nodes->first()]);
 686          }
 687  
 688          return $nodes->reduce(function($carry, $node) {
 689              return $carry . $this->document()->saveHTML($node);
 690          }, '');
 691      }
 692  
 693      /**
 694       * @param int $isIncludeAll
 695       *
 696       * @return string
 697       */
 698      public function getHtml(bool $isIncludeAll = false): string {
 699          $nodes = $this->collection();
 700  
 701          if (!$isIncludeAll) {
 702              $nodes = $this->newNodeList([$nodes->first()]);
 703          }
 704  
 705          return $nodes->contents()->reduce(function($carry, $node) {
 706              return $carry . $this->document()->saveHTML($node);
 707          }, '');
 708      }
 709  
 710      /**
 711       * @param string|NodeList|\DOMNode|callable $input
 712       *
 713       * @return self
 714       */
 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();
 719  
 720              // Add new contents in it's place.
 721              $node->appendWith($newNodes);
 722          });
 723  
 724          return $this;
 725      }
 726  
 727      /**
 728       * @param string|NodeList|\DOMNode|callable $input
 729       *
 730       * @return string|self
 731       */
 732      public function html($input = null) {
 733          if (is_null($input)) {
 734              return $this->getHtml();
 735          } else {
 736              return $this->setHtml($input);
 737          }
 738      }
 739  
 740      /**
 741       * @param string|NodeList|\DOMNode $input
 742       *
 743       * @return NodeList
 744       */
 745      public function create($input): NodeList {
 746          return $this->inputAsNodeList($input);
 747      }
 748  }