File Source for stringparser.class.php

# Source for file stringparser.class.php

Documentation is available at stringparser.class.php

1. <?php
2. /**
3. * Generic string parsing infrastructure
4. *
5. * These classes provide the means to parse any kind of string into a tree-like
6. * memory structure. It would e.g. be possible to create an HTML parser based
7. * upon this class.
8. *
9. * Version: 0.3.2
10. *
11. * @author Christian Seiler <spam@christian-seiler.de>
12. * @copyright Christian Seiler 2006
13. * @package stringparser
14. *
15. * The MIT License
16. *
17. * Copyright (c) 2004-2007 Christian Seiler
18. *
19. * Permission is hereby granted, free of charge, to any person obtaining a copy
20. * of this software and associated documentation files (the "Software"), to deal
21. * in the Software without restriction, including without limitation the rights
22. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23. * copies of the Software, and to permit persons to whom the Software is
24. * furnished to do so, subject to the following conditions:
25. *
26. * The above copyright notice and this permission notice shall be included in
27. * all copies or substantial portions of the Software.
28. *
29. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35. * THE SOFTWARE.
36. */
38. /**
39. * String parser mode: Search for the next character
40. * @see StringParser::_parserMode
41. */
42. define ('STRINGPARSER_MODE_SEARCH', 1);
43. /**
44. * String parser mode: Look at each character of the string
45. * @see StringParser::_parserMode
46. */
47. define ('STRINGPARSER_MODE_LOOP', 2);
48. /**
49. * Filter type: Prefilter
50. * @see StringParser::addFilter, StringParser::_prefilters
51. */
52. define ('STRINGPARSER_FILTER_PRE', 1);
53. /**
54. * Filter type: Postfilter
55. * @see StringParser::addFilter, StringParser::_postfilters
56. */
57. define ('STRINGPARSER_FILTER_POST', 2);
59. /**
60. * Generic string parser class
61. *
62. * This is an abstract class for any type of string parser.
63. *
64. * @package stringparser
65. */
66. class StringParser {
67. /**
68. * String parser mode
69. *
70. * There are two possible modes: searchmode and loop mode. In loop mode
71. * every single character is looked at in a loop and it is then decided
72. * what action to take. This is the most straight-forward approach to
73. * string parsing but due to the nature of PHP as a scripting language,
74. * it can also cost performance. In search mode the class posseses a
75. * list of relevant characters for parsing and uses the
76. * {@link PHP_MANUAL#strpos strpos} function to search for the next
77. * relevant character. The search mode will be faster than the loop mode
78. * in most circumstances but it is also more difficult to implement.
79. * The subclass that does the string parsing itself will define which
80. * mode it will implement.
81. *
82. * @access protected
83. * @var int
84. * @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
85. */
86. var $_parserMode = STRINGPARSER_MODE_SEARCH;
88. /**
89. * Raw text
90. * @access protected
91. * @var string
92. */
93. var $_text = '';
95. /**
96. * Parse stack
97. * @access protected
98. * @var array
99. */
100. var $_stack = array ();
102. /**
103. * Current position in raw text
104. * @access protected
105. * @var integer
106. */
107. var $_cpos = -1;
109. /**
110. * Root node
111. * @access protected
112. * @var mixed
113. */
114. var $_root = null;
116. /**
117. * Length of the text
118. * @access protected
119. * @var integer
120. */
121. var $_length = -1;
123. /**
124. * Flag if this object is already parsing a text
125. *
126. * This flag is to prevent recursive calls to the parse() function that
127. * would cause very nasty things.
128. *
129. * @access protected
130. * @var boolean
131. */
132. var $_parsing = false;
134. /**
135. * Strict mode
136. *
137. * Whether to stop parsing if a parse error occurs.
138. *
139. * @access public
140. * @var boolean
141. */
142. var $strict = false;
144. /**
145. * Characters or strings to look for
146. * @access protected
147. * @var array
148. */
149. var $_charactersSearch = array ();
151. /**
152. * Characters currently allowed
153. *
154. * Note that this will only be evaluated in loop mode; in search mode
155. * this would ruin every performance increase. Note that only single
156. * characters are permitted here, no strings. Please also note that in
157. * loop mode, {@link StringParser::_charactersSearch _charactersSearch}
158. * is evaluated before this variable.
159. *
160. * If in strict mode, parsing is stopped if a character that is not
161. * allowed is encountered. If not in strict mode, the character is
162. * simply ignored.
163. *
164. * @access protected
165. * @var array
166. */
167. var $_charactersAllowed = array ();
169. /**
170. * Current parser status
171. * @access protected
172. * @var int
173. */
174. var $_status = 0;
176. /**
177. * Prefilters
178. * @access protected
179. * @var array
180. */
181. var $_prefilters = array ();
183. /**
184. * Postfilters
185. * @access protected
186. * @var array
187. */
188. var $_postfilters = array ();
190. /**
191. * Recently reparsed?
192. * @access protected
193. * @var bool
194. */
195. var $_recentlyReparsed = false;
197. /**
198. * Constructor
199. *
200. * @access public
201. */
202. function StringParser () {
203. }
205. /**
206. * Add a filter
207. *
208. * @access public
209. * @param int $type The type of the filter
210. * @param mixed $callback The callback to call
211. * @return bool
212. * @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
213. */
214. function addFilter ($type, $callback) {
215. // make sure the function is callable
216. if (!is_callable ($callback)) {
217. return false;
218. }
220. switch ($type) {
221. case STRINGPARSER_FILTER_PRE:
222. $this->_prefilters[] = $callback;
223. break;
224. case STRINGPARSER_FILTER_POST:
225. $this->_postfilters[] = $callback;
226. break;
227. default:
228. return false;
229. }
231. return true;
232. }
234. /**
235. * Remove all filters
236. *
237. * @access public
238. * @param int $type The type of the filter or 0 for all
239. * @return bool
240. * @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
241. */
242. function clearFilters ($type = 0) {
243. switch ($type) {
244. case 0:
245. $this->_prefilters = array ();
246. $this->_postfilters = array ();
247. break;
248. case STRINGPARSER_FILTER_PRE:
249. $this->_prefilters = array ();
250. break;
251. case STRINGPARSER_FILTER_POST:
252. $this->_postfilters = array ();
253. break;
254. default:
255. return false;
256. }
257. return true;
258. }
260. /**
261. * This function parses the text
262. *
263. * @access public
264. * @param string $text The text to parse
265. * @return mixed Either the root object of the tree if no output method
266. * is defined, the tree reoutput to e.g. a string or false
267. * if an internal error occured, such as a parse error if
268. * in strict mode or the object is already parsing a text.
269. */
270. function parse ($text) {
271. if ($this->_parsing) {
272. return false;
273. }
274. $this->_parsing = true;
275. $this->_text = $this->_applyPrefilters ($text);
276. $this->_output = null;
277. $this->_length = strlen ($this->_text);
278. $this->_cpos = 0;
279. unset ($this->_stack);
280. $this->_stack = array ();
281. if (is_object ($this->_root)) {
282. StringParser_Node::destroyNode ($this->_root);
283. }
284. unset ($this->_root);
285. $this->_root =& new StringParser_Node_Root ();
286. $this->_stack[0] =& $this->_root;
288. $this->_parserInit ();
290. $finished = false;
292. while (!$finished) {
293. switch ($this->_parserMode) {
294. case STRINGPARSER_MODE_SEARCH:
295. $res = $this->_searchLoop ();
296. if (!$res) {
297. $this->_parsing = false;
298. return false;
299. }
300. break;
301. case STRINGPARSER_MODE_LOOP:
302. $res = $this->_loop ();
303. if (!$res) {
304. $this->_parsing = false;
305. return false;
306. }
307. break;
308. default:
309. $this->_parsing = false;
310. return false;
311. }
313. $res = $this->_closeRemainingBlocks ();
314. if (!$res) {
315. if ($this->strict) {
316. $this->_parsing = false;
317. return false;
318. } else {
319. $res = $this->_reparseAfterCurrentBlock ();
320. if (!$res) {
321. $this->_parsing = false;
322. return false;
323. }
324. continue;
325. }
326. }
327. $finished = true;
328. }
330. $res = $this->_modifyTree ();
332. if (!$res) {
333. $this->_parsing = false;
334. return false;
335. }
337. $res = $this->_outputTree ();
339. if (!$res) {
340. $this->_parsing = false;
341. return false;
342. }
344. if (is_null ($this->_output)) {
345. $root =& $this->_root;
346. unset ($this->_root);
347. $this->_root = null;
348. while (count ($this->_stack)) {
349. unset ($this->_stack[count($this->_stack)-1]);
350. }
351. $this->_stack = array ();
352. $this->_parsing = false;
353. return $root;
354. }
356. $res = StringParser_Node::destroyNode ($this->_root);
357. if (!$res) {
358. $this->_parsing = false;
359. return false;
360. }
361. unset ($this->_root);
362. $this->_root = null;
363. while (count ($this->_stack)) {
364. unset ($this->_stack[count($this->_stack)-1]);
365. }
366. $this->_stack = array ();
368. $this->_parsing = false;
369. return $this->_output;
370. }
372. /**
373. * Apply prefilters
374. *
375. * It is possible to specify prefilters for the parser to do some
376. * manipulating of the string beforehand.
377. */
378. function _applyPrefilters ($text) {
379. foreach ($this->_prefilters as $filter) {
380. if (is_callable ($filter)) {
381. $ntext = call_user_func ($filter, $text);
382. if (is_string ($ntext)) {
383. $text = $ntext;
384. }
385. }
386. }
387. return $text;
388. }
390. /**
391. * Apply postfilters
392. *
393. * It is possible to specify postfilters for the parser to do some
394. * manipulating of the string afterwards.
395. */
396. function _applyPostfilters ($text) {
397. foreach ($this->_postfilters as $filter) {
398. if (is_callable ($filter)) {
399. $ntext = call_user_func ($filter, $text);
400. if (is_string ($ntext)) {
401. $text = $ntext;
402. }
403. }
404. }
405. return $text;
406. }
408. /**
409. * Abstract method: Manipulate the tree
410. * @access protected
411. * @return bool
412. */
413. function _modifyTree () {
414. return true;
415. }
417. /**
418. * Abstract method: Output tree
419. * @access protected
420. * @return bool
421. */
422. function _outputTree () {
423. // this could e.g. call _applyPostfilters
424. return true;
425. }
427. /**
428. * Restart parsing after current block
429. *
430. * To achieve this the current top stack object is removed from the
431. * tree. Then the current item
432. *
433. * @access protected
434. * @return bool
435. */
436. function _reparseAfterCurrentBlock () {
437. // this should definitely not happen!
438. if (($stack_count = count ($this->_stack)) < 2) {
439. return false;
440. }
441. $topelem =& $this->_stack[$stack_count-1];
443. $node_parent =& $topelem->_parent;
444. // remove the child from the tree
445. $res = $node_parent->removeChild ($topelem, false);
446. if (!$res) {
447. return false;
448. }
449. $res = $this->_popNode ();
450. if (!$res) {
451. return false;
452. }
454. // now try to get the position of the object
455. if ($topelem->occurredAt < 0) {
456. return false;
457. }
458. // HACK: could it be necessary to set a different status
459. // if yes, how should this be achieved? Another member of
460. // StringParser_Node?
461. $this->_setStatus (0);
462. $res = $this->_appendText ($this->_text{$topelem->occurredAt});
463. if (!$res) {
464. return false;
465. }
467. $this->_cpos = $topelem->occurredAt + 1;
468. $this->_recentlyReparsed = true;
470. return true;
471. }
473. /**
474. * Abstract method: Close remaining blocks
475. * @access protected
476. */
477. function _closeRemainingBlocks () {
478. // everything closed
479. if (count ($this->_stack) == 1) {
480. return true;
481. }
482. // not everything closed
483. if ($this->strict) {
484. return false;
485. }
486. while (count ($this->_stack) > 1) {
487. $res = $this->_popNode ();
488. if (!$res) {
489. return false;
490. }
491. }
492. return true;
493. }
495. /**
496. * Abstract method: Initialize the parser
497. * @access protected
498. */
499. function _parserInit () {
500. $this->_setStatus (0);
501. }
503. /**
504. * Abstract method: Set a specific status
505. * @access protected
506. */
507. function _setStatus ($status) {
508. if ($status != 0) {
509. return false;
510. }
511. $this->_charactersSearch = array ();
512. $this->_charactersAllowed = array ();
513. $this->_status = $status;
514. return true;
515. }
517. /**
518. * Abstract method: Handle status
519. * @access protected
520. * @param int $status The current status
521. * @param string $needle The needle that was found
522. * @return bool
523. */
524. function _handleStatus ($status, $needle) {
525. $this->_appendText ($needle);
526. $this->_cpos += strlen ($needle);
527. return true;
528. }
530. /**
531. * Search mode loop
532. * @access protected
533. * @return bool
534. */
535. function _searchLoop () {
536. $i = 0;
537. while (1) {
538. // make sure this is false!
539. $this->_recentlyReparsed = false;
541. list ($needle, $offset) = $this->_strpos ($this->_charactersSearch, $this->_cpos);
542. // parser ends here
543. if ($needle === false) {
544. // original status 0 => no problem
545. if (!$this->_status) {
546. break;
547. }
548. // not in original status? strict mode?
549. if ($this->strict) {
550. return false;
551. }
552. // break up parsing operation of current node
553. $res = $this->_reparseAfterCurrentBlock ();
554. if (!$res) {
555. return false;
556. }
557. continue;
558. }
559. // get subtext
560. $subtext = substr ($this->_text, $this->_cpos, $offset - $this->_cpos);
561. $res = $this->_appendText ($subtext);
562. if (!$res) {
563. return false;
564. }
565. $this->_cpos = $offset;
566. $res = $this->_handleStatus ($this->_status, $needle);
567. if (!$res && $this->strict) {
568. return false;
569. }
570. if (!$res) {
571. $res = $this->_appendText ($this->_text{$this->_cpos});
572. if (!$res) {
573. return false;
574. }
575. $this->_cpos++;
576. continue;
577. }
578. if ($this->_recentlyReparsed) {
579. $this->_recentlyReparsed = false;
580. continue;
581. }
582. $this->_cpos += strlen ($needle);
583. }
585. // get subtext
586. if ($this->_cpos < strlen ($this->_text)) {
587. $subtext = substr ($this->_text, $this->_cpos);
588. $res = $this->_appendText ($subtext);
589. if (!$res) {
590. return false;
591. }
592. }
594. return true;
595. }
597. /**
598. * Loop mode loop
599. *
600. * @access protected
601. * @return bool
602. */
603. function _loop () {
604. // HACK: This method ist not yet implemented correctly, the code below
605. // DOES NOT WORK! Do not use!
607. return false;
608. /*
609. while ($this->_cpos < $this->_length) {
610. $needle = $this->_strDetect ($this->_charactersSearch, $this->_cpos);
612. if ($needle === false) {
613. // not found => see if character is allowed
614. if (!in_array ($this->_text{$this->_cpos}, $this->_charactersAllowed)) {
615. if ($strict) {
616. return false;
617. }
618. // ignore
619. continue;
620. }
621. // lot's of FIXMES
622. $res = $this->_appendText ($this->_text{$this->_cpos});
623. if (!$res) {
624. return false;
625. }
626. }
628. // get subtext
629. $subtext = substr ($this->_text, $offset, $offset - $this->_cpos);
630. $res = $this->_appendText ($subtext);
631. if (!$res) {
632. return false;
633. }
634. $this->_cpos = $subtext;
635. $res = $this->_handleStatus ($this->_status, $needle);
636. if (!$res && $strict) {
637. return false;
638. }
639. }
640. // original status 0 => no problem
641. if (!$this->_status) {
642. return true;
643. }
644. // not in original status? strict mode?
645. if ($this->strict) {
646. return false;
647. }
648. // break up parsing operation of current node
649. $res = $this->_reparseAfterCurrentBlock ();
650. if (!$res) {
651. return false;
652. }
653. // this will not cause an infinite loop because
654. // _reparseAfterCurrentBlock will increase _cpos by one!
655. return $this->_loop ();
656. */
657. }
659. /**
660. * Abstract method Append text depending on current status
661. * @access protected
662. * @param string $text The text to append
663. * @return bool On success, the function returns true, else false
664. */
665. function _appendText ($text) {
666. if (!strlen ($text)) {
667. return true;
668. }
669. // default: call _appendToLastTextChild
670. return $this->_appendToLastTextChild ($text);
671. }
673. /**
674. * Append text to last text child of current top parser stack node
675. * @access protected
676. * @param string $text The text to append
677. * @return bool On success, the function returns true, else false
678. */
679. function _appendToLastTextChild ($text) {
680. $scount = count ($this->_stack);
681. if ($scount == 0) {
682. return false;
683. }
684. return $this->_stack[$scount-1]->appendToLastTextChild ($text);
685. }
687. /**
688. * Searches {@link StringParser::_text _text} for every needle that is
689. * specified by using the {@link PHP_MANUAL#strpos strpos} function. It
690. * returns an associative array with the key <code>'needle'</code>
691. * pointing at the string that was found first and the key
692. * <code>'offset'</code> pointing at the offset at which the string was
693. * found first. If no needle was found, the <code>'needle'</code>
694. * element is <code>false</code> and the <code>'offset'</code> element
695. * is <code>-1</code>.
696. *
697. * @access protected
698. * @param array $needles
699. * @param int $offset
700. * @return array
701. * @see StringParser::_text
702. */
703. function _strpos ($needles, $offset) {
704. $cur_needle = false;
705. $cur_offset = -1;
707. if ($offset < strlen ($this->_text)) {
708. foreach ($needles as $needle) {
709. $n_offset = strpos ($this->_text, $needle, $offset);
710. if ($n_offset !== false && ($n_offset < $cur_offset || $cur_offset < 0)) {
711. $cur_needle = $needle;
712. $cur_offset = $n_offset;
713. }
714. }
715. }
717. return array ($cur_needle, $cur_offset, 'needle' => $cur_needle, 'offset' => $cur_offset);
718. }
720. /**
721. * Detects a string at the current position
722. *
723. * @access protected
724. * @param array $needles The strings that are to be detected
725. * @param int $offset The current offset
726. * @return mixed The string that was detected or the needle
727. */
728. function _strDetect ($needles, $offset) {
729. foreach ($needles as $needle) {
730. $l = strlen ($needle);
731. if (substr ($this->_text, $offset, $l) == $needle) {
732. return $needle;
733. }
734. }
735. return false;
736. }
739. /**
740. * Adds a node to the current parse stack
741. *
742. * @access protected
743. * @param object $node The node that is to be added
744. * @return bool True on success, else false.
745. * @see StringParser_Node, StringParser::_stack
746. */
747. function _pushNode (&$node) {
748. $stack_count = count ($this->_stack);
749. $max_node =& $this->_stack[$stack_count-1];
750. if (!$max_node->appendChild ($node)) {
751. return false;
752. }
753. $this->_stack[$stack_count] =& $node;
754. return true;
755. }
757. /**
758. * Removes a node from the current parse stack
759. *
760. * @access protected
761. * @return bool True on success, else false.
762. * @see StringParser_Node, StringParser::_stack
763. */
764. function _popNode () {
765. $stack_count = count ($this->_stack);
766. unset ($this->_stack[$stack_count-1]);
767. return true;
768. }
770. /**
771. * Execute a method on the top element
772. *
773. * @access protected
774. * @return mixed
775. */
776. function _topNode () {
777. $args = func_get_args ();
778. if (!count ($args)) {
779. return; // oops?
780. }
781. $method = array_shift ($args);
782. $stack_count = count ($this->_stack);
783. $method = array (&$this->_stack[$stack_count-1], $method);
784. if (!is_callable ($method)) {
785. return; // oops?
786. }
787. return call_user_func_array ($method, $args);
788. }
790. /**
791. * Get a variable of the top element
792. *
793. * @access protected
794. * @return mixed
795. */
796. function _topNodeVar ($var) {
797. $stack_count = count ($this->_stack);
798. return $this->_stack[$stack_count-1]->$var;
799. }
800. }
802. /**
803. * Node type: Unknown node
804. * @see StringParser_Node::_type
805. */
806. define ('STRINGPARSER_NODE_UNKNOWN', 0);
808. /**
809. * Node type: Root node
810. * @see StringParser_Node::_type
811. */
812. define ('STRINGPARSER_NODE_ROOT', 1);
814. /**
815. * Node type: Text node
816. * @see StringParser_Node::_type
817. */
818. define ('STRINGPARSER_NODE_TEXT', 2);
820. /**
821. * Global value that is a counter of string parser node ids. Compare it to a
822. * sequence in databases.
823. * @var int
824. */
825. $GLOBALS['__STRINGPARSER_NODE_ID'] = 0;
827. /**
828. * Generic string parser node class
829. *
830. * This is an abstract class for any type of node that is used within the
831. * string parser. General warning: This class contains code regarding references
832. * that is very tricky. Please do not touch this code unless you exactly know
833. * what you are doing. Incorrect handling of references may cause PHP to crash
834. * with a segmentation fault! You have been warned.
835. *
836. * @package stringparser
837. */
838. class StringParser_Node {
839. /**
840. * The type of this node.
841. *
842. * There are three standard node types: root node, text node and unknown
843. * node. All node types are integer constants. Any node type of a
844. * subclass must be at least 32 to allow future developements.
845. *
846. * @access protected
847. * @var int
848. * @see STRINGPARSER_NODE_ROOT, STRINGPARSER_NODE_TEXT
849. * @see STRINGPARSER_NODE_UNKNOWN
850. */
851. var $_type = STRINGPARSER_NODE_UNKNOWN;
853. /**
854. * The node ID
855. *
856. * This ID uniquely identifies this node. This is needed when searching
857. * for a specific node in the children array. Please note that this is
858. * only an internal variable and should never be used - not even in
859. * subclasses and especially not in external data structures. This ID
860. * has nothing to do with any type of ID in HTML oder XML.
861. *
862. * @access protected
863. * @var int
864. * @see StringParser_Node::_children
865. */
866. var $_id = -1;
868. /**
869. * The parent of this node.
870. *
871. * It is either null (root node) or a reference to the parent object.
872. *
873. * @access protected
874. * @var mixed
875. * @see StringParser_Node::_children
876. */
877. var $_parent = null;
879. /**
880. * The children of this node.
881. *
882. * It contains an array of references to all the children nodes of this
883. * node.
884. *
885. * @access protected
886. * @var array
887. * @see StringParser_Node::_parent
888. */
889. var $_children = array ();
891. /**
892. * Occured at
893. *
894. * This defines the position in the parsed text where this node occurred
895. * at. If -1, this value was not possible to be determined.
896. *
897. * @access public
898. * @var int
899. */
900. var $occurredAt = -1;
902. /**
903. * Constructor
904. *
905. * Currently, the constructor only allocates a new ID for the node and
906. * assigns it.
907. *
908. * @access public
909. * @param int $occurredAt The position in the text where this node
910. * occurred at. If not determinable, it is -1.
911. * @global __STRINGPARSER_NODE_ID
912. */
913. function StringParser_Node ($occurredAt = -1) {
914. $this->_id = $GLOBALS['__STRINGPARSER_NODE_ID']++;
915. $this->occurredAt = $occurredAt;
916. }
918. /**
919. * Type of the node
920. *
921. * This function returns the type of the node
922. *
923. * @access public
924. * @return int
925. */
926. function type () {
927. return $this->_type;
928. }
930. /**
931. * Prepend a node
932. *
933. * @access public
934. * @param object $node The node to be prepended.
935. * @return bool On success, the function returns true, else false.
936. */
937. function prependChild (&$node) {
938. if (!is_object ($node)) {
939. return false;
940. }
942. // root nodes may not be children of other nodes!
943. if ($node->_type == STRINGPARSER_NODE_ROOT) {
944. return false;
945. }
947. // if node already has a parent
948. if ($node->_parent !== false) {
949. // remove node from there
950. $parent =& $node->_parent;
951. if (!$parent->removeChild ($node, false)) {
952. return false;
953. }
954. unset ($parent);
955. }
957. $index = count ($this->_children) - 1;
958. // move all nodes to a new index
959. while ($index >= 0) {
960. // save object
961. $object =& $this->_children[$index];
962. // we have to unset it because else it will be
963. // overridden in in the loop
964. unset ($this->_children[$index]);
965. // put object to new position
966. $this->_children[$index+1] =& $object;
967. $index--;
968. }
969. $this->_children[0] =& $node;
970. return true;
971. }
973. /**
974. * Append text to last text child
975. * @access public
976. * @param string $text The text to append
977. * @return bool On success, the function returns true, else false
978. */
979. function appendToLastTextChild ($text) {
980. $ccount = count ($this->_children);
981. if ($ccount == 0 || $this->_children[$ccount-1]->_type != STRINGPARSER_NODE_TEXT) {
982. $ntextnode =& new StringParser_Node_Text ($text);
983. return $this->appendChild ($ntextnode);
984. } else {
985. $this->_children[$ccount-1]->appendText ($text);
986. return true;
987. }
988. }
990. /**
991. * Append a node to the children
992. *
993. * This function appends a node to the children array(). It
994. * automatically sets the {@link StrinParser_Node::_parent _parent}
995. * property of the node that is to be appended.
996. *
997. * @access public
998. * @param object $node The node that is to be appended.
999. * @return bool On success, the function returns true, else false.
1000. */
1001. function appendChild (&$node) {
1002. if (!is_object ($node)) {
1003. return false;
1004. }
1006. // root nodes may not be children of other nodes!
1007. if ($node->_type == STRINGPARSER_NODE_ROOT) {
1008. return false;
1009. }
1011. // if node already has a parent
1012. if ($node->_parent !== null) {
1013. // remove node from there
1014. $parent =& $node->_parent;
1015. if (!$parent->removeChild ($node, false)) {
1016. return false;
1017. }
1018. unset ($parent);
1019. }
1021. // append it to current node
1022. $new_index = count ($this->_children);
1023. $this->_children[$new_index] =& $node;
1024. $node->_parent =& $this;
1025. return true;
1026. }
1028. /**
1029. * Insert a node before another node
1030. *
1031. * @access public
1032. * @param object $node The node to be inserted.
1033. * @param object $reference The reference node where the new node is
1034. * to be inserted before.
1035. * @return bool On success, the function returns true, else false.
1036. */
1037. function insertChildBefore (&$node, &$reference) {
1038. if (!is_object ($node)) {
1039. return false;
1040. }
1042. // root nodes may not be children of other nodes!
1043. if ($node->_type == STRINGPARSER_NODE_ROOT) {
1044. return false;
1045. }
1047. // is the reference node a child?
1048. $child = $this->_findChild ($reference);
1050. if ($child === false) {
1051. return false;
1052. }
1054. // if node already has a parent
1055. if ($node->_parent !== null) {
1056. // remove node from there
1057. $parent =& $node->_parent;
1058. if (!$parent->removeChild ($node, false)) {
1059. return false;
1060. }
1061. unset ($parent);
1062. }
1064. $index = count ($this->_children) - 1;
1065. // move all nodes to a new index
1066. while ($index >= $child) {
1067. // save object
1068. $object =& $this->_children[$index];
1069. // we have to unset it because else it will be
1070. // overridden in in the loop
1071. unset ($this->_children[$index]);
1072. // put object to new position
1073. $this->_children[$index+1] =& $object;
1074. $index--;
1075. }
1076. $this->_children[$child] =& $node;
1077. return true;
1078. }
1080. /**
1081. * Insert a node after another node
1082. *
1083. * @access public
1084. * @param object $node The node to be inserted.
1085. * @param object $reference The reference node where the new node is
1086. * to be inserted after.
1087. * @return bool On success, the function returns true, else false.
1088. */
1089. function insertChildAfter (&$node, &$reference) {
1090. if (!is_object ($node)) {
1091. return false;
1092. }
1094. // root nodes may not be children of other nodes!
1095. if ($node->_type == STRINGPARSER_NODE_ROOT) {
1096. return false;
1097. }
1099. // is the reference node a child?
1100. $child = $this->_findChild ($reference);
1102. if ($child === false) {
1103. return false;
1104. }
1106. // if node already has a parent
1107. if ($node->_parent !== false) {
1108. // remove node from there
1109. $parent =& $node->_parent;
1110. if (!$parent->removeChild ($node, false)) {
1111. return false;
1112. }
1113. unset ($parent);
1114. }
1116. $index = count ($this->_children) - 1;
1117. // move all nodes to a new index
1118. while ($index >= $child + 1) {
1119. // save object
1120. $object =& $this->_children[$index];
1121. // we have to unset it because else it will be
1122. // overridden in in the loop
1123. unset ($this->_children[$index]);
1124. // put object to new position
1125. $this->_children[$index+1] =& $object;
1126. $index--;
1127. }
1128. $this->_children[$child + 1] =& $node;
1129. return true;
1130. }
1132. /**
1133. * Remove a child node
1134. *
1135. * This function removes a child from the children array. A parameter
1136. * tells the function whether to destroy the child afterwards or not.
1137. * If the specified node is not a child of this node, the function will
1138. * return false.
1139. *
1140. * @access public
1141. * @param mixed $child The child to destroy; either an integer
1142. * specifying the index of the child or a reference
1143. * to the child itself.
1144. * @param bool $destroy Destroy the child afterwards.
1145. * @return bool On success, the function returns true, else false.
1146. */
1147. function removeChild (&$child, $destroy = false) {
1148. if (is_object ($child)) {
1149. // if object: get index
1150. $object =& $child;
1151. unset ($child);
1152. $child = $this->_findChild ($object);
1153. if ($child === false) {
1154. return false;
1155. }
1156. } else {
1157. // remove reference on $child
1158. $save = $child;
1159. unset($child);
1160. $child = $save;
1162. // else: get object
1163. if (!isset($this->_children[$child])) {
1164. return false;
1165. }
1166. $object =& $this->_children[$child];
1167. }
1169. // store count for later use
1170. $ccount = count ($this->_children);
1172. // index out of bounds
1173. if (!is_int ($child) || $child < 0 || $child >= $ccount) {
1174. return false;
1175. }
1177. // inkonsistency
1178. if ($this->_children[$child]->_parent === null ||
1179. $this->_children[$child]->_parent->_id != $this->_id) {
1180. return false;
1181. }
1183. // $object->_parent = null would equal to $this = null
1184. // as $object->_parent is a reference to $this!
1185. // because of this, we have to unset the variable to remove
1186. // the reference and then redeclare the variable
1187. unset ($object->_parent); $object->_parent = null;
1189. // we have to unset it because else it will be overridden in
1190. // in the loop
1191. unset ($this->_children[$child]);
1193. // move all remaining objects one index higher
1194. while ($child < $ccount - 1) {
1195. // save object
1196. $obj =& $this->_children[$child+1];
1197. // we have to unset it because else it will be
1198. // overridden in in the loop
1199. unset ($this->_children[$child+1]);
1200. // put object to new position
1201. $this->_children[$child] =& $obj;
1202. // UNSET THE OBJECT!
1203. unset ($obj);
1204. $child++;
1205. }
1207. if ($destroy) {
1208. return StringParser_Node::destroyNode ($object);
1209. unset ($object);
1210. }
1211. return true;
1212. }
1214. /**
1215. * Get the first child of this node
1216. *
1217. * @access public
1218. * @return mixed
1219. */
1220. function &firstChild () {
1221. $ret = null;
1222. if (!count ($this->_children)) {
1223. return $ret;
1224. }
1225. return $this->_children[0];
1226. }
1228. /**
1229. * Get the last child of this node
1230. *
1231. * @access public
1232. * @return mixed
1233. */
1234. function &lastChild () {
1235. $ret = null;
1236. $c = count ($this->_children);
1237. if (!$c) {
1238. return $ret;
1239. }
1240. return $this->_children[$c-1];
1241. }
1243. /**
1244. * Destroy a node
1245. *
1246. * @access public
1247. * @static
1248. * @param object $node The node to destroy
1249. * @return bool True on success, else false.
1250. */
1251. function destroyNode (&$node) {
1252. if ($node === null) {
1253. return false;
1254. }
1255. // if parent exists: remove node from tree!
1256. if ($node->_parent !== null) {
1257. $parent =& $node->_parent;
1258. // directly return that result because the removeChild
1259. // method will call destroyNode again
1260. return $parent->removeChild ($node, true);
1261. }
1263. // node has children
1264. while (count ($node->_children)) {
1265. $child = 0;
1266. // remove first child until no more children remain
1267. if (!$node->removeChild ($child, true)) {
1268. return false;
1269. }
1270. unset($child);
1271. }
1273. // now call the nodes destructor
1274. if (!$node->_destroy ()) {
1275. return false;
1276. }
1278. // now just unset it and prey that there are no more references
1279. // to this node
1280. unset ($node);
1282. return true;
1283. }
1285. /**
1286. * Destroy this node
1287. *
1288. *
1289. * @access protected
1290. * @return bool True on success, else false.
1291. */
1292. function _destroy () {
1293. return true;
1294. }
1296. /**
1297. * Find a child node
1298. *
1299. * This function searches for a node in the own children and returns
1300. * the index of the node or false if the node is not a child of this
1301. * node.
1302. *
1303. * @access protected
1304. * @param mixed $child The node to look for.
1305. * @return mixed The index of the child node on success, else false.
1306. */
1307. function _findChild (&$child) {
1308. if (!is_object ($child)) {
1309. return false;
1310. }
1312. $ccount = count ($this->_children);
1313. for ($i = 0; $i < $ccount; $i++) {
1314. if ($this->_children[$i]->_id == $child->_id) {
1315. return $i;
1316. }
1317. }
1319. return false;
1320. }
1322. /**
1323. * Checks equality of this node and another node
1324. *
1325. * @access public
1326. * @param mixed $node The node to be compared with
1327. * @return bool True if the other node equals to this node, else false.
1328. */
1329. function equals (&$node) {
1330. return ($this->_id == $node->_id);
1331. }
1333. /**
1334. * Determines whether a criterium matches this node
1335. *
1336. * @access public
1337. * @param string $criterium The criterium that is to be checked
1338. * @param mixed $value The value that is to be compared
1339. * @return bool True if this node matches that criterium
1340. */
1341. function matchesCriterium ($criterium, $value) {
1342. return false;
1343. }
1345. /**
1346. * Search for nodes with a certain criterium
1347. *
1348. * This may be used to implement getElementsByTagName etc.
1349. *
1350. * @access public
1351. * @param string $criterium The criterium that is to be checked
1352. * @param mixed $value The value that is to be compared
1353. * @return array All subnodes that match this criterium
1354. */
1355. function &getNodesByCriterium ($criterium, $value) {
1356. $nodes = array ();
1357. $node_ctr = 0;
1358. for ($i = 0; $i < count ($this->_children); $i++) {
1359. if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
1360. $nodes[$node_ctr++] =& $this->_children[$i];
1361. }
1362. $subnodes = $this->_children[$i]->getNodesByCriterium ($criterium, $value);
1363. if (count ($subnodes)) {
1364. $subnodes_count = count ($subnodes);
1365. for ($j = 0; $j < $subnodes_count; $j++) {
1366. $nodes[$node_ctr++] =& $subnodes[$j];
1367. unset ($subnodes[$j]);
1368. }
1369. }
1370. unset ($subnodes);
1371. }
1372. return $nodes;
1373. }
1375. /**
1376. * Search for nodes with a certain criterium and return the count
1377. *
1378. * Similar to getNodesByCriterium
1379. *
1380. * @access public
1381. * @param string $criterium The criterium that is to be checked
1382. * @param mixed $value The value that is to be compared
1383. * @return int The number of subnodes that match this criterium
1384. */
1385. function getNodeCountByCriterium ($criterium, $value) {
1386. $node_ctr = 0;
1387. for ($i = 0; $i < count ($this->_children); $i++) {
1388. if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
1389. $node_ctr++;
1390. }
1391. $subnodes = $this->_children[$i]->getNodeCountByCriterium ($criterium, $value);
1392. $node_ctr += $subnodes;
1393. }
1394. return $node_ctr;
1395. }
1397. /**
1398. * Dump nodes
1399. *
1400. * This dumps a tree of nodes
1401. *
1402. * @access public
1403. * @param string $prefix The prefix that is to be used for indentation
1404. * @param string $linesep The line separator
1405. * @param int $level The initial level of indentation
1406. * @return string
1407. */
1408. function dump ($prefix = " ", $linesep = "\n", $level = 0) {
1409. $str = str_repeat ($prefix, $level) . $this->_id . ": " . $this->_dumpToString () . $linesep;
1410. for ($i = 0; $i < count ($this->_children); $i++) {
1411. $str .= $this->_children[$i]->dump ($prefix, $linesep, $level + 1);
1412. }
1413. return $str;
1414. }
1416. /**
1417. * Dump this node to a string
1418. *
1419. * @access protected
1420. * @return string
1421. */
1422. function _dumpToString () {
1423. if ($this->_type == STRINGPARSER_NODE_ROOT) {
1424. return "root";
1425. }
1426. return (string)$this->_type;
1427. }
1428. }
1430. /**
1431. * String parser root node class
1432. *
1433. * @package stringparser
1434. */
1435. class StringParser_Node_Root extends StringParser_Node {
1436. /**
1437. * The type of this node.
1438. *
1439. * This node is a root node.
1440. *
1441. * @access protected
1442. * @var int
1443. * @see STRINGPARSER_NODE_ROOT
1444. */
1445. var $_type = STRINGPARSER_NODE_ROOT;
1446. }
1448. /**
1449. * String parser text node class
1450. *
1451. * @package stringparser
1452. */
1453. class StringParser_Node_Text extends StringParser_Node {
1454. /**
1455. * The type of this node.
1456. *
1457. * This node is a text node.
1458. *
1459. * @access protected
1460. * @var int
1461. * @see STRINGPARSER_NODE_TEXT
1462. */
1463. var $_type = STRINGPARSER_NODE_TEXT;
1465. /**
1466. * Node flags
1467. *
1468. * @access protected
1469. * @var array
1470. */
1471. var $_flags = array ();
1473. /**
1474. * The content of this node
1475. * @access public
1476. * @var string
1477. */
1478. var $content = '';
1480. /**
1481. * Constructor
1482. *
1483. * @access public
1484. * @param string $content The initial content of this element
1485. * @param int $occurredAt The position in the text where this node
1486. * occurred at. If not determinable, it is -1.
1487. * @see StringParser_Node_Text::content
1488. */
1489. function StringParser_Node_Text ($content, $occurredAt = -1) {
1490. parent::StringParser_Node ($occurredAt);
1491. $this->content = $content;
1492. }
1494. /**
1495. * Append text to content
1496. *
1497. * @access public
1498. * @param string $text The text to append
1499. * @see StringParser_Node_Text::content
1500. */
1501. function appendText ($text) {
1502. $this->content .= $text;
1503. }
1505. /**
1506. * Set a flag
1507. *
1508. * @access public
1509. * @param string $name The name of the flag
1510. * @param mixed $value The value of the flag
1511. */
1512. function setFlag ($name, $value) {
1513. $this->_flags[$name] = $value;
1514. return true;
1515. }
1517. /**
1518. * Get Flag
1519. *
1520. * @access public
1521. * @param string $flag The requested flag
1522. * @param string $type The requested type of the return value
1523. * @param mixed $default The default return value
1524. */
1525. function getFlag ($flag, $type = 'mixed', $default = null) {
1526. if (!isset ($this->_flags[$flag])) {
1527. return $default;
1528. }
1529. $return = $this->_flags[$flag];
1530. if ($type != 'mixed') {
1531. settype ($return, $type);
1532. }
1533. return $return;
1534. }
1536. /**
1537. * Dump this node to a string
1538. */
1539. function _dumpToString () {
1540. return "text \"".substr (preg_replace ('/\s+/', ' ', $this->content), 0, 40)."\" [f:".preg_replace ('/\s+/', ' ', join(':', array_keys ($this->_flags)))."]";
1541. }
1542. }
1544. ?>

Documentation generated on Mon, 10 Dec 2007 13:29:54 +0100 by phpDocumentor 1.4.0