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