1<?php
2// vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:
3/**
4 * Parse structured wiki text and render into arbitrary formats such as XHTML.
5 *
6 * PHP versions 4 and 5
7 *
8 * @category   Text
9 * @package    Text_Wiki
10 * @author     Paul M. Jones <pmjones@php.net>
11 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
12 * @version    CVS: $Id$
13 * @link       http://pear.php.net/package/Text_Wiki
14 */
15
16/**
17 * The baseline abstract parser class.
18 */
19require_once 'Text/Wiki/Parse.php';
20
21/**
22 * The baseline abstract render class.
23 */
24require_once 'Text/Wiki/Render.php';
25
26/**
27 * Parse structured wiki text and render into arbitrary formats such as XHTML.
28 *
29 * This is the "master" class for handling the management and convenience
30 * functions to transform Wiki-formatted text.
31 *
32 * @category   Text
33 * @package    Text_Wiki
34 * @author     Paul M. Jones <pmjones@php.net>
35 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL License 2.1
36 * @version    Release: @package_version@
37 * @link       http://pear.php.net/package/Text_Wiki
38 */
39class Text_Wiki {
40
41    /**
42    *
43    * The default list of rules, in order, to apply to the source text.
44    *
45    * @access public
46    *
47    * @var array
48    *
49    */
50
51    var $rules = array(
52        'Prefilter',
53        'Delimiter',
54        'Code',
55        'Function',
56        'Html',
57        'Raw',
58        'Include',
59        'Embed',
60        'Anchor',
61        'Heading',
62        'Toc',
63        'Horiz',
64        'Break',
65        'Blockquote',
66        'List',
67        'Deflist',
68        'Table',
69        'Image',
70        'Phplookup',
71        'Center',
72        'Newline',
73        'Paragraph',
74        'Url',
75        'Freelink',
76        'Interwiki',
77        'Wikilink',
78        'Colortext',
79        'Strong',
80        'Bold',
81        'Emphasis',
82        'Italic',
83        'Underline',
84        'Tt',
85        'Superscript',
86        'Subscript',
87        'Revise',
88        'Tighten'
89    );
90
91
92    /**
93    *
94    * The list of rules to not-apply to the source text.
95    *
96    * @access public
97    *
98    * @var array
99    *
100    */
101
102    var $disable = array(
103        'Html',
104        'Include',
105        'Embed'
106    );
107
108
109    /**
110    *
111    * Custom configuration for rules at the parsing stage.
112    *
113    * In this array, the key is the parsing rule name, and the value is
114    * an array of key-value configuration pairs corresponding to the $conf
115    * property in the target parsing rule.
116    *
117    * For example:
118    *
119    * <code>
120    * $parseConf = array(
121    *     'Include' => array(
122    *         'base' => '/path/to/scripts/'
123    *     )
124    * );
125    * </code>
126    *
127    * Note that most default rules do not need any parsing configuration.
128    *
129    * @access public
130    *
131    * @var array
132    *
133    */
134
135    var $parseConf = array();
136
137
138    /**
139    *
140    * Custom configuration for rules at the rendering stage.
141    *
142    * Because rendering may be different for each target format, the
143    * first-level element in this array is always a format name (e.g.,
144    * 'Xhtml').
145    *
146    * Within that first level element, the subsequent elements match the
147    * $parseConf format. That is, the sub-key is the rendering rule name,
148    * and the sub-value is an array of key-value configuration pairs
149    * corresponding to the $conf property in the target rendering rule.
150    *
151    * @access public
152    *
153    * @var array
154    *
155    */
156
157    var $renderConf = array(
158        'Docbook' => array(),
159        'Latex' => array(),
160        'Pdf' => array(),
161        'Plain' => array(),
162        'Rtf' => array(),
163        'Xhtml' => array()
164    );
165
166
167    /**
168    *
169    * Custom configuration for the output format itself.
170    *
171    * Even though Text_Wiki will render the tokens from parsed text,
172    * the format itself may require some configuration.  For example,
173    * RTF needs to know font names and sizes, PDF requires page layout
174    * information, and DocBook needs a section hierarchy.  This array
175    * matches the $conf property of the the format-level renderer
176    * (e.g., Text_Wiki_Render_Xhtml).
177    *
178    * In this array, the key is the rendering format name, and the value is
179    * an array of key-value configuration pairs corresponding to the $conf
180    * property in the rendering format rule.
181    *
182    * @access public
183    *
184    * @var array
185    *
186    */
187
188    var $formatConf = array(
189        'Docbook' => array(),
190        'Latex' => array(),
191        'Pdf' => array(),
192        'Plain' => array(),
193        'Rtf' => array(),
194        'Xhtml' => array()
195    );
196
197
198    /**
199    *
200    * The delimiter for token numbers of parsed elements in source text.
201    *
202    * @access public
203    *
204    * @var string
205    *
206    */
207
208    var $delim = "\31";
209
210
211    /**
212    *
213    * The tokens generated by rules as the source text is parsed.
214    *
215    * As Text_Wiki applies rule classes to the source text, it will
216    * replace portions of the text with a delimited token number.  This
217    * is the array of those tokens, representing the replaced text and
218    * any options set by the parser for that replaced text.
219    *
220    * The tokens array is sequential; each element is itself a sequential
221    * array where element 0 is the name of the rule that generated the
222    * token, and element 1 is an associative array where the key is an
223    * option name and the value is an option value.
224    *
225    * @access private
226    *
227    * @var array
228    *
229    */
230
231    var $tokens = array();
232
233    /**
234    * How many tokens generated pro rules.
235    *
236    * Intended to load only necessary render objects
237    *
238    * @access private
239    * @var array
240    */
241    var $_countRulesTokens = array();
242
243
244    /**
245    *
246    * The source text to which rules will be applied.
247    *
248    * This text will be transformed in-place, which means that it will
249    * change as the rules are applied.
250    *
251    * @access private
252    *
253    * @var string
254    *
255    */
256
257    var $source = '';
258
259    /**
260     * The output text
261     *
262     * @var string
263     */
264    var $output = '';
265
266
267    /**
268    *
269    * Array of rule parsers.
270    *
271    * Text_Wiki creates one instance of every rule that is applied to
272    * the source text; this array holds those instances.  The array key
273    * is the rule name, and the array value is an instance of the rule
274    * class.
275    *
276    * @access private
277    *
278    * @var array
279    *
280    */
281
282    var $parseObj = array();
283
284
285    /**
286    *
287    * Array of rule renderers.
288    *
289    * Text_Wiki creates one instance of every rule that is applied to
290    * the source text; this array holds those instances.  The array key
291    * is the rule name, and the array value is an instance of the rule
292    * class.
293    *
294    * @access private
295    *
296    * @var array
297    *
298    */
299
300    var $renderObj = array();
301
302
303    /**
304    *
305    * Array of format renderers.
306    *
307    * @access private
308    *
309    * @var array
310    *
311    */
312
313    var $formatObj = array();
314
315
316    /**
317    *
318    * Array of paths to search, in order, for parsing and rendering rules.
319    *
320    * @access private
321    *
322    * @var array
323    *
324    */
325
326    var $path = array(
327        'parse' => array(),
328        'render' => array()
329    );
330
331
332
333    /**
334    *
335    * The directory separator character.
336    *
337    * @access private
338    *
339    * @var string
340    *
341    */
342
343    var $_dirSep = DIRECTORY_SEPARATOR;
344
345    /**
346     * Temporary configuration variable
347     *
348     * @var string
349     */
350    var $renderingType = 'normal';
351
352    /**
353     * Stack of rendering callbacks
354     *
355     * @var Array
356     */
357    var $_renderCallbacks = array();
358
359    /**
360     * Current output block
361     *
362     * @var string
363     */
364    var $_block;
365
366    /**
367     * A stack of blocks
368     *
369     * @param Array
370     */
371    var $_blocks;
372
373    /**
374     * A fix for PHP5.
375     *
376     * **DEPRECATED**
377     * Please use the singleton() or factory() methods.
378     *
379     * @param mixed $rules null or an array.
380     *
381     * @return $this
382     * @uses   self::Text_Wiki()
383     */
384    function __construct($rules = null)
385    {
386        if (is_array($rules)) {
387            $this->rules = array();
388            foreach ($rules as $rule) {
389                $this->rules[] = ucfirst($rule);
390            }
391        }
392
393        $this->addPath(
394            'parse',
395            $this->fixPath(dirname(__FILE__)) . 'Wiki/Parse/Default/'
396        );
397        $this->addPath(
398            'render',
399            $this->fixPath(dirname(__FILE__)) . 'Wiki/Render/'
400        );
401    }
402
403    /**
404    * Singleton.
405    *
406    * This avoids instantiating multiple Text_Wiki instances where a number
407    * of objects are required in one call, e.g. to save memory in a
408    * CMS invironment where several parsers are required in a single page.
409    *
410    * $single = singleton();
411    *
412    * or
413    *
414    * $single = singleton('Parser', array('Prefilter', 'Delimiter', 'Code', 'Function',
415    *   'Html', 'Raw', 'Include', 'Embed', 'Anchor', 'Heading', 'Toc', 'Horiz',
416    *   'Break', 'Blockquote', 'List', 'Deflist', 'Table', 'Image', 'Phplookup',
417    *   'Center', 'Newline', 'Paragraph', 'Url', 'Freelink', 'Interwiki', 'Wikilink',
418    *   'Colortext', 'Strong', 'Bold', 'Emphasis', 'Italic', 'Underline', 'Tt',
419    *   'Superscript', 'Subscript', 'Revise', 'Tighten'));
420    *
421    * Call using a subset of this list.  The order of passing rulesets in the
422    * $rules array is important!
423    *
424    * After calling this, call $single->setParseConf(), setRenderConf() or setFormatConf()
425    * as usual for a constructed object of this class.
426    *
427    * The internal static array of singleton objects has no index on the parser
428    * rules, the only index is on the parser name.  So if you call this multiple
429    * times with different rules but the same parser name, you will get the same
430    * static parser object each time.
431    *
432    * @since Method available since Release 1.1.0
433    * @param string $parser The parser to be used (defaults to 'Default').
434    * @param array $rules   The set of rules to instantiate the object. This
435    *    will only be used when the first call to singleton is made, if included
436    *    in further calls it will be effectively ignored.
437    * @return &object a reference to the Text_Wiki unique instantiation.
438    */
439    public static function singleton($parser = 'Default', $rules = null)
440    {
441        static $only = array();
442        if (!isset($only[$parser])) {
443            $ret = Text_Wiki::factory($parser, $rules);
444            if (Text_Wiki::isError($ret)) {
445                return $ret;
446            }
447            $only[$parser] = $ret;
448        }
449        return $only[$parser];
450    }
451
452    /**
453     * Returns a Text_Wiki Parser class for the specified parser.
454     *
455     * @param string $parser The name of the parse to instantiate
456     * you need to have Text_Wiki_XXX installed to use $parser = 'XXX', it's E_FATAL
457     * @param array $rules The rules to pass into the constructor
458     *    {@see Text_Wiki::singleton} for a list of rules
459     * @return Text_Wiki a Parser object extended from Text_Wiki
460     */
461    public static function factory($parser = 'Default', $rules = null)
462    {
463        $class = 'Text_Wiki_' . $parser;
464        $file = str_replace('_', '/', $class).'.php';
465        if (!class_exists($class)) {
466            require_once $file;
467            if (!class_exists($class)) {
468                return Text_Wiki::error(
469                    'Class ' . $class . ' does not exist after requiring '. $file .
470                        ', install package ' . $class . "\n");
471            }
472        }
473
474        return new $class($rules);
475    }
476
477    /**
478    *
479    * Set parser configuration for a specific rule and key.
480    *
481    * @access public
482    *
483    * @param string $rule The parse rule to set config for.
484    *
485    * @param array|string $arg1 The full config array to use for the
486    * parse rule, or a conf key in that array.
487    *
488    * @param string $arg2 The config value for the key.
489    *
490    * @return void
491    *
492    */
493
494    function setParseConf($rule, $arg1, $arg2 = null)
495    {
496        $rule = ucwords(strtolower($rule));
497
498        if (! isset($this->parseConf[$rule])) {
499            $this->parseConf[$rule] = array();
500        }
501
502        // if first arg is an array, use it as the entire
503        // conf array for the rule.  otherwise, treat arg1
504        // as a key and arg2 as a value for the rule conf.
505        if (is_array($arg1)) {
506            $this->parseConf[$rule] = $arg1;
507        } else {
508            $this->parseConf[$rule][$arg1] = $arg2;
509        }
510    }
511
512
513    /**
514    *
515    * Get parser configuration for a specific rule and key.
516    *
517    * @access public
518    *
519    * @param string $rule The parse rule to get config for.
520    *
521    * @param string $key A key in the conf array; if null,
522    * returns the entire conf array.
523    *
524    * @return mixed The whole conf array if no key is specified,
525    * or the specific conf key value.
526    *
527    */
528
529    function getParseConf($rule, $key = null)
530    {
531        $rule = ucwords(strtolower($rule));
532
533        // the rule does not exist
534        if (! isset($this->parseConf[$rule])) {
535            return null;
536        }
537
538        // no key requested, return the whole array
539        if (is_null($key)) {
540            return $this->parseConf[$rule];
541        }
542
543        // does the requested key exist?
544        if (isset($this->parseConf[$rule][$key])) {
545            // yes, return that value
546            return $this->parseConf[$rule][$key];
547        } else {
548            // no
549            return null;
550        }
551    }
552
553
554    /**
555    *
556    * Set renderer configuration for a specific format, rule, and key.
557    *
558    * @access public
559    *
560    * @param string $format The render format to set config for.
561    *
562    * @param string $rule The render rule to set config for in the format.
563    *
564    * @param array|string $arg1 The config array, or the config key
565    * within the render rule.
566    *
567    * @param string $arg2 The config value for the key.
568    *
569    * @return void
570    *
571    */
572
573    function setRenderConf($format, $rule, $arg1, $arg2 = null)
574    {
575        $format = ucwords(strtolower($format));
576        $rule = ucwords(strtolower($rule));
577
578        if (! isset($this->renderConf[$format])) {
579            $this->renderConf[$format] = array();
580        }
581
582        if (! isset($this->renderConf[$format][$rule])) {
583            $this->renderConf[$format][$rule] = array();
584        }
585
586        // if first arg is an array, use it as the entire
587        // conf array for the render rule.  otherwise, treat arg1
588        // as a key and arg2 as a value for the render rule conf.
589        if (is_array($arg1)) {
590            $this->renderConf[$format][$rule] = $arg1;
591        } else {
592            $this->renderConf[$format][$rule][$arg1] = $arg2;
593        }
594    }
595
596
597    /**
598    *
599    * Get renderer configuration for a specific format, rule, and key.
600    *
601    * @access public
602    *
603    * @param string $format The render format to get config for.
604    *
605    * @param string $rule The render format rule to get config for.
606    *
607    * @param string $key A key in the conf array; if null,
608    * returns the entire conf array.
609    *
610    * @return mixed The whole conf array if no key is specified,
611    * or the specific conf key value.
612    *
613    */
614
615    function getRenderConf($format, $rule, $key = null)
616    {
617        $format = ucwords(strtolower($format));
618        $rule = ucwords(strtolower($rule));
619
620        if (! isset($this->renderConf[$format]) ||
621            ! isset($this->renderConf[$format][$rule])) {
622            return null;
623        }
624
625        // no key requested, return the whole array
626        if (is_null($key)) {
627            return $this->renderConf[$format][$rule];
628        }
629
630        // does the requested key exist?
631        if (isset($this->renderConf[$format][$rule][$key])) {
632            // yes, return that value
633            return $this->renderConf[$format][$rule][$key];
634        } else {
635            // no
636            return null;
637        }
638
639    }
640
641    /**
642    *
643    * Set format configuration for a specific rule and key.
644    *
645    * @access public
646    *
647    * @param string $format The format to set config for.
648    *
649    * @param string $key The config key within the format.
650    *
651    * @param string $val The config value for the key.
652    *
653    * @return void
654    *
655    */
656
657    function setFormatConf($format, $arg1, $arg2 = null)
658    {
659        if (! isset($this->formatConf[$format]) || ! is_array($this->formatConf[$format])) {
660            $this->formatConf[$format] = array();
661        }
662
663        // if first arg is an array, use it as the entire
664        // conf array for the format.  otherwise, treat arg1
665        // as a key and arg2 as a value for the format conf.
666        if (is_array($arg1)) {
667            $this->formatConf[$format] = $arg1;
668        } else {
669            $this->formatConf[$format][$arg1] = $arg2;
670        }
671    }
672
673
674
675    /**
676    *
677    * Get configuration for a specific format and key.
678    *
679    * @access public
680    *
681    * @param string $format The format to get config for.
682    *
683    * @param mixed $key A key in the conf array; if null,
684    * returns the entire conf array.
685    *
686    * @return mixed The whole conf array if no key is specified,
687    * or the specific conf key value.
688    *
689    */
690
691    function getFormatConf($format, $key = null)
692    {
693        // the format does not exist
694        if (! isset($this->formatConf[$format])) {
695            return null;
696        }
697
698        // no key requested, return the whole array
699        if (is_null($key)) {
700            return $this->formatConf[$format];
701        }
702
703        // does the requested key exist?
704        if (isset($this->formatConf[$format][$key])) {
705            // yes, return that value
706            return $this->formatConf[$format][$key];
707        } else {
708            // no
709            return null;
710        }
711    }
712
713
714    /**
715    *
716    * Inserts a rule into to the rule set.
717    *
718    * @access public
719    *
720    * @param string $name The name of the rule.  Should be different from
721    * all other keys in the rule set.
722    *
723    * @param string $tgt The rule after which to insert this new rule.  By
724    * default (null) the rule is inserted at the end; if set to '', inserts
725    * at the beginning.
726    *
727    * @return void
728    *
729    */
730
731    function insertRule($name, $tgt = null)
732    {
733        $name = ucwords(strtolower($name));
734        if (! is_null($tgt)) {
735            $tgt = ucwords(strtolower($tgt));
736        }
737
738        // does the rule name to be inserted already exist?
739        if (in_array($name, $this->rules)) {
740            // yes, return
741            return null;
742        }
743
744        // the target name is not null, and not '', but does not exist
745        // in the list of rules. this means we're trying to insert after
746        // a target key, but the target key isn't there.
747        if (! is_null($tgt) && $tgt != '' &&
748            ! in_array($tgt, $this->rules)) {
749            return false;
750        }
751
752        // if $tgt is null, insert at the end.  We know this is at the
753        // end (instead of resetting an existing rule) becuase we exited
754        // at the top of this method if the rule was already in place.
755        if (is_null($tgt)) {
756            $this->rules[] = $name;
757            return true;
758        }
759
760        // save a copy of the current rules, then reset the rule set
761        // so we can insert in the proper place later.
762        // where to insert the rule?
763        if ($tgt == '') {
764            // insert at the beginning
765            array_unshift($this->rules, $name);
766            return true;
767        }
768
769        // insert after the named rule
770        $tmp = $this->rules;
771        $this->rules = array();
772
773        foreach ($tmp as $val) {
774            $this->rules[] = $val;
775            if ($val == $tgt) {
776                $this->rules[] = $name;
777            }
778        }
779
780        return true;
781
782    }
783
784
785    /**
786    *
787    * Delete (remove or unset) a rule from the $rules property.
788    *
789    * @access public
790    *
791    * @param string $rule The name of the rule to remove.
792    *
793    * @return void
794    *
795    */
796
797    function deleteRule($name)
798    {
799        $name = ucwords(strtolower($name));
800        $key = array_search($name, $this->rules);
801        if ($key !== false) {
802            unset($this->rules[$key]);
803        }
804    }
805
806
807    /**
808    *
809    * Change from one rule to another in-place.
810    *
811    * @access public
812    *
813    * @param string $old The name of the rule to change from.
814    *
815    * @param string $new The name of the rule to change to.
816    *
817    * @return void
818    *
819    */
820
821    function changeRule($old, $new)
822    {
823        $old = ucwords(strtolower($old));
824        $new = ucwords(strtolower($new));
825        $key = array_search($old, $this->rules);
826        if ($key !== false) {
827            // delete the new name , case it was already there
828            $this->deleteRule($new);
829            $this->rules[$key] = $new;
830        }
831    }
832
833
834    /**
835    *
836    * Enables a rule so that it is applied when parsing.
837    *
838    * @access public
839    *
840    * @param string $rule The name of the rule to enable.
841    *
842    * @return void
843    *
844    */
845
846    function enableRule($name)
847    {
848        $name = ucwords(strtolower($name));
849        $key = array_search($name, $this->disable);
850        if ($key !== false) {
851            unset($this->disable[$key]);
852        }
853    }
854
855
856    /**
857    *
858    * Disables a rule so that it is not applied when parsing.
859    *
860    * @access public
861    *
862    * @param string $rule The name of the rule to disable.
863    *
864    * @return void
865    *
866    */
867
868    function disableRule($name)
869    {
870        $name = ucwords(strtolower($name));
871        $key = array_search($name, $this->disable);
872        if ($key === false) {
873            $this->disable[] = $name;
874        }
875    }
876
877
878    /**
879    *
880    * Parses and renders the text passed to it, and returns the results.
881    *
882    * First, the method parses the source text, applying rules to the
883    * text as it goes.  These rules will modify the source text
884    * in-place, replacing some text with delimited tokens (and
885    * populating the $this->tokens array as it goes).
886    *
887    * Next, the method renders the in-place tokens into the requested
888    * output format.
889    *
890    * Finally, the method returns the transformed text.  Note that the
891    * source text is transformed in place; once it is transformed, it is
892    * no longer the same as the original source text.
893    *
894    * @access public
895    *
896    * @param string $text The source text to which wiki rules should be
897    * applied, both for parsing and for rendering.
898    *
899    * @param string $format The target output format, typically 'xhtml'.
900    *  If a rule does not support a given format, the output from that
901    * rule is rule-specific.
902    *
903    * @return string The transformed wiki text.
904    *
905    */
906
907    function transform($text, $format = 'Xhtml')
908    {
909        $this->parse($text);
910        return $this->render($format);
911    }
912
913
914    /**
915    *
916    * Sets the $_source text property, then parses it in place and
917    * retains tokens in the $_tokens array property.
918    *
919    * @access public
920    *
921    * @param string $text The source text to which wiki rules should be
922    * applied, both for parsing and for rendering.
923    *
924    * @return void
925    *
926    */
927
928    function parse($text)
929    {
930        // set the object property for the source text
931        $this->source = $text;
932
933        // reset the tokens.
934        $this->tokens = array();
935        $this->_countRulesTokens = array();
936
937        // apply the parse() method of each requested rule to the source
938        // text.
939        foreach ($this->rules as $name) {
940            // do not parse the rules listed in $disable
941            if (! in_array($name, $this->disable)) {
942
943                // load the parsing object
944                $this->loadParseObj($name);
945
946                // load may have failed; only parse if
947                // an object is in the array now
948                if (is_object($this->parseObj[$name])) {
949                    $this->parseObj[$name]->parse();
950                }
951            }
952        }
953    }
954
955
956    /**
957    *
958    * Renders tokens back into the source text, based on the requested format.
959    *
960    * @access public
961    *
962    * @param string $format The target output format, typically 'xhtml'.
963    * If a rule does not support a given format, the output from that
964    * rule is rule-specific.
965    *
966    * @return string The transformed wiki text.
967    *
968    */
969
970    function render($format = 'Xhtml')
971    {
972        // the rendering method we're going to use from each rule
973        $format = ucwords(strtolower($format));
974
975        // the eventual output text
976        $this->output = '';
977
978        // when passing through the parsed source text, keep track of when
979        // we are in a delimited section
980        $in_delim = false;
981
982        // when in a delimited section, capture the token key number
983        $key = '';
984
985        // load the format object, or crap out if we can't find it
986        $result = $this->loadFormatObj($format);
987        if ($this->isError($result)) {
988            return $result;
989        }
990
991        // pre-rendering activity
992        if (is_object($this->formatObj[$format])) {
993            $this->output .= $this->formatObj[$format]->pre();
994        }
995
996        // load the render objects
997        foreach (array_keys($this->_countRulesTokens) as $rule) {
998            $this->loadRenderObj($format, $rule);
999        }
1000
1001        if ($this->renderingType == 'preg') {
1002            $this->output = preg_replace_callback('/'.$this->delim.'(\d+)'.$this->delim.'/',
1003                                            array(&$this, '_renderToken'),
1004                                            $this->source);
1005            /*
1006//Damn strtok()! Why does it "skip" empty parts of the string. It's useless now!
1007        } elseif ($this->renderingType == 'strtok') {
1008            echo '<pre>'.htmlentities($this->source).'</pre>';
1009            $t = strtok($this->source, $this->delim);
1010            $inToken = true;
1011            $i = 0;
1012            while ($t !== false) {
1013                echo 'Token: '.$i.'<pre>"'.htmlentities($t).'"</pre><br/><br/>';
1014                if ($inToken) {
1015                    //$this->output .= $this->renderObj[$this->tokens[$t][0]]->token($this->tokens[$t][1]);
1016                } else {
1017                    $this->output .= $t;
1018                }
1019                $inToken = !$inToken;
1020                $t = strtok($this->delim);
1021                ++$i;
1022            }
1023            */
1024        } else {
1025            // pass through the parsed source text character by character
1026            $this->_block = '';
1027            $tokenStack = array();
1028            $k = strlen($this->source);
1029            for ($i = 0; $i < $k; $i++) {
1030
1031                // the current character
1032                $char = $this->source{$i};
1033
1034                // are alredy in a delimited section?
1035                if ($in_delim) {
1036
1037                    // yes; are we ending the section?
1038                    if ($char == $this->delim) {
1039
1040                        if (count($this->_renderCallbacks) == 0) {
1041                            $this->output .= $this->_block;
1042                            $this->_block = '';
1043                        }
1044                        if (isset($opts['type'])) {
1045                            if ($opts['type'] == 'start') {
1046                                array_push($tokenStack, $rule);
1047                            } elseif ($opts['type'] == 'end') {
1048                                if ($tokenStack[count($tokenStack) - 1] != $rule) {
1049                                    return Text_Wiki::error('Unbalanced tokens, check your syntax');
1050                                } else {
1051                                    array_pop($tokenStack);
1052                                }
1053                            }
1054                        }
1055
1056                        // yes, get the replacement text for the delimited
1057                        // token number and unset the flag.
1058                        $key = (int)$key;
1059                        $rule = $this->tokens[$key][0];
1060                        $opts = $this->tokens[$key][1];
1061                        $this->_block .= $this->renderObj[$rule]->token($opts);
1062                        $in_delim = false;
1063
1064                    } else {
1065
1066                        // no, add to the delimited token key number
1067                        $key .= $char;
1068
1069                    }
1070
1071                } else {
1072
1073                    // not currently in a delimited section.
1074                    // are we starting into a delimited section?
1075                    if ($char == $this->delim) {
1076                        // yes, reset the previous key and
1077                        // set the flag.
1078                        $key = '';
1079                        $in_delim = true;
1080
1081                    } else {
1082                        // no, add to the output as-is
1083                        $this->_block .= $char;
1084                    }
1085                }
1086            }
1087        }
1088
1089        if (count($this->_renderCallbacks)) {
1090            return $this->error('Render callbacks left over after processing finished');
1091        }
1092        /*
1093        while (count($this->_renderCallbacks)) {
1094            $this->popRenderCallback();
1095        }
1096        */
1097        if (strlen($this->_block)) {
1098            $this->output .= $this->_block;
1099            $this->_block = '';
1100        }
1101
1102        // post-rendering activity
1103        if (is_object($this->formatObj[$format])) {
1104            $this->output .= $this->formatObj[$format]->post();
1105        }
1106
1107        // return the rendered source text.
1108        return $this->output;
1109    }
1110
1111    /**
1112     * Renders a token, for use only as an internal callback
1113     *
1114     * @param array Matches from preg_rpelace_callback, [1] is the token number
1115     * @return string The rendered text for the token
1116     * @access private
1117     */
1118    function _renderToken($matches) {
1119        return $this->renderObj[$this->tokens[$matches[1]][0]]->token($this->tokens[$matches[1]][1]);
1120    }
1121
1122    function registerRenderCallback($callback) {
1123        $this->_blocks[] = $this->_block;
1124        $this->_block = '';
1125        $this->_renderCallbacks[] = $callback;
1126    }
1127
1128    function popRenderCallback() {
1129        if (count($this->_renderCallbacks) == 0) {
1130            return Text_Wiki::error('Render callback popped when no render callbacks in stack');
1131        } else {
1132            $callback = array_pop($this->_renderCallbacks);
1133            $this->_block = call_user_func($callback, $this->_block);
1134            if (count($this->_blocks)) {
1135                $parentBlock = array_pop($this->_blocks);
1136                $this->_block = $parentBlock.$this->_block;
1137            }
1138            if (count($this->_renderCallbacks) == 0) {
1139                $this->output .= $this->_block;
1140                $this->_block = '';
1141            }
1142        }
1143    }
1144
1145    /**
1146    *
1147    * Returns the parsed source text with delimited token placeholders.
1148    *
1149    * @access public
1150    *
1151    * @return string The parsed source text.
1152    *
1153    */
1154
1155    function getSource()
1156    {
1157        return $this->source;
1158    }
1159
1160
1161    /**
1162    *
1163    * Returns tokens that have been parsed out of the source text.
1164    *
1165    * @access public
1166    *
1167    * @param array $rules If an array of rule names is passed, only return
1168    * tokens matching these rule names.  If no array is passed, return all
1169    * tokens.
1170    *
1171    * @return array An array of tokens.
1172    *
1173    */
1174
1175    function getTokens($rules = null)
1176    {
1177        if (is_null($rules)) {
1178            return $this->tokens;
1179        } else {
1180            settype($rules, 'array');
1181            $result = array();
1182            foreach ($this->tokens as $key => $val) {
1183                if (in_array($val[0], $rules)) {
1184                    $result[$key] = $val;
1185                }
1186            }
1187            return $result;
1188        }
1189    }
1190
1191
1192    /**
1193    *
1194    * Add a token to the Text_Wiki tokens array, and return a delimited
1195    * token number.
1196    *
1197    * @access public
1198    *
1199    * @param array $options An associative array of options for the new
1200    * token array element.  The keys and values are specific to the
1201    * rule, and may or may not be common to other rule options.  Typical
1202    * options keys are 'text' and 'type' but may include others.
1203    *
1204    * @param boolean $id_only If true, return only the token number, not
1205    * a delimited token string.
1206    *
1207    * @return string|int By default, return the number of the
1208    * newly-created token array element with a delimiter prefix and
1209    * suffix; however, if $id_only is set to true, return only the token
1210    * number (no delimiters).
1211    *
1212    */
1213
1214    function addToken($rule, $options = array(), $id_only = false)
1215    {
1216        // increment the token ID number.  note that if you parse
1217        // multiple times with the same Text_Wiki object, the ID number
1218        // will not reset to zero.
1219        static $id;
1220        if (! isset($id)) {
1221            $id = 0;
1222        } else {
1223            $id ++;
1224        }
1225
1226        // force the options to be an array
1227        settype($options, 'array');
1228
1229        // add the token
1230        $this->tokens[$id] = array(
1231            0 => $rule,
1232            1 => $options
1233        );
1234        if (!isset($this->_countRulesTokens[$rule])) {
1235            $this->_countRulesTokens[$rule] = 1;
1236        } else {
1237            ++$this->_countRulesTokens[$rule];
1238        }
1239
1240        // return a value
1241        if ($id_only) {
1242            // return the last token number
1243            return $id;
1244        } else {
1245            // return the token number with delimiters
1246            return $this->delim . $id . $this->delim;
1247        }
1248    }
1249
1250
1251    /**
1252    *
1253    * Set or re-set a token with specific information, overwriting any
1254    * previous rule name and rule options.
1255    *
1256    * @access public
1257    *
1258    * @param int $id The token number to reset.
1259    *
1260    * @param int $rule The rule name to use.
1261    *
1262    * @param array $options An associative array of options for the
1263    * token array element.  The keys and values are specific to the
1264    * rule, and may or may not be common to other rule options.  Typical
1265    * options keys are 'text' and 'type' but may include others.
1266    *
1267    * @return void
1268    *
1269    */
1270
1271    function setToken($id, $rule, $options = array())
1272    {
1273        $oldRule = isset($this->tokens[$id]) ? $this->tokens[$id][0] : null;
1274        // reset the token
1275        $this->tokens[$id] = array(
1276            0 => $rule,
1277            1 => $options
1278        );
1279        if ($rule != $oldRule) {
1280            if (isset($oldRule) && !($this->_countRulesTokens[$oldRule]--)) {
1281                unset($this->_countRulesTokens[$oldRule]);
1282            }
1283            if (!isset($this->_countRulesTokens[$rule])) {
1284                $this->_countRulesTokens[$rule] = 1;
1285            } else {
1286                ++$this->_countRulesTokens[$rule];
1287            }
1288        }
1289    }
1290
1291
1292    /**
1293    *
1294    * Load a rule parser class file.
1295    *
1296    * @access public
1297    *
1298    * @return bool True if loaded, false if not.
1299    *
1300    */
1301
1302    function loadParseObj($rule)
1303    {
1304        $rule = ucwords(strtolower($rule));
1305        $file = $rule . '.php';
1306        $class = "Text_Wiki_Parse_$rule";
1307
1308        if (! class_exists($class)) {
1309            $loc = $this->findFile('parse', $file);
1310            if ($loc) {
1311                // found the class
1312                include_once $loc;
1313            } else {
1314                // can't find the class
1315                $this->parseObj[$rule] = null;
1316                // can't find the class
1317                return $this->error(
1318                    "Parse rule '$rule' not found"
1319                );
1320            }
1321        }
1322
1323        $this->parseObj[$rule] = new $class($this);
1324
1325    }
1326
1327
1328    /**
1329    *
1330    * Load a rule-render class file.
1331    *
1332    * @access public
1333    *
1334    * @return bool True if loaded, false if not.
1335    *
1336    */
1337
1338    function loadRenderObj($format, $rule)
1339    {
1340        $format = ucwords(strtolower($format));
1341        $rule = ucwords(strtolower($rule));
1342        $file = "$format/$rule.php";
1343        $class = "Text_Wiki_Render_$format" . "_$rule";
1344
1345        if (! class_exists($class)) {
1346            // load the class
1347            $loc = $this->findFile('render', $file);
1348            if ($loc) {
1349                // found the class
1350                include_once $loc;
1351            } else {
1352                // can't find the class
1353                return $this->error(
1354                    "Render rule '$rule' in format '$format' not found"
1355                );
1356            }
1357        }
1358
1359        $this->renderObj[$rule] = new $class($this);
1360    }
1361
1362
1363    /**
1364    *
1365    * Load a format-render class file.
1366    *
1367    * @access public
1368    *
1369    * @return bool True if loaded, false if not.
1370    *
1371    */
1372
1373    function loadFormatObj($format)
1374    {
1375        $format = ucwords(strtolower($format));
1376        $file = $format . '.php';
1377        $class = "Text_Wiki_Render_$format";
1378
1379        if (! class_exists($class)) {
1380            $loc = $this->findFile('render', $file);
1381            if ($loc) {
1382                // found the class
1383                include_once $loc;
1384            } else {
1385                // can't find the class
1386                return $this->error(
1387                    "Rendering format class '$class' not found"
1388                );
1389            }
1390        }
1391
1392        $this->formatObj[$format] = new $class($this);
1393    }
1394
1395
1396    /**
1397    *
1398    * Add a path to a path array.
1399    *
1400    * @access public
1401    *
1402    * @param string $type The path-type to add (parse or render).
1403    *
1404    * @param string $dir The directory to add to the path-type.
1405    *
1406    * @return void
1407    *
1408    */
1409
1410    function addPath($type, $dir)
1411    {
1412        $dir = $this->fixPath($dir);
1413        if (! isset($this->path[$type])) {
1414            $this->path[$type] = array($dir);
1415        } else {
1416            array_unshift($this->path[$type], $dir);
1417        }
1418    }
1419
1420
1421    /**
1422    *
1423    * Get the current path array for a path-type.
1424    *
1425    * @access public
1426    *
1427    * @param string $type The path-type to look up (plugin, filter, or
1428    * template).  If not set, returns all path types.
1429    *
1430    * @return array The array of paths for the requested type.
1431    *
1432    */
1433
1434    function getPath($type = null)
1435    {
1436        if (is_null($type)) {
1437            return $this->path;
1438        } elseif (! isset($this->path[$type])) {
1439            return array();
1440        } else {
1441            return $this->path[$type];
1442        }
1443    }
1444
1445
1446    /**
1447    *
1448    * Searches a series of paths for a given file.
1449    *
1450    * @param array $type The type of paths to search (template, plugin,
1451    * or filter).
1452    *
1453    * @param string $file The file name to look for.
1454    *
1455    * @return string|bool The full path and file name for the target file,
1456    * or boolean false if the file is not found in any of the paths.
1457    *
1458    */
1459
1460    function findFile($type, $file)
1461    {
1462        // get the set of paths
1463        $set = $this->getPath($type);
1464
1465        // start looping through them
1466        foreach ($set as $path) {
1467            $fullname = $path . $file;
1468            if (file_exists($fullname) && is_readable($fullname)) {
1469                return $fullname;
1470            }
1471        }
1472
1473        // could not find the file in the set of paths
1474        return false;
1475    }
1476
1477
1478    /**
1479    *
1480    * Append a trailing '/' to paths, unless the path is empty.
1481    *
1482    * @access private
1483    *
1484    * @param string $path The file path to fix
1485    *
1486    * @return string The fixed file path
1487    *
1488    */
1489
1490    function fixPath($path)
1491    {
1492        $len = strlen($this->_dirSep);
1493
1494        if (! empty($path) &&
1495            substr($path, -1 * $len, $len) != $this->_dirSep)    {
1496            return $path . $this->_dirSep;
1497        } else {
1498            return $path;
1499        }
1500    }
1501
1502
1503    /**
1504    *
1505    * Simple error-object generator.
1506    *
1507    * @access public
1508    *
1509    * @param string $message The error message.
1510    *
1511    * @return object PEAR_Error
1512    *
1513    */
1514
1515    function error($message)
1516    {
1517        if (! class_exists('PEAR_Error')) {
1518            include_once 'PEAR.php';
1519        }
1520        $pear = new PEAR();
1521        return $pear->throwError($message);
1522    }
1523
1524
1525    /**
1526    *
1527    * Simple error checker.
1528    *
1529    * @param mixed $obj Check if this is a PEAR_Error object or not.
1530    *
1531    * @return bool True if a PEAR_Error, false if not.
1532    *
1533    */
1534
1535    public static function isError(&$obj)
1536    {
1537        return is_a($obj, 'PEAR_Error');
1538    }
1539}
1540