]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Wikicreole placeholder
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id$');
2 /* Copyright (C) 2002 Geoffrey T. Dairiki <dairiki@dairiki.org>
3  * Copyright (C) 2004,2005 Reini Urban
4  * Copyright (C) 2008-2009 Marc-Etienne Vargenau, Alcatel-Lucent
5  *
6  * This file is part of PhpWiki.
7  * 
8  * PhpWiki is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  * 
13  * PhpWiki is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  * 
18  * You should have received a copy of the GNU General Public License
19  * along with PhpWiki; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  */
22 require_once('lib/HtmlElement.php');
23 require_once('lib/CachedMarkup.php');
24 require_once('lib/InlineParser.php');
25
26 /**
27  * Deal with paragraphs and proper, recursive block indents 
28  * for the new style markup (version 2)
29  *
30  * Everything which goes over more than line:
31  * automatic lists, UL, OL, DL, table, blockquote, verbatim, 
32  * p, pre, plugin, ...
33  *
34  * FIXME:
35  *  Still to do:
36  *    (old-style) tables
37  * FIXME: unify this with the RegexpSet in InlineParser.
38  *
39  * FIXME: This is very php5 sensitive: It was fixed for 1.3.9, 
40  *        but is again broken with the 1.3.11 
41  *        allow_call_time_pass_reference clean fixes
42  *
43  * @package Markup
44  * @author: Geoffrey T. Dairiki 
45  */
46
47 /**
48  * Return type from RegexpSet::match and RegexpSet::nextMatch.
49  *
50  * @see RegexpSet
51  */
52 class AnchoredRegexpSet_match {
53     /**
54      * The matched text.
55      */
56     var $match;
57
58     /**
59      * The text following the matched text.
60      */
61     var $postmatch;
62
63     /**
64      * Index of the regular expression which matched.
65      */
66     var $regexp_ind;
67 }
68
69 /**
70  * A set of regular expressions.
71  *
72  * This class is probably only useful for InlineTransformer.
73  */
74 class AnchoredRegexpSet
75 {
76     /** Constructor
77      *
78      * @param $regexps array A list of regular expressions.  The
79      * regular expressions should not include any sub-pattern groups
80      * "(...)".  (Anonymous groups, like "(?:...)", as well as
81      * look-ahead and look-behind assertions are fine.)
82      */
83     function AnchoredRegexpSet ($regexps) {
84         $this->_regexps = $regexps;
85         $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
86     }
87
88     /**
89      * Search text for the next matching regexp from the Regexp Set.
90      *
91      * @param $text string The text to search.
92      *
93      * @return object  A RegexpSet_match object, or false if no match.
94      */
95     function match ($text) {
96         if (!is_string($text)) return false;
97         if (! preg_match($this->_re, $text, $m)) {
98             return false;
99         }
100         
101         $match = new AnchoredRegexpSet_match;
102         $match->postmatch = substr($text, strlen($m[0]));
103         $match->match = $m[1];
104         $match->regexp_ind = count($m) - 3;
105         return $match;
106     }
107
108     /**
109      * Search for next matching regexp.
110      *
111      * Here, 'next' has two meanings:
112      *
113      * Match the next regexp(s) in the set, at the same position as the last match.
114      *
115      * If that fails, match the whole RegexpSet, starting after the position of the
116      * previous match.
117      *
118      * @param $text string Text to search.
119      *
120      * @param $prevMatch A RegexpSet_match object
121      *
122      * $prevMatch should be a match object obtained by a previous
123      * match upon the same value of $text.
124      *
125      * @return object  A RegexpSet_match object, or false if no match.
126      */
127     function nextMatch ($text, $prevMatch) {
128         // Try to find match at same position.
129         $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
130         if (!$regexps) {
131             return false;
132         }
133
134         $pat= "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
135
136         if (! preg_match($pat, $text, $m)) {
137             return false;
138         }
139         
140         $match = new AnchoredRegexpSet_match;
141         $match->postmatch = substr($text, strlen($m[0]));
142         $match->match = $m[1];
143         $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;;
144         return $match;
145     }
146 }
147
148
149     
150 class BlockParser_Input {
151
152     function BlockParser_Input ($text) {
153         
154         // Expand leading tabs.
155         // FIXME: do this better.
156         //
157         // We want to ensure the only characters matching \s are ' ' and "\n".
158         //
159         $text = preg_replace('/(?![ \n])\s/', ' ', $text);
160         assert(!preg_match('/(?![ \n])\s/', $text));
161
162         $this->_lines = preg_split('/[^\S\n]*\n/', $text);
163         $this->_pos = 0;
164
165         // Strip leading blank lines.
166         while ($this->_lines and ! $this->_lines[0])
167             array_shift($this->_lines);
168         $this->_atSpace = false;
169     }
170
171     function skipSpace () {
172         $nlines = count($this->_lines);
173         while (1) {
174             if ($this->_pos >= $nlines) {
175                 $this->_atSpace = false;
176                 break;
177             }
178             if ($this->_lines[$this->_pos] != '')
179                 break;
180             $this->_pos++;
181             $this->_atSpace = true;
182         }
183         return $this->_atSpace;
184     }
185         
186     function currentLine () {
187         if ($this->_pos >= count($this->_lines)) {
188             return false;
189         }
190         return $this->_lines[$this->_pos];
191     }
192         
193     function nextLine () {
194         $this->_atSpace = $this->_lines[$this->_pos++] === '';
195         if ($this->_pos >= count($this->_lines)) {
196             return false;
197         }
198         return $this->_lines[$this->_pos];
199     }
200
201     function advance () {
202         $this->_atSpace = ($this->_lines[$this->_pos] === '');
203         $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         flush();                   
237     }
238 }
239
240 class BlockParser_InputSubBlock extends BlockParser_Input
241 {
242     function BlockParser_InputSubBlock (&$input, $prefix_re, $initial_prefix = false) {
243         $this->_input = &$input;
244         $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
245         $this->_atSpace = false;
246
247         if (($line = $input->currentLine()) === false)
248             $this->_line = false;
249         elseif ($initial_prefix) {
250             assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
251             $this->_line = (string) substr($line, strlen($initial_prefix));
252             $this->_atBlank = ! ltrim($line);
253         }
254         elseif (preg_match($this->_prefix_pat, $line, $m)) {
255             $this->_line = (string) substr($line, strlen($m[0]));
256             $this->_atBlank = ! ltrim($line);
257         }
258         else
259             $this->_line = false;
260     }
261
262     function skipSpace () {
263         // In contrast to the case for top-level blocks,
264         // for sub-blocks, there never appears to be any trailing space.
265         // (The last block in the sub-block should always be of class tight-bottom.)
266         while ($this->_line === '')
267             $this->advance();
268
269         if ($this->_line === false)
270             return $this->_atSpace == 'strong_space';
271         else
272             return $this->_atSpace;
273     }
274         
275     function currentLine () {
276         return $this->_line;
277     }
278
279     function nextLine () {
280         if ($this->_line === '')
281             $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
282         else
283             $this->_atSpace = false;
284
285         $line = $this->_input->nextLine();
286         if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
287             $this->_line = (string) substr($line, strlen($m[0]));
288             $this->_atBlank = ! ltrim($line);
289         }
290         else
291             $this->_line = false;
292
293         return $this->_line;
294     }
295
296     function advance () {
297         $this->nextLine();
298     }
299         
300     function getPos () {
301         return array($this->_line, $this->_atSpace, $this->_input->getPos());
302     }
303
304     function setPos ($pos) {
305         $this->_line = $pos[0];
306         $this->_atSpace = $pos[1];
307         $this->_input->setPos($pos[2]);
308     }
309     
310     function getPrefix () {
311         assert ($this->_line !== false);
312         $line = $this->_input->currentLine();
313         assert ($line !== false && strlen($line) >= strlen($this->_line));
314         return substr($line, 0, strlen($line) - strlen($this->_line));
315     }
316
317     function getDepth () {
318         return $this->_input->getDepth() + 1;
319     }
320
321     function where () {
322         return $this->_input->where();
323     }
324 }
325     
326
327 class Block_HtmlElement extends HtmlElement
328 {
329     function Block_HtmlElement($tag /*, ... */) {
330         $this->_init(func_get_args());
331     }
332
333     function setTightness($top, $bottom) {
334     }
335 }
336
337 class ParsedBlock extends Block_HtmlElement {
338     
339     function ParsedBlock (&$input, $tag = 'div', $attr = false) {
340         $this->Block_HtmlElement($tag, $attr);
341         $this->_initBlockTypes();
342         $this->_parse($input);
343     }
344
345     function _parse (&$input) {
346         // php5 failed to advance the block. php5 copies objects by ref.
347         // nextBlock == block, both are the same objects. So we have to clone it.
348         for ($block = $this->_getBlock($input); 
349              $block; 
350              $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock))
351         {
352             while ($nextBlock = $this->_getBlock($input)) {
353                 // Attempt to merge current with following block.
354                 if (! ($merged = $block->merge($nextBlock)) ) {
355                     break;      // can't merge
356                 }
357                 $block = $merged;
358             }
359             $this->pushContent($block->finish());
360         }
361     }
362
363     // FIXME: hackish. This should only be called once.
364     function _initBlockTypes () {
365         // better static or global?
366         static $_regexpset, $_block_types;
367
368         if (!is_object($_regexpset)) {
369             // nowiki_wikicreole must be before template_plugin
370             $Block_types = array
371                     ('nowiki_wikicreole', 'template_plugin', 'placeholder', 'oldlists', 'list', 'dl', 'table_dl', 'table_wikicreole', 'table_mediawiki',
372                      'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre', 'email_blockquote',
373                      'plugin', 'plugin_wikicreole', 'p');
374             // insert it before p!
375             if (ENABLE_MARKUP_DIVSPAN) {
376                 array_pop($Block_types);
377                 $Block_types[] = 'divspan';
378                 $Block_types[] = 'p';
379             }
380             foreach ($Block_types as $type) {
381                 $class = "Block_$type";
382                 $proto = new $class;
383                 $this->_block_types[] = $proto;
384                 $this->_regexps[] = $proto->_re;
385             }
386             $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
387             $_regexpset = $this->_regexpset;
388             $_block_types = $this->_block_types;
389             unset($Block_types);
390         } else {
391              $this->_regexpset = $_regexpset;
392              $this->_block_types = $_block_types;
393         }
394     }
395
396     function _getBlock (&$input) {
397         $this->_atSpace = $input->skipSpace();
398
399         $line = $input->currentLine();
400         if ($line === false or $line === '') { // allow $line === '0' 
401             return false;
402         }
403         $tight_top = !$this->_atSpace;
404         $re_set = &$this->_regexpset;
405         //FIXME: php5 fails to advance here!
406         for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
407             $block = clone($this->_block_types[$m->regexp_ind]);
408             if (DEBUG & _DEBUG_PARSER)
409                 $input->_debug('>', get_class($block));
410             
411             if ($block->_match($input, $m)) {
412                 //$block->_text = $line;
413                 if (DEBUG & _DEBUG_PARSER)
414                     $input->_debug('<', get_class($block));
415                 $tight_bottom = ! $input->skipSpace();
416                 $block->_setTightness($tight_top, $tight_bottom);
417                 return $block;
418             }
419             if (DEBUG & _DEBUG_PARSER)
420                 $input->_debug('[', "_match failed");
421         }
422         if ($line === false or $line === '') // allow $line === '0' 
423             return false;
424
425         trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
426         return false;
427     }
428 }
429
430 class WikiText extends ParsedBlock {
431     function WikiText ($text) {
432         $input = new BlockParser_Input($text);
433         $this->ParsedBlock($input);
434     }
435 }
436
437 class SubBlock extends ParsedBlock {
438     function SubBlock (&$input, $indent_re, $initial_indent = false,
439                        $tag = 'div', $attr = false) {
440         $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
441         $this->ParsedBlock($subinput, $tag, $attr);
442     }
443 }
444
445 /**
446  * TightSubBlock is for use in parsing lists item bodies.
447  *
448  * If the sub-block consists of a single paragraph, it omits
449  * the paragraph element.
450  *
451  * We go to this trouble so that "tight" lists look somewhat reasonable
452  * in older (non-CSS) browsers.  (If you don't do this, then, without
453  * CSS, you only get "loose" lists.
454  */
455 class TightSubBlock extends SubBlock {
456     function TightSubBlock (&$input, $indent_re, $initial_indent = false,
457                             $tag = 'div', $attr = false) {
458         $this->SubBlock($input, $indent_re, $initial_indent, $tag, $attr);
459
460         // If content is a single paragraph, eliminate the paragraph...
461         if (count($this->_content) == 1) {
462             $elem = $this->_content[0];
463             if (isa($elem, 'XmlElement') and $elem->getTag() == 'p') {
464                 $this->setContent($elem->getContent());
465             }
466         }
467     }
468 }
469
470 class BlockMarkup {
471     var $_re;
472
473     function _match (&$input, $match) {
474         trigger_error('pure virtual', E_USER_ERROR);
475     }
476
477     function _setTightness ($top, $bot) {
478         // $this->_element->setTightness($top, $bot);
479     }
480
481     function merge ($followingBlock) {
482         return false;
483     }
484
485     function finish () {
486         return $this->_element;
487     }
488 }
489
490 class Block_blockquote extends BlockMarkup
491 {
492     var $_depth;
493     var $_re = '\ +(?=\S)';
494
495     function _match (&$input, $m) {
496         $this->_depth = strlen($m->match);
497         $indent = sprintf("\\ {%d}", $this->_depth);
498         $this->_element = new SubBlock($input, $indent, $m->match,
499                                        'blockquote');
500         return true;
501     }
502     
503     function merge ($nextBlock) {
504         if (get_class($nextBlock) == get_class($this)) {
505             assert ($nextBlock->_depth < $this->_depth);
506             $nextBlock->_element->unshiftContent($this->_element);
507             if (!empty($this->_tight_top))
508             $nextBlock->_tight_top = $this->_tight_top;
509             return $nextBlock;
510         }
511         return false;
512     }
513 }
514
515 class Block_list extends BlockMarkup
516 {
517     //var $_tag = 'ol' or 'ul';
518     var $_re = '\ {0,4}
519                 (?: \+
520                   | \\# (?!\[.*\])
521                   | -(?!-)
522                   | [o](?=\ )
523                   | [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
524                 )\ *(?=\S)';
525     var $_content = array();
526
527     function _match (&$input, $m) {
528         // A list as the first content in a list is not allowed.
529         // E.g.:
530         //   *  * Item
531         // Should markup as <ul><li>* Item</li></ul>,
532         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
533         //
534         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
535             return false;
536         }
537         
538         $prefix = $m->match;
539         $indent = sprintf("\\ {%d}", strlen($prefix));
540
541         $bullet = trim($m->match);
542         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
543         $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
544         return true;
545     }
546
547     function _setTightness($top, $bot) {
548         $li = &$this->_content[0];
549         $li->setTightness($top, $bot);
550     }
551     
552     function merge ($nextBlock) {
553         if (isa($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
554             if ($nextBlock->_content === $this->_content) {
555                 trigger_error("Internal Error: no block advance", E_USER_NOTICE);
556                 return false;
557             }
558             array_splice($this->_content, count($this->_content), 0,
559                          $nextBlock->_content);
560             return $this;
561         }
562         return false;
563     }
564
565     function finish () {
566         return new Block_HtmlElement($this->_tag, false, $this->_content);
567     }
568 }
569
570 class Block_dl extends Block_list
571 {
572     var $_tag = 'dl';
573
574     function Block_dl () {
575         $this->_re = '\ {0,4}\S.*(?<!'.ESCAPE_CHAR.'):\s*$';
576     }
577
578     function _match (&$input, $m) {
579         if (!($p = $this->_do_match($input, $m)))
580             return false;
581         list ($term, $defn, $loose) = $p;
582
583         $this->_content[] = new Block_HtmlElement('dt', false, $term);
584         $this->_content[] = $defn;
585         $this->_tight_defn = !$loose;
586         return true;
587     }
588
589     function _setTightness($top, $bot) {
590         $dt = &$this->_content[0];
591         $dd = &$this->_content[1];
592
593         $dt->setTightness($top, $this->_tight_defn);
594         $dd->setTightness($this->_tight_defn, $bot);
595     }
596
597     function _do_match (&$input, $m) {
598         $pos = $input->getPos();
599
600         $firstIndent = strspn($m->match, ' ');
601         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
602
603         $input->advance();
604         $loose = $input->skipSpace();
605         $line = $input->currentLine();
606
607         if (!$line || !preg_match($pat, $line, $mm)) {
608             $input->setPos($pos);
609             return false;       // No body found.
610         }
611
612         $indent = strlen($mm[0]);
613         $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
614         $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
615         return array($term, $defn, $loose);
616     }
617 }
618
619
620
621 class Block_table_dl_defn extends XmlContent
622 {
623     var $nrows;
624     var $ncols;
625     
626     function Block_table_dl_defn ($term, $defn) {
627         $this->XmlContent();
628         if (!is_array($defn))
629             $defn = $defn->getContent();
630
631         $this->_next_tight_top = false; // value irrelevant - gets fixed later
632         $this->_ncols = $this->_ComputeNcols($defn);
633         $this->_nrows = 0;
634
635         foreach ($defn as $item) {
636             if ($this->_IsASubtable($item))
637                 $this->_addSubtable($item);
638             else
639                 $this->_addToRow($item);
640         }
641         $this->_flushRow();
642
643         $th = HTML::th($term);
644         if ($this->_nrows > 1)
645             $th->setAttr('rowspan', $this->_nrows);
646         $this->_setTerm($th);
647     }
648
649     function setTightness($tight_top, $tight_bot) {
650         $this->_tight_top = $tight_top;
651         $this->_tight_bot = $tight_bot;
652         $first = &$this->firstTR();
653         $last  = &$this->lastTR();
654         $first->setInClass('top', $tight_top);
655         if (!empty($last)) {
656             $last->setInClass('bottom', $tight_bot);
657         } else {
658             trigger_error(sprintf("no lastTR: %s",AsXML($this->_content[0])), E_USER_WARNING);
659         }
660     }
661     
662     function _addToRow ($item) {
663         if (empty($this->_accum)) {
664             $this->_accum = HTML::td();
665             if ($this->_ncols > 2)
666                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
667         }
668         $this->_accum->pushContent($item);
669     }
670
671     function _flushRow ($tight_bottom=false) {
672         if (!empty($this->_accum)) {
673             $row = new Block_HtmlElement('tr', false, $this->_accum);
674
675             $row->setTightness($this->_next_tight_top, $tight_bottom);
676             $this->_next_tight_top = $tight_bottom;
677             
678             $this->pushContent($row);
679             $this->_accum = false;
680             $this->_nrows++;
681         }
682     }
683
684     function _addSubtable ($table) {
685         if (!($table_rows = $table->getContent()))
686             return;
687
688         $this->_flushRow($table_rows[0]->_tight_top);
689             
690         foreach ($table_rows as $subdef) {
691             $this->pushContent($subdef);
692             $this->_nrows += $subdef->nrows();
693             $this->_next_tight_top = $subdef->_tight_bot;
694         }
695     }
696
697     function _setTerm ($th) {
698         $first_row = &$this->_content[0];
699         if (isa($first_row, 'Block_table_dl_defn'))
700             $first_row->_setTerm($th);
701         else
702             $first_row->unshiftContent($th);
703     }
704     
705     function _ComputeNcols ($defn) {
706         $ncols = 2;
707         foreach ($defn as $item) {
708             if ($this->_IsASubtable($item)) {
709                 $row = $this->_FirstDefn($item);
710                 $ncols = max($ncols, $row->ncols() + 1);
711             }
712         }
713         return $ncols;
714     }
715
716     function _IsASubtable ($item) {
717         return isa($item, 'HtmlElement')
718             && $item->getTag() == 'table'
719             && $item->getAttr('class') == 'wiki-dl-table';
720     }
721
722     function _FirstDefn ($subtable) {
723         $defs = $subtable->getContent();
724         return $defs[0];
725     }
726
727     function ncols () {
728         return $this->_ncols;
729     }
730
731     function nrows () {
732         return $this->_nrows;
733     }
734
735     function & firstTR() {
736         $first = &$this->_content[0];
737         if (isa($first, 'Block_table_dl_defn'))
738             return $first->firstTR();
739         return $first;
740     }
741
742     function & lastTR() {
743         $last = &$this->_content[$this->_nrows - 1];
744         if (isa($last, 'Block_table_dl_defn'))
745             return $last->lastTR();
746         return $last;
747     }
748
749     function setWidth ($ncols) {
750         assert($ncols >= $this->_ncols);
751         if ($ncols <= $this->_ncols)
752             return;
753         $rows = &$this->_content;
754         for ($i = 0; $i < count($rows); $i++) {
755             $row = &$rows[$i];
756             if (isa($row, 'Block_table_dl_defn'))
757                 $row->setWidth($ncols - 1);
758             else {
759                 $n = count($row->_content);
760                 $lastcol = &$row->_content[$n - 1];
761                 if (!empty($lastcol))
762                   $lastcol->setAttr('colspan', $ncols - 1);
763             }
764         }
765     }
766 }
767
768 class Block_table_dl extends Block_dl
769 {
770     var $_tag = 'dl-table';     // phony.
771
772     function Block_table_dl() {
773         $this->_re = '\ {0,4} (?:\S.*)? (?<!'.ESCAPE_CHAR.') \| \s* $';
774     }
775
776     function _match (&$input, $m) {
777         if (!($p = $this->_do_match($input, $m)))
778             return false;
779         list ($term, $defn, $loose) = $p;
780
781         $this->_content[] = new Block_table_dl_defn($term, $defn);
782         return true;
783     }
784
785     function _setTightness($top, $bot) {
786         $this->_content[0]->setTightness($top, $bot);
787     }
788     
789     function finish () {
790
791         $defs = &$this->_content;
792
793         $ncols = 0;
794         foreach ($defs as $defn)
795             $ncols = max($ncols, $defn->ncols());
796         
797         foreach ($defs as $key => $defn)
798             $defs[$key]->setWidth($ncols);
799
800         return HTML::table(array('class' => 'wiki-dl-table',
801                                  'border' => 1,
802                                  'cellspacing' => 0,
803                                  'cellpadding' => 6),
804                            $defs);
805     }
806 }
807
808 class Block_oldlists extends Block_list
809 {
810     //var $_tag = 'ol', 'ul', or 'dl';
811     var $_re = '(?: [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
812                   | [#] (?! \[ .*? \] )
813                   | ; .*? :
814                 ) .*? (?=\S)';
815
816     function _match (&$input, $m) {
817         // FIXME:
818         if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
819             return false;
820         }
821         
822
823         $prefix = $m->match;
824         $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
825         $newindent = sprintf('\\ {%d}', strlen($prefix));
826         $indent = "(?:$oldindent|$newindent)";
827
828         $bullet = $prefix[0];
829         if ($bullet == '*') {
830             $this->_tag = 'ul';
831             $itemtag = 'li';
832         }
833         elseif ($bullet == '#') {
834             $this->_tag = 'ol';
835             $itemtag = 'li';
836         }
837         else {
838             $this->_tag = 'dl';
839             list ($term,) = explode(':', substr($prefix, 1), 2);
840             $term = trim($term);
841             if ($term)
842                 $this->_content[] = new Block_HtmlElement('dt', false,
843                                                           TransformInline($term));
844             $itemtag = 'dd';
845         }
846
847         $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
848         return true;
849     }
850
851     function _setTightness($top, $bot) {
852         if (count($this->_content) == 1) {
853             $li = &$this->_content[0];
854             $li->setTightness($top, $bot);
855         }
856         else {
857             // This is where php5 usually brakes.
858             // wrong duplicated <li> contents
859             if (DEBUG and DEBUG & _DEBUG_PARSER and check_php_version(5)) {
860                 if (count($this->_content) != 2) {
861                     echo "<pre>";
862                     /*
863                     $class = new Reflection_Class('XmlElement');
864                     // Print out basic information
865                     printf(
866                            "===> The %s%s%s %s '%s' [extends %s]\n".
867                            "     declared in %s\n".
868                            "     lines %d to %d\n".
869                            "     having the modifiers %d [%s]\n",
870                            $class->isInternal() ? 'internal' : 'user-defined',
871                            $class->isAbstract() ? ' abstract' : '',
872                            $class->isFinal() ? ' final' : '',
873                            $class->isInterface() ? 'interface' : 'class',
874                            $class->getName(),
875                            var_export($class->getParentClass(), 1),
876                            $class->getFileName(),
877                            $class->getStartLine(),
878                            $class->getEndline(),
879                            $class->getModifiers(),
880                            implode(' ', Reflection::getModifierNames($class->getModifiers()))
881                            );
882                     // Print class properties
883                     printf("---> Properties: %s\n", var_export($class->getProperties(), 1));
884                     */
885                     echo 'count($this->_content): ', count($this->_content),"\n";
886                     echo "\$this->_content[0]: "; var_dump ($this->_content[0]);
887                     
888                     for ($i=1; $i < min(5, count($this->_content)); $i++) {
889                         $c =& $this->_content[$i];
890                         echo '$this->_content[',$i,"]: \n";
891                         echo "_tag: "; var_dump ($c->_tag);
892                         echo "_content: "; var_dump ($c->_content);
893                         echo "_properties: "; var_dump ($c->_properties);
894                     }
895                     debug_print_backtrace();
896                     if (DEBUG & _DEBUG_APD) {
897                         if (function_exists("xdebug_get_function_stack")) {
898                             var_dump (xdebug_get_function_stack());
899                         }
900                     }
901                     echo "</pre>";
902                 }
903             }
904             if (!check_php_version(5))
905                 assert(count($this->_content) == 2);
906             $dt = &$this->_content[0];
907             $dd = &$this->_content[1];
908             $dt->setTightness($top, false);
909             $dd->setTightness(false, $bot);
910         }
911     }
912 }
913
914 class Block_pre extends BlockMarkup
915 {
916     var $_re = '<(?:pre|verbatim|nowiki|noinclude)>';
917
918     function _match (&$input, $m) {
919         $endtag = '</' . substr($m->match, 1);
920         $text = array();
921         $pos = $input->getPos();
922
923         $line = $m->postmatch;
924         while (ltrim($line) != $endtag) {
925             $text[] = $line;
926             if (($line = $input->nextLine()) === false) {
927                 $input->setPos($pos);
928                 return false;
929             }
930         }
931         $input->advance();
932         
933         if ($m->match == '<nowiki>')
934             $text = join("<br>\n", $text);
935         else
936             $text = join("\n", $text);
937         
938         // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
939         // in a <pre>.
940         if ($m->match == '<pre>') {
941             $text = TransformInline($text);
942         }
943         if ($m->match == '<noinclude>') {
944             $text = TransformText($text);
945             $this->_element = new Block_HtmlElement('div', false, $text);
946         } else if ($m->match == '<nowiki>') {
947             $text = TransformInlineNowiki($text);
948             $this->_element = new Block_HtmlElement('p', false, $text);
949         } else {
950             $this->_element = new Block_HtmlElement('pre', false, $text);
951         }
952         return true;
953     }
954 }
955
956 // Wikicreole placeholder
957 // <<<placeholder>>>
958 class Block_placeholder extends BlockMarkup
959 {
960     var $_re = '<<<';
961
962     function _match (&$input, $m) {
963         $endtag = '>>>';
964         $text = array();
965         $pos = $input->getPos();
966
967         $line = $m->postmatch;
968         while (ltrim($line) != $endtag) {
969             $text[] = $line;
970             if (($line = $input->nextLine()) === false) {
971                 $input->setPos($pos);
972                 return false;
973             }
974         }
975         $input->advance();
976
977         $text = join("\n", $text);
978         $text = '<<<' . $text . '>>>';
979         $this->_element = new Block_HtmlElement('div', false, $text);
980         return true;
981     }
982 }
983
984 class Block_nowiki_wikicreole extends BlockMarkup
985 {
986     var $_re = '{{{';
987
988     function _match (&$input, $m) {
989         $endtag = '}}}';
990         $text = array();
991         $pos = $input->getPos();
992
993         $line = $m->postmatch;
994         while (ltrim($line) != $endtag) {
995             $text[] = $line;
996             if (($line = $input->nextLine()) === false) {
997                 $input->setPos($pos);
998                 return false;
999             }
1000         }
1001         $input->advance();
1002
1003         $text = join("\n", $text);
1004         $this->_element = new Block_HtmlElement('pre', false, $text);
1005         return true;
1006     }
1007 }
1008
1009 class Block_plugin extends Block_pre
1010 {
1011     var $_re = '<\?plugin(?:-form)?(?!\S)';
1012
1013     // FIXME:
1014     /* <?plugin Backlinks
1015      *       page=ThisPage ?>
1016     /* <?plugin ListPages pages=<!plugin-list Backlinks!>
1017      *                    exclude=<!plugin-list TitleSearch s=T*!> ?>
1018      *
1019      * should all work.
1020      */
1021     function _match (&$input, $m) {
1022         $pos = $input->getPos();
1023         $pi = $m->match . $m->postmatch;
1024         while (!preg_match('/(?<!'.ESCAPE_CHAR.')\?>\s*$/', $pi)) {
1025             if (($line = $input->nextLine()) === false) {
1026                 $input->setPos($pos);
1027                 return false;
1028             }
1029             $pi .= "\n$line";
1030         }
1031         $input->advance();
1032
1033         $this->_element = new Cached_PluginInvocation($pi);
1034         return true;
1035     }
1036 }
1037
1038 class Block_plugin_wikicreole extends Block_pre
1039 {
1040     // var $_re = '<<(?!\S)';
1041     var $_re = '<<';
1042
1043     function _match (&$input, $m) {
1044         $pos = $input->getPos();
1045         $pi = "<?plugin " . $m->postmatch;
1046         while (!preg_match('/(?<!'.ESCAPE_CHAR.')>>\s*$/', $pi)) {
1047             if (($line = $input->nextLine()) === false) {
1048                 $input->setPos($pos);
1049                 return false;
1050             }
1051             $pi .= "\n$line";
1052         }
1053         $input->advance();
1054
1055         $pi = str_replace(">>", "?>", $pi);
1056
1057         $this->_element = new Cached_PluginInvocation($pi);
1058         return true;
1059     }
1060 }
1061
1062 class Block_table_wikicreole extends Block_pre
1063 {
1064     var $_re = '\s*\|';
1065
1066     function _match (&$input, $m) {
1067         $pos = $input->getPos();
1068         $pi = "|" . $m->postmatch;
1069
1070         $intable = true;
1071         while ($intable) {
1072             if (($line = $input->nextLine()) === false) {
1073                 $input->setPos($pos);
1074                 return false;
1075             } 
1076             if (!$line) {
1077                 $intable = false;
1078                 $trimline = $line;
1079             } else {
1080                 $trimline = trim($line);
1081                 if ($trimline[0] != "|") {
1082                     $intable = false;
1083                 }
1084             }
1085             $pi .= "\n$trimline";
1086         }
1087
1088         $pi = '<'.'?plugin WikicreoleTable ' . $pi . '?'.'>';
1089
1090         $this->_element = new Cached_PluginInvocation($pi);
1091         return true;
1092     }
1093 }
1094
1095 /** ENABLE_MARKUP_MEDIAWIKI_TABLE
1096  *  Table syntax similar to Mediawiki
1097  *  {|
1098  * => <?plugin MediawikiTable
1099  *  |}
1100  * => ?>
1101  */
1102 class Block_table_mediawiki extends Block_pre
1103 {
1104     var $_re = '{\|';
1105
1106     function _match (&$input, $m) {
1107         $pos = $input->getPos();
1108         $pi = $m->postmatch;
1109         while (!preg_match('/(?<!'.ESCAPE_CHAR.')\|}\s*$/', $pi)) {
1110             if (($line = $input->nextLine()) === false) {
1111                 $input->setPos($pos);
1112                 return false;
1113             }
1114             $pi .= "\n$line";
1115         }
1116         $input->advance();
1117
1118         $pi = str_replace("\|}", "", $pi);
1119         $pi = '<'.'?plugin MediawikiTable ' . $pi . '?'.'>';
1120         $this->_element = new Cached_PluginInvocation($pi);
1121         return true;
1122     }
1123 }
1124
1125 class Block_template_plugin extends Block_pre
1126 {
1127     var $_re = '{{';
1128
1129     function _match (&$input, $m) {
1130         $pos = $input->getPos();
1131         // $pi = "<?plugin Template page=" . $m->postmatch;
1132         $pi = $m->postmatch;
1133         while (!preg_match('/(?<!'.ESCAPE_CHAR.')}}\s*$/', $pi)) {
1134             if (($line = $input->nextLine()) === false) {
1135                 $input->setPos($pos);
1136                 return false;
1137             }
1138             $pi .= "\n$line";
1139         }
1140         $input->advance();
1141
1142         $pi = trim($pi);
1143         $pi = trim($pi, "}}");
1144
1145         if (strpos($pi, "|") === false) {
1146             $imagename = $pi;
1147             $alt = "";
1148         } else {
1149             $imagename = substr($pi, 0, strpos($pi, "|"));
1150             $alt = ltrim(strstr($pi, "|"), "|");
1151         }
1152
1153         // It's not a Mediawiki template, it's a Wikicreole image
1154         if (is_image($imagename)) {
1155             $this->_element = LinkImage(UPLOAD_DATA_PATH . $imagename, $alt);
1156             return true;
1157         }
1158
1159         $pi = str_replace("\n", "", $pi);
1160         $vars = '';
1161
1162         if (preg_match('/^(\S+?)\|(.*)$/', $pi, $_m)) {
1163             $pi = $_m[1];
1164             $vars = '"' . preg_replace('/\|/', '" "', $_m[2]) . '"';
1165             $vars = preg_replace('/"(\S+)=([^"]*)"/', '\\1="\\2"', $vars);
1166         }
1167
1168         // pi may contain a version number
1169         // {{foo?version=5}}
1170         // in that case, output is "page=foo rev=5"
1171         if (strstr($pi, "?")) {
1172             $pi = str_replace("?version=", "\" rev=\"", $page);
1173         }
1174
1175         if ($vars)
1176             $pi = '<'.'?plugin Template page="'.$pi.'" '.$vars . ' ?>';
1177         else
1178             $pi = '<'.'?plugin Template page="' . $pi . '" ?>';
1179         $this->_element = new Cached_PluginInvocation($pi);
1180         return true;
1181     }
1182 }
1183
1184 class Block_email_blockquote extends BlockMarkup
1185 {
1186     var $_attr = array('class' => 'mail-style-quote');
1187     var $_re = '>\ ?';
1188     
1189     function _match (&$input, $m) {
1190         //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1191         $indent = $this->_re;
1192         $this->_element = new SubBlock($input, $indent, $m->match,
1193                                        'blockquote', $this->_attr);
1194         return true;
1195     }
1196 }
1197
1198 class Block_hr extends BlockMarkup
1199 {
1200     var $_re = '-{4,}\s*$';
1201
1202     function _match (&$input, $m) {
1203         $input->advance();
1204         $this->_element = new Block_HtmlElement('hr');
1205         return true;
1206     }
1207
1208     function _setTightness($top, $bot) {
1209         // Don't tighten <hr/>s
1210     }
1211 }
1212
1213 class Block_heading extends BlockMarkup
1214 {
1215     var $_re = '!{1,3}';
1216     
1217     function _match (&$input, $m) {
1218         $tag = "h" . (5 - strlen($m->match));
1219         $text = TransformInline(trim($m->postmatch));
1220         $input->advance();
1221
1222         $this->_element = new Block_HtmlElement($tag, false, $text);
1223         
1224         return true;
1225     }
1226
1227     function _setTightness($top, $bot) {
1228         // Don't tighten headers.
1229     }
1230 }
1231
1232 class Block_heading_wikicreole extends BlockMarkup
1233 {
1234     var $_re = '={2,6}';
1235     
1236     function _match (&$input, $m) {
1237         $tag = "h" . strlen($m->match);
1238         // Remove spaces
1239         $header = trim($m->postmatch);
1240         // Remove '='s at the end so that Mediawiki syntax is recognized
1241         $header = trim($header, "=");
1242         $text = TransformInline(trim($header));
1243         $input->advance();
1244
1245         $this->_element = new Block_HtmlElement($tag, false, $text);
1246         
1247         return true;
1248     }
1249
1250     function _setTightness($top, $bot) {
1251         // Don't tighten headers.
1252     }
1253 }
1254
1255 class Block_p extends BlockMarkup
1256 {
1257     var $_tag = 'p';
1258     var $_re = '\S.*';
1259     var $_text = '';
1260
1261     function _match (&$input, $m) {
1262         $this->_text = $m->match;
1263         $input->advance();
1264         return true;
1265     }
1266
1267     function _setTightness ($top, $bot) {
1268         $this->_tight_top = $top;
1269         $this->_tight_bot = $bot;
1270     }
1271
1272     function merge ($nextBlock) {
1273         $class = get_class($nextBlock);
1274         if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1275             $this->_text .= "\n" . $nextBlock->_text;
1276             $this->_tight_bot = $nextBlock->_tight_bot;
1277             return $this;
1278         }
1279         return false;
1280     }
1281
1282     function finish () {
1283         $content = TransformInline(trim($this->_text));
1284         $p = new Block_HtmlElement('p', false, $content);
1285         $p->setTightness($this->_tight_top, $this->_tight_bot);
1286         return $p;
1287     }
1288 }
1289
1290 class Block_divspan extends BlockMarkup
1291 {
1292     var $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1293
1294     function _match (&$input, $m) {
1295         if (substr($m->match,1,4) == 'span') {
1296             $tag = 'span';
1297         } else {
1298             $tag = 'div';
1299         }
1300         // without last >
1301         $argstr = substr(trim(substr($m->match,strlen($tag)+1)),0,-1); 
1302         $pos = $input->getPos();
1303         $pi  = $content = $m->postmatch;
1304         while (!preg_match('/^(.*)\<\/'.$tag.'\>(.*)$/i', $pi, $me)) {
1305             if ($pi != $content)
1306                 $content .= "\n$pi";
1307             if (($pi = $input->nextLine()) === false) {
1308                 $input->setPos($pos);
1309                 return false;
1310             }
1311         }
1312         if ($pi != $content)
1313             $content .= $me[1]; // prematch
1314         else
1315             $content = $me[1];
1316         $input->advance();
1317         if (strstr($content, "\n"))
1318             $content = TransformText($content);
1319         else    
1320             $content = TransformInline($content);
1321         if (!$argstr) 
1322             $args = false;
1323         else {
1324             $args = array();
1325             while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1326                 $k = $m[1]; $v = $m[2];
1327                 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1328                     $v = $m[1];
1329                     $argstr = $m[2];
1330                 } else {
1331                     preg_match("/^(\s+)(.*)$/", $v, $m);
1332                     $v = $m[1];
1333                     $argstr = $m[2];
1334                 }
1335                 if (trim($k) and trim($v)) $args[$k] = $v;
1336             }
1337         }
1338         $this->_element = new Block_HtmlElement($tag, $args, $content);
1339         //$this->_element->setTightness($tag == 'span', $tag == 'span');
1340         return true;
1341     }
1342     function _setTightness($top, $bot) {
1343         // Don't tighten user <div|span>
1344     }
1345 }
1346
1347
1348 ////////////////////////////////////////////////////////////////
1349 //
1350
1351 /**
1352  * Transform the text of a page, and return a parse tree.
1353  */
1354 function TransformTextPre ($text, $markup = 2.0, $basepage=false) {
1355     if (isa($text, 'WikiDB_PageRevision')) {
1356         $rev = $text;
1357         $text = $rev->getPackedContent();
1358         $markup = $rev->get('markup');
1359     }
1360     // NEW: default markup is new, to increase stability
1361     if (!empty($markup) && $markup < 2.0) {
1362         $text = ConvertOldMarkup($text);
1363     }
1364     // WikiCreole
1365     /*if (!empty($markup) && $markup == 3) {
1366         $text = ConvertFromCreole($text);
1367     }*/
1368     // Expand leading tabs.
1369     $text = expand_tabs($text);
1370     //set_time_limit(3);
1371     $output = new WikiText($text);
1372
1373     return $output;
1374 }
1375
1376 /**
1377  * Transform the text of a page, and return an XmlContent,
1378  * suitable for printXml()-ing.
1379  */
1380 function TransformText ($text, $markup = 2.0, $basepage = false) {
1381     $output = TransformTextPre($text, $markup, $basepage);
1382     if ($basepage) {
1383         // This is for immediate consumption.
1384         // We must bind the contents to a base pagename so that
1385         // relative page links can be properly linkified...
1386         return new CacheableMarkup($output->getContent(), $basepage);
1387     }
1388     return new XmlContent($output->getContent());
1389 }
1390
1391 // (c-file-style: "gnu")
1392 // Local Variables:
1393 // mode: php
1394 // tab-width: 8
1395 // c-basic-offset: 4
1396 // c-hanging-comment-ender-p: nil
1397 // indent-tabs-mode: nil
1398 // End:   
1399 ?>