[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body