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