1 : <?php
2 : /**
3 : * FluentDOM implements a jQuery like replacement for DOMNodeList
4 : *
5 : * @version $Id: FluentDOM.php 185 2009-06-16 21:59:30Z lapis $
6 : * @license http://www.opensource.org/licenses/mit-license.php The MIT License
7 : * @copyright Copyright (c) 2009 Bastian Feder, Thomas Weinert
8 : */
9 :
10 : /**
11 : * Function to create a new FluentDOM instance
12 : *
13 : * This is a shortcut for "new FluentDOM($source)"
14 : *
15 : * @param mixed $source
16 : * @access public
17 : * @return object FluentDOM
18 : */
19 : function FluentDOM($source) {
20 98 : return new FluentDOM($source);
21 : }
22 :
23 : /**
24 : * FluentDOM implements a jQuery like replacement for DOMNodeList
25 : */
26 : class FluentDOM implements RecursiveIterator, SeekableIterator, Countable, ArrayAccess {
27 :
28 : /**
29 : * document object
30 : * @var object DOMDocument
31 : * @access private
32 : */
33 : private $_document = NULL;
34 :
35 : /**
36 : * use document context for expression
37 : * @var boolean
38 : * @access private
39 : */
40 : private $_useDocumentContext = FALSE;
41 :
42 : /**
43 : * parent node list (last selection in chain)
44 : * @var object FluentDOM
45 : * @access private
46 : */
47 : private $_parent = NULL;
48 :
49 : /**
50 : * current iterator position
51 : * @var integer
52 : * @access private
53 : */
54 : private $_position = 0;
55 :
56 : /**
57 : * element nodes
58 : * @var array
59 : * @access protected
60 : */
61 : protected $_array = array();
62 :
63 : /**
64 : * internal xpath instance
65 : * @var object DOMXPath
66 : * @access private
67 : */
68 : private $_xpath = NULL;
69 :
70 : /**
71 : * initialize the FluentDOM instance and set private properties
72 : *
73 : * @param object FluentDOM | object DOMElement | object DOMDocument $source
74 : * source to create FluentDOM from
75 : * @access public
76 : */
77 : public function __construct($source) {
78 119 : if ($source instanceof FluentDOM) {
79 106 : $this->_document = $source->document;
80 106 : $this->_xpath = $source->_xpath;
81 106 : $this->_parent = $source;
82 119 : } elseif ($source instanceof DOMDocument) {
83 1 : $this->_document = $source;
84 1 : $this->_useDocumentContext = TRUE;
85 119 : } elseif ($this->_isNode($source)) {
86 4 : $this->_document = $source->ownerDocument;
87 4 : $this->_push($source);
88 118 : } elseif (is_string($source)) {
89 116 : $this->_document = new DOMDocument();
90 116 : $this->_document->loadXML($source);
91 116 : $this->_useDocumentContext = TRUE;
92 116 : } else {
93 1 : throw new InvalidArgumentException('Invalid source object.');
94 : }
95 118 : }
96 :
97 : /**
98 : * implement dynamic properties using magic methods
99 : *
100 : * @param string $name
101 : * @access public
102 : * @return mixed
103 : */
104 : public function __get($name) {
105 : switch ($name) {
106 111 : case 'length' :
107 12 : return count($this->_array);
108 110 : case 'document' :
109 108 : return $this->_document;
110 3 : case 'xpath' :
111 2 : return $this->_xpath();
112 1 : default :
113 1 : return NULL;
114 1 : }
115 : }
116 :
117 : /**
118 : * block changes of dynamic readonly property length
119 : *
120 : * @param $name
121 : * @param $value
122 : * @access public
123 : * @return void
124 : */
125 : public function __set($name, $value) {
126 : switch ($name) {
127 4 : case 'length' :
128 4 : case 'document' :
129 4 : case 'xpath' :
130 3 : throw new BadMethodCallException('Can not set readonly value.');
131 : break;
132 1 : default :
133 1 : $this->$name = $value;
134 1 : break;
135 1 : }
136 1 : }
137 :
138 : /**
139 : * support isst for dynic properties length and document
140 : *
141 : * @param $name
142 : * @access public
143 : * @return boolean
144 : */
145 : public function __isset($name) {
146 : switch ($name) {
147 4 : case 'length' :
148 4 : case 'xpath' :
149 2 : return TRUE;
150 2 : case 'document' :
151 1 : return isset($this->_document);
152 : }
153 1 : return FALSE;
154 : }
155 :
156 : /**
157 : * declaring an empty() method will crash the parser so we use some magic
158 : *
159 : * @param string $name
160 : * @param array $arguments
161 : * @access public
162 : * @return mixed
163 : */
164 : public function __call($name, $arguments) {
165 3 : switch (strtolower($name)) {
166 3 : case 'empty' :
167 1 : return $this->_emptyNodes();
168 2 : case 'clone' :
169 1 : return $this->_cloneNodes();
170 1 : default :
171 1 : throw new BadMethodCallException('Unknown method '.get_class($this).'::'.$name);
172 1 : }
173 : }
174 :
175 : /**
176 : * Return the XML output of the internal dom document
177 : *
178 : * @access public
179 : * @return string
180 : */
181 : public function __toString() {
182 46 : return $this->_document->saveXML();
183 : }
184 :
185 : /**
186 : * the item() method is used to access elements in the node list
187 : *
188 : * @param $position
189 : * @access public
190 : * @return object DOMNode
191 : */
192 : public function item($position) {
193 3 : if (isset($this->_array[$position])) {
194 3 : return $this->_array[$position];
195 : }
196 1 : return NULL;
197 : }
198 :
199 : /*
200 : * Interface - Iterator, SeekableIterator
201 : */
202 :
203 : /**
204 : * Get current iterator element
205 : *
206 : * @access public
207 : * @return DOMNode
208 : */
209 : public function current() {
210 11 : return $this->_array[$this->_position];
211 : }
212 :
213 : /**
214 : * Get current iterator pointer
215 : *
216 : * @access public
217 : * @return integer
218 : */
219 : public function key() {
220 3 : return $this->_position;
221 : }
222 :
223 : /**
224 : * Move iterator pointer to next element
225 : *
226 : * @access public
227 : * @return void
228 : */
229 : public function next() {
230 10 : ++$this->_position;
231 10 : }
232 :
233 : /**
234 : * Reset iterator pointer
235 : *
236 : * @access public
237 : * @return void
238 : */
239 : public function rewind() {
240 12 : $this->_position = 0;
241 12 : }
242 :
243 : /**
244 : * Move iterator pointer to specified element
245 : *
246 : * @param integer $position
247 : * @access public
248 : * @return void
249 : */
250 : public function seek($position) {
251 1 : if (isset($this->_array[$position])) {
252 1 : $this->_position = $position;
253 1 : } else {
254 1 : throw new InvalidArgumentException('Unknown position');
255 : }
256 1 : }
257 :
258 : /**
259 : * Check if current iterator pointer contains a valid element
260 : *
261 : * @access public
262 : * @return boolean
263 : */
264 : public function valid() {
265 11 : return isset($this->_array[$this->_position]);
266 : }
267 :
268 : /**
269 : * Get children of the current iterator element
270 : *
271 : * @access public
272 : * @return object FluentDOM
273 : */
274 : public function getChildren() {
275 1 : $result = $this->_spawn();
276 1 : $result->_push($this->_match('node()', $this->_array[$this->_position]));
277 1 : return $result;
278 : }
279 :
280 : /**
281 : * Check if the current iterator element has children
282 : *
283 : * @access public
284 : * @return object FluentDOM
285 : */
286 : public function hasChildren() {
287 1 : return $this->_test('node()', $this->_array[$this->_position]);
288 : }
289 :
290 : /*
291 : * Interface - Countable
292 : */
293 :
294 : /**
295 : * get element count (Countable)
296 : *
297 : * @access public
298 : * @return
299 : */
300 : public function count() {
301 5 : return count($this->_array);
302 : }
303 :
304 : /*
305 : * Interface - ArrayAccess
306 : */
307 :
308 : /**
309 : * If somebody tries to modify the internal array throw an exception.
310 : *
311 : * @param integer $offset
312 : * @param mixed $value
313 : * @access public
314 : * @return void
315 : */
316 : public function offsetSet($offset, $value) {
317 1 : throw new BadMethodCallException('List is read only');
318 : }
319 :
320 : /**
321 : * Check if index exists in internal array
322 : *
323 : * @param integer $offset
324 : * @access public
325 : * @return boolean
326 : */
327 : public function offsetExists($offset) {
328 1 : return isset($this->_array[$offset]);
329 : }
330 :
331 : /**
332 : * If somebody tries to remove an element from the internal array throw an exception.
333 : *
334 : * @param integer $offset
335 : * @access public
336 : * @return void
337 : */
338 : public function offsetUnset($offset) {
339 1 : throw new BadMethodCallException('List is read only');
340 : }
341 :
342 : /**
343 : * Get element from internal array
344 : *
345 : * @param $offset
346 : * @access public
347 : * @return void
348 : */
349 : public function offsetGet($offset) {
350 13 : return isset($this->_array[$offset]) ? $this->_array[$offset] : null;
351 : }
352 :
353 : /*
354 : * Core functions
355 : */
356 :
357 : /**
358 : * Create a new instance of the same class with the $this as the parent.
359 : *
360 : * This is used for the chaining and needs to be overloaded in child classes.
361 : *
362 : * @access private
363 : * @return
364 : */
365 : protected function _spawn() {
366 91 : return new FluentDOM($this);
367 : }
368 :
369 : /**
370 : * create a new xpath object an register default namespaces from the current document
371 : *
372 : * @access private
373 : * @return object DOMXPath
374 : */
375 : private function _xpath() {
376 101 : if (empty($this->_xpath) || $this->_xpath->document != $this->_document) {
377 101 : $this->_xpath = new DOMXPath($this->_document);
378 101 : if ($this->_document->documentElement) {
379 101 : $uri = $this->_document->documentElement->lookupnamespaceURI('_');
380 101 : if (!isset($uri)) {
381 101 : $uri = $this->_document->documentElement->lookupnamespaceURI(NULL);
382 101 : if (isset($uri)) {
383 1 : $this->_xpath->registerNamespace('_', $uri);
384 1 : }
385 101 : }
386 101 : }
387 101 : }
388 101 : return $this->_xpath;
389 : }
390 :
391 : /**
392 : * match xpath expression agains context and return matched elements
393 : *
394 : * @param string$expr
395 : * @param DOMElement $context optional, default value NULL
396 : * @access private
397 : * @return DOMNodeList
398 : */
399 : private function _match($expr, $context = NULL) {
400 100 : if (isset($context)) {
401 13 : return $this->_xpath()->query($expr, $context);
402 : } else {
403 100 : return $this->_xpath()->query($expr);
404 : }
405 : }
406 :
407 : /**
408 : * test xpath expression against context and return true/false
409 : *
410 : * @param string$expr
411 : * @param DOMElement $context optional, default value NULL
412 : * @access private
413 : * @return boolean
414 : */
415 : private function _test($expr, $context) {
416 6 : $check = $this->_xpath()->evaluate($expr, $context);
417 6 : if ($check instanceof DOMNodeList) {
418 2 : return $check->length > 0;
419 : } else {
420 4 : return (bool)$check;
421 : }
422 : }
423 :
424 : /**
425 : * push new elements an the list
426 : *
427 : * @param object DOMElement | object DOMNodeList | object FluentDOM $elements
428 : * @access private
429 : * @return void
430 : */
431 : private function _push($elements, $unique = FALSE) {
432 104 : if ($this->_isNode($elements)) {
433 39 : if ($elements->ownerDocument === $this->_document) {
434 38 : if (!$unique || !$this->_inList($elements, $this->_array)) {
435 38 : $this->_array[] = $elements;
436 38 : }
437 38 : } else {
438 1 : throw new OutOfBoundsException('Node is not a part of this document');
439 : }
440 104 : } elseif ($elements instanceof DOMNodeList ||
441 22 : $elements instanceof DOMDocumentFragment ||
442 22 : $elements instanceof Iterator ||
443 103 : is_array($elements)) {
444 103 : foreach ($elements as $node) {
445 101 : if ($this->_isNode($node)) {
446 101 : if ($node->ownerDocument === $this->_document) {
447 101 : if (!$unique || !$this->_inList($node, $this->_array)) {
448 101 : $this->_array[] = $node;
449 101 : }
450 101 : } else {
451 1 : throw new OutOfBoundsException('Node is not a part of this document');
452 : }
453 101 : }
454 103 : }
455 103 : }
456 104 : }
457 :
458 : /**
459 : * check if object is already in internal list
460 : *
461 : * @param object DOMElement $node
462 : * @access private
463 : * @return boolean
464 : */
465 : private function _inList($node) {
466 12 : foreach ($this->_array as $compareNode) {
467 8 : if ($compareNode === $node) {
468 2 : return TRUE;
469 : }
470 12 : }
471 12 : return FALSE;
472 : }
473 :
474 : /**
475 : * validate string as qualified tag name
476 : *
477 : * @todo implement isQName
478 : * @param string $name
479 : * @access private
480 : * @return boolean
481 : */
482 : private function _isQName($name) {
483 11 : if (preg_match('((\w[\w\d]*:)?(\w[\w\d-]*))', $name)) {
484 10 : return TRUE;
485 : } else {
486 1 : throw new UnexpectedValueException('Invalid QName');
487 : }
488 : }
489 :
490 : /**
491 : * Check if the DOMNode is DOMElement or DOMText with content
492 : *
493 : * @param DOMNode $node
494 : * @access private
495 : * @return boolean
496 : */
497 : private function _isNode($node) {
498 118 : if (is_object($node)) {
499 104 : if ($node instanceof DOMElement) {
500 100 : return TRUE;
501 101 : } elseif ($node instanceof DOMText &&
502 101 : !$node->isWhitespaceInElementContent()) {
503 9 : return TRUE;
504 : }
505 100 : }
506 117 : return FALSE;
507 : }
508 :
509 : /**
510 : * check if parameter is a valid callback function
511 : *
512 : * @param $callback
513 : * @access protected
514 : * @return boolean
515 : */
516 : protected function _isCallback($callback) {
517 10 : if ($callback instanceof Closure) {
518 0 : return TRUE;
519 10 : } elseif (is_string($callback) &&
520 10 : function_exists($callback)) {
521 5 : return is_callable($callback);
522 5 : } elseif (is_array($callback) &&
523 3 : count($callback) == 2 &&
524 3 : (is_object($callback[0]) || is_string($callback[0])) &&
525 5 : is_string($callback[1])) {
526 3 : return is_callable($callback);
527 : } else {
528 2 : throw new BadFunctionCallException('Invalid callback argument');
529 : }
530 : }
531 :
532 : /**
533 : * Convert a given content into and array of nodes
534 : *
535 : * @param string | object DOMElement | object DOMText | object Iterator $content
536 : * @param boolean $includeTextNodes
537 : * @param integer $limit
538 : * @access private
539 : * @return array
540 : */
541 : private function _getContentNodes($content, $includeTextNodes = TRUE, $limit = 0) {
542 19 : $result = array();
543 19 : if ($content instanceof DOMElement) {
544 1 : $result = array($content);
545 19 : } elseif ($includeTextNodes && $this->_isNode($content)) {
546 1 : $result = array($content);
547 18 : } elseif (is_string($content)) {
548 11 : $fragment = $this->_document->createDocumentFragment();
549 11 : if ($fragment->appendXML($content)) {
550 10 : foreach ($fragment->childNodes as $element) {
551 10 : if ($element instanceof DOMElement ||
552 10 : ($includeTextNodes && $this->_isNode($element))) {
553 10 : $element->parentNode->removeChild($element);
554 10 : $result[] = $element;
555 10 : if ($limit > 0 && count($result) >= $limit) {
556 4 : break;
557 : }
558 6 : }
559 10 : }
560 10 : return $result;
561 : } else {
562 1 : throw new UnexpectedValueException('Invalid document fragment');
563 : }
564 6 : } elseif ($content instanceof DOMNodeList ||
565 5 : $content instanceof Iterator ||
566 6 : is_array($content)) {
567 4 : foreach ($content as $element) {
568 3 : if ($element instanceof DOMElement ||
569 3 : ($includeTextNodes && $this->_isNode($element))) {
570 3 : $result[] = $element;
571 3 : if ($limit > 0 && count($result) >= $limit) {
572 2 : break;
573 : }
574 1 : }
575 4 : }
576 4 : } else {
577 2 : throw new InvalidArgumentException('Invalid content parameter');
578 : }
579 6 : if (empty($result)) {
580 1 : throw new UnexpectedValueException('No element found');
581 : } else {
582 : //if a node is not in the current document import it
583 5 : foreach ($result as $index => $node) {
584 5 : if ($node->ownerDocument !== $this->_document) {
585 1 : $result[$index] = $this->_document->importNode($node, TRUE);
586 1 : }
587 5 : }
588 : }
589 5 : return $result;
590 : }
591 :
592 : private function _getTargetNodes($selector) {
593 13 : if ($this->_isNode($selector)) {
594 1 : return array($selector);
595 13 : } elseif (is_string($selector)) {
596 10 : return $this->_match($selector);
597 13 : } elseif (is_array($selector) ||
598 7 : $selector instanceof Iterator ||
599 13 : $selector instanceof DOMNodeList) {
600 12 : return $selector;
601 : } else {
602 1 : throw new InvalidArgumentException('Invalid selector');
603 : }
604 : }
605 :
606 : /**
607 : * Remove nodes from document tree
608 : *
609 : * @param $selector
610 : * @access private
611 : * @return array removed nodes
612 : */
613 : private function _removeNodes($selector) {
614 12 : $targetNodes = $this->_getTargetNodes($selector);
615 12 : $result = array();
616 12 : foreach ($targetNodes as $node) {
617 12 : if ($node instanceof DOMNode &&
618 12 : isset($node->parentNode)) {
619 12 : $result[] = $node->parentNode->removeChild($node);
620 12 : }
621 12 : }
622 12 : return $result;
623 : }
624 :
625 : /**
626 : * Convert content to DOMElement
627 : *
628 : * @param string | array | object DOMElement | object FluentDOM $content
629 : * @access private
630 : * @return object DOMElement
631 : */
632 : private function _getContentElement($content) {
633 8 : if ($content instanceof DOMElement) {
634 1 : return $content;
635 : } else {
636 7 : $contentNodes = $this->_getContentNodes($content, FALSE, 1);
637 6 : return $contentNodes[0];
638 : }
639 : }
640 :
641 : /*
642 : * Object Accessors
643 : */
644 :
645 : /**
646 : * Execute a function within the context of every matched element.
647 : *
648 : * @param callback | object Closure $function
649 : * @access public
650 : * @return object FluentDOM
651 : */
652 : public function each($function) {
653 3 : if ($this->_isCallback($function)) {
654 2 : foreach ($this->_array as $index => $node) {
655 2 : call_user_func($function, $node, $index);
656 2 : }
657 2 : }
658 2 : return $this;
659 : }
660 :
661 : /**
662 : * Formats the current document, resets internal node array and other properties.
663 : *
664 : * The document is saved and reloaded, all variables with DOMNodes of this document will get invalid.
665 : *
666 : * @access public
667 : * @return object FluentDOM
668 : */
669 : public function formatOutput() {
670 3 : $this->_array = array();
671 3 : $this->_position = 0;
672 3 : $this->_useDocumentContext = TRUE;
673 3 : $this->_parent = NULL;
674 3 : $this->_document->preserveWhiteSpace = FALSE;
675 3 : $this->_document->formatOutput = TRUE;
676 3 : $this->_document->loadXML($this->_document->saveXML());
677 3 : return $this;
678 : }
679 :
680 : /*
681 : * Traversing - Filtering
682 : */
683 :
684 : /**
685 : * Reduce the set of matched elements to a single element.
686 : *
687 : * @param integer $position Element index (start with 0)
688 : * @access public
689 : * @return object FluentDOM
690 : */
691 : public function eq($position) {
692 4 : $result = $this->_spawn();
693 4 : if (isset($this->_array[$position])) {
694 4 : $result->_push($this->_array[$position]);
695 4 : }
696 4 : return $result;
697 : }
698 :
699 : /**
700 : * Removes all elements from the set of matched elements that do not match the specified expression(s).
701 : *
702 : * @param string $expr | callback | object Closure XPath expression or callback function
703 : * @access public
704 : * @return object FluentDOM
705 : */
706 : public function filter($expr) {
707 2 : $result = $this->_spawn();
708 2 : foreach ($this->_array as $index => $node) {
709 2 : $check = TRUE;
710 2 : if (is_string($expr)) {
711 1 : $check = $this->_test($expr, $node, $index);
712 2 : } elseif ($this->_isCallback($expr)) {
713 1 : $check = call_user_func($expr, $node, $index);
714 1 : }
715 2 : if ($check) {
716 2 : $result->_push($node);
717 2 : }
718 2 : }
719 2 : return $result;
720 : }
721 :
722 : /**
723 : * Checks the current selection against an expression and returns true,
724 : * if at least one element of the selection fits the given expression.
725 : *
726 : * @param string $expr XPath expression
727 : * @access public
728 : * @return boolean
729 : */
730 : public function is($expr) {
731 2 : foreach ($this->_array as $node) {
732 1 : return $this->_test($expr, $node);
733 1 : }
734 1 : return FALSE;
735 : }
736 :
737 : /**
738 : * Translate a set of elements in the FluentDOM object into
739 : * another set of values in an array (which may, or may not contain elements).
740 : *
741 : * @param callback | object Closure $function
742 : * @access public
743 : * @return array
744 : */
745 : public function map($function) {
746 4 : $result = array();
747 4 : foreach ($this->_array as $index => $node) {
748 4 : if ($this->_isCallback($function)) {
749 3 : $mapped = call_user_func($function, $node, $index);
750 3 : }
751 3 : if ($mapped === NULL) {
752 1 : continue;
753 3 : } elseif ($mapped instanceof DOMNodeList ||
754 3 : $mapped instanceof Iterator ||
755 3 : is_array($mapped)) {
756 1 : foreach ($mapped as $element) {
757 1 : if ($element !== NULL) {
758 1 : $result[] = $element;
759 1 : }
760 1 : }
761 1 : } else {
762 3 : $result[] = $mapped;
763 : }
764 3 : }
765 3 : return $result;
766 : }
767 :
768 : /**
769 : * Removes elements matching the specified expression from the set of matched elements.
770 : *
771 : * @param string $expr | callback | object Closure XPath expression or callback function
772 : * @access public
773 : * @return object FluentDOM
774 : */
775 : public function not($expr) {
776 2 : $result = $this->_spawn();
777 2 : foreach ($this->_array as $index => $node) {
778 2 : $check = FALSE;
779 2 : if (is_string($expr)) {
780 1 : $check = $this->_test($expr, $node, $index);
781 2 : } elseif ($this->_isCallback($expr)) {
782 1 : $check = call_user_func($expr, $node, $index);
783 1 : }
784 2 : if (!$check) {
785 2 : $result->_push($node);
786 2 : }
787 2 : }
788 2 : return $result;
789 : }
790 :
791 : /**
792 : * Selects a subset of the matched elements.
793 : *
794 : * @param integer $start
795 : * @param integer $end
796 : * @access public
797 : * @return object FluentDOM
798 : */
799 : public function slice($start, $end = NULL) {
800 4 : $result = $this->_spawn();
801 4 : if ($end === NULL) {
802 1 : $result->_push(array_slice($this->_array, $start));
803 4 : } elseif ($end < 0) {
804 1 : $result->_push(array_slice($this->_array, $start, $end));
805 3 : } elseif ($end > $start) {
806 1 : $result->_push(array_slice($this->_array, $start, $end - $start));
807 1 : } else {
808 1 : $result->_push(array_slice($this->_array, $end, $start - $end));
809 : }
810 4 : return $result;
811 : }
812 :
813 : /*
814 : * Traversing - Finding
815 : */
816 :
817 : /**
818 : * Adds more elements, matched by the given expression, to the set of matched elements.
819 : *
820 : * @param string $expr XPath expression
821 : * @access public
822 : * @return object FluentDOM
823 : */
824 : public function add($expr) {
825 5 : $result = $this->_spawn();
826 5 : $result->_push($this->_array);
827 5 : if (is_object($expr)) {
828 3 : $result->_push($expr);
829 3 : } elseif (isset($this->_parent)) {
830 1 : $result->_push($this->_parent->find($expr));
831 1 : } else {
832 1 : $result->_push($this->find($expr));
833 : }
834 3 : return $result;
835 : }
836 :
837 : /**
838 : * Get a set of elements containing all of the unique immediate
839 : * children of each of the matched set of elements.
840 : *
841 : * @param string $expr XPath expression
842 : * @access public
843 : * @return object FluentDOM
844 : */
845 : public function children($expr = NULL) {
846 3 : $result = $this->_spawn();
847 3 : foreach ($this->_array as $node) {
848 3 : if (empty($expr)) {
849 2 : $result->_push($node->childNodes, TRUE);
850 2 : } else {
851 1 : foreach ($node->childNodes as $childNode) {
852 1 : if ($this->_test($expr, $childNode)) {
853 1 : $result->_push($childNode, TRUE);
854 1 : }
855 1 : }
856 : }
857 3 : }
858 3 : return $result;
859 : }
860 :
861 : /**
862 : * Searches for descendent elements that match the specified expression.
863 : *
864 : * @param string $expr XPath expression
865 : * @access public
866 : * @return object FluentDOM
867 : */
868 : public function find($expr) {
869 99 : $result = $this->_spawn();
870 99 : if ($this->_useDocumentContext) {
871 99 : $result->_push($this->_match($expr));
872 99 : } else {
873 4 : foreach ($this->_array as $contextNode) {
874 4 : $result->_push($this->_match($expr, $contextNode));
875 4 : }
876 : }
877 99 : return $result;
878 : }
879 :
880 : /**
881 : * Get a set of elements containing the unique next siblings of each of the given set of elements.
882 : *
883 : * Like jQuerys next() method but renamed because of a conflict with Iterator
884 : *
885 : * @param string $expr XPath expression
886 : * @access public
887 : * @return FluentDOM
888 : */
889 : public function nextSiblings($expr = NULL) {
890 1 : $result = $this->_spawn();
891 1 : foreach ($this->_array as $node) {
892 1 : $next = $node->nextSibling;
893 1 : while ($next instanceof DOMNode && !$this->_isNode($next)) {
894 1 : $next = $next->nextSibling;
895 1 : }
896 1 : if (!empty($next)) {
897 1 : if (empty($expr) || $this->_test($expr, $next)) {
898 1 : $result->_push($next, TRUE);
899 1 : }
900 1 : }
901 1 : }
902 1 : return $result;
903 : }
904 :
905 : /**
906 : * Find all sibling elements after the current element.
907 : *
908 : * Like jQuerys nextAll() method but renamed for consistency with nextSiblings()
909 : *
910 : * @param string $expr XPath expression
911 : * @access public
912 : * @return FluentDOM
913 : */
914 : public function nextAllSiblings($expr = NULL) {
915 1 : $result = $this->_spawn();
916 1 : foreach ($this->_array as $node) {
917 1 : $next = $node->nextSibling;
918 1 : while ($next instanceof DOMNode) {
919 1 : if ($this->_isNode($next)) {
920 1 : if (empty($expr) || $this->_test($expr, $next)) {
921 1 : $result->_push($next, TRUE);
922 1 : }
923 1 : }
924 1 : $next = $next->nextSibling;
925 1 : }
926 1 : }
927 1 : return $result;
928 : }
929 :
930 : /**
931 : * Get a set of elements containing the unique parents of the matched set of elements.
932 : *
933 : * @access public
934 : * @return FluentDOM
935 : */
936 : public function parent() {
937 1 : $result = $this->_spawn();
938 1 : foreach ($this->_array as $node) {
939 1 : if (isset($node->parentNode)) {
940 1 : $result->_push($node->parentNode, TRUE);
941 1 : }
942 1 : }
943 1 : return $result;
944 : }
945 :
946 : /**
947 : * Get a set of elements containing the unique ancestors of the matched set of elements.
948 : *
949 : * @param string $expr XPath expression
950 : * @access public
951 : * @return FluentDOM
952 : */
953 : public function parents($expr = NULL) {
954 1 : $result = $this->_spawn();
955 1 : foreach ($this->_array as $node) {
956 1 : $parents = $this->_match('ancestor::*', $node);
957 1 : for ($i = $parents->length - 1; $i >= 0; --$i) {
958 1 : $parentNode = $parents->item($i);
959 1 : if (empty($expr) || $this->_test($expr, $parentNode)) {
960 1 : $result->_push($parentNode, TRUE);
961 1 : }
962 1 : }
963 1 : }
964 1 : return $result;
965 : }
966 :
967 : /**
968 : * Get a set of elements containing the unique previous siblings of each of the matched set of elements.
969 : *
970 : * Like jQuerys prev() method but renamed for consistency with nextSiblings()
971 : *
972 : * @param string $expr XPath expression
973 : * @access public
974 : * @return object FluentDOM
975 : */
976 : public function prevSiblings($expr = NULL) {
977 3 : $result = $this->_spawn();
978 3 : foreach ($this->_array as $node) {
979 3 : $previous = $node->previousSibling;
980 3 : while ($previous instanceof DOMNode && !$this->_isNode($previous)) {
981 3 : $previous = $previous->previousSibling;
982 3 : }
983 3 : if (!empty($previous)) {
984 3 : if (empty($expr) || $this->_test($expr, $previous)) {
985 3 : $result->_push($previous, TRUE);
986 3 : }
987 3 : }
988 3 : }
989 3 : return $result;
990 : }
991 :
992 : /**
993 : * Find all sibling elements in front of the current element.
994 : *
995 : * Like jQuerys prevAll() method but renamed for consistency with nextSiblings()
996 : *
997 : * @param string $expr XPath expression
998 : * @access public
999 : * @return object FluentDOM
1000 : */
1001 : public function prevAllSiblings($expr = NULL) {
1002 1 : $result = $this->_spawn();
1003 1 : foreach ($this->_array as $node) {
1004 1 : $previous = $node->previousSibling;
1005 1 : while ($previous instanceof DOMNode) {
1006 1 : if ($this->_isNode($previous)) {
1007 1 : if (empty($expr) || $this->_test($expr, $previous)) {
1008 1 : $result->_push($previous, TRUE);
1009 1 : }
1010 1 : }
1011 1 : $previous = $previous->previousSibling;
1012 1 : }
1013 1 : }
1014 1 : return $result;
1015 : }
1016 :
1017 : /**
1018 : * Get a set of elements containing all of the unique siblings of each of the matched set of elements.
1019 : *
1020 : * @param string $expr XPath expression
1021 : * @access public
1022 : * @return object FluentDOM
1023 : */
1024 : public function siblings($expr = NULL) {
1025 1 : $result = $this->_spawn();
1026 1 : foreach ($this->_array as $node) {
1027 1 : if (isset($node->parentNode)) {
1028 1 : $siblings = $node->parentNode->childNodes;
1029 1 : foreach ($node->parentNode->childNodes as $childNode) {
1030 1 : if ($this->_isNode($childNode) &&
1031 1 : $childNode !== $node) {
1032 1 : if (empty($expr) || $this->_test($expr, $childNode)) {
1033 1 : $result->_push($childNode, TRUE);
1034 1 : }
1035 1 : }
1036 1 : }
1037 1 : }
1038 1 : }
1039 1 : return $result;
1040 : }
1041 :
1042 : /*
1043 : * Traversing - Chaining
1044 : */
1045 :
1046 : /**
1047 : * Add the previous selection to the current selection.
1048 : *
1049 : * @access public
1050 : * @return object FluentDOM
1051 : */
1052 : public function andSelf() {
1053 2 : $result = $this->_spawn();
1054 2 : $result->_push($this->_array);
1055 2 : $result->_push($this->_parent);
1056 2 : return $result;
1057 : }
1058 :
1059 : /**
1060 : * Revert the most recent traversing operation,
1061 : * changing the set of matched elements to its previous state.
1062 : *
1063 : * @access public
1064 : * @return object FluentDOM
1065 : */
1066 : public function end() {
1067 1 : if ($this->_parent instanceof FluentDOM) {
1068 1 : return $this->_parent;
1069 : } else {
1070 1 : return $this;
1071 : }
1072 : }
1073 :
1074 : /*
1075 : * Manipulation - Changing Contents
1076 : */
1077 :
1078 : /**
1079 : * Get or set the xml contents of the first matched element.
1080 : *
1081 : * @param string $xml XML fragment
1082 : * @access public
1083 : * @return string | object FluentDOM
1084 : */
1085 : public function xml($xml = NULL) {
1086 2 : if (isset($xml)) {
1087 1 : if (!empty($xml)) {
1088 1 : $fragment = $this->_document->createDocumentFragment();
1089 1 : if ($fragment->appendXML($xml)) {
1090 1 : foreach ($this->_array as $node) {
1091 1 : $node->nodeValue = '';
1092 1 : $node->appendChild($fragment->cloneNode(TRUE));
1093 1 : }
1094 1 : }
1095 1 : }
1096 1 : return $this;
1097 : } else {
1098 1 : $result = '';
1099 1 : if (isset($this->_array[0])) {
1100 1 : foreach ($this->_array[0]->childNodes as $childNode) {
1101 1 : if ($this->_isNode($childNode)) {
1102 1 : $result .= $this->_document->saveXML($childNode);
1103 1 : }
1104 1 : }
1105 1 : }
1106 1 : return $result;
1107 : }
1108 : }
1109 :
1110 : /**
1111 : * Get the combined text contents of all matched elements or
1112 : * set the text contents of all matched elements.
1113 : *
1114 : * @param string $text
1115 : * @access public
1116 : * @return string | object FluentDOM
1117 : */
1118 : public function text($text = NULL) {
1119 4 : if (isset($text)) {
1120 2 : foreach ($this->_array as $node) {
1121 2 : $node->nodeValue = $text;
1122 2 : }
1123 2 : return $this;
1124 : } else {
1125 2 : $result = '';
1126 2 : foreach ($this->_array as $node) {
1127 2 : $result .= $node->textContent;
1128 2 : }
1129 2 : return $result;
1130 : }
1131 : }
1132 :
1133 : /*
1134 : * Manipulation - Inserting Inside
1135 : */
1136 :
1137 : /**
1138 : * Append content to the inside of every matched element.
1139 : *
1140 : * @param string | object DOMNode | object FluentDOM $content DOMNode or DOMNodeList or xml fragment string
1141 : * @access public
1142 : * @return string | object FluentDOM
1143 : */
1144 : public function append($content) {
1145 5 : return $this->_insertChild($content, FALSE);
1146 : }
1147 :
1148 : /**
1149 : * Append all of the matched elements to another, specified, set of elements.
1150 : * Returns all of the inserted elements.
1151 : *
1152 : * @param string | object DOMElement | object FluentDOM $expr XPath expression, element or list of elements
1153 : * @access public
1154 : * @return object FluentDOM
1155 : */
1156 : public function appendTo($expr) {
1157 1 : return $this->_insertChildTo($expr, FALSE);
1158 : }
1159 :
1160 : /**
1161 : * Prepend content to the inside of every matched element.
1162 : *
1163 : * @param string | object DOMNode | object FluentDOM $content DOMNode or DOMNodeList or xml fragment string
1164 : * @access public
1165 : * @return string | object FluentDOM
1166 : */
1167 : public function prepend($content) {
1168 3 : return $this->_insertChild($content, TRUE);
1169 : }
1170 :
1171 : /**
1172 : * Prepend all of the matched elements to another, specified, set of elements.
1173 : * Returns all of the inserted elements.
1174 : *
1175 : * @param string | object DOMElement | object FluentDOM $expr XPath expression, element or list of elements
1176 : * @access public
1177 : * @return object FluentDOM list of all new elements
1178 : */
1179 : public function prependTo($expr) {
1180 1 : return $this->_insertChildTo($expr, TRUE);
1181 : }
1182 :
1183 : /**
1184 : * Insert content to the inside of every matched element.
1185 : *
1186 : * @param string | object DOMNode | object FluentDOM $content DOMNode or DOMNodeList or xml fragment string
1187 : * @param boolean $first insert at first position (or last)
1188 : * @access private
1189 : * @return object FluentDOM
1190 : */
1191 : private function _insertChild($content, $first) {
1192 8 : if (!empty($content)) {
1193 8 : if ($content instanceof DOMNode) {
1194 1 : foreach ($this->_array as $node) {
1195 1 : $node->insertBefore(
1196 1 : $content->cloneNode(TRUE),
1197 1 : ($first && $node->hasChildNodes()) ? $node->childNodes->item(0) : NULL
1198 1 : );
1199 1 : }
1200 8 : } elseif ($content instanceof FluentDOM) {
1201 1 : foreach ($this->_array as $node) {
1202 1 : foreach ($content as $contentNode) {
1203 1 : $node->insertBefore(
1204 1 : $contentNode->cloneNode(TRUE),
1205 1 : ($first && $node->hasChildNodes()) ? $node->childNodes->item(0) : NULL
1206 1 : );
1207 1 : }
1208 1 : }
1209 1 : } else {
1210 6 : $fragment = $this->_document->createDocumentFragment();
1211 6 : if ($fragment->appendXML($content)) {
1212 6 : foreach ($this->_array as $node) {
1213 6 : $node->insertBefore(
1214 6 : $fragment->cloneNode(TRUE),
1215 6 : ($first && $node->hasChildNodes()) ? $node->childNodes->item(0) : NULL
1216 6 : );
1217 6 : }
1218 6 : }
1219 : }
1220 8 : }
1221 8 : return $this;
1222 : }
1223 :
1224 : /**
1225 : * Insert all of the matched elements to another, specified, set of elements.
1226 : * Returns all of the inserted elements.
1227 : *
1228 : * @param string | object DOMElement | object FluentDOM $selector XPath expression, element or list of elements
1229 : * @param boolean $first insert at first position (or last)
1230 : * @access public
1231 : * @return object FluentDOM
1232 : */
1233 : private function _insertChildTo($selector, $first) {
1234 2 : $result = $this->_spawn();
1235 2 : $targets = $this->_getTargetNodes($selector);
1236 2 : if (!empty($targets)) {
1237 2 : foreach ($targets as $targetNode) {
1238 2 : if ($targetNode instanceof DOMElement) {
1239 2 : foreach ($this->_array as $node) {
1240 2 : $result->_push(
1241 2 : $targetNode->insertBefore(
1242 2 : $node->cloneNode(TRUE),
1243 1 : ($first && $targetNode->hasChildNodes())
1244 2 : ? $targetNode->childNodes->item(0) : NULL
1245 2 : )
1246 2 : );
1247 2 : }
1248 2 : }
1249 2 : $this->_removeNodes($this->_array);
1250 2 : }
1251 2 : }
1252 2 : return $result;
1253 : }
1254 :
1255 : /*
1256 : * Manipulation - Inserting Outside
1257 : */
1258 :
1259 : /**
1260 : * Insert content after each of the matched elements.
1261 : *
1262 : * @param $content
1263 : * @access public
1264 : * @return
1265 : */
1266 : public function after($content) {
1267 1 : $result = $this->_spawn();
1268 1 : if ($contentNodes = $this->_getContentNodes($content, TRUE)) {
1269 1 : foreach ($this->_array as $node) {
1270 1 : $beforeNode = $node->nextSibling;
1271 1 : if (isset($node->parentNode)) {
1272 1 : foreach ($contentNodes as $contentNode) {
1273 1 : $result->_push(
1274 1 : $node->parentNode->insertBefore(
1275 1 : $contentNode->cloneNode(TRUE),
1276 : $beforeNode
1277 1 : )
1278 1 : );
1279 1 : }
1280 1 : }
1281 1 : }
1282 1 : }
1283 1 : return $result;
1284 : }
1285 :
1286 : /**
1287 : * Insert content before each of the matched elements.
1288 : *
1289 : * @param $content
1290 : * @access public
1291 : * @return object FluentDOM
1292 : */
1293 : public function before($content) {
1294 1 : $result = $this->_spawn();
1295 1 : if ($contentNodes = $this->_getContentNodes($content, TRUE)) {
1296 1 : foreach ($this->_array as $node) {
1297 1 : if (isset($node->parentNode)) {
1298 1 : foreach ($contentNodes as $contentNode) {
1299 1 : $result->_push(
1300 1 : $node->parentNode->insertBefore(
1301 1 : $contentNode->cloneNode(TRUE),
1302 : $node
1303 1 : )
1304 1 : );
1305 1 : }
1306 1 : }
1307 1 : }
1308 1 : }
1309 1 : return $result;
1310 : }
1311 :
1312 : /**
1313 : * Insert all of the matched elements after another, specified, set of elements.
1314 : *
1315 : * @param $selector
1316 : * @access public
1317 : * @return object FluentDOM
1318 : */
1319 : public function insertAfter($selector) {
1320 1 : $result = $this->_spawn();
1321 1 : $targets = $this->_getTargetNodes($selector);
1322 1 : if (!empty($targets)) {
1323 1 : foreach ($targets as $targetNode) {
1324 1 : if ($this->_isNode($targetNode) && isset($targetNode->parentNode)) {
1325 1 : $beforeNode = $targetNode->nextSibling;
1326 1 : foreach ($this->_array as $node) {
1327 1 : $result->_push(
1328 1 : $targetNode->parentNode->insertBefore(
1329 1 : $node->cloneNode(TRUE),
1330 : $beforeNode
1331 1 : )
1332 1 : );
1333 1 : }
1334 1 : }
1335 1 : $this->_removeNodes($this->_array);
1336 1 : }
1337 1 : }
1338 1 : return $result;
1339 : }
1340 :
1341 : /**
1342 : * Insert all of the matched elements before another, specified, set of elements.
1343 : *
1344 : * @param $selector
1345 : * @access public
1346 : * @return object FluentDOM
1347 : */
1348 : public function insertBefore($selector) {
1349 1 : $result = $this->_spawn();
1350 1 : $targets = $this->_getTargetNodes($selector);
1351 1 : if (!empty($targets)) {
1352 1 : foreach ($targets as $targetNode) {
1353 1 : if ($this->_isNode($targetNode) && isset($targetNode->parentNode)) {
1354 1 : foreach ($this->_array as $node) {
1355 1 : $result->_push(
1356 1 : $targetNode->parentNode->insertBefore(
1357 1 : $node->cloneNode(TRUE),
1358 : $targetNode
1359 1 : )
1360 1 : );
1361 1 : }
1362 1 : }
1363 1 : $this->_removeNodes($this->_array);
1364 1 : }
1365 1 : }
1366 1 : return $result;
1367 : }
1368 :
1369 : /*
1370 : * Manipulation - Inserting Around
1371 : */
1372 :
1373 : /**
1374 : * Wrap $content around a set of elements
1375 : *
1376 : * @param array $elements
1377 : * @param string | array | object DOMElement | object FluentDOM $content
1378 : * @access private
1379 : * @return object FluentDOM
1380 : */
1381 : private function _wrap($elements, $content) {
1382 6 : $wrapperTemplate = $this->_getContentElement($content);
1383 5 : $result = array();
1384 5 : if ($wrapperTemplate instanceof DOMElement) {
1385 5 : $simple = FALSE;
1386 5 : foreach ($elements as $node) {
1387 5 : $wrapper = $wrapperTemplate->cloneNode(TRUE);
1388 5 : if (!$simple) {
1389 5 : $targets = $this->_match('.//*[count(*) = 0]', $wrapper);
1390 5 : }
1391 5 : if ($simple || $targets->length == 0) {
1392 4 : $target = $wrapper;
1393 4 : $simple = TRUE;
1394 4 : } else {
1395 1 : $target = $targets->item(0);
1396 : }
1397 5 : if (isset($node->parentNode)) {
1398 5 : $node->parentNode->insertBefore($wrapper, $node);
1399 5 : }
1400 5 : $target->appendChild($node);
1401 5 : $result[] = $node;
1402 5 : }
1403 5 : }
1404 5 : return $result;
1405 : }
1406 :
1407 : /**
1408 : * Wrap each matched element with the specified content.
1409 : *
1410 : * If $content contains several elements the first one is used
1411 : *
1412 : * @param string | array | object DOMElement | object FluentDOM $content
1413 : * @access public
1414 : * @return object FluentDOM
1415 : */
1416 : public function wrap($content) {
1417 5 : $result = $this->_spawn();
1418 5 : $result->_push($this->_wrap($this->_array, $content));
1419 4 : return $result;
1420 : }
1421 :
1422 : /**
1423 : * Wrap al matched elements with the specified content
1424 : *
1425 : * If the matched elemetns are not siblings, wrap each group of siblings.
1426 : *
1427 : * @param string | array | object DOMElement | object FluentDOM $content
1428 : * @access public
1429 : * @return object FluentDOM
1430 : */
1431 : public function wrapAll($content) {
1432 2 : $result = $this->_spawn();
1433 2 : $current = NULL;
1434 2 : $counter = 0;
1435 2 : $groups = array();
1436 : //group elements by previous node - ignore whitespace text nodes
1437 2 : foreach ($this->_array as $node) {
1438 2 : $previous = $node->previousSibling;
1439 2 : while ($previous instanceof DOMText && $previous->isWhitespaceInElementContent()) {
1440 2 : $previous = $previous->previousSibling;
1441 2 : }
1442 2 : if ($previous !== $current) {
1443 2 : $counter++;
1444 2 : }
1445 2 : $groups[$counter][] = $node;
1446 2 : $current = $node;
1447 2 : }
1448 2 : if (count($groups) > 0) {
1449 2 : $wrapperTemplate = $this->_getContentElement($content);
1450 2 : $simple = FALSE;
1451 2 : foreach ($groups as $group) {
1452 2 : if (isset($group[0])) {
1453 2 : $node = $group[0];
1454 2 : $wrapper = $wrapperTemplate->cloneNode(TRUE);
1455 2 : if (!$simple) {
1456 2 : $targets = $this->_match('.//*[count(*) = 0]', $wrapper);
1457 2 : }
1458 2 : if ($simple || $targets->length == 0) {
1459 1 : $target = $wrapper;
1460 1 : $simple = TRUE;
1461 1 : } else {
1462 1 : $target = $targets->item(0);
1463 : }
1464 2 : if (isset($node->parentNode)) {
1465 2 : $node->parentNode->insertBefore($wrapper, $node);
1466 2 : }
1467 2 : foreach ($group as $node) {
1468 2 : $target->appendChild($node);
1469 2 : }
1470 2 : $result->_push($node);
1471 2 : }
1472 2 : }
1473 2 : }
1474 2 : return $result;
1475 : }
1476 :
1477 : /**
1478 : * Wrap the inner child contents of each matched element
1479 : * (including text nodes) with an XML structure.
1480 : *
1481 : * @param string | array | object DOMElement | object FluentDOM $content
1482 : * @access public
1483 : * @return FluentDOM
1484 : */
1485 : public function wrapInner($content) {
1486 1 : $result = $this->_spawn();
1487 1 : $elements = array();
1488 1 : foreach ($this->_array as $node) {
1489 1 : foreach ($node->childNodes as $childNode) {
1490 1 : if ($this->_isNode($childNode)) {
1491 1 : $elements[] = $childNode;
1492 1 : }
1493 1 : }
1494 1 : }
1495 1 : $result->_push($this->_wrap($elements, $content));
1496 1 : return $result;
1497 : }
1498 :
1499 : /*
1500 : * Manipulation - Replacing
1501 : */
1502 :
1503 : /**
1504 : * Replaces all matched elements with the specified HTML or DOM elements.
1505 : * This returns the JQuery element that was just replaced,
1506 : * which has been removed from the DOM.
1507 : *
1508 : * @param $content
1509 : * @access public
1510 : * @return
1511 : */
1512 : public function replaceWith($content) {
1513 1 : $contentNodes = $this->_getContentNodes($content);
1514 1 : foreach ($this->_array as $node) {
1515 1 : if (isset($node->parentNode)) {
1516 1 : foreach ($contentNodes as $contentNode) {
1517 1 : $node->parentNode->insertBefore(
1518 1 : $contentNode->cloneNode(TRUE),
1519 : $node
1520 1 : );
1521 1 : }
1522 1 : }
1523 1 : }
1524 1 : $this->_removeNodes($this->_array);
1525 1 : return $this;
1526 : }
1527 :
1528 : /**
1529 : * Replaces the elements matched by the specified selector with the matched elements.
1530 : *
1531 : * @param $selector
1532 : * @access public
1533 : * @return object FluentDOM
1534 : */
1535 : public function replaceAll($selector) {
1536 8 : $result = $this->_spawn();
1537 8 : $targetNodes = $this->_getTargetNodes($selector);
1538 7 : foreach ($targetNodes as $targetNode) {
1539 7 : if (isset($targetNode->parentNode)) {
1540 7 : foreach ($this->_array as $node) {
1541 7 : $result->_push(
1542 7 : $targetNode->parentNode->insertBefore(
1543 7 : $node->cloneNode(TRUE),
1544 : $targetNode
1545 7 : )
1546 7 : );
1547 7 : }
1548 7 : }
1549 7 : }
1550 7 : $this->_removeNodes($targetNodes);
1551 7 : $this->_removeNodes($this->_array);
1552 7 : return $result;
1553 : }
1554 :
1555 : /*
1556 : * Manipulation - Removing
1557 : */
1558 :
1559 : /**
1560 : * this is the empty() method - but because empty
1561 : * is a reserved word we can no declare it directly
1562 : * @see __call
1563 : *
1564 : * @access private
1565 : * @return object FluentDOM
1566 : */
1567 : private function _emptyNodes() {
1568 1 : foreach ($this->_array as $node) {
1569 1 : if ($node instanceof DOMElement ||
1570 1 : $node instanceof DOMText) {
1571 1 : $node->nodeValue = '';
1572 1 : }
1573 1 : }
1574 1 : return $this;
1575 : }
1576 :
1577 : /**
1578 : * Removes all matched elements from the DOM.
1579 : *
1580 : * @param string $expr XPath expression
1581 : * @access public
1582 : * @return object FluentDOM removed elements
1583 : */
1584 : public function remove($expr = NULL) {
1585 1 : $result = $this->_spawn();
1586 1 : foreach ($this->_array as $node) {
1587 1 : if (isset($node->parentNode)) {
1588 1 : if (empty($expr) || $this->test($expr, $node)) {
1589 1 : $result->_push($node->parentNode->removeChild($node));
1590 1 : }
1591 1 : }
1592 1 : }
1593 1 : return $result;
1594 : }
1595 :
1596 : /*
1597 : * Manipulation - Creation
1598 : */
1599 :
1600 : /**
1601 : * create nodes list from content, if $content contains node(s)
1602 : * from another document the are imported.
1603 : *
1604 : * @param $content
1605 : * @access public
1606 : * @return object FluentDOM
1607 : */
1608 : public function node($content) {
1609 9 : $result = $this->_spawn();
1610 9 : $result->_push($this->_getContentNodes($content));
1611 6 : return $result;
1612 : }
1613 :
1614 : /*
1615 : * Manipulation - Copying
1616 : */
1617 :
1618 : /**
1619 : * Clone matched DOM Elements and select the clones.
1620 : *
1621 : * @access private
1622 : * @return object FluentDOM
1623 : */
1624 : private function _cloneNodes() {
1625 1 : $result = $this->_spawn();
1626 1 : foreach ($this->_array as $node) {
1627 1 : $result->_push($node->cloneNode(TRUE));
1628 1 : }
1629 1 : return $result;
1630 : }
1631 :
1632 : /*
1633 : * Attributes - General
1634 : */
1635 :
1636 : /**
1637 : * Access a property on the first matched element or set the attribute(s) of all matched elements
1638 : *
1639 : * @param string | array $expr attribute name or attribute list
1640 : * @param callback | string $value function callback or value
1641 : * @access public
1642 : * @return string | object FluentDOM attribute value or $this
1643 : */
1644 : public function attr($attribute, $value = NULL) {
1645 11 : if (is_array($attribute) && count($attribute) > 0) {
1646 : //expr is an array of attributes and values - set on each element
1647 1 : foreach ($attribute as $key => $value) {
1648 1 : if ($this->_isQName($key)) {
1649 1 : foreach ($this->_array as $node) {
1650 1 : if ($node instanceof DOMElement) {
1651 1 : $node->setAttribute($key, $value);
1652 1 : }
1653 1 : }
1654 1 : }
1655 1 : }
1656 11 : } elseif (is_null($value)) {
1657 : //empty value - read attribute from first element in list
1658 11 : if ($this->_isQName($attribute) &&
1659 10 : count($this->_array) > 0) {
1660 9 : $node = $this->_array[0];
1661 9 : if ($node instanceof DOMElement) {
1662 8 : return $node->getAttribute($attribute);
1663 : }
1664 1 : }
1665 2 : return NULL;
1666 2 : } elseif (is_array($value) ||
1667 2 : $value instanceof Closure) {
1668 : //value is an array (function callback) - execute ist and set result on each element
1669 1 : if ($this->_isQName($attribute)) {
1670 1 : foreach ($this->_array as $index => $node) {
1671 1 : if ($node instanceof DOMElement) {
1672 1 : $node->setAttribute(
1673 1 : $attribute,
1674 1 : call_user_func($value, $node, $index)
1675 1 : );
1676 1 : }
1677 1 : }
1678 1 : }
1679 1 : } else {
1680 : // set attribute value of each element
1681 1 : if ($this->_isQName($attribute)) {
1682 1 : foreach ($this->_array as $node) {
1683 1 : if ($node instanceof DOMElement) {
1684 1 : $node->setAttribute($attribute, (string)$value);
1685 1 : }
1686 1 : }
1687 1 : }
1688 : }
1689 3 : return $this;
1690 : }
1691 :
1692 : /**
1693 : * Remove an attribute from each of the matched elements.
1694 : *
1695 : * @param string $name
1696 : * @access public
1697 : * @return object FluentDOM
1698 : */
1699 : public function removeAttr($name) {
1700 2 : if (!empty($name)) {
1701 2 : foreach ($t
|