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