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