PageRenderTime 76ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/PHPUnit/Util/XML.php

https://github.com/item/sugarcrm_dev
PHP | 784 lines | 446 code | 120 blank | 218 comment | 114 complexity | 74f8f3c2ac08cac9e1610652d9d7004f MD5 | raw file
Possible License(s): AGPL-3.0, LGPL-2.1
  1. <?php
  2. /**
  3. * PHPUnit
  4. *
  5. * Copyright (c) 2002-2009, Sebastian Bergmann <sb@sebastian-bergmann.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. * @category Testing
  38. * @package PHPUnit
  39. * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
  40. * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
  41. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  42. * @link http://www.phpunit.de/
  43. * @since File available since Release 3.2.0
  44. */
  45. require_once 'PHPUnit/Util/Filter.php';
  46. PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
  47. /**
  48. * XML helpers.
  49. *
  50. * @category Testing
  51. * @package PHPUnit
  52. * @author Sebastian Bergmann <sb@sebastian-bergmann.de>
  53. * @copyright 2002-2009 Sebastian Bergmann <sb@sebastian-bergmann.de>
  54. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  55. * @version Release: 3.3.17
  56. * @link http://www.phpunit.de/
  57. * @since Class available since Release 3.2.0
  58. */
  59. class PHPUnit_Util_XML
  60. {
  61. /**
  62. * Converts a string to UTF-8 encoding.
  63. *
  64. * @param string $string
  65. * @return string
  66. * @since Method available since Release 3.2.19
  67. */
  68. public static function convertToUtf8($string)
  69. {
  70. if (!self::isUtf8($string)) {
  71. if (function_exists('mb_convert_encoding')) {
  72. $string = mb_convert_encoding($string, 'UTF-8');
  73. } else {
  74. $string = utf8_encode($string);
  75. }
  76. }
  77. return $string;
  78. }
  79. /**
  80. * Checks a string for UTF-8 encoding.
  81. *
  82. * @param string $string
  83. * @return boolean
  84. * @since Method available since Release 3.3.0
  85. */
  86. public static function isUtf8($string)
  87. {
  88. $length = strlen($string);
  89. for ($i = 0; $i < $length; $i++) {
  90. if (ord($string[$i]) < 0x80) $n = 0;
  91. elseif ((ord($string[$i]) & 0xE0) == 0xC0) $n = 1;
  92. elseif ((ord($string[$i]) & 0xF0) == 0xE0) $n = 2;
  93. elseif ((ord($string[$i]) & 0xF0) == 0xF0) $n = 3;
  94. else return FALSE;
  95. for ($j = 0; $j < $n; $j++) {
  96. if ((++$i == $length) || ((ord($string[$i]) & 0xC0) != 0x80)) return FALSE;
  97. }
  98. }
  99. return TRUE;
  100. }
  101. /**
  102. * Loads an XML (or HTML) file into a DOMDocument object.
  103. *
  104. * @param string $filename
  105. * @param boolean $isHtml
  106. * @return DOMDocument
  107. * @since Method available since Release 3.3.0
  108. */
  109. public static function loadFile($filename, $isHtml = FALSE)
  110. {
  111. $reporting = error_reporting(0);
  112. $contents = file_get_contents($filename);
  113. error_reporting($reporting);
  114. if ($contents === FALSE) {
  115. throw new RuntimeException(
  116. sprintf(
  117. 'Could not read "%s".',
  118. $filename
  119. )
  120. );
  121. }
  122. return self::load($contents, $isHtml, $filename);
  123. }
  124. /**
  125. * Load an $actual document into a DOMDocument. This is called
  126. * from the selector assertions.
  127. *
  128. * If $actual is already a DOMDocument, it is returned with
  129. * no changes. Otherwise, $actual is loaded into a new DOMDocument
  130. * as either HTML or XML, depending on the value of $isHtml.
  131. *
  132. * Note: prior to PHPUnit 3.3.0, this method loaded a file and
  133. * not a string as it currently does. To load a file into a
  134. * DOMDocument, use loadFile() instead.
  135. *
  136. * @param string|DOMDocument $actual
  137. * @param boolean $isHtml
  138. * @param string $filename
  139. * @return DOMDocument
  140. * @since Method available since Release 3.3.0
  141. * @author Mike Naberezny <mike@maintainable.com>
  142. * @author Derek DeVries <derek@maintainable.com>
  143. */
  144. public static function load($actual, $isHtml = FALSE, $filename = '')
  145. {
  146. if ($actual instanceof DOMDocument) {
  147. return $actual;
  148. }
  149. $internal = libxml_use_internal_errors(TRUE);
  150. $reporting = error_reporting(0);
  151. $dom = new DOMDocument;
  152. if ($isHtml) {
  153. $loaded = $dom->loadHTML($actual);
  154. } else {
  155. $loaded = $dom->loadXML($actual);
  156. }
  157. libxml_use_internal_errors($internal);
  158. error_reporting($reporting);
  159. if ($loaded === FALSE) {
  160. $message = '';
  161. foreach (libxml_get_errors() as $error) {
  162. $message .= $error->message;
  163. }
  164. if ($filename != '') {
  165. throw new RuntimeException(
  166. sprintf(
  167. 'Could not load "%s".%s',
  168. $filename,
  169. $message != '' ? "\n" . $message : ''
  170. )
  171. );
  172. } else {
  173. throw new RuntimeException($message);
  174. }
  175. }
  176. return $dom;
  177. }
  178. /**
  179. *
  180. *
  181. * @param DOMNode $node
  182. * @since Method available since Release 3.3.0
  183. * @author Mattis Stordalen Flister <mattis@xait.no>
  184. */
  185. public static function removeCharacterDataNodes(DOMNode $node)
  186. {
  187. if ($node->hasChildNodes()) {
  188. for ($i = $node->childNodes->length - 1; $i >= 0; $i--) {
  189. if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) {
  190. $node->removeChild($child);
  191. }
  192. }
  193. }
  194. }
  195. /**
  196. * Validate list of keys in the associative array.
  197. *
  198. * @param array $hash
  199. * @param array $validKeys
  200. * @return array
  201. * @throws InvalidArgumentException
  202. * @since Method available since Release 3.3.0
  203. * @author Mike Naberezny <mike@maintainable.com>
  204. * @author Derek DeVries <derek@maintainable.com>
  205. */
  206. public static function assertValidKeys(array $hash, array $validKeys)
  207. {
  208. $valids = array();
  209. // Normalize validation keys so that we can use both indexed and
  210. // associative arrays.
  211. foreach ($validKeys as $key => $val) {
  212. is_int($key) ? $valids[$val] = NULL : $valids[$key] = $val;
  213. }
  214. $validKeys = array_keys($valids);
  215. // Check for invalid keys.
  216. foreach ($hash as $key => $value) {
  217. if (!in_array($key, $validKeys)) {
  218. $unknown[] = $key;
  219. }
  220. }
  221. if (!empty($unknown)) {
  222. throw new InvalidArgumentException(
  223. 'Unknown key(s): ' . implode(', ', $unknown)
  224. );
  225. }
  226. // Add default values for any valid keys that are empty.
  227. foreach ($valids as $key => $value) {
  228. if (!isset($hash[$key])) {
  229. $hash[$key] = $value;
  230. }
  231. }
  232. return $hash;
  233. }
  234. /**
  235. * Parse a CSS selector into an associative array suitable for
  236. * use with findNodes().
  237. *
  238. * @param string $selector
  239. * @param mixed $content
  240. * @return array
  241. * @since Method available since Release 3.3.0
  242. * @author Mike Naberezny <mike@maintainable.com>
  243. * @author Derek DeVries <derek@maintainable.com>
  244. */
  245. public static function convertSelectToTag($selector, $content = TRUE)
  246. {
  247. $selector = trim(preg_replace("/\s+/", " ", $selector));
  248. // substitute spaces within attribute value
  249. while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) {
  250. $selector = preg_replace('/(\[[^\]]+"[^"]+)\s([^"]+"\])/', "$1__SPACE__$2", $selector);
  251. }
  252. $elements = strstr($selector, ' ') ? explode(' ', $selector) : array($selector);
  253. $previousTag = array();
  254. foreach (array_reverse($elements) as $element) {
  255. $element = str_replace('__SPACE__', ' ', $element);
  256. // child selector
  257. if ($element == '>') {
  258. $previousTag = array('child' => $previousTag['descendant']);
  259. continue;
  260. }
  261. $tag = array();
  262. // match element tag
  263. preg_match("/^([^\.#\[]*)/", $element, $eltMatches);
  264. if (!empty($eltMatches[1])) {
  265. $tag['tag'] = $eltMatches[1];
  266. }
  267. // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*), and classes (\.[^\.#\[]*))
  268. preg_match_all("/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/", $element, $matches);
  269. if (!empty($matches[1])) {
  270. $classes = array();
  271. $attrs = array();
  272. foreach ($matches[1] as $match) {
  273. // id matched
  274. if (substr($match, 0, 1) == '#') {
  275. $tag['id'] = substr($match, 1);
  276. }
  277. // class matched
  278. else if (substr($match, 0, 1) == '.') {
  279. $classes[] = substr($match, 1);
  280. }
  281. // attribute matched
  282. else if (substr($match, 0, 1) == '[' && substr($match, -1, 1) == ']') {
  283. $attribute = substr($match, 1, strlen($match) - 2);
  284. $attribute = str_replace('"', '', $attribute);
  285. // match single word
  286. if (strstr($attribute, '~=')) {
  287. list($key, $value) = explode('~=', $attribute);
  288. $value = "regexp:/.*\b$value\b.*/";
  289. }
  290. // match substring
  291. else if (strstr($attribute, '*=')) {
  292. list($key, $value) = explode('*=', $attribute);
  293. $value = "regexp:/.*$value.*/";
  294. }
  295. // exact match
  296. else {
  297. list($key, $value) = explode('=', $attribute);
  298. }
  299. $attrs[$key] = $value;
  300. }
  301. }
  302. if ($classes) {
  303. $tag['class'] = join(' ', $classes);
  304. }
  305. if ($attrs) {
  306. $tag['attributes'] = $attrs;
  307. }
  308. }
  309. // tag content
  310. if (is_string($content)) {
  311. $tag['content'] = $content;
  312. }
  313. // determine previous child/descendants
  314. if (!empty($previousTag['descendant'])) {
  315. $tag['descendant'] = $previousTag['descendant'];
  316. }
  317. else if (!empty($previousTag['child'])) {
  318. $tag['child'] = $previousTag['child'];
  319. }
  320. $previousTag = array('descendant' => $tag);
  321. }
  322. return $tag;
  323. }
  324. /**
  325. * Parse an $actual document and return an array of DOMNodes
  326. * matching the CSS $selector. If an error occurs, it will
  327. * return FALSE.
  328. *
  329. * To only return nodes containing a certain content, give
  330. * the $content to match as a string. Otherwise, setting
  331. * $content to TRUE will return all nodes matching $selector.
  332. *
  333. * The $actual document may be a DOMDocument or a string
  334. * containing XML or HTML, identified by $isHtml.
  335. *
  336. * @param array $selector
  337. * @param string $content
  338. * @param mixed $actual
  339. * @param boolean $isHtml
  340. * @return false|array
  341. * @since Method available since Release 3.3.0
  342. * @author Mike Naberezny <mike@maintainable.com>
  343. * @author Derek DeVries <derek@maintainable.com>
  344. */
  345. public static function cssSelect($selector, $content, $actual, $isHtml = TRUE)
  346. {
  347. $matcher = self::convertSelectToTag($selector, $content);
  348. $dom = self::load($actual, $isHtml);
  349. $tags = self::findNodes($dom, $matcher);
  350. return $tags;
  351. }
  352. /**
  353. * Parse out the options from the tag using DOM object tree.
  354. *
  355. * @param DOMDocument $dom
  356. * @param array $options
  357. * @return array
  358. * @since Method available since Release 3.3.0
  359. * @author Mike Naberezny <mike@maintainable.com>
  360. * @author Derek DeVries <derek@maintainable.com>
  361. */
  362. public static function findNodes(DOMDocument $dom, array $options)
  363. {
  364. $valid = array(
  365. 'id', 'class', 'tag', 'content', 'attributes', 'parent',
  366. 'child', 'ancestor', 'descendant', 'children'
  367. );
  368. $filtered = array();
  369. $options = self::assertValidKeys($options, $valid);
  370. // find the element by id
  371. if ($options['id']) {
  372. $options['attributes']['id'] = $options['id'];
  373. }
  374. if ($options['class']) {
  375. $options['attributes']['class'] = $options['class'];
  376. }
  377. // find the element by a tag type
  378. if ($options['tag']) {
  379. $elements = $dom->getElementsByTagName($options['tag']);
  380. foreach ($elements as $element) {
  381. $nodes[] = $element;
  382. }
  383. if (empty($nodes)) {
  384. return FALSE;
  385. }
  386. // no tag selected, get them all
  387. } else {
  388. $tags = array(
  389. 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo',
  390. 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite',
  391. 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl',
  392. 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2',
  393. 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe',
  394. 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link',
  395. 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup',
  396. 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select',
  397. 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table',
  398. 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title',
  399. 'tr', 'tt', 'ul', 'var'
  400. );
  401. foreach ($tags as $tag) {
  402. $elements = $dom->getElementsByTagName($tag);
  403. foreach ($elements as $element) {
  404. $nodes[] = $element;
  405. }
  406. }
  407. if (empty($nodes)) {
  408. return FALSE;
  409. }
  410. }
  411. // filter by attributes
  412. if ($options['attributes']) {
  413. foreach ($nodes as $node) {
  414. $invalid = FALSE;
  415. foreach ($options['attributes'] as $name => $value) {
  416. // match by regexp if like "regexp:/foo/i"
  417. if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) {
  418. if (!preg_match($matches[1], $node->getAttribute($name))) {
  419. $invalid = TRUE;
  420. }
  421. }
  422. // class can match only a part
  423. else if ($name == 'class') {
  424. // split to individual classes
  425. $findClasses = explode(' ', preg_replace("/\s+/", " ", $value));
  426. $allClasses = explode(' ', preg_replace("/\s+/", " ", $node->getAttribute($name)));
  427. // make sure each class given is in the actual node
  428. foreach ($findClasses as $findClass) {
  429. if (!in_array($findClass, $allClasses)) {
  430. $invalid = TRUE;
  431. }
  432. }
  433. }
  434. // match by exact string
  435. else {
  436. if ($node->getAttribute($name) != $value) {
  437. $invalid = TRUE;
  438. }
  439. }
  440. }
  441. // if every attribute given matched
  442. if (!$invalid) {
  443. $filtered[] = $node;
  444. }
  445. }
  446. $nodes = $filtered;
  447. $filtered = array();
  448. if (empty($nodes)) {
  449. return FALSE;
  450. }
  451. }
  452. // filter by content
  453. if ($options['content'] !== NULL) {
  454. foreach ($nodes as $node) {
  455. $invalid = FALSE;
  456. // match by regexp if like "regexp:/foo/i"
  457. if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) {
  458. if (!preg_match($matches[1], self::getNodeText($node))) {
  459. $invalid = TRUE;
  460. }
  461. }
  462. // match by exact string
  463. else if (strstr(self::getNodeText($node), $options['content']) === FALSE) {
  464. $invalid = TRUE;
  465. }
  466. if (!$invalid) {
  467. $filtered[] = $node;
  468. }
  469. }
  470. $nodes = $filtered;
  471. $filtered = array();
  472. if (empty($nodes)) {
  473. return FALSE;
  474. }
  475. }
  476. // filter by parent node
  477. if ($options['parent']) {
  478. $parentNodes = self::findNodes($dom, $options['parent']);
  479. $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : NULL;
  480. foreach ($nodes as $node) {
  481. if ($parentNode !== $node->parentNode) {
  482. break;
  483. }
  484. $filtered[] = $node;
  485. }
  486. $nodes = $filtered;
  487. $filtered = array();
  488. if (empty($nodes)) {
  489. return FALSE;
  490. }
  491. }
  492. // filter by child node
  493. if ($options['child']) {
  494. $childNodes = self::findNodes($dom, $options['child']);
  495. $childNodes = !empty($childNodes) ? $childNodes : array();
  496. foreach ($nodes as $node) {
  497. foreach ($node->childNodes as $child) {
  498. foreach ($childNodes as $childNode) {
  499. if ($childNode === $child) {
  500. $filtered[] = $node;
  501. }
  502. }
  503. }
  504. }
  505. $nodes = $filtered;
  506. $filtered = array();
  507. if (empty($nodes)) {
  508. return FALSE;
  509. }
  510. }
  511. // filter by ancestor
  512. if ($options['ancestor']) {
  513. $ancestorNodes = self::findNodes($dom, $options['ancestor']);
  514. $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : NULL;
  515. foreach ($nodes as $node) {
  516. $parent = $node->parentNode;
  517. while ($parent->nodeType != XML_HTML_DOCUMENT_NODE) {
  518. if ($parent === $ancestorNode) {
  519. $filtered[] = $node;
  520. }
  521. $parent = $parent->parentNode;
  522. }
  523. }
  524. $nodes = $filtered;
  525. $filtered = array();
  526. if (empty($nodes)) {
  527. return FALSE;
  528. }
  529. }
  530. // filter by descendant
  531. if ($options['descendant']) {
  532. $descendantNodes = self::findNodes($dom, $options['descendant']);
  533. $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array();
  534. foreach ($nodes as $node) {
  535. foreach (self::getDescendants($node) as $descendant) {
  536. foreach ($descendantNodes as $descendantNode) {
  537. if ($descendantNode === $descendant) {
  538. $filtered[] = $node;
  539. }
  540. }
  541. }
  542. }
  543. $nodes = $filtered;
  544. $filtered = array();
  545. if (empty($nodes)) {
  546. return FALSE;
  547. }
  548. }
  549. // filter by children
  550. if ($options['children']) {
  551. $validChild = array('count', 'greater_than', 'less_than', 'only');
  552. $childOptions = self::assertValidKeys($options['children'], $validChild);
  553. foreach ($nodes as $node) {
  554. $childNodes = $node->childNodes;
  555. foreach ($childNodes as $childNode) {
  556. if ($childNode->nodeType !== XML_CDATA_SECTION_NODE &&
  557. $childNode->nodeType !== XML_TEXT_NODE) {
  558. $children[] = $childNode;
  559. }
  560. }
  561. // we must have children to pass this filter
  562. if (!empty($children)) {
  563. // exact count of children
  564. if ($childOptions['count'] !== NULL) {
  565. if (count($children) !== $childOptions['count']) {
  566. break;
  567. }
  568. }
  569. // range count of children
  570. else if ($childOptions['less_than'] !== NULL &&
  571. $childOptions['greater_than'] !== NULL) {
  572. if (count($children) >= $childOptions['less_than'] ||
  573. count($children) <= $childOptions['greater_than']) {
  574. break;
  575. }
  576. }
  577. // less than a given count
  578. else if ($childOptions['less_than'] !== NULL) {
  579. if (count($children) >= $childOptions['less_than']) {
  580. break;
  581. }
  582. }
  583. // more than a given count
  584. else if ($childOptions['greater_than'] !== NULL) {
  585. if (count($children) <= $childOptions['greater_than']) {
  586. break;
  587. }
  588. }
  589. // match each child against a specific tag
  590. if ($childOptions['only']) {
  591. $onlyNodes = self::findNodes($dom, $childOptions['only']);
  592. // try to match each child to one of the 'only' nodes
  593. foreach ($children as $child) {
  594. $matched = FALSE;
  595. foreach ($onlyNodes as $onlyNode) {
  596. if ($onlyNode === $child) {
  597. $matched = TRUE;
  598. }
  599. }
  600. if (!$matched) {
  601. break(2);
  602. }
  603. }
  604. }
  605. $filtered[] = $node;
  606. }
  607. }
  608. $nodes = $filtered;
  609. $filtered = array();
  610. if (empty($nodes)) {
  611. return;
  612. }
  613. }
  614. // return the first node that matches all criteria
  615. return !empty($nodes) ? $nodes : array();
  616. }
  617. /**
  618. * Recursively get flat array of all descendants of this node.
  619. *
  620. * @param DOMNode $node
  621. * @return array
  622. * @since Method available since Release 3.3.0
  623. * @author Mike Naberezny <mike@maintainable.com>
  624. * @author Derek DeVries <derek@maintainable.com>
  625. */
  626. protected static function getDescendants(DOMNode $node)
  627. {
  628. $allChildren = array();
  629. $childNodes = $node->childNodes ? $node->childNodes : array();
  630. foreach ($childNodes as $child) {
  631. if ($child->nodeType === XML_CDATA_SECTION_NODE ||
  632. $child->nodeType === XML_TEXT_NODE) {
  633. continue;
  634. }
  635. $children = self::getDescendants($child);
  636. $allChildren = array_merge($allChildren, $children, array($child));
  637. }
  638. return isset($allChildren) ? $allChildren : array();
  639. }
  640. /**
  641. * Get the text value of this node's child text node.
  642. *
  643. * @param DOMNode $node
  644. * @return string
  645. * @since Method available since Release 3.3.0
  646. * @author Mike Naberezny <mike@maintainable.com>
  647. * @author Derek DeVries <derek@maintainable.com>
  648. */
  649. protected static function getNodeText(DOMNode $node)
  650. {
  651. $childNodes = $node->childNodes instanceof DOMNodeList ? $node->childNodes : array();
  652. $text = '';
  653. foreach ($childNodes as $child) {
  654. if ($child->nodeType === XML_TEXT_NODE) {
  655. $text .= trim($child->data).' ';
  656. } else {
  657. $text .= self::getNodeText($child);
  658. }
  659. }
  660. return str_replace(' ', ' ', $text);
  661. }
  662. }
  663. ?>