PageRenderTime 131ms CodeModel.GetById 50ms app.highlight 35ms RepoModel.GetById 15ms app.codeStats 1ms

/vendor/phpunit/phpunit/PHPUnit/Util/XML.php

https://bitbucket.org/sklyarov_ivan/trap
PHP | 916 lines | 537 code | 138 blank | 241 comment | 124 complexity | e806feaf0d596a0b0a1c3bf8616651e8 MD5 | raw file
  1<?php
  2/**
  3 * PHPUnit
  4 *
  5 * Copyright (c) 2001-2012, Sebastian Bergmann <sebastian@phpunit.de>.
  6 * All rights reserved.
  7 *
  8 * Redistribution and use in source and binary forms, with or without
  9 * modification, are permitted provided that the following conditions
 10 * are met:
 11 *
 12 *   * Redistributions of source code must retain the above copyright
 13 *     notice, this list of conditions and the following disclaimer.
 14 *
 15 *   * Redistributions in binary form must reproduce the above copyright
 16 *     notice, this list of conditions and the following disclaimer in
 17 *     the documentation and/or other materials provided with the
 18 *     distribution.
 19 *
 20 *   * Neither the name of Sebastian Bergmann nor the names of his
 21 *     contributors may be used to endorse or promote products derived
 22 *     from this software without specific prior written permission.
 23 *
 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 25 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 26 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 27 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 28 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 29 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 34 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 35 * POSSIBILITY OF SUCH DAMAGE.
 36 *
 37 * @package    PHPUnit
 38 * @subpackage Util
 39 * @author     Sebastian Bergmann <sebastian@phpunit.de>
 40 * @copyright  2001-2012 Sebastian Bergmann <sebastian@phpunit.de>
 41 * @license    http://www.opensource.org/licenses/BSD-3-Clause  The BSD 3-Clause License
 42 * @link       http://www.phpunit.de/
 43 * @since      File available since Release 3.2.0
 44 */
 45
 46/**
 47 * XML helpers.
 48 *
 49 * @package    PHPUnit
 50 * @subpackage Util
 51 * @author     Sebastian Bergmann <sebastian@phpunit.de>
 52 * @copyright  2001-2012 Sebastian Bergmann <sebastian@phpunit.de>
 53 * @license    http://www.opensource.org/licenses/BSD-3-Clause  The BSD 3-Clause License
 54 * @link       http://www.phpunit.de/
 55 * @since      Class available since Release 3.2.0
 56 */
 57class PHPUnit_Util_XML
 58{
 59    /**
 60     * @param  string $string
 61     * @return string
 62     * @author Kore Nordmann <mail@kore-nordmann.de>
 63     * @since  Method available since Release 3.4.6
 64     */
 65    public static function prepareString($string)
 66    {
 67        return preg_replace(
 68          '([\\x00-\\x04\\x0b\\x0c\\x0e-\\x1f\\x7f])e',
 69          'sprintf( "&#x%02x;", ord( "\\1" ) )',
 70          htmlspecialchars(
 71            PHPUnit_Util_String::convertToUtf8($string), ENT_COMPAT, 'UTF-8'
 72          )
 73        );
 74    }
 75
 76    /**
 77     * Loads an XML (or HTML) file into a DOMDocument object.
 78     *
 79     * @param  string  $filename
 80     * @param  boolean $isHtml
 81     * @param  boolean $xinclude
 82     * @return DOMDocument
 83     * @since  Method available since Release 3.3.0
 84     */
 85    public static function loadFile($filename, $isHtml = FALSE, $xinclude = FALSE)
 86    {
 87        $reporting = error_reporting(0);
 88        $contents  = file_get_contents($filename);
 89        error_reporting($reporting);
 90
 91        if ($contents === FALSE) {
 92            throw new PHPUnit_Framework_Exception(
 93              sprintf(
 94                'Could not read "%s".',
 95                $filename
 96              )
 97            );
 98        }
 99
100        return self::load($contents, $isHtml, $filename, $xinclude);
101    }
102
103    /**
104     * Load an $actual document into a DOMDocument.  This is called
105     * from the selector assertions.
106     *
107     * If $actual is already a DOMDocument, it is returned with
108     * no changes.  Otherwise, $actual is loaded into a new DOMDocument
109     * as either HTML or XML, depending on the value of $isHtml. If $isHtml is
110     * false and $xinclude is true, xinclude is performed on the loaded
111     * DOMDocument.
112     *
113     * Note: prior to PHPUnit 3.3.0, this method loaded a file and
114     * not a string as it currently does.  To load a file into a
115     * DOMDocument, use loadFile() instead.
116     *
117     * @param  string|DOMDocument  $actual
118     * @param  boolean             $isHtml
119     * @param  string              $filename
120     * @param  boolean             $xinclude
121     * @return DOMDocument
122     * @since  Method available since Release 3.3.0
123     * @author Mike Naberezny <mike@maintainable.com>
124     * @author Derek DeVries <derek@maintainable.com>
125     * @author Tobias Schlitt <toby@php.net>
126     */
127    public static function load($actual, $isHtml = FALSE, $filename = '', $xinclude = FALSE)
128    {
129        if ($actual instanceof DOMDocument) {
130            return $actual;
131        }
132
133        $document  = new DOMDocument;
134
135        $internal  = libxml_use_internal_errors(TRUE);
136        $message   = '';
137        $reporting = error_reporting(0);
138
139        if ($isHtml) {
140            $loaded = $document->loadHTML($actual);
141        } else {
142            $loaded = $document->loadXML($actual);
143        }
144
145        if ('' !== $filename) {
146            // Necessary for xinclude
147            $document->documentURI = $filename;
148        }
149
150        if (!$isHtml && $xinclude) {
151            $document->xinclude();
152        }
153
154        foreach (libxml_get_errors() as $error) {
155            $message .= $error->message;
156        }
157
158        libxml_use_internal_errors($internal);
159        error_reporting($reporting);
160
161        if ($loaded === FALSE) {
162            if ($filename != '') {
163                throw new PHPUnit_Framework_Exception(
164                  sprintf(
165                    'Could not load "%s".%s',
166
167                    $filename,
168                    $message != '' ? "\n" . $message : ''
169                  )
170                );
171            } else {
172                throw new PHPUnit_Framework_Exception($message);
173            }
174        }
175
176        return $document;
177    }
178
179    /**
180     *
181     *
182     * @param  DOMNode $node
183     * @return string
184     * @since  Method available since Release 3.4.0
185     */
186    public static function nodeToText(DOMNode $node)
187    {
188        if ($node->childNodes->length == 1) {
189            return $node->nodeValue;
190        }
191
192        $result = '';
193
194        foreach ($node->childNodes as $childNode) {
195            $result .= $node->ownerDocument->saveXML($childNode);
196        }
197
198        return $result;
199    }
200
201    /**
202     *
203     *
204     * @param  DOMNode $node
205     * @since  Method available since Release 3.3.0
206     * @author Mattis Stordalen Flister <mattis@xait.no>
207     */
208    public static function removeCharacterDataNodes(DOMNode $node)
209    {
210        if ($node->hasChildNodes()) {
211            for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
212                if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
213                    $node->removeChild($child);
214                }
215            }
216        }
217    }
218
219    /**
220     * "Convert" a DOMElement object into a PHP variable.
221     *
222     * @param  DOMElement $element
223     * @return mixed
224     * @since  Method available since Release 3.4.0
225     */
226    public static function xmlToVariable(DOMElement $element)
227    {
228        $variable = NULL;
229
230        switch ($element->tagName) {
231            case 'array': {
232                $variable = array();
233
234                foreach ($element->getElementsByTagName('element') as $element) {
235                    $value = self::xmlToVariable($element->childNodes->item(1));
236
237                    if ($element->hasAttribute('key')) {
238                        $variable[(string)$element->getAttribute('key')] = $value;
239                    } else {
240                        $variable[] = $value;
241                    }
242                }
243            }
244            break;
245
246            case 'object': {
247                $className = $element->getAttribute('class');
248
249                if ($element->hasChildNodes()) {
250                    $arguments       = $element->childNodes->item(1)->childNodes;
251                    $constructorArgs = array();
252
253                    foreach ($arguments as $argument) {
254                        if ($argument instanceof DOMElement) {
255                            $constructorArgs[] = self::xmlToVariable($argument);
256                        }
257                    }
258
259                    $class    = new ReflectionClass($className);
260                    $variable = $class->newInstanceArgs($constructorArgs);
261                } else {
262                    $variable = new $className;
263                }
264            }
265            break;
266
267            case 'boolean': {
268                $variable = $element->nodeValue == 'true' ? TRUE : FALSE;
269            }
270            break;
271
272            case 'integer':
273            case 'double':
274            case 'string': {
275                $variable = $element->nodeValue;
276
277                settype($variable, $element->tagName);
278            }
279            break;
280        }
281
282        return $variable;
283    }
284
285    /**
286     * Validate list of keys in the associative array.
287     *
288     * @param  array $hash
289     * @param  array $validKeys
290     * @return array
291     * @throws PHPUnit_Framework_Exception
292     * @since  Method available since Release 3.3.0
293     * @author Mike Naberezny <mike@maintainable.com>
294     * @author Derek DeVries <derek@maintainable.com>
295     */
296    public static function assertValidKeys(array $hash, array $validKeys)
297    {
298        $valids = array();
299
300        // Normalize validation keys so that we can use both indexed and
301        // associative arrays.
302        foreach ($validKeys as $key => $val) {
303            is_int($key) ? $valids[$val] = NULL : $valids[$key] = $val;
304        }
305
306        $validKeys = array_keys($valids);
307
308        // Check for invalid keys.
309        foreach ($hash as $key => $value) {
310            if (!in_array($key, $validKeys)) {
311                $unknown[] = $key;
312            }
313        }
314
315        if (!empty($unknown)) {
316            throw new PHPUnit_Framework_Exception(
317              'Unknown key(s): ' . implode(', ', $unknown)
318            );
319        }
320
321        // Add default values for any valid keys that are empty.
322        foreach ($valids as $key => $value) {
323            if (!isset($hash[$key])) {
324                $hash[$key] = $value;
325            }
326        }
327
328        return $hash;
329    }
330
331    /**
332     * Parse a CSS selector into an associative array suitable for
333     * use with findNodes().
334     *
335     * @param  string $selector
336     * @param  mixed  $content
337     * @return array
338     * @since  Method available since Release 3.3.0
339     * @author Mike Naberezny <mike@maintainable.com>
340     * @author Derek DeVries <derek@maintainable.com>
341     */
342    public static function convertSelectToTag($selector, $content = TRUE)
343    {
344        $selector = trim(preg_replace("/\s+/", " ", $selector));
345
346        // substitute spaces within attribute value
347        while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
348            $selector = preg_replace(
349              '/(\[[^\]]+"[^"]+)\s([^"]+"\])/', "$1__SPACE__$2", $selector
350            );
351        }
352
353        if (strstr($selector, ' ')) {
354            $elements = explode(' ', $selector);
355        } else {
356            $elements = array($selector);
357        }
358
359        $previousTag = array();
360
361        foreach (array_reverse($elements) as $element) {
362            $element = str_replace('__SPACE__', ' ', $element);
363
364            // child selector
365            if ($element == '>') {
366                $previousTag = array('child' => $previousTag['descendant']);
367                continue;
368            }
369
370            $tag = array();
371
372            // match element tag
373            preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
374
375            if (!empty($eltMatches[1])) {
376                $tag['tag'] = $eltMatches[1];
377            }
378
379            // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*),
380            // and classes (\.[^\.#\[]*))
381            preg_match_all(
382              "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/", $element, $matches
383            );
384
385            if (!empty($matches[1])) {
386                $classes = array();
387                $attrs   = array();
388
389                foreach ($matches[1] as $match) {
390                    // id matched
391                    if (substr($match, 0, 1) == '#') {
392                        $tag['id'] = substr($match, 1);
393                    }
394
395                    // class matched
396                    else if (substr($match, 0, 1) == '.') {
397                        $classes[] = substr($match, 1);
398                    }
399
400                    // attribute matched
401                    else if (substr($match, 0, 1) == '[' &&
402                             substr($match, -1, 1) == ']') {
403                        $attribute = substr($match, 1, strlen($match) - 2);
404                        $attribute = str_replace('"', '', $attribute);
405
406                        // match single word
407                        if (strstr($attribute, '~=')) {
408                            list($key, $value) = explode('~=', $attribute);
409                            $value             = "regexp:/.*\b$value\b.*/";
410                        }
411
412                        // match substring
413                        else if (strstr($attribute, '*=')) {
414                            list($key, $value) = explode('*=', $attribute);
415                            $value             = "regexp:/.*$value.*/";
416                        }
417
418                        // exact match
419                        else {
420                            list($key, $value) = explode('=', $attribute);
421                        }
422
423                        $attrs[$key] = $value;
424                    }
425                }
426
427                if ($classes) {
428                    $tag['class'] = join(' ', $classes);
429                }
430
431                if ($attrs) {
432                    $tag['attributes'] = $attrs;
433                }
434            }
435
436            // tag content
437            if (is_string($content)) {
438                $tag['content'] = $content;
439            }
440
441            // determine previous child/descendants
442            if (!empty($previousTag['descendant'])) {
443                $tag['descendant'] = $previousTag['descendant'];
444            }
445
446            else if (!empty($previousTag['child'])) {
447                $tag['child'] = $previousTag['child'];
448            }
449
450            $previousTag = array('descendant' => $tag);
451        }
452
453        return $tag;
454    }
455
456    /**
457     * Parse an $actual document and return an array of DOMNodes
458     * matching the CSS $selector.  If an error occurs, it will
459     * return FALSE.
460     *
461     * To only return nodes containing a certain content, give
462     * the $content to match as a string.  Otherwise, setting
463     * $content to TRUE will return all nodes matching $selector.
464     *
465     * The $actual document may be a DOMDocument or a string
466     * containing XML or HTML, identified by $isHtml.
467     *
468     * @param  array   $selector
469     * @param  string  $content
470     * @param  mixed   $actual
471     * @param  boolean $isHtml
472     * @return false|array
473     * @since  Method available since Release 3.3.0
474     * @author Mike Naberezny <mike@maintainable.com>
475     * @author Derek DeVries <derek@maintainable.com>
476     * @author Tobias Schlitt <toby@php.net>
477     */
478    public static function cssSelect($selector, $content, $actual, $isHtml = TRUE)
479    {
480        $matcher = self::convertSelectToTag($selector, $content);
481        $dom     = self::load($actual, $isHtml);
482        $tags    = self::findNodes($dom, $matcher, $isHtml);
483
484        return $tags;
485    }
486
487    /**
488     * Parse out the options from the tag using DOM object tree.
489     *
490     * @param  DOMDocument $dom
491     * @param  array       $options
492     * @param  boolean     $isHtml
493     * @return array
494     * @since  Method available since Release 3.3.0
495     * @author Mike Naberezny <mike@maintainable.com>
496     * @author Derek DeVries <derek@maintainable.com>
497     * @author Tobias Schlitt <toby@php.net>
498     */
499    public static function findNodes(DOMDocument $dom, array $options, $isHtml = TRUE)
500    {
501        $valid = array(
502          'id', 'class', 'tag', 'content', 'attributes', 'parent',
503          'child', 'ancestor', 'descendant', 'children'
504        );
505
506        $filtered = array();
507        $options  = self::assertValidKeys($options, $valid);
508
509        // find the element by id
510        if ($options['id']) {
511            $options['attributes']['id'] = $options['id'];
512        }
513
514        if ($options['class']) {
515            $options['attributes']['class'] = $options['class'];
516        }
517
518        // find the element by a tag type
519        if ($options['tag']) {
520            if ($isHtml) {
521                $elements = self::getElementsByCaseInsensitiveTagName(
522                  $dom, $options['tag']
523                );
524            } else {
525                $elements = $dom->getElementsByTagName($options['tag']);
526            }
527
528            foreach ($elements as $element) {
529                $nodes[] = $element;
530            }
531
532            if (empty($nodes)) {
533                return FALSE;
534            }
535        }
536
537        // no tag selected, get them all
538        else {
539            $tags = array(
540              'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
541              'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
542              'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
543              'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
544              'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
545              'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
546              'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
547              'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
548              'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
549              'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
550              'tr', 'tt', 'ul', 'var'
551            );
552
553            foreach ($tags as $tag) {
554                if ($isHtml) {
555                    $elements = self::getElementsByCaseInsensitiveTagName(
556                      $dom, $tag
557                    );
558                } else {
559                    $elements = $dom->getElementsByTagName($tag);
560                }
561
562                foreach ($elements as $element) {
563                    $nodes[] = $element;
564                }
565            }
566
567            if (empty($nodes)) {
568                return FALSE;
569            }
570        }
571
572        // filter by attributes
573        if ($options['attributes']) {
574            foreach ($nodes as $node) {
575                $invalid = FALSE;
576
577                foreach ($options['attributes'] as $name => $value) {
578                    // match by regexp if like "regexp:/foo/i"
579                    if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
580                        if (!preg_match($matches[1], $node->getAttribute($name))) {
581                            $invalid = TRUE;
582                        }
583                    }
584
585                    // class can match only a part
586                    else if ($name == 'class') {
587                        // split to individual classes
588                        $findClasses = explode(
589                          ' ', preg_replace("/\s+/", " ", $value)
590                        );
591
592                        $allClasses = explode(
593                          ' ',
594                          preg_replace("/\s+/", " ", $node->getAttribute($name))
595                        );
596
597                        // make sure each class given is in the actual node
598                        foreach ($findClasses as $findClass) {
599                            if (!in_array($findClass, $allClasses)) {
600                                $invalid = TRUE;
601                            }
602                        }
603                    }
604
605                    // match by exact string
606                    else {
607                        if ($node->getAttribute($name) != $value) {
608                            $invalid = TRUE;
609                        }
610                    }
611                }
612
613                // if every attribute given matched
614                if (!$invalid) {
615                    $filtered[] = $node;
616                }
617            }
618
619            $nodes    = $filtered;
620            $filtered = array();
621
622            if (empty($nodes)) {
623                return FALSE;
624            }
625        }
626
627        // filter by content
628        if ($options['content'] !== NULL) {
629            foreach ($nodes as $node) {
630                $invalid = FALSE;
631
632                // match by regexp if like "regexp:/foo/i"
633                if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
634                    if (!preg_match($matches[1], self::getNodeText($node))) {
635                        $invalid = TRUE;
636                    }
637                }
638
639                // match empty string
640                else if ($options['content'] === '') {
641                    if (self::getNodeText($node) !== '') {
642                        $invalid = TRUE;
643                    }
644                }
645
646                // match by exact string
647                else if (strstr(self::getNodeText($node), $options['content']) === FALSE) {
648                    $invalid = TRUE;
649                }
650
651                if (!$invalid) {
652                    $filtered[] = $node;
653                }
654            }
655
656            $nodes    = $filtered;
657            $filtered = array();
658
659            if (empty($nodes)) {
660                return FALSE;
661            }
662        }
663
664        // filter by parent node
665        if ($options['parent']) {
666            $parentNodes = self::findNodes($dom, $options['parent'], $isHtml);
667            $parentNode  = isset($parentNodes[0]) ? $parentNodes[0] : NULL;
668
669            foreach ($nodes as $node) {
670                if ($parentNode !== $node->parentNode) {
671                    continue;
672                }
673
674                $filtered[] = $node;
675            }
676
677            $nodes    = $filtered;
678            $filtered = array();
679
680            if (empty($nodes)) {
681                return FALSE;
682            }
683        }
684
685        // filter by child node
686        if ($options['child']) {
687            $childNodes = self::findNodes($dom, $options['child'], $isHtml);
688            $childNodes = !empty($childNodes) ? $childNodes : array();
689
690            foreach ($nodes as $node) {
691                foreach ($node->childNodes as $child) {
692                    foreach ($childNodes as $childNode) {
693                        if ($childNode === $child) {
694                            $filtered[] = $node;
695                        }
696                    }
697                }
698            }
699
700            $nodes    = $filtered;
701            $filtered = array();
702
703            if (empty($nodes)) {
704                return FALSE;
705            }
706        }
707
708        // filter by ancestor
709        if ($options['ancestor']) {
710            $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml);
711            $ancestorNode  = isset($ancestorNodes[0]) ? $ancestorNodes[0] : NULL;
712
713            foreach ($nodes as $node) {
714                $parent = $node->parentNode;
715
716                while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) {
717                    if ($parent === $ancestorNode) {
718                        $filtered[] = $node;
719                    }
720
721                    $parent = $parent->parentNode;
722                }
723            }
724
725            $nodes    = $filtered;
726            $filtered = array();
727
728            if (empty($nodes)) {
729                return FALSE;
730            }
731        }
732
733        // filter by descendant
734        if ($options['descendant']) {
735            $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml);
736            $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
737
738            foreach ($nodes as $node) {
739                foreach (self::getDescendants($node) as $descendant) {
740                    foreach ($descendantNodes as $descendantNode) {
741                        if ($descendantNode === $descendant) {
742                            $filtered[] = $node;
743                        }
744                    }
745                }
746            }
747
748            $nodes    = $filtered;
749            $filtered = array();
750
751            if (empty($nodes)) {
752                return FALSE;
753            }
754        }
755
756        // filter by children
757        if ($options['children']) {
758            $validChild   = array('count', 'greater_than', 'less_than', 'only');
759            $childOptions = self::assertValidKeys(
760                              $options['children'], $validChild
761                            );
762
763            foreach ($nodes as $node) {
764                $childNodes = $node->childNodes;
765
766                foreach ($childNodes as $childNode) {
767                    if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
768                        $childNode->nodeType !== XML_TEXT_NODE) {
769                        $children[] = $childNode;
770                    }
771                }
772
773                // we must have children to pass this filter
774                if (!empty($children)) {
775                    // exact count of children
776                    if ($childOptions['count'] !== NULL) {
777                        if (count($children) !== $childOptions['count']) {
778                            break;
779                        }
780                    }
781
782                    // range count of children
783                    else if ($childOptions['less_than']    !== NULL &&
784                            $childOptions['greater_than'] !== NULL) {
785                        if (count($children) >= $childOptions['less_than'] ||
786                            count($children) <= $childOptions['greater_than']) {
787                            break;
788                        }
789                    }
790
791                    // less than a given count
792                    else if ($childOptions['less_than'] !== NULL) {
793                        if (count($children) >= $childOptions['less_than']) {
794                            break;
795                        }
796                    }
797
798                    // more than a given count
799                    else if ($childOptions['greater_than'] !== NULL) {
800                        if (count($children) <= $childOptions['greater_than']) {
801                            break;
802                        }
803                    }
804
805                    // match each child against a specific tag
806                    if ($childOptions['only']) {
807                        $onlyNodes = self::findNodes(
808                          $dom, $childOptions['only'], $isHtml
809                        );
810
811                        // try to match each child to one of the 'only' nodes
812                        foreach ($children as $child) {
813                            $matched = FALSE;
814
815                            foreach ($onlyNodes as $onlyNode) {
816                                if ($onlyNode === $child) {
817                                    $matched = TRUE;
818                                }
819                            }
820
821                            if (!$matched) {
822                                break(2);
823                            }
824                        }
825                    }
826
827                    $filtered[] = $node;
828                }
829            }
830
831            $nodes    = $filtered;
832            $filtered = array();
833
834            if (empty($nodes)) {
835                return;
836            }
837        }
838
839        // return the first node that matches all criteria
840        return !empty($nodes) ? $nodes : array();
841    }
842
843    /**
844     * Recursively get flat array of all descendants of this node.
845     *
846     * @param  DOMNode $node
847     * @return array
848     * @since  Method available since Release 3.3.0
849     * @author Mike Naberezny <mike@maintainable.com>
850     * @author Derek DeVries <derek@maintainable.com>
851     */
852    protected static function getDescendants(DOMNode $node)
853    {
854        $allChildren = array();
855        $childNodes  = $node->childNodes ? $node->childNodes : array();
856
857        foreach ($childNodes as $child) {
858            if ($child->nodeType === XML_CDATA_SECTION_NODE ||
859                $child->nodeType === XML_TEXT_NODE) {
860                continue;
861            }
862
863            $children    = self::getDescendants($child);
864            $allChildren = array_merge($allChildren, $children, array($child));
865        }
866
867        return isset($allChildren) ? $allChildren : array();
868    }
869
870    /**
871     * Gets elements by case insensitive tagname.
872     *
873     * @param  DOMDocument $dom
874     * @param  string      $tag
875     * @return DOMNodeList
876     * @since  Method available since Release 3.4.0
877     */
878    protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag)
879    {
880        $elements = $dom->getElementsByTagName(strtolower($tag));
881
882        if ($elements->length == 0) {
883            $elements = $dom->getElementsByTagName(strtoupper($tag));
884        }
885
886        return $elements;
887    }
888
889    /**
890     * Get the text value of this node's child text node.
891     *
892     * @param  DOMNode $node
893     * @return string
894     * @since  Method available since Release 3.3.0
895     * @author Mike Naberezny <mike@maintainable.com>
896     * @author Derek DeVries <derek@maintainable.com>
897     */
898    protected static function getNodeText(DOMNode $node)
899    {
900        if (!$node->childNodes instanceof DOMNodeList) {
901            return '';
902        }
903
904        $result = '';
905
906        foreach ($node->childNodes as $childNode) {
907            if ($childNode->nodeType === XML_TEXT_NODE) {
908                $result .= trim($childNode->data) . ' ';
909            } else {
910                $result .= self::getNodeText($childNode);
911            }
912        }
913
914        return str_replace('  ', ' ', $result);
915    }
916}