parser.php 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. <?php
  2. /**
  3. * base include file for SimpleTest
  4. * @package SimpleTest
  5. * @subpackage MockObjects
  6. * @version $Id: parser.php 1786 2008-04-26 17:32:20Z pp11 $
  7. */
  8. /**#@+
  9. * Lexer mode stack constants
  10. */
  11. foreach (array('LEXER_ENTER', 'LEXER_MATCHED',
  12. 'LEXER_UNMATCHED', 'LEXER_EXIT',
  13. 'LEXER_SPECIAL') as $i => $constant) {
  14. if (! defined($constant)) {
  15. define($constant, $i + 1);
  16. }
  17. }
  18. /**#@-*/
  19. /**
  20. * Compounded regular expression. Any of
  21. * the contained patterns could match and
  22. * when one does, it's label is returned.
  23. * @package SimpleTest
  24. * @subpackage WebTester
  25. */
  26. class ParallelRegex {
  27. private $patterns;
  28. private $labels;
  29. private $regex;
  30. private $case;
  31. /**
  32. * Constructor. Starts with no patterns.
  33. * @param boolean $case True for case sensitive, false
  34. * for insensitive.
  35. * @access public
  36. */
  37. function __construct($case) {
  38. $this->case = $case;
  39. $this->patterns = array();
  40. $this->labels = array();
  41. $this->regex = null;
  42. }
  43. /**
  44. * Adds a pattern with an optional label.
  45. * @param string $pattern Perl style regex, but ( and )
  46. * lose the usual meaning.
  47. * @param string $label Label of regex to be returned
  48. * on a match.
  49. * @access public
  50. */
  51. function addPattern($pattern, $label = true) {
  52. $count = count($this->patterns);
  53. $this->patterns[$count] = $pattern;
  54. $this->labels[$count] = $label;
  55. $this->regex = null;
  56. }
  57. /**
  58. * Attempts to match all patterns at once against
  59. * a string.
  60. * @param string $subject String to match against.
  61. * @param string $match First matched portion of
  62. * subject.
  63. * @return boolean True on success.
  64. * @access public
  65. */
  66. function match($subject, &$match) {
  67. if (count($this->patterns) == 0) {
  68. return false;
  69. }
  70. if (! preg_match($this->getCompoundedRegex(), $subject, $matches)) {
  71. $match = '';
  72. return false;
  73. }
  74. $match = $matches[0];
  75. for ($i = 1; $i < count($matches); $i++) {
  76. if ($matches[$i]) {
  77. return $this->labels[$i - 1];
  78. }
  79. }
  80. return true;
  81. }
  82. /**
  83. * Compounds the patterns into a single
  84. * regular expression separated with the
  85. * "or" operator. Caches the regex.
  86. * Will automatically escape (, ) and / tokens.
  87. * @param array $patterns List of patterns in order.
  88. * @access private
  89. */
  90. protected function getCompoundedRegex() {
  91. if ($this->regex == null) {
  92. for ($i = 0, $count = count($this->patterns); $i < $count; $i++) {
  93. $this->patterns[$i] = '(' . str_replace(
  94. array('/', '(', ')'),
  95. array('\/', '\(', '\)'),
  96. $this->patterns[$i]) . ')';
  97. }
  98. $this->regex = "/" . implode("|", $this->patterns) . "/" . $this->getPerlMatchingFlags();
  99. }
  100. return $this->regex;
  101. }
  102. /**
  103. * Accessor for perl regex mode flags to use.
  104. * @return string Perl regex flags.
  105. * @access private
  106. */
  107. protected function getPerlMatchingFlags() {
  108. return ($this->case ? "msS" : "msSi");
  109. }
  110. }
  111. /**
  112. * States for a stack machine.
  113. * @package SimpleTest
  114. * @subpackage WebTester
  115. */
  116. class SimpleStateStack {
  117. private $stack;
  118. /**
  119. * Constructor. Starts in named state.
  120. * @param string $start Starting state name.
  121. * @access public
  122. */
  123. function __construct($start) {
  124. $this->stack = array($start);
  125. }
  126. /**
  127. * Accessor for current state.
  128. * @return string State.
  129. * @access public
  130. */
  131. function getCurrent() {
  132. return $this->stack[count($this->stack) - 1];
  133. }
  134. /**
  135. * Adds a state to the stack and sets it
  136. * to be the current state.
  137. * @param string $state New state.
  138. * @access public
  139. */
  140. function enter($state) {
  141. array_push($this->stack, $state);
  142. }
  143. /**
  144. * Leaves the current state and reverts
  145. * to the previous one.
  146. * @return boolean False if we drop off
  147. * the bottom of the list.
  148. * @access public
  149. */
  150. function leave() {
  151. if (count($this->stack) == 1) {
  152. return false;
  153. }
  154. array_pop($this->stack);
  155. return true;
  156. }
  157. }
  158. /**
  159. * Accepts text and breaks it into tokens.
  160. * Some optimisation to make the sure the
  161. * content is only scanned by the PHP regex
  162. * parser once. Lexer modes must not start
  163. * with leading underscores.
  164. * @package SimpleTest
  165. * @subpackage WebTester
  166. */
  167. class SimpleLexer {
  168. private $regexes;
  169. private $parser;
  170. private $mode;
  171. private $mode_handlers;
  172. private $case;
  173. /**
  174. * Sets up the lexer in case insensitive matching
  175. * by default.
  176. * @param SimpleSaxParser $parser Handling strategy by
  177. * reference.
  178. * @param string $start Starting handler.
  179. * @param boolean $case True for case sensitive.
  180. * @access public
  181. */
  182. function __construct($parser, $start = "accept", $case = false) {
  183. $this->case = $case;
  184. $this->regexes = array();
  185. $this->parser = $parser;
  186. $this->mode = new SimpleStateStack($start);
  187. $this->mode_handlers = array($start => $start);
  188. }
  189. /**
  190. * Adds a token search pattern for a particular
  191. * parsing mode. The pattern does not change the
  192. * current mode.
  193. * @param string $pattern Perl style regex, but ( and )
  194. * lose the usual meaning.
  195. * @param string $mode Should only apply this
  196. * pattern when dealing with
  197. * this type of input.
  198. * @access public
  199. */
  200. function addPattern($pattern, $mode = "accept") {
  201. if (! isset($this->regexes[$mode])) {
  202. $this->regexes[$mode] = new ParallelRegex($this->case);
  203. }
  204. $this->regexes[$mode]->addPattern($pattern);
  205. if (! isset($this->mode_handlers[$mode])) {
  206. $this->mode_handlers[$mode] = $mode;
  207. }
  208. }
  209. /**
  210. * Adds a pattern that will enter a new parsing
  211. * mode. Useful for entering parenthesis, strings,
  212. * tags, etc.
  213. * @param string $pattern Perl style regex, but ( and )
  214. * lose the usual meaning.
  215. * @param string $mode Should only apply this
  216. * pattern when dealing with
  217. * this type of input.
  218. * @param string $new_mode Change parsing to this new
  219. * nested mode.
  220. * @access public
  221. */
  222. function addEntryPattern($pattern, $mode, $new_mode) {
  223. if (! isset($this->regexes[$mode])) {
  224. $this->regexes[$mode] = new ParallelRegex($this->case);
  225. }
  226. $this->regexes[$mode]->addPattern($pattern, $new_mode);
  227. if (! isset($this->mode_handlers[$new_mode])) {
  228. $this->mode_handlers[$new_mode] = $new_mode;
  229. }
  230. }
  231. /**
  232. * Adds a pattern that will exit the current mode
  233. * and re-enter the previous one.
  234. * @param string $pattern Perl style regex, but ( and )
  235. * lose the usual meaning.
  236. * @param string $mode Mode to leave.
  237. * @access public
  238. */
  239. function addExitPattern($pattern, $mode) {
  240. if (! isset($this->regexes[$mode])) {
  241. $this->regexes[$mode] = new ParallelRegex($this->case);
  242. }
  243. $this->regexes[$mode]->addPattern($pattern, "__exit");
  244. if (! isset($this->mode_handlers[$mode])) {
  245. $this->mode_handlers[$mode] = $mode;
  246. }
  247. }
  248. /**
  249. * Adds a pattern that has a special mode. Acts as an entry
  250. * and exit pattern in one go, effectively calling a special
  251. * parser handler for this token only.
  252. * @param string $pattern Perl style regex, but ( and )
  253. * lose the usual meaning.
  254. * @param string $mode Should only apply this
  255. * pattern when dealing with
  256. * this type of input.
  257. * @param string $special Use this mode for this one token.
  258. * @access public
  259. */
  260. function addSpecialPattern($pattern, $mode, $special) {
  261. if (! isset($this->regexes[$mode])) {
  262. $this->regexes[$mode] = new ParallelRegex($this->case);
  263. }
  264. $this->regexes[$mode]->addPattern($pattern, "_$special");
  265. if (! isset($this->mode_handlers[$special])) {
  266. $this->mode_handlers[$special] = $special;
  267. }
  268. }
  269. /**
  270. * Adds a mapping from a mode to another handler.
  271. * @param string $mode Mode to be remapped.
  272. * @param string $handler New target handler.
  273. * @access public
  274. */
  275. function mapHandler($mode, $handler) {
  276. $this->mode_handlers[$mode] = $handler;
  277. }
  278. /**
  279. * Splits the page text into tokens. Will fail
  280. * if the handlers report an error or if no
  281. * content is consumed. If successful then each
  282. * unparsed and parsed token invokes a call to the
  283. * held listener.
  284. * @param string $raw Raw HTML text.
  285. * @return boolean True on success, else false.
  286. * @access public
  287. */
  288. function parse($raw) {
  289. if (! isset($this->parser)) {
  290. return false;
  291. }
  292. $length = strlen($raw);
  293. while (is_array($parsed = $this->reduce($raw))) {
  294. list($raw, $unmatched, $matched, $mode) = $parsed;
  295. if (! $this->dispatchTokens($unmatched, $matched, $mode)) {
  296. return false;
  297. }
  298. if ($raw === '') {
  299. return true;
  300. }
  301. if (strlen($raw) == $length) {
  302. return false;
  303. }
  304. $length = strlen($raw);
  305. }
  306. if (! $parsed) {
  307. return false;
  308. }
  309. return $this->invokeParser($raw, LEXER_UNMATCHED);
  310. }
  311. /**
  312. * Sends the matched token and any leading unmatched
  313. * text to the parser changing the lexer to a new
  314. * mode if one is listed.
  315. * @param string $unmatched Unmatched leading portion.
  316. * @param string $matched Actual token match.
  317. * @param string $mode Mode after match. A boolean
  318. * false mode causes no change.
  319. * @return boolean False if there was any error
  320. * from the parser.
  321. * @access private
  322. */
  323. protected function dispatchTokens($unmatched, $matched, $mode = false) {
  324. if (! $this->invokeParser($unmatched, LEXER_UNMATCHED)) {
  325. return false;
  326. }
  327. if (is_bool($mode)) {
  328. return $this->invokeParser($matched, LEXER_MATCHED);
  329. }
  330. if ($this->isModeEnd($mode)) {
  331. if (! $this->invokeParser($matched, LEXER_EXIT)) {
  332. return false;
  333. }
  334. return $this->mode->leave();
  335. }
  336. if ($this->isSpecialMode($mode)) {
  337. $this->mode->enter($this->decodeSpecial($mode));
  338. if (! $this->invokeParser($matched, LEXER_SPECIAL)) {
  339. return false;
  340. }
  341. return $this->mode->leave();
  342. }
  343. $this->mode->enter($mode);
  344. return $this->invokeParser($matched, LEXER_ENTER);
  345. }
  346. /**
  347. * Tests to see if the new mode is actually to leave
  348. * the current mode and pop an item from the matching
  349. * mode stack.
  350. * @param string $mode Mode to test.
  351. * @return boolean True if this is the exit mode.
  352. * @access private
  353. */
  354. protected function isModeEnd($mode) {
  355. return ($mode === "__exit");
  356. }
  357. /**
  358. * Test to see if the mode is one where this mode
  359. * is entered for this token only and automatically
  360. * leaves immediately afterwoods.
  361. * @param string $mode Mode to test.
  362. * @return boolean True if this is the exit mode.
  363. * @access private
  364. */
  365. protected function isSpecialMode($mode) {
  366. return (strncmp($mode, "_", 1) == 0);
  367. }
  368. /**
  369. * Strips the magic underscore marking single token
  370. * modes.
  371. * @param string $mode Mode to decode.
  372. * @return string Underlying mode name.
  373. * @access private
  374. */
  375. protected function decodeSpecial($mode) {
  376. return substr($mode, 1);
  377. }
  378. /**
  379. * Calls the parser method named after the current
  380. * mode. Empty content will be ignored. The lexer
  381. * has a parser handler for each mode in the lexer.
  382. * @param string $content Text parsed.
  383. * @param boolean $is_match Token is recognised rather
  384. * than unparsed data.
  385. * @access private
  386. */
  387. protected function invokeParser($content, $is_match) {
  388. if (($content === '') || ($content === false)) {
  389. return true;
  390. }
  391. $handler = $this->mode_handlers[$this->mode->getCurrent()];
  392. return $this->parser->$handler($content, $is_match);
  393. }
  394. /**
  395. * Tries to match a chunk of text and if successful
  396. * removes the recognised chunk and any leading
  397. * unparsed data. Empty strings will not be matched.
  398. * @param string $raw The subject to parse. This is the
  399. * content that will be eaten.
  400. * @return array/boolean Three item list of unparsed
  401. * content followed by the
  402. * recognised token and finally the
  403. * action the parser is to take.
  404. * True if no match, false if there
  405. * is a parsing error.
  406. * @access private
  407. */
  408. protected function reduce($raw) {
  409. if ($action = $this->regexes[$this->mode->getCurrent()]->match($raw, $match)) {
  410. $unparsed_character_count = strpos($raw, $match);
  411. $unparsed = substr($raw, 0, $unparsed_character_count);
  412. $raw = substr($raw, $unparsed_character_count + strlen($match));
  413. return array($raw, $unparsed, $match, $action);
  414. }
  415. return true;
  416. }
  417. }
  418. /**
  419. * Breaks HTML into SAX events.
  420. * @package SimpleTest
  421. * @subpackage WebTester
  422. */
  423. class SimpleHtmlLexer extends SimpleLexer {
  424. /**
  425. * Sets up the lexer with case insensitive matching
  426. * and adds the HTML handlers.
  427. * @param SimpleSaxParser $parser Handling strategy by
  428. * reference.
  429. * @access public
  430. */
  431. function __construct($parser) {
  432. parent::__construct($parser, 'text');
  433. $this->mapHandler('text', 'acceptTextToken');
  434. $this->addSkipping();
  435. foreach ($this->getParsedTags() as $tag) {
  436. $this->addTag($tag);
  437. }
  438. $this->addInTagTokens();
  439. }
  440. /**
  441. * List of parsed tags. Others are ignored.
  442. * @return array List of searched for tags.
  443. * @access private
  444. */
  445. protected function getParsedTags() {
  446. return array('a', 'base', 'title', 'form', 'input', 'button', 'textarea', 'select',
  447. 'option', 'frameset', 'frame', 'label');
  448. }
  449. /**
  450. * The lexer has to skip certain sections such
  451. * as server code, client code and styles.
  452. * @access private
  453. */
  454. protected function addSkipping() {
  455. $this->mapHandler('css', 'ignore');
  456. $this->addEntryPattern('<style', 'text', 'css');
  457. $this->addExitPattern('</style>', 'css');
  458. $this->mapHandler('js', 'ignore');
  459. $this->addEntryPattern('<script', 'text', 'js');
  460. $this->addExitPattern('</script>', 'js');
  461. $this->mapHandler('comment', 'ignore');
  462. $this->addEntryPattern('<!--', 'text', 'comment');
  463. $this->addExitPattern('-->', 'comment');
  464. }
  465. /**
  466. * Pattern matches to start and end a tag.
  467. * @param string $tag Name of tag to scan for.
  468. * @access private
  469. */
  470. protected function addTag($tag) {
  471. $this->addSpecialPattern("</$tag>", 'text', 'acceptEndToken');
  472. $this->addEntryPattern("<$tag", 'text', 'tag');
  473. }
  474. /**
  475. * Pattern matches to parse the inside of a tag
  476. * including the attributes and their quoting.
  477. * @access private
  478. */
  479. protected function addInTagTokens() {
  480. $this->mapHandler('tag', 'acceptStartToken');
  481. $this->addSpecialPattern('\s+', 'tag', 'ignore');
  482. $this->addAttributeTokens();
  483. $this->addExitPattern('/>', 'tag');
  484. $this->addExitPattern('>', 'tag');
  485. }
  486. /**
  487. * Matches attributes that are either single quoted,
  488. * double quoted or unquoted.
  489. * @access private
  490. */
  491. protected function addAttributeTokens() {
  492. $this->mapHandler('dq_attribute', 'acceptAttributeToken');
  493. $this->addEntryPattern('=\s*"', 'tag', 'dq_attribute');
  494. $this->addPattern("\\\\\"", 'dq_attribute');
  495. $this->addExitPattern('"', 'dq_attribute');
  496. $this->mapHandler('sq_attribute', 'acceptAttributeToken');
  497. $this->addEntryPattern("=\s*'", 'tag', 'sq_attribute');
  498. $this->addPattern("\\\\'", 'sq_attribute');
  499. $this->addExitPattern("'", 'sq_attribute');
  500. $this->mapHandler('uq_attribute', 'acceptAttributeToken');
  501. $this->addSpecialPattern('=\s*[^>\s]*', 'tag', 'uq_attribute');
  502. }
  503. }
  504. /**
  505. * Converts HTML tokens into selected SAX events.
  506. * @package SimpleTest
  507. * @subpackage WebTester
  508. */
  509. class SimpleHtmlSaxParser {
  510. private $lexer;
  511. private $listener;
  512. private $tag;
  513. private $attributes;
  514. private $current_attribute;
  515. /**
  516. * Sets the listener.
  517. * @param SimpleSaxListener $listener SAX event handler.
  518. * @access public
  519. */
  520. function __construct($listener) {
  521. $this->listener = $listener;
  522. $this->lexer = $this->createLexer($this);
  523. $this->tag = '';
  524. $this->attributes = array();
  525. $this->current_attribute = '';
  526. }
  527. /**
  528. * Runs the content through the lexer which
  529. * should call back to the acceptors.
  530. * @param string $raw Page text to parse.
  531. * @return boolean False if parse error.
  532. * @access public
  533. */
  534. function parse($raw) {
  535. return $this->lexer->parse($raw);
  536. }
  537. /**
  538. * Sets up the matching lexer. Starts in 'text' mode.
  539. * @param SimpleSaxParser $parser Event generator, usually $self.
  540. * @return SimpleLexer Lexer suitable for this parser.
  541. * @access public
  542. */
  543. static function createLexer(&$parser) {
  544. return new SimpleHtmlLexer($parser);
  545. }
  546. /**
  547. * Accepts a token from the tag mode. If the
  548. * starting element completes then the element
  549. * is dispatched and the current attributes
  550. * set back to empty. The element or attribute
  551. * name is converted to lower case.
  552. * @param string $token Incoming characters.
  553. * @param integer $event Lexer event type.
  554. * @return boolean False if parse error.
  555. * @access public
  556. */
  557. function acceptStartToken($token, $event) {
  558. if ($event == LEXER_ENTER) {
  559. $this->tag = strtolower(substr($token, 1));
  560. return true;
  561. }
  562. if ($event == LEXER_EXIT) {
  563. $success = $this->listener->startElement(
  564. $this->tag,
  565. $this->attributes);
  566. $this->tag = '';
  567. $this->attributes = array();
  568. return $success;
  569. }
  570. if ($token != '=') {
  571. $this->current_attribute = strtolower(SimpleHtmlSaxParser::decodeHtml($token));
  572. $this->attributes[$this->current_attribute] = '';
  573. }
  574. return true;
  575. }
  576. /**
  577. * Accepts a token from the end tag mode.
  578. * The element name is converted to lower case.
  579. * @param string $token Incoming characters.
  580. * @param integer $event Lexer event type.
  581. * @return boolean False if parse error.
  582. * @access public
  583. */
  584. function acceptEndToken($token, $event) {
  585. if (! preg_match('/<\/(.*)>/', $token, $matches)) {
  586. return false;
  587. }
  588. return $this->listener->endElement(strtolower($matches[1]));
  589. }
  590. /**
  591. * Part of the tag data.
  592. * @param string $token Incoming characters.
  593. * @param integer $event Lexer event type.
  594. * @return boolean False if parse error.
  595. * @access public
  596. */
  597. function acceptAttributeToken($token, $event) {
  598. if ($this->current_attribute) {
  599. if ($event == LEXER_UNMATCHED) {
  600. $this->attributes[$this->current_attribute] .=
  601. SimpleHtmlSaxParser::decodeHtml($token);
  602. }
  603. if ($event == LEXER_SPECIAL) {
  604. $this->attributes[$this->current_attribute] .=
  605. preg_replace('/^=\s*/' , '', SimpleHtmlSaxParser::decodeHtml($token));
  606. }
  607. }
  608. return true;
  609. }
  610. /**
  611. * A character entity.
  612. * @param string $token Incoming characters.
  613. * @param integer $event Lexer event type.
  614. * @return boolean False if parse error.
  615. * @access public
  616. */
  617. function acceptEntityToken($token, $event) {
  618. }
  619. /**
  620. * Character data between tags regarded as
  621. * important.
  622. * @param string $token Incoming characters.
  623. * @param integer $event Lexer event type.
  624. * @return boolean False if parse error.
  625. * @access public
  626. */
  627. function acceptTextToken($token, $event) {
  628. return $this->listener->addContent($token);
  629. }
  630. /**
  631. * Incoming data to be ignored.
  632. * @param string $token Incoming characters.
  633. * @param integer $event Lexer event type.
  634. * @return boolean False if parse error.
  635. * @access public
  636. */
  637. function ignore($token, $event) {
  638. return true;
  639. }
  640. /**
  641. * Decodes any HTML entities.
  642. * @param string $html Incoming HTML.
  643. * @return string Outgoing plain text.
  644. * @access public
  645. */
  646. static function decodeHtml($html) {
  647. return html_entity_decode($html, ENT_QUOTES);
  648. }
  649. /**
  650. * Turns HTML into text browser visible text. Images
  651. * are converted to their alt text and tags are supressed.
  652. * Entities are converted to their visible representation.
  653. * @param string $html HTML to convert.
  654. * @return string Plain text.
  655. * @access public
  656. */
  657. static function normalise($html) {
  658. $text = preg_replace('|<!--.*?-->|', '', $html);
  659. $text = preg_replace('|<script[^>]*>.*?</script>|', '', $text);
  660. $text = preg_replace('|<img[^>]*alt\s*=\s*"([^"]*)"[^>]*>|', ' \1 ', $text);
  661. $text = preg_replace('|<img[^>]*alt\s*=\s*\'([^\']*)\'[^>]*>|', ' \1 ', $text);
  662. $text = preg_replace('|<img[^>]*alt\s*=\s*([a-zA-Z_]+)[^>]*>|', ' \1 ', $text);
  663. $text = preg_replace('|<[^>]*>|', '', $text);
  664. $text = SimpleHtmlSaxParser::decodeHtml($text);
  665. $text = preg_replace('|\s+|', ' ', $text);
  666. return trim(trim($text), "\xA0"); // TODO: The \xAO is a &nbsp;. Add a test for this.
  667. }
  668. }
  669. /**
  670. * SAX event handler.
  671. * @package SimpleTest
  672. * @subpackage WebTester
  673. * @abstract
  674. */
  675. class SimpleSaxListener {
  676. /**
  677. * Sets the document to write to.
  678. * @access public
  679. */
  680. function __construct() {
  681. }
  682. /**
  683. * Start of element event.
  684. * @param string $name Element name.
  685. * @param hash $attributes Name value pairs.
  686. * Attributes without content
  687. * are marked as true.
  688. * @return boolean False on parse error.
  689. * @access public
  690. */
  691. function startElement($name, $attributes) {
  692. }
  693. /**
  694. * End of element event.
  695. * @param string $name Element name.
  696. * @return boolean False on parse error.
  697. * @access public
  698. */
  699. function endElement($name) {
  700. }
  701. /**
  702. * Unparsed, but relevant data.
  703. * @param string $text May include unparsed tags.
  704. * @return boolean False on parse error.
  705. * @access public
  706. */
  707. function addContent($text) {
  708. }
  709. }
  710. ?>