]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Finish tight/loose list refactor.
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id: BlockParser.php,v 1.32 2003-02-18 02:49:20 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     
434     function _match (&$input, $match) {
435         trigger_error('pure virtual', E_USER_ERROR);
436     }
437
438     function _setTightness ($top, $bot) {
439         $this->_tight_top = $top;
440         $this->_tight_bot = $bot;
441     }
442
443     function merge ($followingBlock) {
444         return false;
445     }
446
447     function finish () {
448         $this->_element->setTightness($this->_tight_top, $this->_tight_bot);
449         return $this->_element;
450     }
451 }
452
453 class Block_blockquote extends BlockMarkup
454 {
455     var $_depth;
456
457     var $_re = '\ +(?=\S)';
458
459     function _match (&$input, $m) {
460         $this->_depth = strlen($m->match);
461         $indent = sprintf("\\ {%d}", $this->_depth);
462         $this->_element = new SubBlock($input, $indent, $m->match,
463                                        'blockquote');
464         return true;
465     }
466     
467     function merge ($nextBlock) {
468         if (get_class($nextBlock) == get_class($this)) {
469             assert ($nextBlock->_depth < $this->_depth);
470             $nextBlock->_element->unshiftContent($this->_element);
471             $nextBlock->_tight_top = $this->_tight_top;
472             return $nextBlock;
473         }
474         return false;
475     }
476 }
477
478 class Block_list extends BlockMarkup
479 {
480     //var $_tag = 'ol' or 'ul';
481     var $_re = '\ {0,4}
482                 (?: \+
483                   | \\# (?!\[.*\])
484                   | -(?!-)
485                   | [o](?=\ )
486                   | [*] (?! \S[^*]*(?<=\S)[*](?!\S) )
487                 )\ *(?=\S)';
488
489     var $_content = array();
490
491     function _match (&$input, $m) {
492         // A list as the first content in a list is not allowed.
493         // E.g.:
494         //   *  * Item
495         // Should markup as <ul><li>* Item</li></ul>,
496         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
497         //
498         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
499             return false;
500         }
501         
502         $prefix = $m->match;
503         $indent = sprintf("\\ {%d}", strlen($prefix));
504
505         $bullet = trim($m->match);
506         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
507         $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
508         return true;
509     }
510
511     function _setTightness($top, $bot) {
512         $li = &$this->_content[0];
513         $li->setTightness($top, $bot);
514     }
515     
516     function merge ($nextBlock) {
517         if (isa($nextBlock, 'Block_list') && $this->_tag == $nextBlock->_tag) {
518             array_splice($this->_content, count($this->_content), 0,
519                          $nextBlock->_content);
520             return $this;
521         }
522         return false;
523     }
524
525     function finish () {
526         return new Block_HtmlElement($this->_tag, false, $this->_content);
527     }
528 }
529
530 class Block_dl extends Block_list
531 {
532     var $_tag = 'dl';
533     var $_re = '\ {0,4}\S.*(?<! ~):\s*$';
534
535     function _match (&$input, $m) {
536         if (!($p = $this->_do_match($input, $m)))
537             return false;
538         list ($term, $defn) = $p;
539         
540         $this->_content[] = new Block_HtmlElement('dt', false, $term);
541         $this->_content[] = $defn;
542         return true;
543     }
544
545     function _setTightness($top, $bot) {
546         $dt = &$this->_content[0];
547         $dd = &$this->_content[1];
548         
549         $dt->setTightness($top, false);
550         $dd->setTightness(false, $bot);
551     }
552
553     function _do_match (&$input, $m) {
554         $pos = $input->getPos();
555
556         $firstIndent = strspn($m->match, ' ');
557         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
558
559         $input->advance();
560         $input->skipSpace();
561         $line = $input->currentLine();
562         
563         if (!$line || !preg_match($pat, $line, $mm)) {
564             $input->setPos($pos);
565             return false;       // No body found.
566         }
567
568         $indent = strlen($mm[0]);
569         $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
570         $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
571         return array($term, $defn);
572     }
573 }
574
575
576
577 class Block_table_dl_defn extends XmlContent
578 {
579     var $nrows;
580     var $ncols;
581     
582     function Block_table_dl_defn ($term, $defn) {
583         $this->XmlContent();
584         if (!is_array($defn))
585             $defn = $defn->getContent();
586
587         $this->_ncols = $this->_ComputeNcols($defn);
588         
589         $this->_tight_top = false;
590         $this->_tight_bot = false;
591         $this->_atSpace = true;
592         $this->_nrows = 0;
593         foreach ($defn as $item) {
594             if ($this->_IsASubtable($item))
595                 $this->_addSubtable($item);
596             else
597                 $this->_addToRow($item);
598         }
599         $this->_flushRow();
600
601         $th = HTML::th($term);
602         if ($this->_nrows > 1)
603             $th->setAttr('rowspan', $this->_nrows);
604         $this->_setTerm($th);
605     }
606
607     function setTightness($top, $bot) {
608         $this->_content[0]->setTightness($top, false);
609         $this->_content[$this->_nrows-1]->setTightness(false, $bot);
610         $this->_tight_top = $top;
611         $this->_tight_bot = $bot;
612     }
613     
614     function _addToRow ($item) {
615         if (empty($this->_accum)) {
616             $this->_accum = HTML::td();
617             if ($this->_ncols > 2)
618                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
619         }
620         $this->_accum->pushContent($item);
621     }
622
623     function _flushRow ($tight_bottom=false) {
624         if (!empty($this->_accum)) {
625             $row = new Block_HtmlElement('tr', false, $this->_accum);
626
627             $row->setTightness(!$this->_atSpace, $tight_bottom);
628             $this->_atSpace = !$tight_bottom;
629             
630             $this->pushContent($row);
631             $this->_accum = false;
632             $this->_nrows++;
633         }
634     }
635
636     function _addSubtable ($table) {
637         if (!($table_rows = $table->getContent()))
638             return;
639
640         $this->_flushRow($table_rows[0]->_tight_top);
641             
642         foreach ($table_rows as $subdef) {
643             $this->pushContent($subdef);
644             $this->_nrows += $subdef->nrows();
645             $this->_atSpace = ! $subdef->_tight_bot;
646         }
647     }
648
649     function _setTerm ($th) {
650         $first_row = &$this->_content[0];
651         if (isa($first_row, 'Block_table_dl_defn'))
652             $first_row->_setTerm($th);
653         else
654             $first_row->unshiftContent($th);
655     }
656     
657     function _ComputeNcols ($defn) {
658         $ncols = 2;
659         foreach ($defn as $item) {
660             if ($this->_IsASubtable($item)) {
661                 $row = $this->_FirstDefn($item);
662                 $ncols = max($ncols, $row->ncols() + 1);
663             }
664         }
665         return $ncols;
666     }
667
668     function _IsASubtable ($item) {
669         return isa($item, 'HtmlElement')
670             && $item->getTag() == 'table'
671             && $item->getAttr('class') == 'wiki-dl-table';
672     }
673
674     function _FirstDefn ($subtable) {
675         $defs = $subtable->getContent();
676         return $defs[0];
677     }
678
679     function ncols () {
680         return $this->_ncols;
681     }
682
683     function nrows () {
684         return $this->_nrows;
685     }
686
687     function setWidth ($ncols) {
688         assert($ncols >= $this->_ncols);
689         if ($ncols <= $this->_ncols)
690             return;
691         $rows = &$this->_content;
692         for ($i = 0; $i < count($rows); $i++) {
693             $row = &$rows[$i];
694             if (isa($row, 'Block_table_dl_defn'))
695                 $row->setWidth($ncols - 1);
696             else {
697                 $n = count($row->_content);
698                 $lastcol = &$row->_content[$n - 1];
699                 $lastcol->setAttr('colspan', $ncols - 1);
700             }
701         }
702     }
703 }
704
705 class Block_table_dl extends Block_dl
706 {
707     var $_tag = 'dl-table';     // phony.
708
709     var $_re = '\ {0,4} (?:\S.*)? (?<! ~) \| \s* $';
710
711     function _match (&$input, $m) {
712         if (!($p = $this->_do_match($input, $m)))
713             return false;
714         list ($term, $defn) = $p;
715
716         $this->_content[] = new Block_table_dl_defn($term, $defn);
717         return true;
718     }
719
720     function _setTightness($top, $bot) {
721         $this->_content[0]->setTightness($top, $bot);
722     }
723     
724     function finish () {
725
726         $defs = &$this->_content;
727
728         $ncols = 0;
729         foreach ($defs as $defn)
730             $ncols = max($ncols, $defn->ncols());
731         
732         foreach ($defs as $key => $defn)
733             $defs[$key]->setWidth($ncols);
734
735         return HTML::table(array('class' => 'wiki-dl-table',
736                                  'border' => 1,
737                                  'cellspacing' => 0,
738                                  'cellpadding' => 6),
739                            $defs);
740     }
741 }
742
743 class Block_oldlists extends Block_list
744 {
745     //var $_tag = 'ol', 'ul', or 'dl';
746     var $_re = '(?: [*] (?! \S[^*]* (?<=\S) [*](?!\S) )
747                   | [#] (?! \[ .*? \] )
748                   | ; .*? :
749                 ) .*? (?=\S)';
750
751     function _match (&$input, $m) {
752         // FIXME:
753         if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
754             return false;
755         }
756         
757
758         $prefix = $m->match;
759         $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
760         $newindent = sprintf('\\ {%d}', strlen($prefix));
761         $indent = "(?:$oldindent|$newindent)";
762
763         $bullet = $prefix[0];
764         if ($bullet == '*') {
765             $this->_tag = 'ul';
766             $itemtag = 'li';
767         }
768         elseif ($bullet == '#') {
769             $this->_tag = 'ol';
770             $itemtag = 'li';
771         }
772         else {
773             $this->_tag = 'dl';
774             list ($term,) = explode(':', substr($prefix, 1), 2);
775             $term = trim($term);
776             if ($term)
777                 $this->_content[] = new Block_HtmlElement('dt', false,
778                                                           TransformInline($term));
779             $itemtag = 'dd';
780         }
781
782         $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
783         return true;
784     }
785
786     function _setTightness($top, $bot) {
787         if (count($this->_content) == 1) {
788             $li = &$this->_content[0];
789             $li->setTightness($top, $bot);
790         }
791         else {
792             assert(count($this->_content) == 2);
793             $dt = &$this->_content[0];
794             $dd = &$this->_content[1];
795             $dt->setTightness($top, false);
796             $dd->setTightness(false, $bot);
797         }
798     }
799 }
800
801 class Block_pre extends BlockMarkup
802 {
803     var $_re = '<(?:pre|verbatim)>';
804
805     function _match (&$input, $m) {
806         $endtag = '</' . substr($m->match, 1);
807         $text = array();
808         $pos = $input->getPos();
809
810         $line = $m->postmatch;
811         while (ltrim($line) != $endtag) {
812             $text[] = $line;
813             if (($line = $input->nextLine()) === false) {
814                 $input->setPos($pos);
815                 return false;
816             }
817         }
818         $input->advance();
819         
820         $text = join("\n", $text);
821         
822         // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
823         // in a <pre>.
824         if ($m->match == '<pre>')
825             $text = TransformInline($text);
826
827         $this->_element = new Block_HtmlElement('pre', false, $text);
828         return true;
829     }
830 }
831
832
833 class Block_plugin extends Block_pre
834 {
835     var $_re = '<\?plugin(?:-form)?(?!\S)';
836
837     // FIXME:
838     /* <?plugin Backlinks
839      *       page=ThisPage ?>
840      *
841      * should work. */
842     function _match (&$input, $m) {
843         $pos = $input->getPos();
844         $pi = $m->match . $m->postmatch;
845         while (!preg_match('/(?<!~)\?>\s*$/', $pi)) {
846             if (($line = $input->nextLine()) === false) {
847                 $input->setPos($pos);
848                 return false;
849             }
850             $pi .= "\n$line";
851         }
852         $input->advance();
853
854         global $request;
855         $loader = new WikiPluginLoader;
856         $expansion = $loader->expandPI($pi, $request);
857         $this->_element = new Block_HtmlElement('div', array('class' => 'plugin'),
858                                                 $expansion);
859         return true;
860     }
861 }
862
863 class Block_email_blockquote extends BlockMarkup
864 {
865     var $_attr = array('class' => 'mail-style-quote');
866     var $_re = '>\ ?';
867     
868     function _match (&$input, $m) {
869         //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
870         $indent = $this->_re;
871         $this->_element = new SubBlock($input, $indent, $m->match,
872                                        'blockquote', $this->_attr);
873         return true;
874     }
875 }
876
877 class Block_hr extends BlockMarkup
878 {
879     var $_re = '-{4,}\s*$';
880
881     function _match (&$input, $m) {
882         $input->advance();
883         $this->_element = new Block_HtmlElement('hr');
884         return true;
885     }
886 }
887
888 class Block_heading extends BlockMarkup
889 {
890     var $_re = '!{1,3}';
891     
892     function _match (&$input, $m) {
893         $tag = "h" . (5 - strlen($m->match));
894         $text = TransformInline(trim($m->postmatch));
895         $input->advance();
896
897         $this->_element = new Block_HtmlElement($tag, false, $text);
898         
899         return true;
900     }
901 }
902
903 class Block_p extends BlockMarkup
904 {
905     var $_tag = 'p';
906     var $_re = '\S.*';
907
908     function _match (&$input, $m) {
909         $this->_text = $m->match;
910         $input->advance();
911         return true;
912     }
913     
914     function merge ($nextBlock) {
915         $class = get_class($nextBlock);
916         if ($class == 'block_p' && $this->_tight_bot) {
917             $this->_text .= "\n" . $nextBlock->_text;
918             $this->_tight_bot = $nextBlock->_tight_bot;
919             return $this;
920         }
921         return false;
922     }
923             
924     function finish () {
925         $content = TransformInline(trim($this->_text));
926         $p = new Block_HtmlElement('p', false, $content);
927         $p->setTightness($this->_tight_top, $this->_tight_bot);
928         return $p;
929     }
930 }
931
932 ////////////////////////////////////////////////////////////////
933 //
934
935 function TransformText ($text, $markup = 2.0) {
936     if (isa($text, 'WikiDB_PageRevision')) {
937         $rev = $text;
938         $text = $rev->getPackedContent();
939         $markup = $rev->get('markup');
940     }
941
942     if (empty($markup) || $markup < 2.0) {
943         //include_once("lib/transform.php");
944         //return do_transform($text);
945         $text = ConvertOldMarkup($text);
946     }
947     
948     // Expand leading tabs.
949     $text = expand_tabs($text);
950
951     //set_time_limit(3);
952
953     $output = new WikiText($text);
954     return new XmlContent($output->getContent());
955 }
956
957 // (c-file-style: "gnu")
958 // Local Variables:
959 // mode: php
960 // tab-width: 8
961 // c-basic-offset: 4
962 // c-hanging-comment-ender-p: nil
963 // indent-tabs-mode: nil
964 // End:   
965 ?>