[ 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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body