root/trunk/scripts/stringparser_bbcode.class.php

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

updated Christian Seilers bbcode parser to 0.3.3

Line 
1<?php
2/**
3 * BB code string parsing class
4 *
5 * Version: 0.3.3
6 *
7 * @author Christian Seiler <spam@christian-seiler.de>
8 * @copyright Christian Seiler 2004-2008
9 * @package stringparser
10 *
11 * The MIT License
12 *
13 * Copyright (c) 2004-2008 Christian Seiler
14 *
15 * Permission is hereby granted, free of charge, to any person obtaining a copy
16 * of this software and associated documentation files (the "Software"), to deal
17 * in the Software without restriction, including without limitation the rights
18 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19 * copies of the Software, and to permit persons to whom the Software is
20 * furnished to do so, subject to the following conditions:
21 *
22 * The above copyright notice and this permission notice shall be included in
23 * all copies or substantial portions of the Software.
24 *
25 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
31 * THE SOFTWARE.
32 */
33 
34require_once dirname(__FILE__).'/stringparser.class.php';
35
36define ('BBCODE_CLOSETAG_FORBIDDEN', -1);
37define ('BBCODE_CLOSETAG_OPTIONAL', 0);
38define ('BBCODE_CLOSETAG_IMPLICIT', 1);
39define ('BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY', 2);
40define ('BBCODE_CLOSETAG_MUSTEXIST', 3);
41
42define ('BBCODE_NEWLINE_PARSE', 0);
43define ('BBCODE_NEWLINE_IGNORE', 1);
44define ('BBCODE_NEWLINE_DROP', 2);
45
46define ('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
47define ('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
48define ('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
49
50/**
51 * BB code string parser class
52 *
53 * @package stringparser
54 */
55class StringParser_BBCode extends StringParser {
56        /**
57         * String parser mode
58         *
59         * The BBCode string parser works in search mode
60         *
61         * @access protected
62         * @var int
63         * @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
64         */
65        var $_parserMode = STRINGPARSER_MODE_SEARCH;
66       
67        /**
68         * Defined BB Codes
69         *
70         * The registered BB codes
71         *
72         * @access protected
73         * @var array
74         */
75        var $_codes = array ();
76       
77        /**
78         * Registered parsers
79         *
80         * @access protected
81         * @var array
82         */
83        var $_parsers = array ();
84       
85        /**
86         * Defined maximum occurrences
87         *
88         * @access protected
89         * @var array
90         */
91        var $_maxOccurrences = array ();
92       
93        /**
94         * Root content type
95         *
96         * @access protected
97         * @var string
98         */
99        var $_rootContentType = 'block';
100       
101        /**
102         * Do not output but return the tree
103         *
104         * @access protected
105         * @var bool
106         */
107        var $_noOutput = false;
108       
109        /**
110         * Global setting: case sensitive
111         *
112         * @access protected
113         * @var bool
114         */
115        var $_caseSensitive = true;
116       
117        /**
118         * Root paragraph handling enabled
119         *
120         * @access protected
121         * @var bool
122         */
123        var $_rootParagraphHandling = false;
124       
125        /**
126         * Paragraph handling parameters
127         * @access protected
128         * @var array
129         */
130        var $_paragraphHandling = array (
131                'detect_string' => "\n\n",
132                'start_tag' => '<p>',
133                'end_tag' => "</p>\n"
134        );
135       
136        /**
137         * Allow mixed attribute types (e.g. [code=bla attr=blub])
138         * @access private
139         * @var bool
140         */
141        var $_mixedAttributeTypes = false;
142       
143        /**
144         * Whether to call validation function again (with $action == 'validate_auto') when closetag comes
145         * @access protected
146         * @var bool
147         */
148        var $_validateAgain = false;
149       
150        /**
151         * Add a code
152         *
153         * @access public
154         * @param string $name The name of the code
155         * @param string $callback_type See documentation
156         * @param string $callback_func The callback function to call
157         * @param array $callback_params The callback parameters
158         * @param string $content_type See documentation
159         * @param array $allowed_within See documentation
160         * @param array $not_allowed_within See documentation
161         * @return bool
162         */
163        function addCode ($name, $callback_type, $callback_func, $callback_params, $content_type, $allowed_within, $not_allowed_within) {
164                if (isset ($this->_codes[$name])) {
165                        return false; // already exists
166                }
167                if (!preg_match ('/^[a-zA-Z0-9*_!+-]+$/', $name)) {
168                        return false; // invalid
169                }
170                $this->_codes[$name] = array (
171                        'name' => $name,
172                        'callback_type' => $callback_type,
173                        'callback_func' => $callback_func,
174                        'callback_params' => $callback_params,
175                        'content_type' => $content_type,
176                        'allowed_within' => $allowed_within,
177                        'not_allowed_within' => $not_allowed_within,
178                        'flags' => array ()
179                );
180                return true;
181        }
182       
183        /**
184         * Remove a code
185         *
186         * @access public
187         * @param $name The code to remove
188         * @return bool
189         */
190        function removeCode ($name) {
191                if (isset ($this->_codes[$name])) {
192                        unset ($this->_codes[$name]);
193                        return true;
194                }
195                return false;
196        }
197       
198        /**
199         * Remove all codes
200         *
201         * @access public
202         */
203        function removeAllCodes () {
204                $this->_codes = array ();
205        }
206       
207        /**
208         * Set a code flag
209         *
210         * @access public
211         * @param string $name The name of the code
212         * @param string $flag The name of the flag to set
213         * @param mixed $value The value of the flag to set
214         * @return bool
215         */
216        function setCodeFlag ($name, $flag, $value) {
217                if (!isset ($this->_codes[$name])) {
218                        return false;
219                }
220                $this->_codes[$name]['flags'][$flag] = $value;
221                return true;
222        }
223       
224        /**
225         * Set occurrence type
226         *
227         * Example:
228         *   $bbcode->setOccurrenceType ('url', 'link');
229         *   $bbcode->setMaxOccurrences ('link', 4);
230         * Would create the situation where a link may only occur four
231         * times in the hole text.
232         *
233         * @access public
234         * @param string $code The name of the code
235         * @param string $type The name of the occurrence type to set
236         * @return bool
237         */
238        function setOccurrenceType ($code, $type) {
239                return $this->setCodeFlag ($code, 'occurrence_type', $type);
240        }
241       
242        /**
243         * Set maximum number of occurrences
244         *
245         * @access public
246         * @param string $type The name of the occurrence type
247         * @param int $count The maximum number of occurrences
248         * @return bool
249         */
250        function setMaxOccurrences ($type, $count) {
251                settype ($count, 'integer');
252                if ($count < 0) { // sorry, does not make any sense
253                        return false;
254                }
255                $this->_maxOccurrences[$type] = $count;
256                return true;
257        }
258       
259        /**
260         * Add a parser
261         *
262         * @access public
263         * @param string $type The content type for which the parser is to add
264         * @param mixed $parser The function to call
265         * @return bool
266         */
267        function addParser ($type, $parser) {
268                if (is_array ($type)) {
269                        foreach ($type as $t) {
270                                $this->addParser ($t, $parser);
271                        }
272                        return true;
273                }
274                if (!isset ($this->_parsers[$type])) {
275                        $this->_parsers[$type] = array ();
276                }
277                $this->_parsers[$type][] = $parser;
278                return true;
279        }
280       
281        /**
282         * Set root content type
283         *
284         * @access public
285         * @param string $content_type The new root content type
286         */
287        function setRootContentType ($content_type) {
288                $this->_rootContentType = $content_type;
289        }
290       
291        /**
292         * Set paragraph handling on root element
293         *
294         * @access public
295         * @param bool $enabled The new status of paragraph handling on root element
296         */
297        function setRootParagraphHandling ($enabled) {
298                $this->_rootParagraphHandling = (bool)$enabled;
299        }
300       
301        /**
302         * Set paragraph handling parameters
303         *
304         * @access public
305         * @param string $detect_string The string to detect
306         * @param string $start_tag The replacement for the start tag (e.g. <p>)
307         * @param string $end_tag The replacement for the start tag (e.g. </p>)
308         */
309        function setParagraphHandlingParameters ($detect_string, $start_tag, $end_tag) {
310                $this->_paragraphHandling = array (
311                        'detect_string' => $detect_string,
312                        'start_tag' => $start_tag,
313                        'end_tag' => $end_tag
314                );
315        }
316       
317        /**
318         * Set global case sensitive flag
319         *
320         * If this is set to true, the class normally is case sensitive, but
321         * the case_sensitive code flag may override this for a single code.
322         *
323         * If this is set to false, all codes are case insensitive.
324         *
325         * @access public
326         * @param bool $caseSensitive
327         */
328        function setGlobalCaseSensitive ($caseSensitive) {
329                $this->_caseSensitive = (bool)$caseSensitive;
330        }
331       
332        /**
333         * Get global case sensitive flag
334         *
335         * @access public
336         * @return bool
337         */
338        function globalCaseSensitive () {
339                return $this->_caseSensitive;
340        }
341       
342        /**
343         * Set mixed attribute types flag
344         *
345         * If set, [code=val1 attr=val2] will cause 2 attributes to be parsed:
346         * 'default' will have value 'val1', 'attr' will have value 'val2'.
347         * If not set, only one attribute 'default' will have the value
348         * 'val1 attr=val2' (the default and original behaviour)
349         *
350         * @access public
351         * @param bool $mixedAttributeTypes
352         */
353        function setMixedAttributeTypes ($mixedAttributeTypes) {
354                $this->_mixedAttributeTypes = (bool)$mixedAttributeTypes;
355        }
356       
357        /**
358         * Get mixed attribute types flag
359         *
360         * @access public
361         * @return bool
362         */
363        function mixedAttributeTypes () {
364                return $this->_mixedAttributeTypes;
365        }
366       
367        /**
368         * Set validate again flag
369         *
370         * If this is set to true, the class calls the validation function
371         * again with $action == 'validate_again' when closetag comes.
372         *
373         * @access public
374         * @param bool $validateAgain
375         */
376        function setValidateAgain ($validateAgain) {
377                $this->_validateAgain = (bool)$validateAgain;
378        }
379       
380        /**
381         * Get validate again flag
382         *
383         * @access public
384         * @return bool
385         */
386        function validateAgain () {
387                return $this->_validateAgain;
388        }
389       
390        /**
391         * Get a code flag
392         *
393         * @access public
394         * @param string $name The name of the code
395         * @param string $flag The name of the flag to get
396         * @param string $type The type of the return value
397         * @param mixed $default The default return value
398         * @return bool
399         */
400        function getCodeFlag ($name, $flag, $type = 'mixed', $default = null) {
401                if (!isset ($this->_codes[$name])) {
402                        return $default;
403                }
404                if (!array_key_exists ($flag, $this->_codes[$name]['flags'])) {
405                        return $default;
406                }
407                $return = $this->_codes[$name]['flags'][$flag];
408                if ($type != 'mixed') {
409                        settype ($return, $type);
410                }
411                return $return;
412        }
413       
414        /**
415         * Set a specific status
416         * @access protected
417         */
418        function _setStatus ($status) {
419                switch ($status) {
420                        case 0:
421                                $this->_charactersSearch = array ('[/', '[');
422                                $this->_status = $status;
423                                break;
424                        case 1:
425                                $this->_charactersSearch = array (']', ' = "', '="', ' = \'', '=\'', ' = ', '=', ': ', ':', ' ');
426                                $this->_status = $status;
427                                break;
428                        case 2:
429                                $this->_charactersSearch = array (']');
430                                $this->_status = $status;
431                                $this->_savedName = '';
432                                break;
433                        case 3:
434                                if ($this->_quoting !== null) {
435                                        if ($this->_mixedAttributeTypes) {
436                                                $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
437                                        } else {
438                                                $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.']', $this->_quoting);
439                                        }
440                                        $this->_status = $status;
441                                        break;
442                                }
443                                if ($this->_mixedAttributeTypes) {
444                                        $this->_charactersSearch = array (' ', ']');
445                                } else {
446                                        $this->_charactersSearch = array (']');
447                                }
448                                $this->_status = $status;
449                                break;
450                        case 4:
451                                $this->_charactersSearch = array (' ', ']', '="', '=\'', '=');
452                                $this->_status = $status;
453                                $this->_savedName = '';
454                                $this->_savedValue = '';
455                                break;
456                        case 5:
457                                if ($this->_quoting !== null) {
458                                        $this->_charactersSearch = array ('\\\\', '\\'.$this->_quoting, $this->_quoting.' ', $this->_quoting.']', $this->_quoting);
459                                } else {
460                                        $this->_charactersSearch = array (' ', ']');
461                                }
462                                $this->_status = $status;
463                                $this->_savedValue = '';
464                                break;
465                        case 7:
466                                $this->_charactersSearch = array ('[/'.$this->_topNode ('name').']');
467                                if (!$this->_topNode ('getFlag', 'case_sensitive', 'boolean', true) || !$this->_caseSensitive) {
468                                        $this->_charactersSearch[] = '[/';
469                                }
470                                $this->_status = $status;
471                                break;
472                        default:
473                                return false;
474                }
475                return true;
476        }
477       
478        /**
479         * Abstract method Append text depending on current status
480         * @access protected
481         * @param string $text The text to append
482         * @return bool On success, the function returns true, else false
483         */
484        function _appendText ($text) {
485                if (!strlen ($text)) {
486                        return true;
487                }
488                switch ($this->_status) {
489                        case 0:
490                        case 7:
491                                return $this->_appendToLastTextChild ($text);
492                        case 1:
493                                return $this->_topNode ('appendToName', $text);
494                        case 2:
495                        case 4:
496                                $this->_savedName .= $text;
497                                return true;
498                        case 3:
499                                return $this->_topNode ('appendToAttribute', 'default', $text);
500                        case 5:
501                                $this->_savedValue .= $text;
502                                return true;
503                        default:
504                                return false;
505                }
506        }
507       
508        /**
509         * Restart parsing after current block
510         *
511         * To achieve this the current top stack object is removed from the
512         * tree. Then the current item
513         *
514         * @access protected
515         * @return bool
516         */
517        function _reparseAfterCurrentBlock () {
518                if ($this->_status == 2) {
519                        // this status will *never* call _reparseAfterCurrentBlock itself
520                        // so this is called if the loop ends
521                        // therefore, just add the [/ to the text
522                       
523                        // _savedName should be empty but just in case
524                        $this->_cpos -= strlen ($this->_savedName);
525                        $this->_savedName = '';
526                        $this->_status = 0;
527                        $this->_appendText ('[/');
528                        return true;
529                } else {
530                        return parent::_reparseAfterCurrentBlock ();
531                }
532        }
533       
534        /**
535         * Apply parsers
536         */
537        function _applyParsers ($type, $text) {
538                if (!isset ($this->_parsers[$type])) {
539                        return $text;
540                }
541                foreach ($this->_parsers[$type] as $parser) {
542                        if (is_callable ($parser)) {
543                                $ntext = call_user_func ($parser, $text);
544                                if (is_string ($ntext)) {
545                                        $text = $ntext;
546                                }
547                        }
548                }
549                return $text;
550        }
551       
552        /**
553         * Handle status
554         * @access protected
555         * @param int $status The current status
556         * @param string $needle The needle that was found
557         * @return bool
558         */
559        function _handleStatus ($status, $needle) {
560                switch ($status) {
561                        case 0: // NORMAL TEXT
562                                if ($needle != '[' && $needle != '[/') {
563                                        $this->_appendText ($needle);
564                                        return true;
565                                }
566                                if ($needle == '[') {
567                                        $node =& new StringParser_BBCode_Node_Element ($this->_cpos);
568                                        $res = $this->_pushNode ($node);
569                                        if (!$res) {
570                                                return false;
571                                        }
572                                        $this->_setStatus (1);
573                                } else if ($needle == '[/') {
574                                        if (count ($this->_stack) <= 1) {
575                                                $this->_appendText ($needle);
576                                                return true;
577                                        }
578                                        $this->_setStatus (2);
579                                }
580                                break;
581                        case 1: // OPEN TAG
582                                if ($needle == ']') {
583                                        return $this->_openElement (0);
584                                } else if (trim ($needle) == ':' || trim ($needle) == '=') {
585                                        $this->_quoting = null;
586                                        $this->_setStatus (3); // default value parser
587                                        break;
588                                } else if (trim ($needle) == '="' || trim ($needle) == '= "' || trim ($needle) == '=\'' || trim ($needle) == '= \'') {
589                                        $this->_quoting = substr (trim ($needle), -1);
590                                        $this->_setStatus (3); // default value parser with quotation
591                                        break;
592                                } else if ($needle == ' ') {
593                                        $this->_setStatus (4); // attribute parser
594                                        break;
595                                } else {
596                                        $this->_appendText ($needle);
597                                        return true;
598                                }
599                                // break not necessary because every if clause contains return
600                        case 2: // CLOSE TAG
601                                if ($needle != ']') {
602                                        $this->_appendText ($needle);
603                                        return true;
604                                }
605                                $closecount = 0;
606                                if (!$this->_isCloseable ($this->_savedName, $closecount)) {
607                                        $this->_setStatus (0);
608                                        $this->_appendText ('[/'.$this->_savedName.$needle);
609                                        return true;
610                                }
611                                // this validates the code(s) to be closed after the content tree of
612                                // that code(s) are built - if the second validation fails, we will have
613                                // to reparse. note that as _reparseAfterCurrentBlock will not work correctly
614                                // if we're in $status == 2, we will have to set our status to 0 manually
615                                if (!$this->_validateCloseTags ($closecount)) {
616                                        $this->_setStatus (0);
617                                        return $this->_reparseAfterCurrentBlock ();
618                                }
619                                $this->_setStatus (0);
620                                for ($i = 0; $i < $closecount; $i++) {
621                                        if ($i == $closecount - 1) {
622                                                $this->_topNode ('setHadCloseTag');
623                                        }
624                                        if (!$this->_popNode ()) {
625                                                return false;
626                                        }
627                                }
628                                break;
629                        case 3: // DEFAULT ATTRIBUTE
630                                if ($this->_quoting !== null) {
631                                        if ($needle == '\\\\') {
632                                                $this->_appendText ('\\');
633                                                return true;
634                                        } else if ($needle == '\\'.$this->_quoting) {
635                                                $this->_appendText ($this->_quoting);
636                                                return true;
637                                        } else if ($needle == $this->_quoting.' ') {
638                                                $this->_setStatus (4);
639                                                return true;
640                                        } else if ($needle == $this->_quoting.']') {
641                                                return $this->_openElement (2);
642                                        } else if ($needle == $this->_quoting) {
643                                                // can't be, only ']' and ' ' allowed after quoting char
644                                                return $this->_reparseAfterCurrentBlock ();
645                                        } else {
646                                                $this->_appendText ($needle);
647                                                return true;
648                                        }
649                                } else {
650                                        if ($needle == ' ') {
651                                                $this->_setStatus (4);
652                                                return true;
653                                        } else if ($needle == ']') {
654                                                return $this->_openElement (2);
655                                        } else {
656                                                $this->_appendText ($needle);
657                                                return true;
658                                        }
659                                }
660                                // break not needed because every if clause contains return!
661                        case 4: // ATTRIBUTE NAME
662                                if ($needle == ' ') {
663                                        if (strlen ($this->_savedName)) {
664                                                $this->_topNode