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