]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Prevent from some PHP5 warnings (ref args, no :: object init)
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id: BlockParser.php,v 1.44 2004-04-19 18:27:45 rurban Exp $');
2 /* Copyright (C) 2002, Geoffrey T. Dairiki <dairiki@dairiki.org>
3  *
4  * This file is part of PhpWiki.
5  * 
6  * PhpWiki is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  * 
11  * PhpWiki is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License
17  * along with PhpWiki; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 require_once('lib/HtmlElement.php');
21 require_once('lib/CachedMarkup.php');
22 require_once('lib/InlineParser.php');
23
24 ////////////////////////////////////////////////////////////////
25 //
26 //
27
28 /**
29  * Deal with paragraphs and proper, recursive block indents 
30  * for the new style markup (version 2)
31  *
32  * Everything which goes over more than line:
33  * automatic lists, UL, OL, DL, table, blockquote, verbatim, 
34  * p, pre, plugin, ...
35  *
36  * FIXME:
37  *  Still to do:
38  *    (old-style) tables
39  * FIXME: unify this with the RegexpSet in InlineParser.
40  *
41  * @package Markup
42  * @author: Geoffrey T. Dairiki 
43  */
44
45 /**
46  * Return type from RegexpSet::match and RegexpSet::nextMatch.
47  *
48  * @see RegexpSet
49  */
50 class AnchoredRegexpSet_match {
51     /**
52      * The matched text.
53      */
54     var $match;
55
56     /**
57      * The text following the matched text.
58      */
59     var $postmatch;
60
61     /**
62      * Index of the regular expression which matched.
63      */
64     var $regexp_ind;
65 }
66
67 /**
68  * A set of regular expressions.
69  *
70  * This class is probably only useful for InlineTransformer.
71  */
72 class AnchoredRegexpSet
73 {
74     /** Constructor
75      *
76      * @param $regexps array A list of regular expressions.  The
77      * regular expressions should not include any sub-pattern groups
78      * "(...)".  (Anonymous groups, like "(?:...)", as well as
79      * look-ahead and look-behind assertions are fine.)
80      */
81     function AnchoredRegexpSet ($regexps) {
82         $this->_regexps = $regexps;
83         $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
84     }
85
86     /**
87      * Search text for the next matching regexp from the Regexp Set.
88      *
89      * @param $text string The text to search.
90      *
91      * @return object  A RegexpSet_match object, or false if no match.
92      */
93     function match ($text) {
94         if (! preg_match($this->_re, $text, $m)) {
95             return false;
96         }
97         
98         $match = new AnchoredRegexpSet_match;
99         $match->postmatch = substr($text, strlen($m[0]));
100         $match->match = $m[1];
101         $match->regexp_ind = count($m) - 3;
102         return $match;
103     }
104
105     /**
106      * Search for next matching regexp.
107      *
108      * Here, 'next' has two meanings:
109      *
110      * Match the next regexp(s) in the set, at the same position as the last match.
111      *
112      * If that fails, match the whole RegexpSet, starting after the position of the
113      * previous match.
114      *
115      * @param $text string Text to search.
116      *
117      * @param $prevMatch A RegexpSet_match object
118      *
119      * $prevMatch should be a match object obtained by a previous
120      * match upon the same value of $text.
121      *
122      * @return object  A RegexpSet_match object, or false if no match.
123      */
124     function nextMatch ($text, $prevMatch) {
125         // Try to find match at same position.
126         $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
127         if (!$regexps) {
128             return false;
129         }
130
131         $pat= "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
132
133         if (! preg_match($pat, $text, $m)) {
134             return false;
135         }
136         
137         $match = new AnchoredRegexpSet_match;
138         $match->postmatch = substr($text, strlen($m[0]));
139         $match->match = $m[1];
140         $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;;
141         return $match;
142     }
143 }
144
145
146     
147 class BlockParser_Input {
148
149     function BlockParser_Input ($text) {
150         
151         // Expand leading tabs.
152         // FIXME: do this better.
153         //
154         // We want to ensure the only characters matching \s are ' ' and "\n".
155         //
156         $text = preg_replace('/(?![ \n])\s/', ' ', $text);
157         assert(!preg_match('/(?![ \n])\s/', $text));
158
159         $this->_lines = preg_split('/[^\S\n]*\n/', $text);
160         $this->_pos = 0;
161
162         // Strip leading blank lines.
163         while ($this->_lines and ! $this->_lines[0])
164             array_shift($this->_lines);
165         $this->_atSpace = false;
166     }
167
168     function skipSpace () {
169         $nlines = count($this->_lines);
170         while (1) {
171             if ($this->_pos >= $nlines) {
172                 $this->_atSpace = false;
173                 break;
174             }
175             if ($this->_lines[$this->_pos] != '')
176                 break;
177             $this->_pos++;
178             $this->_atSpace = true;
179         }
180         return $this->_atSpace;
181     }
182         
183     function currentLine () {
184         if ($this->_pos >= count($this->_lines)) {
185             return false;
186         }
187         return $this->_lines[$this->_pos];
188     }
189         
190     function nextLine () {
191         $this->_atSpace = $this->_lines[$this->_pos++] === '';
192         if ($this->_pos >= count($this->_lines)) {
193             return false;
194         }
195         return $this->_lines[$this->_pos];
196     }
197
198     function advance () {
199         $this->_atSpace = $this->_lines[$this->_pos++] === '';
200     }
201     
202     function getPos () {
203         return array($this->_pos, $this->_atSpace);
204     }
205
206     function setPos ($pos) {
207         list($this->_pos, $this->_atSpace) = $pos;
208     }
209
210     function getPrefix () {
211         return '';
212     }
213
214     function getDepth () {
215         return 0;
216     }
217
218     function where () {
219         if ($this->_pos < count($this->_lines))
220             return $this->_lines[$this->_pos];
221         else
222             return "<EOF>";
223     }
224     
225     function _debug ($tab, $msg) {
226         //return ;
227         $where = $this->where();
228         $tab = str_repeat('____', $this->getDepth() ) . $tab;
229         printXML(HTML::div("$tab $msg: at: '",
230                            HTML::tt($where),
231                            "'"));
232     }
233 }
234
235 class BlockParser_InputSubBlock extends BlockParser_Input
236 {
237     function BlockParser_InputSubBlock (&$input, $prefix_re, $initial_prefix = false) {
238         $this->_input = &$input;
239         $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
240         $this->_atSpace = false;
241
242         if (($line = $input->currentLine()) === false)
243             $this->_line = false;
244         elseif ($initial_prefix) {
245             assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
246             $this->_line = (string) substr($line, strlen($initial_prefix));
247             $this->_atBlank = ! ltrim($line);
248         }
249         elseif (preg_match($this->_prefix_pat, $line, $m)) {
250             $this->_line = (string) substr($line, strlen($m[0]));
251             $this->_atBlank = ! ltrim($line);
252         }
253         else
254             $this->_line = false;
255     }
256
257     function skipSpace () {
258         // In contrast to the case for top-level blocks,
259         // for sub-blocks, there never appears to be any trailing space.
260         // (The last block in the sub-block should always be of class tight-bottom.)
261         while ($this->_line === '')
262             $this->advance();
263
264         if ($this->_line === false)
265             return $this->_atSpace == 'strong_space';
266         else
267             return $this->_atSpace;
268     }
269         
270     function currentLine () {
271         return $this->_line;
272     }
273
274     function nextLine () {
275         if ($this->_line === '')
276             $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
277         else
278             $this->_atSpace = false;
279
280         $line = $this->_input->nextLine();
281         if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
282             $this->_line = (string) substr($line, strlen($m[0]));
283             $this->_atBlank = ! ltrim($line);
284         }
285         else
286             $this->_line = false;
287
288         return $this->_line;
289     }
290
291     function advance () {
292         $this->nextLine();
293     }
294         
295     function getPos () {
296         return array($this->_line, $this->_atSpace, $this->_input->getPos());
297     }
298
299     function setPos ($pos) {
300         $this->_line = $pos[0];
301         $this->_atSpace = $pos[1];
302         $this->_input->setPos($pos[2]);
303     }
304     
305     function getPrefix () {
306         assert ($this->_line !== false);
307         $line = $this->_input->currentLine();
308         assert ($line !== false && strlen($line) >= strlen($this->_line));
309         return substr($line, 0, strlen($line) - strlen($this->_line));
310     }
311
312     function getDepth () {
313         return $this->_input->getDepth() + 1;
314     }
315
316     function where () {
317         return $this->_input->where();
318     }
319 }
320     
321
322 class Block_HtmlElement extends HtmlElement
323 {
324     function Block_HtmlElement($tag /*, ... */) {
325         $this->_init(func_get_args());
326     }
327
328     function setTightness($top, $bottom) {
329         $this->setInClass('tightenable');
330         $this->setInClass('top', $top);
331         $this->setInClass('bottom', $bottom);
332     }
333 }
334
335 class ParsedBlock extends Block_HtmlElement {
336     
337     function ParsedBlock (&$input, $tag = 'div', $attr = false) {
338         $this->Block_HtmlElement($tag, $attr);
339         $this->_initBlockTypes();
340         $this->_parse($input);
341     }
342
343     function _parse (&$input) {
344         for ($block = $this->_getBlock($input); $block; $block = $nextBlock) {
345             while ($nextBlock = $this->_getBlock($input)) {
346                 // Attempt to merge current with following block.
347                 if (! ($merged = $block->merge($nextBlock)) ) {
348                     break;      // can't merge
349                 }
350                 $block = $merged;
351             }
352             $this->pushContent($block->finish());
353         }
354     }
355
356     // FIXME: hackish
357     function _initBlockTypes () {
358         foreach (array('oldlists', 'list', 'dl', 'table_dl',
359                        'blockquote', 'heading', 'hr', 'pre', 'email_blockquote',
360                        'plugin', 'p')
361                  as $type) {
362             $class = "Block_$type";
363             $proto = new $class;
364             $this->_block_types[] = $proto;
365             $this->_regexps[] = $proto->_re;
366         }
367         $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
368     }
369
370     function _getBlock (&$input) {
371         $this->_atSpace = $input->skipSpace();
372
373         if (($line = $input->currentLine()) === '')
374             return false;
375
376         $tight_top = !$this->_atSpace;
377         $re_set = &$this->_regexpset;
378         for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
379             $block = $this->_block_types[$m->regexp_ind];
380             //$input->_debug('>', get_class($block));
381             
382             if ($block->_match($input, $m)) {
383                 //$input->_debug('<', get_class($block));
384                 $tight_bottom = ! $input->skipSpace();
385                 $block->_setTightness($tight_top, $tight_bottom);
386                 return $block;
387             }
388             //$input->_debug('[', "_match failed");
389         }
390         if (!$line)
391             return false;
392
393         trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
394         return false;
395     }
396 }
397
398 class WikiText extends ParsedBlock {
399     function WikiText ($text) {
400         $input = new BlockParser_Input($text);
401         $this->ParsedBlock($input);
402     }
403 }
404
405 class SubBlock extends ParsedBlock {
406     function SubBlock (&$input, $indent_re, $initial_indent = false,
407                        $tag = 'div', $attr = false) {
408         $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
409         $this->ParsedBlock($subinput, $tag, $attr);
410     }
411 }
412
413 /**
414  * TightSubBlock is for use in parsing lists item bodies.
415  *
416  * If the sub-block consists of a single paragraph, it omits
417  * the paragraph element.
418  *
419  * We go to this trouble so that "tight" lists look somewhat reasonable
420  * in older (non-CSS) browsers.  (If you don't do this, then, without
421  * CSS, you only get "loose" lists.
422  */
423 class TightSubBlock extends SubBlock {
424     function TightSubBlock (&$input, $indent_re, $initial_indent = false,
425                             $tag = 'div', $attr = false) {
426         $this->SubBlock($input, $indent_re, $initial_indent, $tag, $attr);
427
428         // If content is a single paragraph, eliminate the paragraph...
429         if (count($this->_content) == 1) {
430             $elem = $this->_content[0];
431             if (isa($elem, 'XmlElement') and $elem->getTag() == 'p') {
432                 assert($elem->getAttr('class') == 'tightenable top bottom');
433                 $this->setContent($elem->getContent());
434             }
435         }
436     }
437 }
438
439 class BlockMarkup {
440     var $_re;
441
442     function _match (&$input, $match) {
443         trigger_error('pure virtual', E_USER_ERROR);
444     }
445
446     function _setTightness ($top, $bot) {
447         $this->_element->setTightness($top, $bot);
448     }
449
450     function merge ($followingBlock) {
451         return false;
452     }
453
454     function finish () {
455         return $this->_element;
456     }
457 }
458
459 class Block_blockquote extends BlockMarkup
460 {
461     var $_depth;
462
463     var $_re = '\ +(?=\S)';
464
465     function _match (&$input, $m) {
466         $this->_depth = strlen($m->match);
467         $indent = sprintf("\\ {%d}", $this->_depth);
468         $this->_element = new SubBlock($input, $indent, $m->match,
469                                        'blockquote');
470         return true;
471     }
472     
473     function merge ($nextBlock) {
474         if (get_class($nextBlock) == get_class($this)) {
475             assert ($nextBlock->_depth < $this->_depth);
476             $nextBlock->_element->unshiftContent($this->_element);
477             $nextBlock->_tight_top = $this->_tight_top;
478             return $nextBlock;
479         }
480         return false;
481     }
482 }
483
484 class Block_list extends BlockMarkup
485 {
486     //var $_tag = 'ol' or 'ul';
487     var $_re = '\ {0,4}
488                 (?: \+
489                   | \\# (?!\[.*\])
490                   | -(?!-)
491                   | [o](?=\ )
492                   | [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
493                 )\ *(?=\S)';
494
495     var $_content = array();
496
497     function _match (&$input, $m) {
498         // A list as the first content in a list is not allowed.
499         // E.g.:
500         //   *  * Item
501         // Should markup as <ul><li>* Item</li></ul>,
502         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
503         //
504         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
505             return false;
506         }
507         
508         $prefix = $m->match;
509         $indent = sprintf("\\ {%d}", strlen($prefix));
510
511         $bullet = trim($m->match);
512         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
513         $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
514         return true;
515     }
516
517     function _setTightness($top, $bot) {
518         $li = &$this->_content[0];
519         $li->setTightness($top, $bot);
520     }
521     
522     function merge ($nextBlock) {
523         if (isa($nextBlock, 'Block_list') && $this->_tag == $nextBlock->_tag) {
524             array_splice($this->_content, count($this->_content), 0,
525                          $nextBlock->_content);
526             return $this;
527         }
528         return false;
529     }
530
531     function finish () {
532         return new Block_HtmlElement($this->_tag, false, $this->_content);
533     }
534 }
535
536 class Block_dl extends Block_list
537 {
538     var $_tag = 'dl';
539
540     function Block_dl () {
541         $this->_re = '\ {0,4}\S.*(?<!'.ESCAPE_CHAR.'):\s*$';
542     }
543
544     function _match (&$input, $m) {
545         if (!($p = $this->_do_match($input, $m)))
546             return false;
547         list ($term, $defn, $loose) = $p;
548
549         $this->_content[] = new Block_HtmlElement('dt', false, $term);
550         $this->_content[] = $defn;
551         $this->_tight_defn = !$loose;
552         return true;
553     }
554
555     function _setTightness($top, $bot) {
556         $dt = &$this->_content[0];
557         $dd = &$this->_content[1];
558
559         $dt->setTightness($top, $this->_tight_defn);
560         $dd->setTightness($this->_tight_defn, $bot);
561     }
562
563     function _do_match (&$input, $m) {
564         $pos = $input->getPos();
565
566         $firstIndent = strspn($m->match, ' ');
567         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
568
569         $input->advance();
570         $loose = $input->skipSpace();
571         $line = $input->currentLine();
572
573         if (!$line || !preg_match($pat, $line, $mm)) {
574             $input->setPos($pos);
575             return false;       // No body found.
576         }
577
578         $indent = strlen($mm[0]);
579         $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
580         $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
581         return array($term, $defn, $loose);
582     }
583 }
584
585
586
587 class Block_table_dl_defn extends XmlContent
588 {
589     var $nrows;
590     var $ncols;
591     
592     function Block_table_dl_defn ($term, $defn) {
593         $this->XmlContent();
594         if (!is_array($defn))
595             $defn = $defn->getContent();
596
597         $this->_next_tight_top = false; // value irrelevant - gets fixed later
598         $this->_ncols = $this->_ComputeNcols($defn);
599         $this->_nrows = 0;
600
601         foreach ($defn as $item) {
602             if ($this->_IsASubtable($item))
603                 $this->_addSubtable($item);
604             else
605                 $this->_addToRow($item);
606         }
607         $this->_flushRow();
608
609         $th = HTML::th($term);
610         if ($this->_nrows > 1)
611             $th->setAttr('rowspan', $this->_nrows);
612         $this->_setTerm($th);
613     }
614
615     function setTightness($tight_top, $tight_bot) {
616         $this->_tight_top = $tight_top;
617         $this->_tight_bot = $tight_bot;
618         $first = &$this->firstTR();
619         $last = &$this->lastTR();
620         $first->setInClass('top', $tight_top);
621         if (!empty($last)) {
622             $last->setInClass('bottom', $tight_bot);
623         } else {
624             trigger_error(sprintf("no lastTR: %s",AsXml($this->_content[0])), E_USER_WARNING);
625         }
626     }
627     
628     function _addToRow ($item) {
629         if (empty($this->_accum)) {
630             $this->_accum = HTML::td();
631             if ($this->_ncols > 2)
632                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
633         }
634         $this->_accum->pushContent($item);
635     }
636
637     function _flushRow ($tight_bottom=false) {
638         if (!empty($this->_accum)) {
639             $row = new Block_HtmlElement('tr', false, $this->_accum);
640
641             $row->setTightness($this->_next_tight_top, $tight_bottom);
642             $this->_next_tight_top = $tight_bottom;
643             
644             $this->pushContent($row);
645             $this->_accum = false;
646             $this->_nrows++;
647         }
648     }
649
650     function _addSubtable ($table) {
651         if (!($table_rows = $table->getContent()))
652             return;
653
654         $this->_flushRow($table_rows[0]->_tight_top);
655             
656         foreach ($table_rows as $subdef) {
657             $this->pushContent($subdef);
658             $this->_nrows += $subdef->nrows();
659             $this->_next_tight_top = $subdef->_tight_bot;
660         }
661     }
662
663     function _setTerm ($th) {
664         $first_row = &$this->_content[0];
665         if (isa($first_row, 'Block_table_dl_defn'))
666             $first_row->_setTerm($th);
667         else
668             $first_row->unshiftContent($th);
669     }
670     
671     function _ComputeNcols ($defn) {
672         $ncols = 2;
673         foreach ($defn as $item) {
674             if ($this->_IsASubtable($item)) {
675                 $row = $this->_FirstDefn($item);
676                 $ncols = max($ncols, $row->ncols() + 1);
677             }
678         }
679         return $ncols;
680     }
681
682     function _IsASubtable ($item) {
683         return isa($item, 'HtmlElement')
684             && $item->getTag() == 'table'
685             && $item->getAttr('class') == 'wiki-dl-table';
686     }
687
688     function _FirstDefn ($subtable) {
689         $defs = $subtable->getContent();
690         return $defs[0];
691     }
692
693     function ncols () {
694         return $this->_ncols;
695     }
696
697     function nrows () {
698         return $this->_nrows;
699     }
700
701     function & firstTR() {
702         $first = &$this->_content[0];
703         if (isa($first, 'Block_table_dl_defn'))
704             return $first->firstTR();
705         return $first;
706     }
707
708     function & lastTR() {
709         $last = &$this->_content[$this->_nrows - 1];
710         if (isa($last, 'Block_table_dl_defn'))
711             return $last->lastTR();
712         return $last;
713     }
714
715     function setWidth ($ncols) {
716         assert($ncols >= $this->_ncols);
717         if ($ncols <= $this->_ncols)
718             return;
719         $rows = &$this->_content;
720         for ($i = 0; $i < count($rows); $i++) {
721             $row = &$rows[$i];
722             if (isa($row, 'Block_table_dl_defn'))
723                 $row->setWidth($ncols - 1);
724             else {
725                 $n = count($row->_content);
726                 $lastcol = &$row->_content[$n - 1];
727                 if (!empty($lastcol))
728                   $lastcol->setAttr('colspan', $ncols - 1);
729             }
730         }
731     }
732 }
733
734 class Block_table_dl extends Block_dl
735 {
736     var $_tag = 'dl-table';     // phony.
737
738     function Block_table_dl() {
739         $this->_re = '\ {0,4} (?:\S.*)? (?<!'.ESCAPE_CHAR.') \| \s* $';
740     }
741
742     function _match (&$input, $m) {
743         if (!($p = $this->_do_match($input, $m)))
744             return false;
745         list ($term, $defn, $loose) = $p;
746
747         $this->_content[] = new Block_table_dl_defn($term, $defn);
748         return true;
749     }
750
751     function _setTightness($top, $bot) {
752         $this->_content[0]->setTightness($top, $bot);
753     }
754     
755     function finish () {
756
757         $defs = &$this->_content;
758
759         $ncols = 0;
760         foreach ($defs as $defn)
761             $ncols = max($ncols, $defn->ncols());
762         
763         foreach ($defs as $key => $defn)
764             $defs[$key]->setWidth($ncols);
765
766         return HTML::table(array('class' => 'wiki-dl-table',
767                                  'border' => 1,
768                                  'cellspacing' => 0,
769                                  'cellpadding' => 6),
770                            $defs);
771     }
772 }
773
774 class Block_oldlists extends Block_list
775 {
776     //var $_tag = 'ol', 'ul', or 'dl';
777     var $_re = '(?: [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
778                   | [#] (?! \[ .*? \] )
779                   | ; .*? :
780                 ) .*? (?=\S)';
781
782     function _match (&$input, $m) {
783         // FIXME:
784         if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
785             return false;
786         }
787         
788
789         $prefix = $m->match;
790         $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
791         $newindent = sprintf('\\ {%d}', strlen($prefix));
792         $indent = "(?:$oldindent|$newindent)";
793
794         $bullet = $prefix[0];
795         if ($bullet == '*') {
796             $this->_tag = 'ul';
797             $itemtag = 'li';
798         }
799         elseif ($bullet == '#') {
800             $this->_tag = 'ol';
801             $itemtag = 'li';
802         }
803         else {
804             $this->_tag = 'dl';
805             list ($term,) = explode(':', substr($prefix, 1), 2);
806             $term = trim($term);
807             if ($term)
808                 $this->_content[] = new Block_HtmlElement('dt', false,
809                                                           TransformInline($term));
810             $itemtag = 'dd';
811         }
812
813         $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
814         return true;
815     }
816
817     function _setTightness($top, $bot) {
818         if (count($this->_content) == 1) {
819             $li = &$this->_content[0];
820             $li->setTightness($top, $bot);
821         }
822         else {
823             // this is where php5 breaks
824             if (0 and DEBUG and check_php_version(5)) {
825                 if (function_exists("xdebug_get_function_stack")) {
826                     var_dump (xdebug_get_function_stack());
827                 } elseif (count($this->_content) != 2) {
828                     echo "<pre>";
829                     /*
830                     $class = new Reflection_Class('XmlElement');
831                     // Print out basic information
832                     printf(
833                            "===> The %s%s%s %s '%s' [extends %s]\n".
834                            "     declared in %s\n".
835                            "     lines %d to %d\n".
836                            "     having the modifiers %d [%s]\n",
837                            $class->isInternal() ? 'internal' : 'user-defined',
838                            $class->isAbstract() ? ' abstract' : '',
839                            $class->isFinal() ? ' final' : '',
840                            $class->isInterface() ? 'interface' : 'class',
841                            $class->getName(),
842                            var_export($class->getParentClass(), 1),
843                            $class->getFileName(),
844                            $class->getStartLine(),
845                            $class->getEndline(),
846                            $class->getModifiers(),
847                            implode(' ', Reflection::getModifierNames($class->getModifiers()))
848                            );
849                     // Print class properties
850                     printf("---> Properties: %s\n", var_export($class->getProperties(), 1));
851                     */
852                     echo 'count($this->_content): ', count($this->_content),"\n";
853                     echo "\$this->_content[0]: "; var_dump ($this->_content[0]);
854                     foreach ($this->_content as $c) {
855                         echo "_tag: "; var_dump ($c->_tag);
856                         echo "_content: "; var_dump ($c->_content);
857                         echo "_properties: "; var_dump ($c->_properties);
858                     }
859                     debug_print_backtrace();
860                     echo "</pre>";
861                 }
862             }
863             if (!check_php_version(5))
864                 assert(count($this->_content) == 2);
865             $dt = &$this->_content[0];
866             $dd = &$this->_content[1];
867             $dt->setTightness($top, false);
868             $dd->setTightness(false, $bot);
869         }
870     }
871 }
872
873 class Block_pre extends BlockMarkup
874 {
875     var $_re = '<(?:pre|verbatim)>';
876
877     function _match (&$input, $m) {
878         $endtag = '</' . substr($m->match, 1);
879         $text = array();
880         $pos = $input->getPos();
881
882         $line = $m->postmatch;
883         while (ltrim($line) != $endtag) {
884             $text[] = $line;
885             if (($line = $input->nextLine()) === false) {
886                 $input->setPos($pos);
887                 return false;
888             }
889         }
890         $input->advance();
891         
892         $text = join("\n", $text);
893         
894         // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
895         // in a <pre>.
896         if ($m->match == '<pre>')
897             $text = TransformInline($text);
898
899         $this->_element = new Block_HtmlElement('pre', false, $text);
900         return true;
901     }
902 }
903
904
905 class Block_plugin extends Block_pre
906 {
907     var $_re = '<\?plugin(?:-form)?(?!\S)';
908
909     // FIXME:
910     /* <?plugin Backlinks
911      *       page=ThisPage ?>
912      *
913      * should work. */
914     function _match (&$input, $m) {
915         $pos = $input->getPos();
916         $pi = $m->match . $m->postmatch;
917         while (!preg_match('/(?<!'.ESCAPE_CHAR.')\?>\s*$/', $pi)) {
918             if (($line = $input->nextLine()) === false) {
919                 $input->setPos($pos);
920                 return false;
921             }
922             $pi .= "\n$line";
923         }
924         $input->advance();
925
926         $this->_element = new Cached_PluginInvocation($pi);
927         return true;
928     }
929 }
930
931 class Block_email_blockquote extends BlockMarkup
932 {
933     var $_attr = array('class' => 'mail-style-quote');
934     var $_re = '>\ ?';
935     
936     function _match (&$input, $m) {
937         //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
938         $indent = $this->_re;
939         $this->_element = new SubBlock($input, $indent, $m->match,
940                                        'blockquote', $this->_attr);
941         return true;
942     }
943 }
944
945 class Block_hr extends BlockMarkup
946 {
947     var $_re = '-{4,}\s*$';
948
949     function _match (&$input, $m) {
950         $input->advance();
951         $this->_element = new Block_HtmlElement('hr');
952         return true;
953     }
954
955     function _setTightness($top, $bot) {
956         // Don't tighten <hr/>s
957     }
958 }
959
960 class Block_heading extends BlockMarkup
961 {
962     var $_re = '!{1,3}';
963     
964     function _match (&$input, $m) {
965         $tag = "h" . (5 - strlen($m->match));
966         $text = TransformInline(trim($m->postmatch));
967         $input->advance();
968
969         $this->_element = new Block_HtmlElement($tag, false, $text);
970         
971         return true;
972     }
973
974     function _setTightness($top, $bot) {
975         // Don't tighten headers.
976     }
977 }
978
979 class Block_p extends BlockMarkup
980 {
981     var $_tag = 'p';
982     var $_re = '\S.*';
983
984     function _match (&$input, $m) {
985         $this->_text = $m->match;
986         $input->advance();
987         return true;
988     }
989
990     function _setTightness ($top, $bot) {
991         $this->_tight_top = $top;
992         $this->_tight_bot = $bot;
993     }
994
995     function merge ($nextBlock) {
996         $class = get_class($nextBlock);
997         if ($class == 'block_p' && $this->_tight_bot) {
998             $this->_text .= "\n" . $nextBlock->_text;
999             $this->_tight_bot = $nextBlock->_tight_bot;
1000             return $this;
1001         }
1002         return false;
1003     }
1004             
1005     function finish () {
1006         $content = TransformInline(trim($this->_text));
1007         $p = new Block_HtmlElement('p', false, $content);
1008         $p->setTightness($this->_tight_top, $this->_tight_bot);
1009         return $p;
1010     }
1011 }
1012
1013 ////////////////////////////////////////////////////////////////
1014 //
1015
1016 function TransformText ($text, $markup = 2.0, $basepage=false) {
1017     if (isa($text, 'WikiDB_PageRevision')) {
1018         $rev = $text;
1019         $text = $rev->getPackedContent();
1020         $markup = $rev->get('markup');
1021     }
1022
1023     if (empty($markup) || $markup < 2.0) {
1024         //include_once("lib/transform.php");
1025         //return do_transform($text);
1026         $text = ConvertOldMarkup($text);
1027     }
1028     
1029     // Expand leading tabs.
1030     $text = expand_tabs($text);
1031
1032     //set_time_limit(3);
1033
1034     $output = new WikiText($text);
1035     if (0 && DEBUG && check_php_version(5)) {
1036         echo "<pre>"; var_dump($output); echo "</pre>"; 
1037     }
1038
1039     if ($basepage) {
1040         // This is for immediate consumption.
1041         // We must bind the contents to a base pagename so that
1042         // relative page links can be properly linkified...
1043         return new CacheableMarkup($output->getContent(), $basepage);
1044     }
1045     
1046     return new XmlContent($output->getContent());
1047 }
1048
1049 // (c-file-style: "gnu")
1050 // Local Variables:
1051 // mode: php
1052 // tab-width: 8
1053 // c-basic-offset: 4
1054 // c-hanging-comment-ender-p: nil
1055 // indent-tabs-mode: nil
1056 // End:   
1057 ?>