[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

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

   1  <?php declare(strict_types=1);
   2  
   3  namespace DOMWrap\Traits;
   4  
   5  use DOMWrap\{
   6      Element,
   7      NodeList
   8  };
   9  use Symfony\Component\CssSelector\CssSelectorConverter;
  10  
  11  /**
  12   * Traversal Trait
  13   *
  14   * @package DOMWrap\Traits
  15   * @license http://opensource.org/licenses/BSD-3-Clause BSD 3 Clause
  16   */
  17  trait TraversalTrait
  18  {
  19      /**
  20       * @param iterable $nodes
  21       *
  22       * @return NodeList
  23       */
  24      public function newNodeList(iterable $nodes = null): NodeList {
  25  
  26          if (!is_iterable($nodes)) {
  27              if (!is_null($nodes)) {
  28                  $nodes = [$nodes];
  29              } else {
  30                  $nodes = [];
  31              }
  32          }
  33  
  34          return new NodeList($this->document(), $nodes);
  35      }
  36  
  37      /**
  38       * @param string $selector
  39       * @param string $prefix
  40       *
  41       * @return NodeList
  42       */
  43      public function find(string $selector, string $prefix = 'descendant::'): NodeList {
  44          $converter = new CssSelectorConverter();
  45  
  46          return $this->findXPath($converter->toXPath($selector, $prefix));
  47      }
  48  
  49      /**
  50       * @param string $xpath
  51       *
  52       * @return NodeList
  53       */
  54      public function findXPath(string $xpath): NodeList {
  55          $results = $this->newNodeList();
  56  
  57          if ($this->isRemoved()) {
  58              return $results;
  59          }
  60  
  61          $domxpath = new \DOMXPath($this->document());
  62  
  63          foreach ($this->collection() as $node) {
  64              $results = $results->merge(
  65                  $node->newNodeList($domxpath->query($xpath, $node))
  66              );
  67          }
  68  
  69          return $results;
  70      }
  71  
  72      /**
  73       * @param string|NodeList|\DOMNode|callable $input
  74       * @param bool $matchType
  75       *
  76       * @return NodeList
  77       */
  78      protected function getNodesMatchingInput($input, bool $matchType = true): NodeList {
  79          if ($input instanceof NodeList || $input instanceof \DOMNode) {
  80              $inputNodes = $this->inputAsNodeList($input, false);
  81  
  82              $fn = function($node) use ($inputNodes) {
  83                  return $inputNodes->exists($node);
  84              };
  85  
  86  
  87          } elseif (is_callable($input)) {
  88              // Since we're at the behest of the input callable, the 'matched'
  89              //  return value is always true.
  90              $matchType = true;
  91  
  92              $fn = $input;
  93  
  94          } elseif (is_string($input)) {
  95              $fn = function($node) use ($input) {
  96                  return $node->find($input, 'self::')->count() != 0;
  97              };
  98  
  99          } else {
 100              throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
 101          }
 102  
 103          // Build a list of matching nodes.
 104          return $this->collection()->map(function($node) use ($fn, $matchType) {
 105              if ($fn($node) !== $matchType) {
 106                  return null;
 107              }
 108  
 109              return $node;
 110          });
 111      }
 112  
 113      /**
 114       * @param string|NodeList|\DOMNode|callable $input
 115       *
 116       * @return bool
 117       */
 118      public function is($input): bool {
 119          return $this->getNodesMatchingInput($input)->count() != 0;
 120      }
 121  
 122      /**
 123       * @param string|NodeList|\DOMNode|callable $input
 124       *
 125       * @return NodeList
 126       */
 127      public function not($input): NodeList {
 128          return $this->getNodesMatchingInput($input, false);
 129      }
 130  
 131      /**
 132       * @param string|NodeList|\DOMNode|callable $input
 133       *
 134       * @return NodeList
 135       */
 136      public function filter($input): NodeList {
 137          return $this->getNodesMatchingInput($input);
 138      }
 139  
 140      /**
 141       * @param string|NodeList|\DOMNode|callable $input
 142       *
 143       * @return NodeList
 144       */
 145      public function has($input): NodeList {
 146          if ($input instanceof NodeList || $input instanceof \DOMNode) {
 147              $inputNodes = $this->inputAsNodeList($input, false);
 148  
 149              $fn = function($node) use ($inputNodes) {
 150                  $descendantNodes = $node->find('*', 'descendant::');
 151  
 152                  // Determine if we have a descendant match.
 153                  return $inputNodes->reduce(function($carry, $inputNode) use ($descendantNodes) {
 154                      // Match descendant nodes against input nodes.
 155                      if ($descendantNodes->exists($inputNode)) {
 156                          return true;
 157                      }
 158  
 159                      return $carry;
 160                  }, false);
 161              };
 162  
 163          } elseif (is_string($input)) {
 164              $fn = function($node) use ($input) {
 165                  return $node->find($input, 'descendant::')->count() != 0;
 166              };
 167  
 168          } elseif (is_callable($input)) {
 169              $fn = $input;
 170  
 171          } else {
 172              throw new \InvalidArgumentException('Unexpected input value of type "' . gettype($input) . '"');
 173          }
 174  
 175          return $this->getNodesMatchingInput($fn);
 176      }
 177  
 178      /**
 179       * @param string|NodeList|\DOMNode|callable $selector
 180       *
 181       * @return \DOMNode|null
 182       */
 183      public function preceding($selector = null): ?\DOMNode {
 184          return $this->precedingUntil(null, $selector)->first();
 185      }
 186  
 187      /**
 188       * @param string|NodeList|\DOMNode|callable $selector
 189       *
 190       * @return NodeList
 191       */
 192      public function precedingAll($selector = null): NodeList {
 193          return $this->precedingUntil(null, $selector);
 194      }
 195  
 196      /**
 197       * @param string|NodeList|\DOMNode|callable $input
 198       * @param string|NodeList|\DOMNode|callable $selector
 199       *
 200       * @return NodeList
 201       */
 202      public function precedingUntil($input = null, $selector = null): NodeList {
 203          return $this->_walkPathUntil('previousSibling', $input, $selector);
 204      }
 205  
 206      /**
 207       * @param string|NodeList|\DOMNode|callable $selector
 208       *
 209       * @return \DOMNode|null
 210       */
 211      public function following($selector = null): ?\DOMNode {
 212          return $this->followingUntil(null, $selector)->first();
 213      }
 214  
 215      /**
 216       * @param string|NodeList|\DOMNode|callable $selector
 217       *
 218       * @return NodeList
 219       */
 220      public function followingAll($selector = null): NodeList {
 221          return $this->followingUntil(null, $selector);
 222      }
 223  
 224      /**
 225       * @param string|NodeList|\DOMNode|callable $input
 226       * @param string|NodeList|\DOMNode|callable $selector
 227       *
 228       * @return NodeList
 229       */
 230      public function followingUntil($input = null, $selector = null): NodeList {
 231          return $this->_walkPathUntil('nextSibling', $input, $selector);
 232      }
 233  
 234      /**
 235       * @param string|NodeList|\DOMNode|callable $selector
 236       *
 237       * @return NodeList
 238       */
 239      public function siblings($selector = null): NodeList {
 240          $results = $this->collection()->reduce(function($carry, $node) use ($selector) {
 241              return $carry->merge(
 242                  $node->precedingAll($selector)->merge(
 243                      $node->followingAll($selector)
 244                  )
 245              );
 246          }, $this->newNodeList());
 247  
 248          return $results;
 249      }
 250  
 251      /**
 252       * NodeList is only array like. Removing items using foreach() has undesired results.
 253       *
 254       * @return NodeList
 255       */
 256      public function children(): NodeList {
 257          $results = $this->collection()->reduce(function($carry, $node) {
 258              return $carry->merge(
 259                  $node->findXPath('child::*')
 260              );
 261          }, $this->newNodeList());
 262  
 263          return $results;
 264      }
 265  
 266      /**
 267       * @param string|NodeList|\DOMNode|callable $selector
 268       *
 269       * @return Element|NodeList|null
 270       */
 271      public function parent($selector = null) {
 272          $results = $this->_walkPathUntil('parentNode', null, $selector, self::$MATCH_TYPE_FIRST);
 273  
 274          return $this->result($results);
 275      }
 276  
 277      /**
 278       * @param int $index
 279       *
 280       * @return \DOMNode|null
 281       */
 282      public function eq(int $index): ?\DOMNode {
 283          if ($index < 0) {
 284              $index = $this->collection()->count() + $index;
 285          }
 286  
 287          return $this->collection()->offsetGet($index);
 288      }
 289  
 290      /**
 291       * @param string $selector
 292       *
 293       * @return NodeList
 294       */
 295      public function parents(string $selector = null): NodeList {
 296          return $this->parentsUntil(null, $selector);
 297      }
 298  
 299      /**
 300       * @param string|NodeList|\DOMNode|callable $input
 301       * @param string|NodeList|\DOMNode|callable $selector
 302       *
 303       * @return NodeList
 304       */
 305      public function parentsUntil($input = null, $selector = null): NodeList {
 306          return $this->_walkPathUntil('parentNode', $input, $selector);
 307      }
 308  
 309      /**
 310       * @return \DOMNode
 311       */
 312      public function intersect(): \DOMNode {
 313          if ($this->collection()->count() < 2) {
 314              return $this->collection()->first();
 315          }
 316  
 317          $nodeParents = [];
 318  
 319          // Build a multi-dimensional array of the collection nodes parent elements
 320          $this->collection()->each(function($node) use(&$nodeParents) {
 321              $nodeParents[] = $node->parents()->unshift($node)->toArray();
 322          });
 323  
 324          // Find the common parent
 325          $diff = call_user_func_array('array_uintersect', array_merge($nodeParents, [function($a, $b) {
 326              return strcmp(spl_object_hash($a), spl_object_hash($b));
 327          }]));
 328  
 329          return array_shift($diff);
 330      }
 331  
 332      /**
 333       * @param string|NodeList|\DOMNode|callable $input
 334       *
 335       * @return Element|NodeList|null
 336       */
 337      public function closest($input) {
 338          $results = $this->_walkPathUntil('parentNode', $input, null, self::$MATCH_TYPE_LAST);
 339  
 340          return $this->result($results);
 341      }
 342  
 343      /**
 344       * NodeList is only array like. Removing items using foreach() has undesired results.
 345       *
 346       * @return NodeList
 347       */
 348      public function contents(): NodeList {
 349          $results = $this->collection()->reduce(function($carry, $node) {
 350              if ($node->isRemoved()) {
 351                  return $carry;
 352              }
 353  
 354              return $carry->merge(
 355                  $node->newNodeList($node->childNodes)
 356              );
 357          }, $this->newNodeList());
 358  
 359          return $results;
 360      }
 361  
 362      /**
 363       * @param string|NodeList|\DOMNode $input
 364       *
 365       * @return NodeList
 366       */
 367      public function add($input): NodeList {
 368          $nodes = $this->inputAsNodeList($input);
 369  
 370          $results = $this->collection()->merge(
 371              $nodes
 372          );
 373  
 374          return $results;
 375      }
 376  
 377      /** @var int */
 378      private static $MATCH_TYPE_FIRST = 1;
 379  
 380      /** @var int */
 381      private static $MATCH_TYPE_LAST = 2;
 382  
 383      /**
 384       * @param \DOMNode $baseNode
 385       * @param string $property
 386       * @param string|NodeList|\DOMNode|callable $input
 387       * @param string|NodeList|\DOMNode|callable $selector
 388       * @param int $matchType
 389       *
 390       * @return NodeList
 391       */
 392      protected function _buildNodeListUntil(\DOMNode $baseNode, string $property, $input = null, $selector = null, int $matchType = null): NodeList {
 393          $resultNodes = $this->newNodeList();
 394  
 395          // Get our first node
 396          $node = $baseNode->$property;
 397  
 398          // Keep looping until we are out of nodes.
 399          // Allow either FIRST to reach \DOMDocument. Others that return multiple should ignore it.
 400          while ($node instanceof \DOMNode && ($matchType === self::$MATCH_TYPE_FIRST || !($node instanceof \DOMDocument))) {
 401              // Filter nodes if not matching last
 402              if ($matchType != self::$MATCH_TYPE_LAST && (is_null($selector) || $node->is($selector))) {
 403                  $resultNodes[] = $node;
 404              }
 405  
 406              // 'Until' check or first match only
 407              if ($matchType == self::$MATCH_TYPE_FIRST || (!is_null($input) && $node->is($input))) {
 408                  // Set last match
 409                  if ($matchType == self::$MATCH_TYPE_LAST) {
 410                      $resultNodes[] = $node;
 411                  }
 412  
 413                  break;
 414              }
 415  
 416              // Find the next node
 417              $node = $node->{$property};
 418          }
 419  
 420          return $resultNodes;
 421      }
 422  
 423      /**
 424       * @param iterable $nodeLists
 425       *
 426       * @return NodeList
 427       */
 428      protected function _uniqueNodes(iterable $nodeLists): NodeList {
 429          $resultNodes = $this->newNodeList();
 430  
 431          // Loop through our array of NodeLists
 432          foreach ($nodeLists as $nodeList) {
 433              // Each node in the NodeList
 434              foreach ($nodeList as $node) {
 435                  // We're only interested in unique nodes
 436                  if (!$resultNodes->exists($node)) {
 437                      $resultNodes[] = $node;
 438                  }
 439              }
 440          }
 441  
 442          // Sort resulting NodeList: outer-most => inner-most.
 443          return $resultNodes->reverse();
 444      }
 445  
 446      /**
 447       * @param string $property
 448       * @param string|NodeList|\DOMNode|callable $input
 449       * @param string|NodeList|\DOMNode|callable $selector
 450       * @param int $matchType
 451       *
 452       * @return NodeList
 453       */
 454      protected function _walkPathUntil(string $property, $input = null, $selector = null, int $matchType = null): NodeList {
 455          $nodeLists = [];
 456  
 457          $this->collection()->each(function($node) use($property, $input, $selector, $matchType, &$nodeLists) {
 458              $nodeLists[] = $this->_buildNodeListUntil($node, $property, $input, $selector, $matchType);
 459          });
 460  
 461          return $this->_uniqueNodes($nodeLists);
 462      }
 463  }