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