1 : <?php
2 : /**
3 : * FluentDOM implements a jQuery like replacement for DOMNodeList
4 : *
5 : * @version $Id: FluentDOM.php 440 2010-04-16 17:21:08Z 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 core class
15 : */
16 : require_once(dirname(__FILE__).'/FluentDOM/Core.php');
17 : /**
18 : * Include the handler helper class
19 : */
20 : require_once(dirname(__FILE__).'/FluentDOM/Handler.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 : * @return FluentDOM
29 : */
30 : function FluentDOM($source = NULL, $contentType = 'text/xml') {
31 2 : $result = new FluentDOM();
32 2 : if (isset($source)) {
33 1 : return $result->load($source, $contentType);
34 : } else {
35 1 : return $result;
36 : }
37 : }
38 :
39 : /**
40 : * FluentDOM implements a jQuery like replacement for DOMNodeList
41 : **
42 : * @method bool empty() Remove all child nodes from the set of matched elements.
43 : * @method DOMDocument clone() Clone matched DOM Elements and select the clones.
44 : *
45 : * @package FluentDOM
46 : */
47 : class FluentDOM extends FluentDOMCore {
48 :
49 : /**
50 : * declaring an empty() or clone() method will crash the parser so we use some magic
51 : *
52 : * @param string $name
53 : * @param array $arguments
54 : * @return mixed
55 : */
56 : public function __call($name, $arguments) {
57 3 : switch (strtolower($name)) {
58 3 : case 'empty' :
59 1 : return $this->_emptyNodes();
60 2 : case 'clone' :
61 1 : return $this->_cloneNodes();
62 1 : default :
63 1 : throw new BadMethodCallException('Unknown method '.get_class($this).'::'.$name);
64 1 : }
65 : }
66 : /*
67 : * Object Accessors
68 : */
69 :
70 : /**
71 : * Execute a function within the context of every matched element.
72 : *
73 : * @param callback $function
74 : * @return FluentDOM
75 : */
76 : public function each($function) {
77 2 : if ($this->_isCallback($function, TRUE, FALSE)) {
78 1 : foreach ($this->_array as $index => $node) {
79 1 : call_user_func($function, $node, $index);
80 1 : }
81 1 : }
82 1 : return $this;
83 : }
84 :
85 : /*
86 : * Miscellaneous
87 : */
88 :
89 : /**
90 : * Retrieve the matched DOM elements in an array.
91 : *
92 : * @return array
93 : */
94 : public function toArray() {
95 1 : return $this->_array;
96 : }
97 :
98 : /*
99 : * Traversing - Filtering
100 : */
101 :
102 : /**
103 : * Reduce the set of matched elements to a single element.
104 : *
105 : * @example eq.php Usage Example: FluentDOM::eq()
106 : * @param integer $position Element index (start with 0)
107 : * @return FluentDOM
108 : */
109 : public function eq($position) {
110 2 : $result = $this->spawn();
111 2 : if ($position < 0) {
112 1 : $position = count($this->_array) + $position;
113 1 : }
114 2 : if (isset($this->_array[$position])) {
115 2 : $result->push($this->_array[$position]);
116 2 : }
117 2 : return $result;
118 : }
119 :
120 : /**
121 : * Removes all elements from the set of matched elements that do not match
122 : * the specified expression(s).
123 : *
124 : * @example filter-expr.php Usage Example: FluentDOM::filter() with XPath expression
125 : * @example filter-fn.php Usage Example: FluentDOM::filter() with Closure
126 : * @param string|callback $expr XPath expression or callback function
127 : * @return FluentDOM
128 : */
129 : public function filter($expr) {
130 2 : $result = $this->spawn();
131 2 : foreach ($this->_array as $index => $node) {
132 2 : $check = TRUE;
133 2 : if (is_string($expr)) {
134 1 : $check = $this->_test($expr, $node, $index);
135 2 : } elseif ($this->_isCallback($expr, TRUE, FALSE)) {
136 1 : $check = call_user_func($expr, $node, $index);
137 1 : }
138 2 : if ($check) {
139 2 : $result->push($node);
140 2 : }
141 2 : }
142 2 : return $result;
143 : }
144 :
145 : /**
146 : * Retrieve the matched DOM elements in an array. A negative position will be counted from the end.
147 : *
148 : * @param integer|NULL optional offset of a single element to get.
149 : * @return array()
150 : */
151 : public function get($position = NULL) {
152 4 : if (!isset($position)) {
153 1 : return $this->_array;
154 : }
155 3 : if ($position < 0) {
156 1 : $position = count($this->_array) + $position;
157 1 : }
158 3 : if (isset($this->_array[$position])) {
159 2 : return array($this->_array[$position]);
160 : } else {
161 1 : return array();
162 : }
163 : }
164 :
165 : /**
166 : * Search for a given element from among the matched elements.
167 : *
168 : * @param NULL|string|DOMNode|DOMNodelist|Iterator $expr
169 : * @return integer
170 : */
171 : public function index($expr = NULL) {
172 5 : if (count($this->_array) > 0) {
173 4 : if (is_null($expr)) {
174 1 : $counter = -1;
175 1 : $targetNode = $this->_array[0];
176 1 : $nodeList = $this->_match('preceding-sibling::node()', $targetNode);
177 1 : foreach ($nodeList as $node) {
178 1 : if ($this->_isNode($node)) {
179 1 : $counter++;
180 1 : }
181 1 : }
182 1 : return $counter + 1;
183 3 : } elseif (is_string($expr)) {
184 2 : foreach ($this->_array as $index => $node) {
185 2 : if ($this->_test($expr, $node)) {
186 1 : return $index;
187 : }
188 2 : }
189 1 : } else {
190 1 : $targetNode = $this->_getContentElement($expr);
191 1 : foreach ($this->_array as $index => $node) {
192 1 : if ($node->isSameNode($targetNode)) {
193 1 : return $index;
194 : }
195 1 : }
196 : }
197 1 : }
198 2 : return -1;
199 : }
200 :
201 : /**
202 : * Reduce the set of matched elements to those that have
203 : * a descendant that matches the selector or DOM element.
204 : *
205 : * @param string|DOMNode $expr XPath expression or DOMNode
206 : * @return boolean
207 : */
208 : public function has($expr) {
209 2 : $result = $this->spawn();
210 2 : foreach ($this->_array as $node) {
211 2 : if ($node instanceof DOMElement &&
212 2 : $node->hasChildNodes()) {
213 2 : foreach ($node->childNodes as $childNode) {
214 2 : if ($expr instanceof DOMNode) {
215 1 : if ($expr === $childNode) {
216 1 : $result->push($node);
217 1 : return $result;
218 : }
219 2 : } elseif ($this->_test($expr, $childNode)) {
220 1 : $result->push($node);
221 1 : }
222 2 : }
223 2 : }
224 2 : }
225 1 : return $result;
226 : }
227 :
228 : /**
229 : * Checks the current selection against an expression and returns true,
230 : * if at least one element of the selection fits the given expression.
231 : *
232 : * @example is.php Usage Example: FluentDOM::is()
233 : * @param string $expr XPath expression
234 : * @return boolean
235 : */
236 : public function is($expr) {
237 2 : foreach ($this->_array as $node) {
238 1 : return $this->_test($expr, $node);
239 1 : }
240 1 : return FALSE;
241 : }
242 :
243 : /**
244 : * Translate a set of elements in the FluentDOM object into
245 : * another set of values in an array (which may, or may not contain elements).
246 : *
247 : * If the callback function returns an array each element of the array will be added to the
248 : * result array. All other variable types are put directly into the result array.
249 : *
250 : * @example map.php Usage Example: FluentDOM::map()
251 : * @param callback $function
252 : * @return array
253 : */
254 : public function map($function) {
255 3 : $result = array();
256 3 : foreach ($this->_array as $index => $node) {
257 3 : if ($this->_isCallback($function, TRUE, FALSE)) {
258 2 : $mapped = call_user_func($function, $node, $index);
259 2 : }
260 2 : if ($mapped === NULL) {
261 1 : continue;
262 2 : } elseif ($mapped instanceof DOMNodeList ||
263 2 : $mapped instanceof Iterator ||
264 2 : $mapped instanceof IteratorAggregate ||
265 2 : is_array($mapped)) {
266 1 : foreach ($mapped as $element) {
267 1 : if ($element !== NULL) {
268 1 : $result[] = $element;
269 1 : }
270 1 : }
271 1 : } else {
272 2 : $result[] = $mapped;
273 : }
274 2 : }
275 2 : return $result;
276 : }
277 :
278 : /**
279 : * Removes elements matching the specified expression from the set of matched elements.
280 : *
281 : * @example not.php Usage Example: FluentDOM::not()
282 : * @param string|callback $expr XPath expression or callback function
283 : * @return FluentDOM
284 : */
285 : public function not($expr) {
286 2 : $result = $this->spawn();
287 2 : foreach ($this->_array as $index => $node) {
288 2 : $check = FALSE;
289 2 : if (is_string($expr)) {
290 1 : $check = $this->_test($expr, $node, $index);
291 2 : } elseif ($this->_isCallback($expr, TRUE, FALSE)) {
292 1 : $check = call_user_func($expr, $node, $index);
293 1 : }
294 2 : if (!$check) {
295 2 : $result->push($node);
296 2 : }
297 2 : }
298 2 : return $result;
299 : }
300 :
301 : /**
302 : * Selects a subset of the matched elements.
303 : *
304 : * @example slice.php Usage Example: FluentDOM::slice()
305 : * @param integer $start
306 : * @param integer $end
307 : * @return FluentDOM
308 : */
309 : public function slice($start, $end = NULL) {
310 4 : $result = $this->spawn();
311 4 : if ($end === NULL) {
312 1 : $result->push(array_slice($this->_array, $start));
313 4 : } elseif ($end < 0) {
314 1 : $result->push(array_slice($this->_array, $start, $end));
315 3 : } elseif ($end > $start) {
316 1 : $result->push(array_slice($this->_array, $start, $end - $start));
317 1 : } else {
318 1 : $result->push(array_slice($this->_array, $end, $start - $end));
319 : }
320 4 : return $result;
321 : }
322 :
323 : /*
324 : * Traversing - Finding
325 : */
326 :
327 : /**
328 : * Adds more elements, matched by the given expression, to the set of matched elements.
329 : *
330 : * @example add.php Usage Examples: FluentDOM::add()
331 : * @param string $expr XPath expression
332 : * @return FluentDOM
333 : */
334 : public function add($expr, $context = NULL) {
335 4 : $result = $this->spawn();
336 4 : $result->push($this->_array);
337 4 : if (isset($context)) {
338 1 : $targetNodes = $this->_getTargetNodes($context);
339 1 : if (!empty($targetNodes)) {
340 1 : foreach ($targetNodes as $node) {
341 1 : $result->push($this->_match($expr, $context));
342 1 : }
343 1 : }
344 4 : } elseif (is_object($expr) ||
345 3 : (is_string($expr) && substr(ltrim($expr), 0, 1) == '<')) {
346 1 : $result->push($this->_getContentNodes($expr));
347 1 : } else {
348 2 : $result->push($this->find($expr));
349 : }
350 4 : $result->_uniqueSort();
351 4 : return $result;
352 : }
353 :
354 : /**
355 : * Get a set of elements containing of the unique immediate
356 : * childnodes including only elements (not textnodes) of each
357 : * of the matched set of elements.
358 : *
359 : * @example children.php Usage Examples: FluentDOM::children()
360 : * @param string $expr XPath expression
361 : * @return FluentDOM
362 : */
363 : public function children($expr = NULL) {
364 2 : $result = $this->spawn();
365 2 : foreach ($this->_array as $node) {
366 2 : if (empty($expr)) {
367 1 : $result->push($node->childNodes, TRUE);
368 1 : } else {
369 1 : foreach ($node->childNodes as $childNode) {
370 1 : if ($this->_test($expr, $childNode)) {
371 1 : $result->push($childNode, TRUE);
372 1 : }
373 1 : }
374 : }
375 2 : }
376 2 : $result->_uniqueSort();
377 2 : return $result;
378 : }
379 :
380 : /**
381 : * Get a set of elements containing all of the unique immediate
382 : * childnodes including elements and textnodes of each of the matched set of elements.
383 : *
384 : * @return FluentDOM
385 : */
386 : public function contents() {
387 1 : $result = $this->spawn();
388 1 : foreach ($this->_array as $node) {
389 1 : $result->push($node->childNodes, FALSE);
390 1 : }
391 1 : $result->_uniqueSort();
392 1 : return $result;
393 : }
394 :
395 : /**
396 : * Searches for descendent elements that match the specified expression.
397 : *
398 : * @example find.php Usage Example: FluentDOM::find()
399 : * @param string $expr XPath expression
400 : * @param boolean $useDocumentContext ignore current node list
401 : * @return FluentDOM
402 : */
403 : public function find($expr, $useDocumentContext = FALSE) {
404 4 : $result = $this->spawn();
405 4 : if ($useDocumentContext ||
406 4 : $this->_useDocumentContext) {
407 4 : $result->push($this->_match($expr));
408 4 : } else {
409 1 : foreach ($this->_array as $contextNode) {
410 1 : $result->push($this->_match($expr, $contextNode));
411 1 : }
412 : }
413 4 : return $result;
414 : }
415 :
416 : /**
417 : * Get a set of elements containing the unique next siblings of each of the
418 : * given set of elements.
419 : *
420 : * @example next.php Usage Example: FluentDOM::next()
421 : * @param string $expr XPath expression
422 : * @return FluentDOM
423 : */
424 : public function next($expr = NULL) {
425 1 : $result = $this->spawn();
426 1 : foreach ($this->_array as $node) {
427 1 : $next = $node->nextSibling;
428 1 : while ($next instanceof DOMNode && !$this->_isNode($next)) {
429 1 : $next = $next->nextSibling;
430 1 : }
431 1 : if (!empty($next)) {
432 1 : if (empty($expr) || $this->_test($expr, $next)) {
433 1 : $result->push($next);
434 1 : }
435 1 : }
436 1 : }
437 1 : $result->_uniqueSort();
438 1 : return $result;
439 : }
440 :
441 : /**
442 : * Find all sibling elements after the current element.
443 : *
444 : * @example nextAll.php Usage Example: FluentDOM::nextAll()
445 : * @param string $expr XPath expression
446 : * @return FluentDOM
447 : */
448 : public function nextAll($expr = NULL) {
449 1 : $result = $this->spawn();
450 1 : foreach ($this->_array as $node) {
451 1 : $next = $node->nextSibling;
452 1 : while ($next instanceof DOMNode) {
453 1 : if ($this->_isNode($next)) {
454 1 : if (empty($expr) || $this->_test($expr, $next)) {
455 1 : $result->push($next);
456 1 : }
457 1 : }
458 1 : $next = $next->nextSibling;
459 1 : }
460 1 : }
461 1 : return $result;
462 : }
463 :
464 : /**
465 : * Get all following siblings of each element up to but
466 : * not including the element matched by the selector.
467 : *
468 : * @param string $expr XPath expression
469 : * @return FluentDOM
470 : */
471 : public function nextUntil($expr = NULL) {
472 1 : $result = $this->spawn();
473 1 : foreach ($this->_array as $node) {
474 1 : $next = $node->nextSibling;
475 1 : while ($next instanceof DOMNode) {
476 1 : if ($this->_isNode($next)) {
477 1 : if (isset($expr) && $this->_test($expr, $next)) {
478 1 : break;
479 : } else {
480 1 : $result->push($next);
481 : }
482 1 : }
483 1 : $next = $next->nextSibling;
484 1 : }
485 1 : }
486 1 : return $result;
487 : }
488 :
489 : /**
490 : * Get a set of elements containing the unique parents of the matched set of elements.
491 : *
492 : * @example parent.php Usage Example: FluentDOM::parent()
493 : * @return FluentDOM
494 : */
495 : public function parent() {
496 1 : $result = $this->spawn();
497 1 : foreach ($this->_array as $node) {
498 1 : if (isset($node->parentNode)) {
499 1 : $result->push($node->parentNode);
500 1 : }
501 1 : }
502 1 : $result->_uniqueSort();
503 1 : return $result;
504 : }
505 :
506 : /**
507 : * Get the ancestors of each element in the current set of matched elements,
508 : * optionally filtered by a selector.
509 : *
510 : * @example parents.php Usage Example: FluentDOM::parents()
511 : * @param string $expr XPath expression
512 : * @return FluentDOM
513 : */
514 : public function parents($expr = NULL) {
515 1 : $result = $this->spawn();
516 1 : foreach ($this->_array as $node) {
517 1 : $parents = $this->_match('ancestor::*', $node);
518 1 : for ($i = $parents->length - 1; $i >= 0; --$i) {
519 1 : $parentNode = $parents->item($i);
520 1 : if (empty($expr) || $this->_test($expr, $parentNode)) {
521 1 : $result->push($parentNode);
522 1 : }
523 1 : }
524 1 : }
525 1 : return $result;
526 : }
527 :
528 : /**
529 : * Get the ancestors of each element in the current set of matched elements,
530 : * up to but not including the element matched by the selector.
531 : *
532 : * @param string $expr XPath expression
533 : * @return FluentDOM
534 : */
535 : public function parentsUntil($expr = NULL) {
536 1 : $result = $this->spawn();
537 1 : foreach ($this->_array as $node) {
538 1 : $parents = $this->_match('ancestor::*', $node);
539 1 : for ($i = $parents->length - 1; $i >= 0; --$i) {
540 1 : $parentNode = $parents->item($i);
541 1 : if (!empty($expr) && $this->_test($expr, $parentNode)) {
542 1 : break;
543 : }
544 1 : $result->push($parentNode);
545 1 : }
546 1 : }
547 1 : return $result;
548 : }
549 :
550 : /**
551 : * Get a set of elements containing the unique previous siblings of each of the
552 : * matched set of elements.
553 : *
554 : * @example prev.php Usage Example: FluentDOM::prev()
555 : * @param string $expr XPath expression
556 : * @return FluentDOM
557 : */
558 : public function prev($expr = NULL) {
559 2 : $result = $this->spawn();
560 2 : foreach ($this->_array as $node) {
561 2 : $previous = $node->previousSibling;
562 2 : while ($previous instanceof DOMNode && !$this->_isNode($previous)) {
563 2 : $previous = $previous->previousSibling;
564 2 : }
565 2 : if (!empty($previous)) {
566 2 : if (empty($expr) || $this->_test($expr, $previous)) {
567 2 : $result->push($previous);
568 2 : }
569 2 : }
570 2 : }
571 2 : $result->_uniqueSort();
572 2 : return $result;
573 : }
574 :
575 : /**
576 : * Find all sibling elements in front of the current element.
577 : *
578 : * @example prevAll.php Usage Example: FluentDOM::prevAll()
579 : * @param string $expr XPath expression
580 : * @return FluentDOM
581 : */
582 : public function prevAll($expr = NULL) {
583 1 : $result = $this->spawn();
584 1 : foreach ($this->_array as $node) {
585 1 : $previous = $node->previousSibling;
586 1 : while ($previous instanceof DOMNode) {
587 1 : if ($this->_isNode($previous)) {
588 1 : if (empty($expr) || $this->_test($expr, $previous)) {
589 1 : $result->push($previous);
590 1 : }
591 1 : }
592 1 : $previous = $previous->previousSibling;
593 1 : }
594 1 : }
595 1 : return $result;
596 : }
597 :
598 : /**
599 : * Get all preceding siblings of each element up to but not including
600 : * the element matched by the selector.
601 : *
602 : * @param string $expr XPath expression
603 : * @return FluentDOM
604 : */
605 : public function prevUntil($expr = NULL) {
606 1 : $result = $this->spawn();
607 1 : foreach ($this->_array as $node) {
608 1 : $previous = $node->previousSibling;
609 1 : while ($previous instanceof DOMNode) {
610 1 : if (isset($expr) && $this->_isNode($previous)) {
611 1 : if ($this->_test($expr, $previous)) {
612 1 : break;
613 : } else {
614 1 : $result->push($previous);
615 : }
616 1 : }
617 1 : $previous = $previous->previousSibling;
618 1 : }
619 1 : }
620 1 : return $result;
621 : }
622 :
623 : /**
624 : * Get a set of elements containing all of the unique siblings of each of the
625 : * matched set of elements.
626 : *
627 : * @example siblings.php Usage Example: FluentDOM::siblings()
628 : * @param string $expr XPath expression
629 : * @return FluentDOM
630 : */
631 : public function siblings($expr = NULL) {
632 1 : $result = $this->spawn();
633 1 : foreach ($this->_array as $node) {
634 1 : if (isset($node->parentNode)) {
635 1 : foreach ($node->parentNode->childNodes as $childNode) {
636 1 : if ($this->_isNode($childNode) &&
637 1 : $childNode !== $node) {
638 1 : if (empty($expr) || $this->_test($expr, $childNode)) {
639 1 : $result->push($childNode);
640 1 : }
641 1 : }
642 1 : }
643 1 : }
644 1 : }
645 1 : $result->_uniqueSort();
646 1 : return $result;
647 : }
648 :
649 : /**
650 : * Get a set of elements containing the closest parent element that matches the specified
651 : * selector, the starting element included.
652 : *
653 : * @example closest.php Usage Example: FluentDOM::closest()
654 : * @param string $expr XPath expression
655 : * @return FluentDOM
656 : */
657 : public function closest($expr, $context = NULL) {
658 2 : $result = $this->spawn();
659 2 : $context = $this->_getContextNodes($context);
660 2 : foreach ($context as $node) {
661 2 : while (isset($node)) {
662 2 : if ($this->_test($expr, $node)) {
663 2 : $result->push($node);
664 2 : break;
665 : }
666 2 : $node = $node->parentNode;
667 2 : }
668 2 : }
669 2 : return $result;
670 : }
671 :
672 : /**
673 : * Get a set of elements containing only the first of the currently selected elements.
674 : *
675 : * @return FluentDOM
676 : */
677 : public function first() {
678 1 : return $this->eq(0);
679 : }
680 :
681 : /**
682 : * Get a set of elements containing only the last of the currently selected elements.
683 : *
684 : * @return FluentDOM
685 : */
686 : public function last() {
687 1 : return $this->eq(-1);
688 : }
689 :
690 : /*
691 : * Traversing - Chaining
692 : */
693 :
694 : /**
695 : * Add the previous selection to the current selection.
696 : *
697 : * @return FluentDOM
698 : */
699 : public function andSelf() {
700 1 : $result = $this->spawn();
701 1 : $result->push($this->_array);
702 1 : $result->push($this->_parent);
703 1 : return $result;
704 : }
705 :
706 : /**
707 : * Revert the most recent traversing operation,
708 : * changing the set of matched elements to its previous state.
709 : *
710 : * @return FluentDOM
711 : */
712 : public function end() {
713 1 : if ($this->_parent instanceof FluentDOM) {
714 1 : return $this->_parent;
715 : } else {
716 1 : return $this;
717 : }
718 : }
719 :
720 : /*
721 : * Manipulation - Changing Contents
722 : */
723 :
724 : /**
725 : * Get or set the xml contents of the first matched element.
726 : *
727 : * @example xml.php Usage Example: FluentDOM::xml()
728 : * @param string|Callback|Closure $xml XML fragment
729 : * @return string|FluentDOM
730 : */
731 : public function xml($xml = NULL) {
732 5 : if (isset($xml)) {
733 3 : $isCallback = $this->_isCallback($xml, FALSE, TRUE);
734 3 : if ($isCallback) {
735 1 : foreach ($this->_array as $index => $node) {
736 1 : $xmlString = call_user_func(
737 1 : $xml,
738 1 : $node,
739 1 : $index,
740 1 : $this->_getInnerXml($node)
741 1 : );
742 1 : $node->nodeValue = '';
743 1 : if (!empty($xmlString)) {
744 1 : $fragment = $this->_getContentFragment($xmlString, TRUE);
745 1 : foreach ($fragment as $contentNode) {
746 1 : $node->appendChild($contentNode->cloneNode(TRUE));
747 1 : }
748 1 : }
749 1 : }
750 1 : } else {
751 2 : if (!empty($xml)) {
752 1 : $fragment = $this->_getContentFragment($xml, TRUE);
753 1 : } else {
754 1 : $fragment = array();
755 : }
756 2 : foreach ($this->_array as $node) {
757 2 : $node->nodeValue = '';
758 2 : foreach ($fragment as $contentNode) {
759 1 : $node->appendChild($contentNode->cloneNode(TRUE));
760 2 : }
761 2 : }
762 : }
763 3 : return $this;
764 : } else {
765 2 : if (isset($this->_array[0])) {
766 1 : return $this->_getInnerXml($this->_array[0]);
767 : }
768 1 : return '';
769 : }
770 : }
771 :
772 : /**
773 : * Get the combined text contents of all matched elements or
774 : * set the text contents of all matched elements.
775 : *
776 : * @example text.php Usage Example: FluentDOM::text()
777 : * @param string|callback|Closure $text
778 : * @return string|FluentDOM
779 : */
780 : public function text($text = NULL) {
781 3 : if (isset($text)) {
782 2 : $isCallback = $this->_isCallback($text, FALSE, TRUE);
783 2 : foreach ($this->_array as $index => $node) {
784 2 : if ($isCallback) {
785 1 : $node->nodeValue = call_user_func($text, $node, $index, $node->nodeValue);
786 1 : } else {
787 1 : $node->nodeValue = $text;
788 : }
789 2 : }
790 2 : return $this;
791 : } else {
792 1 : $result = '';
793 1 : foreach ($this->_array as $node) {
794 1 : $result .= $node->textContent;
795 1 : }
796 1 : return $result;
797 : }
798 : }
799 :
800 : /*
801 : * Manipulation - Inserting Inside
802 : */
803 :
804 : /**
805 : * Append content to the inside of every matched element.
806 : *
807 : * @example append.php Usage Example: FluentDOM::append()
808 : * @param string|array|DOMNode|Iterator $content DOMNode or DOMNodeList or xml fragment string
809 : * @return FluentDOM
810 : */
811 : public function append($content) {
812 5 : $result = $this->spawn();
813 5 : if (empty($this->_array) &&
814 2 : $this->_useDocumentContext &&
815 5 : !isset($this->_document->documentElement)) {
816 2 : if ($this->_isCallback($content, FALSE, TRUE)) {
817 1 : $contentNode = $this->_getContentElement(
818 1 : $this->_executeEasySetter($content, NULL, 0, '')
819 1 : );
820 1 : } else {
821 1 : $contentNode = $this->_getContentElement($content);
822 : }
823 2 : $result->push($this->_document->appendChild($contentNode));
824 2 : } else {
825 3 : $result->push(
826 3 : $this->_applyContentToNodes(
827 3 : $this->_array,
828 3 : $content,
829 3 : array($this->_getHandler(), 'appendChildren')
830 3 : )
831 3 : );
832 : }
833 5 : return $result;
834 : }
835 :
836 : /**
837 : * Append all of the matched elements to another, specified, set of elements.
838 : * Returns all of the inserted elements.
839 : *
840 : * @example appendTo.php Usage Example: FluentDOM::appendTo()
841 : * @param string|array|DOMNode|DOMNodeList|FluentDOM $selector
842 : * @return FluentDOM
843 : */
844 : public function appendTo($selector) {
845 1 : $result = $this->spawn();
846 1 : $targetNodes = $this->_getTargetNodes($selector);
847 1 : if (!empty($targetNodes)) {
848 1 : $result->push(
849 1 : $this->_applyContentToNodes(
850 1 : $targetNodes,
851 1 : $this->_array,
852 1 : array($this->_getHandler(), 'appendChildren')
853 1 : )
854 1 : );
855 1 : $this->_removeNodes($this->_array);
856 1 : }
857 1 : return $result;
858 : }
859 :
860 : /**
861 : * Prepend content to the inside of every matched element.
862 : *
863 : * @example prepend.php Usage Example: FluentDOM::prepend()
864 : * @param string|array|DOMNode|Iterator $content
865 : * @return FluentDOM
866 : */
867 : public function prepend($content) {
868 2 : $result = $this->spawn();
869 2 : $result->push(
870 2 : $this->_applyContentToNodes(
871 2 : $this->_array,
872 2 : $content,
873 2 : array($this->_getHandler(), 'insertChildrenBefore')
874 2 : )
875 2 : );
876 2 : return $result;
877 : }
878 :
879 : /**
880 : * Prepend all of the matched elements to another, specified, set of elements.
881 : * Returns all of the inserted elements.
882 : *
883 : * @example prependTo.php Usage Example: FluentDOM::prependTo()
884 : * @param string|array|DOMNode|DOMNodeList|FluentDOM $selector
885 : * @return FluentDOM list of all new elements
886 : */
887 : public function prependTo($selector) {
888 1 : $result = $this->spawn();
889 1 : $targetNodes = $this->_getTargetNodes($selector);
890 1 : if (!empty($targetNodes)) {
891 1 : $result->push(
892 1 : $this->_applyContentToNodes(
893 1 : $targetNodes,
894 1 : $this->_array,
895 1 : array($this->_getHandler(), 'insertChildrenBefore')
896 1 : )
897 1 : );
898 1 : $this->_removeNodes($this->_array);
899 1 : }
900 1 : return $result;
901 : }
902 :
903 : /*
904 : * Manipulation - Inserting Outside
905 : */
906 :
907 : /**
908 : * Insert content after each of the matched elements.
909 : *
910 : * @example after.php Usage Example: FluentDOM::after()
911 : * @param string|array|DOMNode|DOMNodeList|Iterator|callback|Closure $content
912 : * @return FluentDOM
913 : */
914 : public function after($content) {
915 2 : $result = $this->spawn();
916 2 : $result->push(
917 2 : $this->_applyContentToNodes(
918 2 : $this->_array, $content, array($this->_getHandler(), 'insertNodesAfter')
919 2 : )
920 2 : );
921 2 : return $result;
922 : }
923 :
924 : /**
925 : * Insert content before each of the matched elements.
926 : *
927 : * @example before.php Usage Example: FluentDOM::before()
928 : * @param string|array|DOMNode|DOMNodeList|Iterator|callback|Closure $content
929 : * @return FluentDOM
930 : */
931 : public function before($content) {
932 2 : $result = $this->spawn();
933 2 : $result->push(
934 2 : $this->_applyContentToNodes(
935 2 : $this->_array, $content, array($this->_getHandler(), 'insertNodesBefore')
936 2 : )
937 2 : );
938 2 : return $result;
939 : }
940 :
941 : /**
942 : * Insert all of the matched elements after another, specified, set of elements.
943 : *
944 : * @example insertAfter.php Usage Example: FluentDOM::insertAfter()
945 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
946 : * @return FluentDOM
947 : */
948 : public function insertAfter($selector) {
949 1 : $result = $this->spawn();
950 1 : $targetNodes = $this->_getTargetNodes($selector);
951 1 : if (!empty($targetNodes)) {
952 1 : $result->push(
953 1 : $this->_applyContentToNodes(
954 1 : $targetNodes,
955 1 : $this->_array,
956 1 : array($this->_getHandler(), 'insertNodesAfter')
957 1 : )
958 1 : );
959 1 : $this->_removeNodes($this->_array);
960 1 : }
961 1 : return $result;
962 : }
963 :
964 : /**
965 : * Insert all of the matched elements before another, specified, set of elements.
966 : *
967 : * @example insertBefore.php Usage Example: FluentDOM::insertBefore()
968 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
969 : * @return FluentDOM
970 : */
971 : public function insertBefore($selector) {
972 1 : $result = $this->spawn();
973 1 : $targetNodes = $this->_getTargetNodes($selector);
974 1 : if (!empty($targetNodes)) {
975 1 : $result->push(
976 1 : $this->_applyContentToNodes(
977 1 : $targetNodes,
978 1 : $this->_array,
979 1 : array($this->_getHandler(), 'insertNodesBefore')
980 1 : )
981 1 : );
982 1 : $this->_removeNodes($this->_array);
983 1 : }
984 1 : return $result;
985 : }
986 :
987 : /*
988 : * Manipulation - Inserting Around
989 : */
990 :
991 : /**
992 : * Wrap $content around a set of elements
993 : *
994 : * @param array $elements
995 : * @param string|array|DOMNode|DOMNodeList|Iterator|callback|Closure $content
996 : * @return FluentDOM
997 : */
998 : protected function _wrap($elements, $content) {
999 8 : $result = array();
1000 8 : $isCallback = $this->_isCallback($content, FALSE, TRUE);
1001 8 : if (!$isCallback) {
1002 6 : $wrapperTemplate = $this->_getContentElement($content);
1003 5 : }
1004 7 : $simple = FALSE;
1005 7 : foreach ($elements as $index => $node) {
1006 7 : if ($isCallback) {
1007 2 : $wrapperTemplate = NULL;
1008 2 : $wrapContent = call_user_func($content, $node, $index);
1009 2 : if (!empty($wrapContent)) {
1010 2 : $wrapperTemplate = $this->_getContentElement($wrapContent);
1011 2 : }
1012 2 : }
1013 7 : if ($wrapperTemplate instanceof DOMElement) {
1014 7 : $wrapper = $wrapperTemplate->cloneNode(TRUE);
1015 7 : if (!$simple) {
1016 7 : $targets = $this->_match('.//*[count(*) = 0]', $wrapper);
1017 7 : }
1018 7 : if ($simple || $targets->length == 0) {
1019 6 : $target = $wrapper;
1020 6 : $simple = TRUE;
1021 6 : } else {
1022 1 : $target = $targets->item(0);
1023 : }
1024 7 : if (isset($node->parentNode)) {
1025 7 : $node->parentNode->insertBefore($wrapper, $node);
1026 7 : }
1027 7 : $target->appendChild($node);
1028 7 : $result[] = $node;
1029 7 : }
1030 7 : }
1031 7 : return $result;
1032 : }
1033 :
1034 : /**
1035 : * Wrap each matched element with the specified content.
1036 : *
1037 : * If $content contains several elements the first one is used
1038 : *
1039 : * @example wrap.php Usage Example: FluentDOM::wrap()
1040 : * @param string|array|DOMNode|DOMNodeList|Iterator|callback|Closure $content
1041 : * @return FluentDOM
1042 : */
1043 : public function wrap($content) {
1044 6 : $result = $this->spawn();
1045 6 : $result->push($this->_wrap($this->_array, $content));
1046 5 : return $result;
1047 : }
1048 :
1049 : /**
1050 : * Wrap al matched elements with the specified content
1051 : *
1052 : * If the matched elemetns are not siblings, wrap each group of siblings.
1053 : *
1054 : * @example wrapAll.php Usage Example: FluentDOM::wrapAll()
1055 : * @param string|array|DOMNode|Iterator $content
1056 : * @return FluentDOM
1057 : */
1058 : public function wrapAll($content) {
1059 2 : $result = $this->spawn();
1060 2 : $current = NULL;
1061 2 : $counter = 0;
1062 2 : $groups = array();
1063 : //group elements by previous node - ignore whitespace text nodes
1064 2 : foreach ($this->_array as $node) {
1065 2 : $previous = $node->previousSibling;
1066 2 : while ($previous instanceof DOMText && $previous->isWhitespaceInElementContent()) {
1067 2 : $previous = $previous->previousSibling;
1068 2 : }
1069 2 : if ($previous !== $current) {
1070 2 : $counter++;
1071 2 : }
1072 2 : $groups[$counter][] = $node;
1073 2 : $current = $node;
1074 2 : }
1075 2 : if (count($groups) > 0) {
1076 2 : $wrapperTemplate = $this->_getContentElement($content);
1077 2 : $simple = FALSE;
1078 2 : foreach ($groups as $group) {
1079 2 : if (isset($group[0])) {
1080 2 : $node = $group[0];
1081 2 : $wrapper = $wrapperTemplate->cloneNode(TRUE);
1082 2 : if (!$simple) {
1083 2 : $targets = $this->_match('.//*[count(*) = 0]', $wrapper);
1084 2 : }
1085 2 : if ($simple || $targets->length == 0) {
1086 1 : $target = $wrapper;
1087 1 : $simple = TRUE;
1088 1 : } else {
1089 1 : $target = $targets->item(0);
1090 : }
1091 2 : if (isset($node->parentNode)) {
1092 2 : $node->parentNode->insertBefore($wrapper, $node);
1093 2 : }
1094 2 : foreach ($group as $node) {
1095 2 : $target->appendChild($node);
1096 2 : }
1097 2 : $result->push($node);
1098 2 : }
1099 2 : }
1100 2 : }
1101 2 : return $result;
1102 : }
1103 :
1104 : /**
1105 : * Wrap the inner child contents of each matched element
1106 : * (including text nodes) with an XML structure.
1107 : *
1108 : * @example wrapInner.php Usage Example: FluentDOM::wrapInner()
1109 : * @param string|array|DOMNode|DOMNodeList|Iterator $content
1110 : * @return FluentDOM
1111 : */
1112 : public function wrapInner($content) {
1113 2 : $result = $this->spawn();
1114 2 : $elements = array();
1115 2 : foreach ($this->_array as $node) {
1116 2 : foreach ($node->childNodes as $childNode) {
1117 2 : if ($this->_isNode($childNode)) {
1118 2 : $elements[] = $childNode;
1119 2 : }
1120 2 : }
1121 2 : }
1122 2 : $result->push($this->_wrap($elements, $content));
1123 2 : return $result;
1124 : }
1125 :
1126 : /*
1127 : * Manipulation - Replacing
1128 : */
1129 :
1130 : /**
1131 : * Replaces all matched elements with the specified HTML or DOM elements.
1132 : * This returns the JQuery element that was just replaced,
1133 : * which has been removed from the DOM.
1134 : *
1135 : * @example replaceWith.php Usage Example: FluentDOM::replaceWith()
1136 : * @param string|array|DOMNode|DOMNodeList|Iterator|callback|Closure $content
1137 : * @return FluentDOM
1138 : */
1139 : public function replaceWith($content) {
1140 2 : $this->_applyContentToNodes(
1141 2 : $this->_array, $content, array($this->_getHandler(), 'insertNodesBefore')
1142 2 : );
1143 2 : $this->_removeNodes($this->_array);
1144 2 : return $this;
1145 : }
1146 :
1147 :
1148 : /**
1149 : * Replaces the elements matched by the specified selector with the matched elements.
1150 : *
1151 : * @example replaceAll.php Usage Example: FluentDOM::replaceAll()
1152 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
1153 : * @return FluentDOM
1154 : */
1155 : public function replaceAll($selector) {
1156 4 : $result = $this->spawn();
1157 4 : $targetNodes = $this->_getTargetNodes($selector);
1158 3 : if (!empty($targetNodes)) {
1159 3 : $this->_applyContentToNodes(
1160 3 : $targetNodes,
1161 3 : $this->_array,
1162 3 : array($this->_getHandler(), 'insertNodesBefore')
1163 3 : );
1164 3 : $this->_removeNodes($targetNodes);
1165 3 : }
1166 3 : $this->_removeNodes($this->_array);
1167 3 : return $result;
1168 : }
1169 :
1170 : /*
1171 : * Manipulation - Removing
1172 : */
1173 :
1174 : /**
1175 : * Remove all child nodes from the set of matched elements.
1176 : *
1177 : * This is the empty() method - but because empty
1178 : * is a reserved word we can no declare it directly
1179 : * @see __call
1180 : *
1181 : * @example empty.php Usage Example: FluentDOM:empty()
1182 : * @return FluentDOM
1183 : */
1184 : protected function _emptyNodes() {
1185 1 : foreach ($this->_array as $node) {
1186 1 : if ($node instanceof DOMElement ||
1187 1 : $node instanceof DOMText) {
1188 1 : $node->nodeValue = '';
1189 1 : }
1190 1 : }
1191 1 : return $this;
1192 : }
1193 :
1194 : /**
1195 : * Removes all matched elements from the DOM.
1196 : *
1197 : * @example remove.php Usage Example: FluentDOM::remove()
1198 : * @param string $expr XPath expression
1199 : * @return FluentDOM removed elements
1200 : */
1201 : public function remove($expr = NULL) {
1202 2 : $result = $this->spawn();
1203 2 : foreach ($this->_array as $node) {
1204 2 : if (isset($node->parentNode)) {
1205 2 : if (empty($expr) || $this->_test($expr, $node)) {
1206 2 : $result->push($node->parentNode->removeChild($node));
1207 2 : }
1208 2 : }
1209 2 : }
1210 2 : return $result;
1211 : }
1212 :
1213 : /*
1214 : * Manipulation - Creation
1215 : */
1216 :
1217 : /*
1218 : * Manipulation - Copying
1219 : */
1220 :
1221 : /**
1222 : * Clone matched DOM Elements and select the clones.
1223 : *
1224 : * This is the clone() method - but because clone
1225 : * is a reserved word we can no declare it directly
1226 : * @see __call
1227 : *
1228 : * @example clone.php Usage Example: FluentDOM:clone()
1229 : * @return FluentDOM
1230 : */
1231 : protected function _cloneNodes() {
1232 1 : $result = $this->spawn();
1233 1 : foreach ($this->_array as $node) {
1234 1 : $result->push($node->cloneNode(TRUE));
1235 1 : }
1236 1 : return $result;
1237 : }
1238 :
1239 : /*
1240 : * Attributes - General
1241 : */
1242 :
1243 : /**
1244 : * Access a property on the first matched element or set the attribute(s) of all matched elements
1245 : *
1246 : * @example attr.php Usage Example: FluentDOM:attr() Read an attribute value.
1247 : * @param string|array $attribute attribute name or attribute list
1248 : * @param string|callback|Closure $value function callback($index, $value) or value
1249 : * @return string|FluentDOM attribute value or $this
1250 : */
1251 : public function attr($attribute, $value = NULL) {
1252 16 : if (is_array($attribute) && count($attribute) > 0) {
1253 : //expr is an array of attributes and values - set on each element
1254 1 : foreach ($attribute as $key => $value) {
1255 1 : if ($this->_isQName($key)) {
1256 1 : foreach ($this->_array as $node) {
1257 1 : if ($node instanceof DOMElement) {
1258 1 : $node->setAttribute($key, $value);
1259 1 : }
1260 1 : }
1261 1 : }
1262 1 : }
1263 16 : } elseif (is_null($value)) {
1264 : //empty value - read attribute from first element in list
1265 8 : if ($this->_isQName($attribute) &&
1266 7 : count($this->_array) > 0) {
1267 6 : $node = $this->_array[0];
1268 6 : if ($node instanceof DOMElement) {
1269 5 : return $node->getAttribute($attribute);
1270 : }
1271 1 : }
1272 2 : return NULL;
1273 10 : } elseif (is_array($value) ||
1274 10 : $value instanceof Closure) {
1275 : //value is function callback - execute it and set result on each element
1276 1 : if ($this->_isQName($attribute)) {
1277 1 : foreach ($this->_array as $index => $node) {
1278 1 : if ($node instanceof DOMElement) {
1279 : $newValue =
1280 1 : $node->setAttribute(
1281 1 : $attribute,
1282 1 : call_user_func($value, $node, $index, $node->getAttribute($attribute))
1283 1 : );
1284 1 : }
1285 1 : }
1286 1 : }
1287 1 : } else {
1288 : // set attribute value of each element
1289 9 : if ($this->_isQName($attribute)) {
1290 3 : foreach ($this->_array as $node) {
1291 3 : if ($node instanceof DOMElement) {
1292 3 : $node->setAttribute($attribute, (string)$value);
1293 3 : }
1294 3 : }
1295 3 : }
1296 : }
1297 5 : return $this;
1298 : }
1299 :
1300 : /**
1301 : * Remove an attribute from each of the matched elements.
1302 : *
1303 : * @example removeAttr.php Usage Example: FluentDOM::removeAttr()
1304 : * @param string $name
1305 : * @return FluentDOM
1306 : */
1307 : public function removeAttr($name) {
1308 4 : if (!empty($name)) {
1309 4 : if (is_string($name) && $name !== '*') {
1310 1 : $attributes = array($name);
1311 4 : } elseif (is_array($name)) {
1312 1 : $attributes = $name;
1313 3 : } elseif ($name !== '*') {
1314 1 : throw new InvalidArgumentException();
1315 : }
1316 3 : foreach ($this->_array as $node) {
1317 3 : if ($node instanceof DOMElement) {
1318 3 : if ($name === '*') {
1319 1 : for ($i = $node->attributes->length - 1; $i >= 0; $i--) {
1320 1 : $node->removeAttribute($node->attributes->item($i)->name);
1321 1 : }
1322 1 : } else {
1323 2 : foreach ($attributes as $attribute) {
1324 2 : if ($node->hasAttribute($attribute)) {
1325 2 : $node->removeAttribute($attribute);
1326 2 : }
1327 2 : }
1328 : }
1329 3 : }
1330 3 : }
1331 3 : }
1332 3 : return $this;
1333 : }
1334 :
1335 : /*
1336 : * Attributes - Classes
1337 : */
1338 :
1339 : /**
1340 : * Adds the specified class(es) to each of the set of matched elements.
1341 : *
1342 : * @param string|callback|Closure $class
1343 : * @return FluentDOM
1344 : */
1345 : public function addClass($class) {
1346 1 : return $this->toggleClass($class, TRUE);
1347 : }
1348 :
1349 : /**
1350 : * Returns true if the specified class is present on at least one of the set of matched elements.
1351 : *
1352 : * @param string|callback|Closure $class
1353 : * @return boolean
1354 : */
1355 : public function hasClass($class) {
1356 2 : foreach ($this->_array as $node) {
1357 2 : if ($node instanceof DOMElement &&
1358 2 : $node->hasAttribute('class')) {
1359 2 : $classes = preg_split('(\s+)', trim($node->getAttribute('class')));
1360 2 : if (in_array($class, $classes)) {
1361 1 : return TRUE;
1362 : }
1363 1 : }
1364 1 : }
1365 1 : return FALSE;
1366 : }
1367 :
1368 : /**
1369 : * Removes all or the specified class(es) from the set of matched elements.
1370 : *
1371 : * @param string|callback|Closure $class
1372 : * @return FluentDOM
1373 : */
1374 : public function removeClass($class = '') {
1375 2 : return $this->toggleClass($class, FALSE);
1376 : }
1377 :
1378 : /**
1379 : * Adds the specified class if the switch is TRUE,
1380 : * removes the specified class if the switch is FALSE,
1381 : * toggles the specified class if the switch is NULL.
1382 : *
1383 : * @example toggleClass.php Usage Example: FluentDOM::toggleClass()
1384 : * @param string|callback|Closure $class
1385 : * @param NULL|boolean $switch toggle if NULL, add if TRUE, remove if FALSE
1386 : * @return FluentDOM
1387 : */
1388 : public function toggleClass($class, $switch = NULL) {
1389 6 : foreach ($this->_array as $index => $node) {
1390 6 : if ($node instanceof DOMElement) {
1391 6 : $isCallback = $this->_isCallback($class, FALSE, TRUE);
1392 6 : if ($isCallback) {
1393 1 : $classString = call_user_func(
1394 1 : $class, $node, $index, $node->getAttribute('class')
1395 1 : );
1396 1 : } else {
1397 5 : $classString = $class;
1398 : }
1399 6 : if (empty($classString) && $switch == FALSE) {
1400 1 : if ($node->hasAttribute('class')) {
1401 1 : $node->removeAttribute('class');
1402 1 : }
1403 1 : } else {
1404 5 : if ($node->hasAttribute('class')) {
1405 5 : $currentClasses = array_flip(
1406 5 : preg_split('(\s+)', trim($node->getAttribute('class')))
1407 5 : );
1408 5 : } else {
1409 5 : $currentClasses = array();
1410 : }
1411 5 : $toggledClasses = array_unique(preg_split('(\s+)', trim($classString)));
1412 5 : $modified = FALSE;
1413 5 : foreach ($toggledClasses as $toggledClass) {
1414 5 : if (isset($currentClasses[$toggledClass])) {
1415 4 : if ($switch === FALSE || is_null($switch)) {
1416 4 : unset($currentClasses[$toggledClass]);
1417 4 : $modified = TRUE;
1418 4 : }
1419 4 : } else {
1420 5 : if ($switch === TRUE || is_null($switch)) {
1421 4 : $currentClasses[$toggledClass] = TRUE;
1422 4 : $modified = TRUE;
1423 4 : }
1424 : }
1425 5 : }
1426 5 : if ($modified) {
1427 5 : if (empty($currentClasses)) {
1428 1 : $node->removeAttribute('class');
1429 1 : } else {
1430 5 : $node->setAttribute('class', implode(' ', array_keys($currentClasses)));
1431 : }
1432 5 : }
1433 : }
1434 6 : }
1435 6 : }
1436 6 : return $this;
1437 : }
|