]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
setTightness no longer needed
[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 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
334 class ParsedBlock extends Block_HtmlElement {
335     
336     function ParsedBlock (&$input, $tag = 'div', $attr = false) {
337         $this->Block_HtmlElement($tag, $attr);
338         $this->_initBlockTypes();
339         $this->_parse($input);
340     }
341
342     function _parse (&$input) {
343         // php5 failed to advance the block. php5 copies objects by ref.
344         // nextBlock == block, both are the same objects. So we have to clone it.
345         for ($block = $this->_getBlock($input); 
346              $block; 
347              $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock))
348         {
349             while ($nextBlock = $this->_getBlock($input)) {
350                 // Attempt to merge current with following block.
351                 if (! ($merged = $block->merge($nextBlock)) ) {
352                     break;      // can't merge
353                 }
354                 $block = $merged;
355             }
356             $this->pushContent($block->finish());
357         }
358     }
359
360     // FIXME: hackish. This should only be called once.
361     function _initBlockTypes () {
362         // better static or global?
363         static $_regexpset, $_block_types;
364
365         if (!is_object($_regexpset)) {
366             $Block_types = array
367                     ('oldlists', 'list', 'dl', 'table_dl',
368                      'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre', 'email_blockquote',
369                      'plugin', 'plugin_wikicreole', 'p');
370             // insert it before p!
371             if (ENABLE_MARKUP_DIVSPAN) {
372                 array_pop($Block_types);
373                 $Block_types[] = 'divspan';
374                 $Block_types[] = 'p';
375             }
376             foreach ($Block_types as $type) {
377                 $class = "Block_$type";
378                 $proto = new $class;
379                 $this->_block_types[] = $proto;
380                 $this->_regexps[] = $proto->_re;
381             }
382             $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
383             $_regexpset = $this->_regexpset;
384             $_block_types = $this->_block_types;
385             unset($Block_types);
386         } else {
387              $this->_regexpset = $_regexpset;
388              $this->_block_types = $_block_types;
389         }
390     }
391
392     function _getBlock (&$input) {
393         $this->_atSpace = $input->skipSpace();
394
395         $line = $input->currentLine();
396         if ($line === false or $line === '') { // allow $line === '0' 
397             return false;
398         }
399         $re_set = &$this->_regexpset;
400         //FIXME: php5 fails to advance here!
401         for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
402             $block = clone($this->_block_types[$m->regexp_ind]);
403             if (DEBUG & _DEBUG_PARSER)
404                 $input->_debug('>', get_class($block));
405             
406             if ($block->_match($input, $m)) {
407                 //$block->_text = $line;
408                 if (DEBUG & _DEBUG_PARSER)
409                     $input->_debug('<', get_class($block));
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                 $this->setContent($elem->getContent());
458             }
459         }
460     }
461 }
462
463 class BlockMarkup {
464     var $_re;
465
466     function _match (&$input, $match) {
467         trigger_error('pure virtual', E_USER_ERROR);
468     }
469
470     function merge ($followingBlock) {
471         return false;
472     }
473
474     function finish () {
475         return $this->_element;
476     }
477 }
478
479 class Block_blockquote extends BlockMarkup
480 {
481     var $_depth;
482     var $_re = '\ +(?=\S)';
483
484     function _match (&$input, $m) {
485         $this->_depth = strlen($m->match);
486         $indent = sprintf("\\ {%d}", $this->_depth);
487         $this->_element = new SubBlock($input, $indent, $m->match,
488                                        'blockquote');
489         return true;
490     }
491     
492     function merge ($nextBlock) {
493         if (get_class($nextBlock) == get_class($this)) {
494             assert ($nextBlock->_depth < $this->_depth);
495             $nextBlock->_element->unshiftContent($this->_element);
496             if (!empty($this->_tight_top))
497             $nextBlock->_tight_top = $this->_tight_top;
498             return $nextBlock;
499         }
500         return false;
501     }
502 }
503
504 class Block_list extends BlockMarkup
505 {
506     //var $_tag = 'ol' or 'ul';
507     var $_re = '\ {0,4}
508                 (?: \+
509                   | \\# (?!\[.*\])
510                   | -(?!-)
511                   | [o](?=\ )
512                   | [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
513                 )\ *(?=\S)';
514     var $_content = array();
515
516     function _match (&$input, $m) {
517         // A list as the first content in a list is not allowed.
518         // E.g.:
519         //   *  * Item
520         // Should markup as <ul><li>* Item</li></ul>,
521         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
522         //
523         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
524             return false;
525         }
526         
527         $prefix = $m->match;
528         $indent = sprintf("\\ {%d}", strlen($prefix));
529
530         $bullet = trim($m->match);
531         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
532         $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
533         return true;
534     }
535
536     function merge ($nextBlock) {
537         if (isa($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
538             if ($nextBlock->_content === $this->_content) {
539                 trigger_error("Internal Error: no block advance", E_USER_NOTICE);
540                 return false;
541             }
542             array_splice($this->_content, count($this->_content), 0,
543                          $nextBlock->_content);
544             return $this;
545         }
546         return false;
547     }
548
549     function finish () {
550         return new Block_HtmlElement($this->_tag, false, $this->_content);
551     }
552 }
553
554 class Block_dl extends Block_list
555 {
556     var $_tag = 'dl';
557
558     function Block_dl () {
559         $this->_re = '\ {0,4}\S.*(?<!'.ESCAPE_CHAR.'):\s*$';
560     }
561
562     function _match (&$input, $m) {
563         if (!($p = $this->_do_match($input, $m)))
564             return false;
565         list ($term, $defn, $loose) = $p;
566
567         $this->_content[] = new Block_HtmlElement('dt', false, $term);
568         $this->_content[] = $defn;
569         $this->_tight_defn = !$loose;
570         return true;
571     }
572
573     function _do_match (&$input, $m) {
574         $pos = $input->getPos();
575
576         $firstIndent = strspn($m->match, ' ');
577         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
578
579         $input->advance();
580         $loose = $input->skipSpace();
581         $line = $input->currentLine();
582
583         if (!$line || !preg_match($pat, $line, $mm)) {
584             $input->setPos($pos);
585             return false;       // No body found.
586         }
587
588         $indent = strlen($mm[0]);
589         $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
590         $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
591         return array($term, $defn, $loose);
592     }
593 }
594
595
596
597 class Block_table_dl_defn extends XmlContent
598 {
599     var $nrows;
600     var $ncols;
601     
602     function Block_table_dl_defn ($term, $defn) {
603         $this->XmlContent();
604         if (!is_array($defn))
605             $defn = $defn->getContent();
606
607         $this->_next_tight_top = false; // value irrelevant - gets fixed later
608         $this->_ncols = $this->_ComputeNcols($defn);
609         $this->_nrows = 0;
610
611         foreach ($defn as $item) {
612             if ($this->_IsASubtable($item))
613                 $this->_addSubtable($item);
614             else
615                 $this->_addToRow($item);
616         }
617         $this->_flushRow();
618
619         $th = HTML::th($term);
620         if ($this->_nrows > 1)
621             $th->setAttr('rowspan', $this->_nrows);
622         $this->_setTerm($th);
623     }
624
625     function _addToRow ($item) {
626         if (empty($this->_accum)) {
627             $this->_accum = HTML::td();
628             if ($this->_ncols > 2)
629                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
630         }
631         $this->_accum->pushContent($item);
632     }
633
634     function _flushRow ($tight_bottom=false) {
635         if (!empty($this->_accum)) {
636             $row = new Block_HtmlElement('tr', false, $this->_accum);
637
638             $this->_next_tight_top = $tight_bottom;
639             
640             $this->pushContent($row);
641             $this->_accum = false;
642             $this->_nrows++;
643         }
644     }
645
646     function _addSubtable ($table) {
647         if (!($table_rows = $table->getContent()))
648             return;
649
650         $this->_flushRow($table_rows[0]->_tight_top);
651             
652         foreach ($table_rows as $subdef) {
653             $this->pushContent($subdef);
654             $this->_nrows += $subdef->nrows();
655             $this->_next_tight_top = $subdef->_tight_bot;
656         }
657     }
658
659     function _setTerm ($th) {
660         $first_row = &$this->_content[0];
661         if (isa($first_row, 'Block_table_dl_defn'))
662             $first_row->_setTerm($th);
663         else
664             $first_row->unshiftContent($th);
665     }
666     
667     function _ComputeNcols ($defn) {
668         $ncols = 2;
669         foreach ($defn as $item) {
670             if ($this->_IsASubtable($item)) {
671                 $row = $this->_FirstDefn($item);
672                 $ncols = max($ncols, $row->ncols() + 1);
673             }
674         }
675         return $ncols;
676     }
677
678     function _IsASubtable ($item) {
679         return isa($item, 'HtmlElement')
680             && $item->getTag() == 'table'
681             && $item->getAttr('class') == 'wiki-dl-table';
682     }
683
684     function _FirstDefn ($subtable) {
685         $defs = $subtable->getContent();
686         return $defs[0];
687     }
688
689     function ncols () {
690         return $this->_ncols;
691     }
692
693     function nrows () {
694         return $this->_nrows;
695     }
696
697     function & firstTR() {
698         $first = &$this->_content[0];
699         if (isa($first, 'Block_table_dl_defn'))
700             return $first->firstTR();
701         return $first;
702     }
703
704     function & lastTR() {
705         $last = &$this->_content[$this->_nrows - 1];
706         if (isa($last, 'Block_table_dl_defn'))
707             return $last->lastTR();
708         return $last;
709     }
710
711     function setWidth ($ncols) {
712         assert($ncols >= $this->_ncols);
713         if ($ncols <= $this->_ncols)
714             return;
715         $rows = &$this->_content;
716         for ($i = 0; $i < count($rows); $i++) {
717             $row = &$rows[$i];
718             if (isa($row, 'Block_table_dl_defn'))
719                 $row->setWidth($ncols - 1);
720             else {
721                 $n = count($row->_content);
722                 $lastcol = &$row->_content[$n - 1];
723                 if (!empty($lastcol))
724                   $lastcol->setAttr('colspan', $ncols - 1);
725             }
726         }
727     }
728 }
729
730 class Block_table_dl extends Block_dl
731 {
732     var $_tag = 'dl-table';     // phony.
733
734     function Block_table_dl() {
735         $this->_re = '\ {0,4} (?:\S.*)? (?<!'.ESCAPE_CHAR.') \| \s* $';
736     }
737
738     function _match (&$input, $m) {
739         if (!($p = $this->_do_match($input, $m)))
740             return false;
741         list ($term, $defn, $loose) = $p;
742
743         $this->_content[] = new Block_table_dl_defn($term, $defn);
744         return true;
745     }
746
747     function finish () {
748
749         $defs = &$this->_content;
750
751         $ncols = 0;
752         foreach ($defs as $defn)
753             $ncols = max($ncols, $defn->ncols());
754         
755         foreach ($defs as $key => $defn)
756             $defs[$key]->setWidth($ncols);
757
758         return HTML::table(array('class' => 'wiki-dl-table',
759                                  'border' => 1,
760                                  'cellspacing' => 0,
761                                  'cellpadding' => 6),
762                            $defs);
763     }
764 }
765
766 class Block_oldlists extends Block_list
767 {
768     //var $_tag = 'ol', 'ul', or 'dl';
769     var $_re = '(?: [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
770                   | [#] (?! \[ .*? \] )
771                   | ; .*? :
772                 ) .*? (?=\S)';
773
774     function _match (&$input, $m) {
775         // FIXME:
776         if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
777             return false;
778         }
779         
780
781         $prefix = $m->match;
782         $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
783         $newindent = sprintf('\\ {%d}', strlen($prefix));
784         $indent = "(?:$oldindent|$newindent)";
785
786         $bullet = $prefix[0];
787         if ($bullet == '*') {
788             $this->_tag = 'ul';
789             $itemtag = 'li';
790         }
791         elseif ($bullet == '#') {
792             $this->_tag = 'ol';
793             $itemtag = 'li';
794         }
795         else {
796             $this->_tag = 'dl';
797             list ($term,) = explode(':', substr($prefix, 1), 2);
798             $term = trim($term);
799             if ($term)
800                 $this->_content[] = new Block_HtmlElement('dt', false,
801                                                           TransformInline($term));
802             $itemtag = 'dd';
803         }
804
805         $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
806         return true;
807     }
808 }
809
810 class Block_pre extends BlockMarkup
811 {
812     var $_re = '<(?:pre|verbatim|nowiki|noinclude)>';
813
814     function _match (&$input, $m) {
815         $endtag = '</' . substr($m->match, 1);
816         $text = array();
817         $pos = $input->getPos();
818
819         $line = $m->postmatch;
820         while (ltrim($line) != $endtag) {
821             $text[] = $line;
822             if (($line = $input->nextLine()) === false) {
823                 $input->setPos($pos);
824                 return false;
825             }
826         }
827         $input->advance();
828         
829         if ($m->match == '<nowiki>')
830             $text = join("<br>\n", $text);
831         else
832             $text = join("\n", $text);
833         
834         // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
835         // in a <pre>.
836         if ($m->match == '<pre>') {
837             $text = TransformInline($text);
838         }
839         if ($m->match == '<noinclude>') {
840             $text = TransformText($text);
841             $this->_element = new Block_HtmlElement('div', false, $text);
842         } else if ($m->match == '<nowiki>') {
843             $text = TransformInlineNowiki($text);
844             $this->_element = new Block_HtmlElement('p', false, $text);
845         } else {
846             $this->_element = new Block_HtmlElement('pre', false, $text);
847         }
848         return true;
849     }
850 }
851
852 class Block_plugin extends Block_pre
853 {
854     var $_re = '<\?plugin(?:-form)?(?!\S)';
855
856     // FIXME:
857     /* <?plugin Backlinks
858      *       page=ThisPage ?>
859     /* <?plugin ListPages pages=<!plugin-list Backlinks!>
860      *                    exclude=<!plugin-list TitleSearch s=T*!> ?>
861      *
862      * should all work.
863      */
864     function _match (&$input, $m) {
865         $pos = $input->getPos();
866         $pi = $m->match . $m->postmatch;
867         while (!preg_match('/(?<!'.ESCAPE_CHAR.')\?>\s*$/', $pi)) {
868             if (($line = $input->nextLine()) === false) {
869                 $input->setPos($pos);
870                 return false;
871             }
872             $pi .= "\n$line";
873         }
874         $input->advance();
875
876         $this->_element = new Cached_PluginInvocation($pi);
877         return true;
878     }
879 }
880
881 class Block_plugin_wikicreole extends Block_pre
882 {
883     // var $_re = '<<(?!\S)';
884     var $_re = '<<';
885
886     function _match (&$input, $m) {
887         $pos = $input->getPos();
888         $pi = "<?plugin " . $m->postmatch;
889         while (!preg_match('/(?<!'.ESCAPE_CHAR.')>>\s*$/', $pi)) {
890             if (($line = $input->nextLine()) === false) {
891                 $input->setPos($pos);
892                 return false;
893             }
894             $pi .= "\n$line";
895         }
896         $input->advance();
897
898         $pi = str_replace(">>", "?>", $pi);
899
900         $this->_element = new Cached_PluginInvocation($pi);
901         return true;
902     }
903 }
904
905 class Block_email_blockquote extends BlockMarkup
906 {
907     var $_attr = array('class' => 'mail-style-quote');
908     var $_re = '>\ ?';
909     
910     function _match (&$input, $m) {
911         //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
912         $indent = $this->_re;
913         $this->_element = new SubBlock($input, $indent, $m->match,
914                                        'blockquote', $this->_attr);
915         return true;
916     }
917 }
918
919 class Block_hr extends BlockMarkup
920 {
921     var $_re = '-{4,}\s*$';
922
923     function _match (&$input, $m) {
924         $input->advance();
925         $this->_element = new Block_HtmlElement('hr');
926         return true;
927     }
928 }
929
930 class Block_heading extends BlockMarkup
931 {
932     var $_re = '!{1,3}';
933     
934     function _match (&$input, $m) {
935         $tag = "h" . (5 - strlen($m->match));
936         $text = TransformInline(trim($m->postmatch));
937         $input->advance();
938
939         $this->_element = new Block_HtmlElement($tag, false, $text);
940         
941         return true;
942     }
943 }
944
945 class Block_heading_wikicreole extends BlockMarkup
946 {
947     var $_re = '={2,6}';
948     
949     function _match (&$input, $m) {
950         $tag = "h" . strlen($m->match);
951         // Remove spaces
952         $header = trim($m->postmatch);
953         // Remove '='s at the end so that Mediawiki syntax is recognized
954         $header = trim($header, "=");
955         $text = TransformInline(trim($header));
956         $input->advance();
957
958         $this->_element = new Block_HtmlElement($tag, false, $text);
959         
960         return true;
961     }
962 }
963
964 class Block_p extends BlockMarkup
965 {
966     var $_tag = 'p';
967     var $_re = '\S.*';
968     var $_text = '';
969
970     function _match (&$input, $m) {
971         $this->_text = $m->match;
972         $input->advance();
973         return true;
974     }
975
976     function merge ($nextBlock) {
977         $class = get_class($nextBlock);
978         if (strtolower($class) == 'block_p' and $this->_tight_bot) {
979             $this->_text .= "\n" . $nextBlock->_text;
980             $this->_tight_bot = $nextBlock->_tight_bot;
981             return $this;
982         }
983         return false;
984     }
985
986     function finish () {
987         $content = TransformInline(trim($this->_text));
988         $p = new Block_HtmlElement('p', false, $content);
989         return $p;
990     }
991 }
992
993 class Block_divspan extends BlockMarkup
994 {
995     var $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
996
997     function _match (&$input, $m) {
998         if (substr($m->match,1,4) == 'span') {
999             $tag = 'span';
1000         } else {
1001             $tag = 'div';
1002         }
1003         // without last >
1004         $argstr = substr(trim(substr($m->match,strlen($tag)+1)),0,-1); 
1005         $pos = $input->getPos();
1006         $pi  = $content = $m->postmatch;
1007         while (!preg_match('/^(.*)\<\/'.$tag.'\>(.*)$/i', $pi, $me)) {
1008             if ($pi != $content)
1009                 $content .= "\n$pi";
1010             if (($pi = $input->nextLine()) === false) {
1011                 $input->setPos($pos);
1012                 return false;
1013             }
1014         }
1015         if ($pi != $content)
1016             $content .= $me[1]; // prematch
1017         else
1018             $content = $me[1];
1019         $input->advance();
1020         if (strstr($content, "\n"))
1021             $content = TransformText($content);
1022         else    
1023             $content = TransformInline($content);
1024         if (!$argstr) 
1025             $args = false;
1026         else {
1027             $args = array();
1028             while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1029                 $k = $m[1]; $v = $m[2];
1030                 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1031                     $v = $m[1];
1032                     $argstr = $m[2];
1033                 } else {
1034                     preg_match("/^(\s+)(.*)$/", $v, $m);
1035                     $v = $m[1];
1036                     $argstr = $m[2];
1037                 }
1038                 if (trim($k) and trim($v)) $args[$k] = $v;
1039             }
1040         }
1041         $this->_element = new Block_HtmlElement($tag, $args, $content);
1042         return true;
1043     }
1044 }
1045
1046 ////////////////////////////////////////////////////////////////
1047 //
1048
1049 /**
1050  * Transform the text of a page, and return a parse tree.
1051  */
1052 function TransformTextPre ($text, $markup = 2.0, $basepage=false) {
1053     if (isa($text, 'WikiDB_PageRevision')) {
1054         $rev = $text;
1055         $text = $rev->getPackedContent();
1056         $markup = $rev->get('markup');
1057     }
1058     // NEW: default markup is new, to increase stability
1059     if (!empty($markup) && $markup < 2.0) {
1060         $text = ConvertOldMarkup($text);
1061     }
1062     // WikiCreole
1063     /*if (!empty($markup) && $markup == 3) {
1064         $text = ConvertFromCreole($text);
1065     }*/
1066     // Expand leading tabs.
1067     $text = expand_tabs($text);
1068     //set_time_limit(3);
1069     $output = new WikiText($text);
1070
1071     return $output;
1072 }
1073
1074 /**
1075  * Transform the text of a page, and return an XmlContent,
1076  * suitable for printXml()-ing.
1077  */
1078 function TransformText ($text, $markup = 2.0, $basepage = false) {
1079     $output = TransformTextPre($text, $markup, $basepage);
1080     if ($basepage) {
1081         // This is for immediate consumption.
1082         // We must bind the contents to a base pagename so that
1083         // relative page links can be properly linkified...
1084         return new CacheableMarkup($output->getContent(), $basepage);
1085     }
1086     return new XmlContent($output->getContent());
1087 }
1088
1089 // (c-file-style: "gnu")
1090 // Local Variables:
1091 // mode: php
1092 // tab-width: 8
1093 // c-basic-offset: 4
1094 // c-hanging-comment-ender-p: nil
1095 // indent-tabs-mode: nil
1096 // End:   
1097 ?>