root/trunk/scripts/stringparser.class.php

Revision 1713, 36.2 KB (checked in by jeena, 9 months ago)

updated Christian Seilers bbcode parser to 0.3.3

Line 
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.3
10 *
11 * @author Christian Seiler <spam@christian-seiler.de>
12 * @copyright Christian Seiler 2004-2008
13 * @package stringparser
14 *
15 * The MIT License
16 *
17 * Copyright (c) 2004-2009 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 */
37
38/**
39 * String parser mode: Search for the next character
40 * @see StringParser::_parserMode
41 */
42define ('STRINGPARSER_MODE_SEARCH', 1);
43/**
44 * String parser mode: Look at each character of the string
45 * @see StringParser::_parserMode
46 */
47define ('STRINGPARSER_MODE_LOOP', 2);
48/**
49 * Filter type: Prefilter
50 * @see StringParser::addFilter, StringParser::_prefilters
51 */
52define ('STRINGPARSER_FILTER_PRE', 1);
53/**
54 * Filter type: Postfilter
55 * @see StringParser::addFilter, StringParser::_postfilters
56 */
57define ('STRINGPARSER_FILTER_POST', 2);
58
59/**
60 * Generic string parser class
61 *
62 * This is an abstract class for any type of string parser.
63 *
64 * @package stringparser
65 */
66class 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;
87       
88        /**
89         * Raw text
90         * @access protected
91         * @var string
92         */
93        var $_text = '';
94       
95        /**
96         * Parse stack
97         * @access protected
98         * @var array
99         */
100        var $_stack = array ();
101       
102        /**
103         * Current position in raw text
104         * @access protected
105         * @var integer
106         */
107        var $_cpos = -1;
108       
109        /**
110         * Root node
111         * @access protected
112         * @var mixed
113         */
114        var $_root = null;
115       
116        /**
117         * Length of the text
118         * @access protected
119         * @var integer
120         */
121        var $_length = -1;
122       
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;
133       
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;
143       
144        /**
145         * Characters or strings to look for
146         * @access protected
147         * @var array
148         */
149        var $_charactersSearch = array ();
150       
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 ();
168       
169        /**
170         * Current parser status
171         * @access protected
172         * @var int
173         */
174        var $_status = 0;
175       
176        /**
177         * Prefilters
178         * @access protected
179         * @var array
180         */
181        var $_prefilters = array ();
182       
183        /**
184         * Postfilters
185         * @access protected
186         * @var array
187         */
188        var $_postfilters = array ();
189       
190        /**
191         * Recently reparsed?
192         * @access protected
193         * @var bool
194         */
195        var $_recentlyReparsed = false;
196         
197        /**
198         * Constructor
199         *
200         * @access public
201         */
202        function StringParser () {
203        }
204       
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                }
219               
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                }
230               
231                return true;
232        }
233       
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        }
259       
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;
287               
288                $this->_parserInit ();
289               
290                $finished = false;
291               
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                        }
312                       
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                }
329               
330                $res = $this->_modifyTree ();
331               
332                if (!$res) {
333                        $this->_parsing = false;
334                        return false;
335                }
336               
337                $res = $this->_outputTree ();
338               
339                if (!$res) {
340                        $this->_parsing = false;
341                        return false;
342                }
343               
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                }
355               
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 ();
367               
368                $this->_parsing = false;
369                return $this->_output;
370        }
371       
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        }
389       
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        }
407       
408        /**
409         * Abstract method: Manipulate the tree
410         * @access protected
411         * @return bool
412         */
413        function _modifyTree () {
414                return true;
415        }
416       
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        }
426       
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];
442               
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                }
453               
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                }
466               
467                $this->_cpos = $topelem->occurredAt + 1;
468                $this->_recentlyReparsed = true;
469               
470                return true;
471        }
472       
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        }
494       
495        /**
496         * Abstract method: Initialize the parser
497         * @access protected
498         */
499        function _parserInit () {
500                $this->_setStatus (0);
501        }
502       
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        }
516       
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        }
529       
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;
540                       
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                }
584               
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                }
593               
594                return true;
595        }
596       
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!
606               
607                return false;
608                /*
609                while ($this->_cpos < $this->_length) {
610                        $needle = $this->_strDetect ($this->_charactersSearch, $this->_cpos);
611                       
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                        }
627                       
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        }
658       
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        }
672       
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        }
686       
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;
706               
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                }
716               
717                return array ($cur_needle, $cur_offset, 'needle' => $cur_needle, 'offset' => $cur_offset);
718        }
719       
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        }
737       
738       
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];