1 : <?php
2 : /**
3 : * FluentDOMCore implements the core and interface functions for FluentDOM
4 : *
5 : * @version $Id: Core.php 441 2010-04-16 18:24: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 external iterator class.
15 : */
16 : require_once(dirname(__FILE__).'/Iterator.php');
17 : /**
18 : * Include the loader interface.
19 : */
20 : require_once(dirname(__FILE__).'/Loader.php');
21 : /**
22 : * Include the handler class.
23 : */
24 : require_once(dirname(__FILE__).'/Handler.php');
25 :
26 : /**
27 : * FluentDOMCore implements the core and interface functions for FluentDOM
28 : *
29 : * @property string $contentType Output type - text/xml or text/html
30 : * @property-read integer $length The amount of elements found by selector.
31 : * @property-read DOMDocument $document Internal DOMDocument object
32 : * @property-read DOMXPath $xpath Internal XPath object
33 : *
34 : * @package FluentDOM
35 : */
36 : class FluentDOMCore implements IteratorAggregate, Countable, ArrayAccess {
37 :
38 : /**
39 : * Associated DOMDocument object.
40 : * @var DOMDocument $_document
41 : */
42 : protected $_document = NULL;
43 :
44 : /**
45 : * XPath object used to execute selectors
46 : * @var DOMXPath $_xpath
47 : */
48 : protected $_xpath = NULL;
49 :
50 : /**
51 : * List of namespaces to be registered for xpath expressions
52 : * @var array
53 : */
54 : protected $_namespaces = array();
55 :
56 : /**
57 : * Use document context for expression (not selected nodes).
58 : * @var boolean $_useDocumentContext
59 : */
60 : protected $_useDocumentContext = TRUE;
61 :
62 : /**
63 : * Content type for output (xml, text/xml, html, text/html).
64 : * @var string $_contentType
65 : */
66 : protected $_contentType = 'text/xml';
67 :
68 : /**
69 : * Parent FluentDOM object (previous selection in chain).
70 : * @var FluentDOM $_parent
71 : */
72 : protected $_parent = NULL;
73 :
74 : /**
75 : * Seleted element and text nodes
76 : * @var array $_array
77 : */
78 : protected $_array = array();
79 :
80 : /**
81 : * Document loader list.
82 : *
83 : * @see _initLoaders
84 : * @see _setLoader
85 : *
86 : * @var array $_loaders
87 : */
88 : protected $_loaders = NULL;
89 :
90 : /**
91 : * Constructor
92 : *
93 : * @return FluentDOM
94 : */
95 : public function __construct() {
96 18 : $this->_document = new DOMDocument();
97 18 : }
98 :
99 : /**
100 : * Load a $source. The type of the source depends on the loaders. If no explicit loaders are set
101 : * FluentDOM will use a set of default loaders for xml/html and DOM.
102 : *
103 : * @param mixed $source
104 : * @param string $contentType optional, default value 'text/xml'
105 : */
106 : public function load($source, $contentType = 'text/xml') {
107 10 : $this->_array = array();
108 10 : $this->_setContentType($contentType);
109 10 : if ($source instanceof FluentDOMCore) {
110 2 : $this->_useDocumentContext = FALSE;
111 2 : $this->_document = $source->document;
112 2 : $this->_xpath = $source->_xpath;
113 2 : $this->_contentType = $source->_contentType;
114 2 : $this->_parent = $source;
115 2 : return $this;
116 : } else {
117 9 : $this->_parent = NULL;
118 9 : $this->_initLoaders();
119 9 : foreach ($this->_loaders as $loader) {
120 9 : if ($loaded = $loader->load($source, $this->_contentType)) {
121 8 : if ($loaded instanceof DOMDocument) {
122 5 : $this->_useDocumentContext = TRUE;
123 5 : $this->_document = $loaded;
124 8 : } elseif (is_array($loaded) &&
125 3 : isset($loaded[0]) &&
126 3 : isset($loaded[1]) &&
127 3 : $loaded[0] instanceof DOMDocument &&
128 3 : is_array($loaded[1])) {
129 3 : $this->_document = $loaded[0];
130 3 : $this->push($loaded[1]);
131 3 : $this->_useDocumentContext = FALSE;
132 3 : }
133 8 : return $this;
134 : }
135 3 : }
136 1 : throw new InvalidArgumentException('Invalid source object.');
137 : }
138 : return $this;
139 : }
140 :
141 : /**
142 : * Initialize default loaders if they are not already initialized
143 : *
144 : * @return void
145 : */
146 : protected function _initLoaders() {
147 6 : if (!is_array($this->_loaders)) {
148 3 : $path = dirname(__FILE__).'/';
149 3 : include_once($path.'/Loader/DOMNode.php');
150 3 : include_once($path.'/Loader/DOMDocument.php');
151 3 : include_once($path.'/Loader/StringXML.php');
152 3 : include_once($path.'/Loader/FileXML.php');
153 3 : include_once($path.'/Loader/StringHTML.php');
154 3 : include_once($path.'/Loader/FileHTML.php');
155 3 : $this->_loaders = array(
156 3 : new FluentDOMLoaderDOMNode(),
157 3 : new FluentDOMLoaderDOMDocument(),
158 3 : new FluentDOMLoaderStringXML(),
159 3 : new FluentDOMLoaderFileXML(),
160 3 : new FluentDOMLoaderStringHTML(),
161 3 : new FluentDOMLoaderFileHTML(),
162 : );
163 3 : }
164 6 : }
165 :
166 : /**
167 : * Define own loading handlers
168 : *
169 : * @example iniloader/iniToXML.php Usage Example: Own loader object
170 : * @param $loaders
171 : * @return FluentDOM
172 : */
173 : public function setLoaders($loaders) {
174 5 : foreach ($loaders as $loader) {
175 5 : if (!($loader instanceof FluentDOMLoader)) {
176 1 : throw new InvalidArgumentException('Array contains invalid loader object');
177 : }
178 4 : }
179 4 : $this->_loaders = $loaders;
180 4 : return $this;
181 : }
182 :
183 : /**
184 : * Setter for FluentDOM::_contentType property
185 : *
186 : * @param string $value
187 : * @return void
188 : */
189 : protected function _setContentType($value) {
190 15 : switch (strtolower($value)) {
191 15 : case 'xml' :
192 15 : case 'application/xml' :
193 15 : case 'text/xml' :
194 9 : $newContentType = 'text/xml';
195 9 : break;
196 7 : case 'html' :
197 7 : case 'text/html' :
198 6 : $newContentType = 'text/html';
199 6 : break;
200 1 : default :
201 1 : throw new UnexpectedValueException('Invalid content type value');
202 15 : }
203 14 : if ($this->_contentType != $newContentType) {
204 6 : $this->_contentType = $newContentType;
205 6 : if (isset($this->_parent)) {
206 1 : $this->_parent->contentType = $newContentType;
207 1 : }
208 6 : }
209 14 : }
210 :
211 : /**
212 : * implement dynamic properties using magic methods
213 : *
214 : * @param string $name
215 : * @return mixed
216 : */
217 : public function __get($name) {
218 : switch ($name) {
219 11 : case 'contentType' :
220 1 : return $this->_contentType;
221 10 : case 'document' :
222 7 : return $this->_document;
223 5 : case 'length' :
224 1 : return count($this->_array);
225 4 : case 'xpath' :
226 3 : return $this->_xpath();
227 1 : default :
228 1 : return NULL;
229 1 : }
230 : }
231 :
232 : /**
233 : * block changes of dynamic readonly property length
234 : *
235 : * @param string $name
236 : * @param mixed $value
237 : * @return void
238 : */
239 : public function __set($name, $value) {
240 : switch ($name) {
241 15 : case 'contentType' :
242 11 : $this->_setContentType($value);
243 9 : break;
244 4 : case 'document' :
245 4 : case 'length' :
246 4 : case 'xpath' :
247 3 : throw new BadMethodCallException('Can not set readonly value.');
248 1 : default :
249 1 : $this->$name = $value;
250 1 : break;
251 1 : }
252 10 : }
253 :
254 : /**
255 : * support isset for dynamic properties length and document
256 : *
257 : * @param string $name
258 : * @return boolean
259 : */
260 : public function __isset($name) {
261 : switch ($name) {
262 5 : case 'length' :
263 5 : case 'xpath' :
264 5 : case 'contentType' :
265 3 : return TRUE;
266 2 : case 'document' :
267 1 : return isset($this->_document);
268 : }
269 1 : return FALSE;
270 : }
271 :
272 :
273 : /**
274 : * Return the XML output of the internal dom document
275 : *
276 : * @return string
277 : */
278 : public function __toString() {
279 3 : switch ($this->_contentType) {
280 3 : case 'html' :
281 3 : case 'text/html' :
282 1 : return $this->_document->saveHTML();
283 2 : default :
284 2 : return $this->_document->saveXML();
285 2 : }
286 : }
287 :
288 : /**
289 : * The item() method is used to access elements in the node list,
290 : * like in a DOMNodelist.
291 : *
292 : * @param integer $position
293 : * @return DOMNode
294 : */
295 : public function item($position) {
296 2 : if (isset($this->_array[$position])) {
297 1 : return $this->_array[$position];
298 : }
299 1 : return NULL;
300 : }
301 :
302 : /**
303 : * Formats the current document, resets internal node array and other properties.
304 : *
305 : * The document is saved and reloaded, all variables with DOMNodes
306 : * of this document will get invalid.
307 : *
308 : * @return FluentDOM
309 : */
310 : public function formatOutput($contentType = NULL) {
311 2 : if (isset($contentType)) {
312 1 : $this->_setContentType($contentType);
313 1 : }
314 2 : $this->_array = array();
315 2 : $this->_position = 0;
316 2 : $this->_useDocumentContext = TRUE;
317 2 : $this->_parent = NULL;
318 2 : $this->_document->preserveWhiteSpace = FALSE;
319 2 : $this->_document->formatOutput = TRUE;
320 2 : if (!empty($this->_document->documentElement)) {
321 2 : $this->_document->loadXML($this->_document->saveXML());
322 2 : }
323 2 : return $this;
324 : }
325 :
326 : /*
327 : * Interface - IteratorAggregate
328 : */
329 :
330 : /**
331 : * Get an iterator for this object.
332 : *
333 : * @example interfaces/Iterator.php Usage Example: Iterator Interface
334 : * @example interfaces/RecursiveIterator.php Usage Example: Recursive Iterator Interface
335 : * @return FluentDOMIterator
336 : */
337 : public function getIterator() {
338 2 : return new FluentDOMIterator($this);
339 : }
340 :
341 : /*
342 : * Interface - Countable
343 : */
344 :
345 : /**
346 : * Get element count (Countable interface)
347 : *
348 : * @example interfaces/Countable.php Usage Example: Countable Interface
349 : * @return integer
350 : */
351 : public function count() {
352 2 : return count($this->_array);
353 : }
354 :
355 : /*
356 : * Interface - ArrayAccess
357 : */
358 :
359 : /**
360 : * If somebody tries to modify the internal array throw an exception.
361 : *
362 : * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
363 : * @param integer $offset
364 : * @param mixed $value
365 : * @return void
366 : */
367 : public function offsetSet($offset, $value) {
368 1 : throw new BadMethodCallException('List is read only');
369 : }
370 :
371 : /**
372 : * Check if index exists in internal array
373 : *
374 : * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
375 : * @param integer $offset
376 : * @return boolean
377 : */
378 : public function offsetExists($offset) {
379 2 : return isset($this->_array[$offset]);
380 : }
381 :
382 : /**
383 : * If somebody tries to remove an element from the internal array throw an exception.
384 : *
385 : * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
386 : * @param integer $offset
387 : * @return void
388 : */
389 : public function offsetUnset($offset) {
390 1 : throw new BadMethodCallException('List is read only');
391 : }
392 :
393 : /**
394 : * Get element from internal array
395 : *
396 : * @example interfaces/ArrayAccess.php Usage Example: ArrayAccess Interface
397 : * @param integer $offset
398 : * @return DOMNode|NULL
399 : */
400 : public function offsetGet($offset) {
401 1 : return isset($this->_array[$offset]) ? $this->_array[$offset] : NULL;
402 : }
403 :
404 : /*
405 : * Core functions
406 : */
407 :
408 : /**
409 : * Create a new instance of the same class with $this as the parent. This is used for the chaining.
410 : *
411 : * @return FluentDOM
412 : */
413 : public function spawn() {
414 2 : $className = get_class($this);
415 2 : $result = new $className();
416 2 : $result->_namespaces = $this->_namespaces;
417 2 : return $result->load($this);
418 : }
419 :
420 : /**
421 : * Push new element(s) an the internal element list
422 : *
423 : * @uses _inList
424 : * @param DOMNode|DOMNodeList|FluentDOM $elements
425 : * @param boolean $ignoreTextNodes ignore text nodes
426 : * @return void
427 : */
428 : public function push($elements, $ignoreTextNodes = FALSE) {
429 8 : if ($this->_isNode($elements, $ignoreTextNodes)) {
430 2 : $elements = array($elements);
431 2 : }
432 8 : if ($this->_isNodeList($elements)) {
433 7 : foreach ($elements as $index => $node) {
434 7 : if ($this->_isNode($node, $ignoreTextNodes)) {
435 7 : if ($node->ownerDocument === $this->_document) {
436 5 : $this->_array[] = $node;
437 5 : } else {
438 2 : throw new OutOfBoundsException(
439 2 : sprintf(
440 2 : 'Node #%d is not a part of this document', $index
441 2 : )
442 2 : );
443 : }
444 5 : }
445 5 : }
446 6 : } elseif (!is_null($elements)) {
447 1 : throw new InvalidArgumentException('Invalid elements variable.');
448 : }
449 5 : }
450 :
451 : /**
452 : * Sorts an array of DOM nodes based on document position, in place, with the duplicates removed.
453 : * Note that this only works on arrays of DOM nodes, not strings or numbers.
454 : *
455 : * @param array $array array of DOM nodes
456 : * @return array
457 : */
458 : public function unique(array $array) {
459 5 : $sortable = array();
460 5 : $unsortable = array();
461 5 : foreach ($array as $node) {
462 5 : if (!($node instanceof DOMNode)) {
463 2 : throw new InvalidArgumentException(
464 2 : sprintf(
465 2 : 'Array must only contain dom nodes, found "%s".',
466 2 : is_object($node) ? get_class($node) : gettype($node)
467 2 : )
468 2 : );
469 : }
470 3 : if (isset($node->parentNode) ||
471 3 : $node === $node->ownerDocument->documentElement) {
472 2 : $position = (integer)$this->_xpath()->evaluate('count(preceding::node())', $node);
473 : /* use the document position as index, ignore duplicates */
474 2 : if (!isset($sortable[$position])) {
475 2 : $sortable[$position] = $node;
476 2 : }
477 2 : } else {
478 2 : $hash = spl_object_hash($node);
479 : /* use the object hash as index, ignore duplicates */
480 2 : if (!isset($unsortable[$hash])) {
481 2 : $unsortable[$hash] = $node;
482 2 : }
483 : }
484 3 : }
485 3 : ksort($sortable, SORT_NUMERIC);
486 3 : $result = array_values($sortable);
487 3 : array_splice($result, count($result), 0, array_values($unsortable));
488 3 : return $result;
489 : }
490 :
491 : /**
492 : * Sorts the selected nodes, with the duplicates removed.
493 : *
494 : * @uses FluentDOMCore::unique
495 : *
496 : * @param array $array array of DOM nodes
497 : * @return array
498 : */
499 : protected function _uniqueSort() {
500 1 : $this->_array = $this->unique($this->_array);
501 1 : }
502 :
503 : /**
504 : * Gives access to an xpath evaluate on the current document
505 : *
506 : * @param string $expr
507 : * @param DOMNode $context
508 : */
509 : public function evaluate($expr, DOMNode $context = NULL) {
510 2 : if (isset($context)) {
511 1 : return $this->_xpath()->evaluate($expr, $context);
512 : } else {
513 1 : return $this->_xpath()->evaluate($expr);
514 : }
515 : }
516 :
517 : /**
518 : * Register namespaces and or get namespaces
519 : *
520 : * @param array $namespaces If this parameter is empty the current namespaces are returned
521 : * @return array|FluentDOMCore
522 : */
523 : public function namespaces(array $namespaces = NULL) {
524 3 : if (is_null($namespaces)) {
525 1 : return $this->_namespaces;
526 : }
527 3 : foreach ($namespaces as $prefix => $uri) {
528 3 : if ($this->_isNCName($prefix)) {
529 3 : $this->_xpath()->registerNamespace($prefix, $uri);
530 3 : $this->_namespaces[$prefix] = $uri;
531 3 : }
532 3 : }
533 3 : return $this;
534 : }
535 :
536 : /**
537 : * Get a XPath object associated with the internal DOMDocument and register
538 : * default namespaces from the document element if availiable.
539 : *
540 : * @return DOMXPath
541 : */
542 : protected function _xpath() {
543 4 : if (empty($this->_xpath) || $this->_xpath->document !== $this->_document) {
544 4 : $this->_xpath = new DOMXPath($this->_document);
545 4 : foreach ($this->_namespaces as $prefix => $uri) {
546 1 : $this->_xpath->registerNamespace($prefix, $uri);
547 4 : }
548 4 : if ($this->_document->documentElement) {
549 4 : $uri = $this->_document->documentElement->lookupnamespaceURI('_');
550 4 : if (!isset($uri)) {
551 4 : $uri = $this->_document->documentElement->lookupnamespaceURI(NULL);
552 4 : if (isset($uri)) {
553 2 : $this->_xpath->registerNamespace('_', $uri);
554 2 : }
555 4 : }
556 4 : }
557 4 : }
558 4 : return $this->_xpath;
559 : }
560 :
561 : /**
562 : * Match XPath expression agains context and return matched elements.
563 : *
564 : * @param string $expr
565 : * @param DOMNode $context optional, default value NULL
566 : * @return DOMNodeList
567 : */
568 : protected function _match($expr, $context = NULL) {
569 3 : if (isset($context)) {
570 1 : return $this->_xpath()->query($expr, $context);
571 : } else {
572 2 : return $this->_xpath()->query($expr);
573 : }
574 : }
575 :
576 : /**
577 : * Test xpath expression against context and return true/false
578 : *
579 : * @param string $expr
580 : * @param DOMNode $context optional, default value NULL
581 : * @return boolean
582 : */
583 : protected function _test($expr, $context = NULL) {
584 2 : if (isset($context)) {
585 1 : $check = $this->_xpath()->evaluate($expr, $context);
586 1 : } else {
587 1 : $check = $this->_xpath()->evaluate($expr);
588 : }
589 2 : if ($check instanceof DOMNodeList) {
590 1 : return $check->length > 0;
591 : } else {
592 1 : return (bool)$check;
593 : }
594 : }
595 :
596 : /**
597 : * Check if object is already in internal list
598 : *
599 : * @param DOMNode $node
600 : * @return boolean
601 : */
602 : protected function _inList($node) {
603 2 : foreach ($this->_array as $compareNode) {
604 2 : if ($compareNode === $node) {
605 1 : return TRUE;
606 : }
607 1 : }
608 1 : return FALSE;
609 : }
610 :
611 : /**
612 : * Validate string as qualified node name
613 : *
614 : * @param string $name
615 : * @return boolean
616 : */
617 : protected function _isQName($name) {
618 6 : if (empty($name)) {
619 1 : throw new UnexpectedValueException('Invalid QName: QName is empty.');
620 5 : } elseif (FALSE !== ($position = strpos($name, ':'))) {
621 2 : $this->_isNCName($name, 0, $position);
622 2 : $this->_isNCName($name, $position + 1);
623 2 : return TRUE;
624 : }
625 3 : $this->_isNCName($name);
626 3 : return TRUE;
627 : }
628 :
629 : /**
630 : * Validate string as qualified node name part (namespace or local name)
631 : *
632 : * @param string $name full QName
633 : * @param integer $offset Offset of NCName part in QName
634 : * @param integer $length Length of NCName part in QName
635 : * @return boolean
636 : */
637 : protected function _isNCName($name, $offset = 0, $length = 0) {
638 : $nameStartChar =
639 : 'A-Z_a-z'.
640 9 : '\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}'.
641 9 : '\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}'.
642 9 : '\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}'.
643 9 : '\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}';
644 : $nameChar =
645 : $nameStartChar.
646 9 : '\\.\\d\\x{B7}\\x{300}-\\x{36F}\\x{203F}-\\x{2040}';
647 9 : if ($length > 0) {
648 1 : $namePart = substr($name, $offset, $length);
649 9 : } elseif ($offset > 0) {
650 4 : $namePart = substr($name, $offset);
651 4 : } else {
652 4 : $namePart = $name;
653 : }
654 9 : if (empty($namePart)) {
655 1 : throw new UnexpectedValueException(
656 1 : 'Invalid QName "'.$name.'": Missing QName part.'
657 1 : );
658 8 : } elseif (preg_match('([^'.$nameChar.'-])u', $namePart, $match, PREG_OFFSET_CAPTURE)) {
659 : //invalid bytes and whitespaces
660 1 : $position = (int)$match[0][1];
661 1 : throw new UnexpectedValueException(
662 1 : 'Invalid QName "'.$name.'": Invalid character at index '.($offset + $position).'.'
663 1 : );
664 7 : } elseif (preg_match('(^[^'.$nameStartChar.'])u', $namePart)) {
665 : //first char is a little more limited
666 1 : throw new UnexpectedValueException(
667 1 : 'Invalid QName "'.$name.'": Invalid character at index '.$offset.'.'
668 1 : );
669 : }
670 6 : return TRUE;
671 : }
672 :
673 : /**
674 : * Check if the DOMNode is DOMElement or DOMText with content
675 : *
676 : * @param DOMNode $node
677 : * @param boolean $ignoreTextNodes
678 : * @return boolean
679 : */
680 : protected function _isNode($node, $ignoreTextNodes = FALSE) {
681 6 : if (is_object($node)) {
682 6 : if ($node instanceof DOMElement) {
683 4 : return TRUE;
684 3 : } elseif ($node instanceof DOMText) {
685 2 : if (!$ignoreTextNodes &&
686 2 : !$node->isWhitespaceInElementContent()) {
687 1 : return TRUE;
688 : }
689 1 : }
690 2 : }
691 4 : return FALSE;
692 : }
693 :
694 : /**
695 : * Check if $elements is a iterateable node list
696 : *
697 : * @param DOMNodeList|DOMDocumentFragment|Iterator|IteratorAggregate|array $list
698 : * @return boolean
699 : */
700 : protected function _isNodeList($elements) {
701 5 : if ($elements instanceof DOMNodeList ||
702 5 : $elements instanceof DOMDocumentFragment ||
703 5 : $elements instanceof Iterator ||
704 5 : $elements instanceof IteratorAggregate ||
705 5 : is_array($elements)) {
706 4 : return TRUE;
707 : }
708 1 : return FALSE;
709 : }
710 :
711 : /**
712 : * check if parameter is a valid callback function
713 : *
714 : * @param callback $callback
715 : * @param boolean $allowGlobalFunctions
716 : * @param boolean $silent (no InvalidArgumentException)
717 : * @return boolean
718 : */
719 : protected function _isCallback($callback, $allowGlobalFunctions, $silent) {
720 6 : if ($callback instanceof Closure) {
721 0 : return TRUE;
722 6 : } elseif (is_string($callback) &&
723 4 : $allowGlobalFunctions &&
724 6 : function_exists($callback)) {
725 1 : return is_callable($callback);
726 5 : } elseif (is_array($callback) &&
727 2 : count($callback) == 2 &&
728 1 : (is_object($callback[0]) || is_string($callback[0])) &&
729 5 : is_string($callback[1])) {
730 1 : return is_callable($callback);
731 4 : } elseif ($silent) {
732 3 : return FALSE;
733 : } else {
734 1 : throw new InvalidArgumentException('Invalid callback argument');
735 : }
736 : }
737 :
738 : /**
739 : * Convert a given content xml string into and array of nodes
740 : *
741 : * @param string $content
742 : * @param boolean $includeTextNodes
743 : * @param integer $limit
744 : * @return array
745 : */
746 : protected function _getContentFragment($content, $includeTextNodes = TRUE, $limit = 0) {
747 5 : $result = array();
748 5 : $fragment = $this->_document->createDocumentFragment();
749 5 : if ($fragment->appendXML($content)) {
750 4 : for ($i = $fragment->childNodes->length - 1; $i >= 0; $i--) {
751 4 : $element = $fragment->childNodes->item($i);
752 4 : if ($element instanceof DOMElement ||
753 4 : ($includeTextNodes && $this->_isNode($element))) {
754 4 : array_unshift($result, $element);
755 4 : $element->parentNode->removeChild($element);
756 4 : }
757 4 : }
758 4 : if ($limit > 0 && count($result) >= $limit) {
759 1 : return array_slice($result, 0, $limit);
760 : }
761 3 : return $result;
762 : } else {
763 1 : throw new UnexpectedValueException('Invalid document fragment');
764 : }
765 : }
766 :
767 : /**
768 : * Convert a given content into and array of nodes
769 : *
770 : * @param string|array|DOMElement|DOMText|Iterator $content
771 : * @param boolean $includeTextNodes
772 : * @param integer $limit
773 : * @return array
774 : */
775 : protected function _getContentNodes($content, $includeTextNodes = TRUE, $limit = 0) {
776 10 : $result = array();
777 10 : if ($content instanceof DOMElement) {
778 1 : $result = array($content);
779 10 : } elseif ($includeTextNodes && $this->_isNode($content)) {
780 1 : $result = array($content);
781 9 : } elseif (is_string($content)) {
782 2 : $result = $this->_getContentFragment($content, $includeTextNodes, $limit);
783 8 : } elseif ($this->_isNodeList($content)) {
784 5 : foreach ($content as $element) {
785 5 : if ($element instanceof DOMElement ||
786 5 : ($includeTextNodes && $this->_isNode($element))) {
787 5 : $result[] = $element;
788 5 : if ($limit > 0 && count($result) >= $limit) {
789 1 : break;
790 : }
791 4 : }
792 5 : }
793 5 : } else {
794 1 : throw new InvalidArgumentException('Invalid content parameter');
795 : }
796 9 : if (empty($result)) {
797 1 : throw new UnexpectedValueException('No element found');
798 : } else {
799 : //if a node is not in the current document import it
800 8 : foreach ($result as $index => $node) {
801 8 : if ($node->ownerDocument !== $this->_document) {
802 1 : $result[$index] = $this->_document->importNode($node, TRUE);
803 1 : }
804 8 : }
805 : }
806 8 : return $result;
807 : }
808 :
809 : /**
810 : * Convert $content to a DOMElement. If $content contains several elements use the first.
811 : *
812 : * @param string|array|DOMElement|DOMNodeList|Iterator $content
813 : * @return DOMElement
814 : */
815 : protected function _getContentElement($content) {
816 2 : if ($content instanceof DOMElement) {
817 1 : return $content;
818 : } else {
819 1 : $contentNodes = $this->_getContentNodes($content, FALSE, 1);
820 1 : return $contentNodes[0];
821 : }
822 : }
823 :
824 : /**
825 : * Get the target nodes from a given $selector.
826 : *
827 : * A string will be used as XPath expression.
828 : *
829 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
830 : * @return array
831 : */
832 : protected function _getTargetNodes($selector) {
833 5 : if ($this->_isNode($selector)) {
834 1 : return array($selector);
835 4 : } elseif (is_string($selector)) {
836 2 : return $this->_match($selector);
837 3 : } elseif ($this->_isNodeList($selector)) {
838 2 : return $selector;
839 : } else {
840 1 : throw new InvalidArgumentException('Invalid selector');
841 : }
842 : }
843 :
844 : /*
845 : * the context is the target of a selector or the current selection
846 : *
847 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
848 : * @return unknown_type
849 : */
850 : protected function _getContextNodes($selector) {
851 2 : if (is_null($selector)) {
852 1 : return $this->_array;
853 : } else {
854 1 : return $this->_getTargetNodes($selector);
855 : }
856 : }
857 :
858 : /**
859 : * Get the inner xml of a given node or in other words the xml of all children.
860 : * @param DOMElement $node
861 : * @return string
862 : */
863 : protected function _getInnerXml($node) {
864 2 : $result = '';
865 2 : if ($node instanceof DOMElement) {
866 1 : foreach ($node->childNodes as $childNode) {
867 1 : if ($this->_isNode($childNode)) {
868 1 : $result .= $this->_document->saveXML($childNode);
869 1 : }
870 1 : }
871 2 : } elseif ($node instanceof DOMText) {
872 1 : return $node->textContent;
873 : }
874 1 : return $result;
875 : }
876 :
877 : /**
878 : * Remove nodes from document tree
879 : *
880 : * @param string|array|DOMNode|DOMNodeList|Iterator $selector
881 : * @return array $result removed nodes
882 : */
883 : protected function _removeNodes($selector) {
884 2 : $targetNodes = $this->_getTargetNodes($selector);
885 2 : $result = array();
886 2 : foreach ($targetNodes as $node) {
887 2 : if ($node instanceof DOMNode &&
888 2 : isset($node->parentNode)) {
889 2 : $result[] = $node->parentNode->removeChild($node);
890 2 : }
891 2 : }
892 2 : return $result;
893 : }
894 :
895 : /**
896 : * Get the class/object providing the handler functions
897 : *
898 : * @return string|object
899 : */
900 : protected function _getHandler() {
901 2 : return 'FluentDOMHandler';
902 : }
903 :
904 : /**
905 : * Use a handler callback to apply a content argument to each node $targetNodes. The content
906 : * argument can be an easy setter function
907 : *
908 : * @param array|DOMNodeList $targetNodes
909 : * @param string|array|DOMElement|DOMText|DOMNodeList|Iterator|callback|Closure $content
910 : * @param callback|Closure $handler
911 : */
912 : protected function _applyContentToNodes($targetNodes, $content, $handler) {
913 3 : $result = array();
914 3 : $isEasySetterFunction = $this->_isCallback($content, FALSE, TRUE);
915 3 : if (!$isEasySetterFunction) {
916 2 : $contentNodes = $this->_getContentNodes($content);
917 2 : }
918 3 : foreach ($targetNodes as $index => $node) {
919 3 : if ($isEasySetterFunction) {
920 1 : $contentNodes = $this->_executeEasySetter(
921 1 : $content, $node, $index, $this->_getInnerXml($node)
922 1 : );
923 1 : }
924 3 : if (!empty($contentNodes)) {
925 3 : $resultNodes = call_user_func($handler, $node, $contentNodes);
926 3 : if (is_array($resultNodes)) {
927 3 : $result = array_merge($result, $resultNodes);
928 3 : }
929 3 : }
930 3 : }
931 3 : return $result;
932 : }
933 :
934 : /**
935 : * Execute the easy setter function for a node and return the new elements
936 : *
937 : * @param callback|Closure $easySetter
938 : * @param DOMNode $node
939 : * @param integer $index
940 : * @param string $value
941 : * @return array
942 : */
943 : protected function _executeEasySetter($easySetter, $node, $index, $value) {
944 2 : $contentData = call_user_func($easySetter, $node, $index, $value);
945 2 : if (!empty($contentData)) {
946 1 : return $this->_getContentNodes($contentData);
947 : }
948 1 : return array();
949 : }
|