]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Refactoring BlockParser in an attempt to fix the tight vs loose list
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id: BlockParser.php,v 1.31 2003-02-17 23:50:10 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         $nlines = count($this->_lines);
156         while (1) {
157             if ($this->_pos >= $nlines) {
158                 $this->_atSpace = true;
159                 break;
160             }
161             if ($this->_lines[$this->_pos] != '')
162                 break;
163             $this->_pos++;
164             $this->_atSpace = true;
165         }
166         return $this->_atSpace;
167     }
168         
169     function currentLine () {
170         if ($this->_pos >= count($this->_lines)) {
171             return false;
172         }
173         return $this->_lines[$this->_pos];
174     }
175         
176     function nextLine () {
177         $this->_atSpace = $this->_lines[$this->_pos++] === '';
178         if ($this->_pos >= count($this->_lines)) {
179             return false;
180         }
181         return $this->_lines[$this->_pos];
182     }
183
184     function advance () {
185         $this->_atSpace = $this->_lines[$this->_pos++] === '';
186     }
187     
188     function getPos () {
189         return array($this->_pos, $this->_atSpace);
190     }
191
192     function setPos ($pos) {
193         list($this->_pos, $this->_atSpace) = $pos;
194     }
195
196     function getPrefix () {
197         return '';
198     }
199
200     function getDepth () {
201         return 0;
202     }
203
204     function where () {
205         if ($this->_pos < count($this->_lines))
206             return $this->_lines[$this->_pos];
207         else
208             return "<EOF>";
209     }
210     
211     function _debug ($tab, $msg) {
212         //return ;
213         $where = $this->where();
214         $tab = str_repeat('____', $this->getDepth() ) . $tab;
215         printXML(HTML::div("$tab $msg: at: '",
216                            HTML::tt($where),
217                            "'"));
218     }
219 }
220
221 class BlockParser_InputSubBlock extends BlockParser_Input
222 {
223     function BlockParser_InputSubBlock (&$input, $prefix_re, $initial_prefix = false) {
224         $this->_input = &$input;
225         $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
226         $this->_atSpace = $input->_atSpace;
227         //$this->_atSpace = false;
228
229         if (($line = $input->currentLine()) === false)
230             $this->_line = false;
231         elseif ($initial_prefix) {
232             assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
233             $this->_line = (string) substr($line, strlen($initial_prefix));
234         }
235         elseif (preg_match($this->_prefix_pat, $line, $m))
236             $this->_line = (string) substr($line, strlen($m[0]));
237         else
238             $this->_line = false;
239     }
240
241     function skipSpace () {
242         while (! $this->_line) {
243             if ($this->_line === false)
244                 return false;
245             $this->advance();
246         }
247         return $this->_atSpace;
248     }
249         
250     function currentLine () {
251         return $this->_line;
252     }
253
254     function nextLine () {
255         $this->_atSpace = $this->_line === '';
256         $line = $this->_input->nextLine();
257         if ($line !== false && preg_match($this->_prefix_pat, $line, $m))
258             $this->_line = (string) substr($line, strlen($m[0]));
259         else
260             $this->_line = false;
261         return $this->_line;
262     }
263
264     function advance () {
265         $this->_atSpace = $this->_line === '';
266         $line = $this->_input->nextLine();
267         if ($line !== false && preg_match($this->_prefix_pat, $line, $m))
268             $this->_line = (string) substr($line, strlen($m[0]));
269         else
270             $this->_line = false;
271     }
272         
273     function getPos () {
274         return array($this->_line, $this->_atSpace, $this->_input->getPos());
275     }
276
277     function setPos ($pos) {
278         $this->_line = $pos[0];
279         $this->_atSpace = $pos[1];
280         $this->_input->setPos($pos[2]);
281     }
282     
283     function getPrefix () {
284         assert ($this->_line !== false);
285         $line = $this->_input->currentLine();
286         assert ($line !== false && strlen($line) >= strlen($this->_line));
287         return substr($line, 0, strlen($line) - strlen($this->_line));
288     }
289
290     function getDepth () {
291         return $this->_input->getDepth() + 1;
292     }
293
294     function where () {
295         return $this->_input->where();
296     }
297 }
298     
299
300 class BlockParser_HtmlElement extends HtmlElement
301 {
302     function BlockHtmlElement($tag /*, ... */) {
303         $this->_init(func_get_args());
304     }
305
306
307     function setTightness($top, $bottom) {
308         $class = (string) $this->getAttr('class');
309         if ($top)
310             $class .= " tight-top";
311         if ($bottom)
312             $class .= " tight-bottom";
313         $class = ltrim($class);
314         if ($class)
315             $this->setAttr('class', $class);
316     }
317 }
318     
319 class Tightenable extends BlockParser_HtmlElement {
320     var $_isTight = false;
321
322     function Tightenable ($tag /*, ...*/) {
323         $this->_init(func_get_args());
324     }
325     
326     function tighten () {
327         if (! $this->_isTight) {
328             $content = &$this->_content;
329             for ($i = 0; $i < count($content); $i++) {
330                 if (!isa($content[$i], 'Tightenable'))
331                     continue;
332                 $content[$i]->tighten();
333             }
334             $this->_isTight = true;
335         }
336     }
337
338     function canTighten () {
339         $content = &$this->_content;
340         for ($i = 0; $i < count($content); $i++) {
341             if (!isa($content[$i], 'Tightenable'))
342                 continue;
343             if (!$content[$i]->canTighten())
344                 return false;
345         }
346         return true;
347     }
348 }
349
350 class TightenableParagraph extends Tightenable {
351     function TightenableParagraph (/*...*/) {
352         $this->_init('p');
353         $this->pushContent(func_get_args());
354     }
355
356     function tighten () {
357         $this->_isTight = true;
358     }
359
360     function canTighten () {
361         return true;
362     }
363
364
365     function printXML () {
366         if ($this->_isTight)
367             return XmlContent::printXML();
368         else
369             return parent::printXML();
370     }
371
372     function asXML () {
373         if ($this->_isTight)
374             return XmlContent::asXML();
375         else
376             return parent::asXML();
377     }
378 }
379
380 class ParsedBlock extends Tightenable {
381     var $_isLoose = false;
382     
383     function ParsedBlock (&$input, $tag = 'div', $attr = false) {
384         $this->Tightenable($tag, $attr);
385         $this->_initBlockTypes();
386         $this->_parse($input);
387     }
388
389     function canTighten () {
390         if ($this->_isLoose)
391             return false;
392         return parent::canTighten();
393     }
394     
395     function _parse (&$input) {
396         for ($block = $this->_getBlock($input); $block; $block = $nextBlock) {
397             while ($nextBlock = $this->_getBlock($input)) {
398                 // Attempt to merge current with following block.
399                 if (! ($merged = $block->merge($nextBlock, $this->_atSpace)) ) {
400                     break;      // can't merge
401                 }
402                 $block = $merged;
403             }
404             $this->pushContent($block->finish());
405         }
406     }
407
408     // FIXME: hackish
409     function _initBlockTypes () {
410         foreach (array('oldlists', 'list', 'dl', 'table_dl',
411                        'blockquote', 'heading', 'hr', 'pre', 'email_blockquote',
412                        'plugin', 'p')
413                  as $type) {
414             $class = "Block_$type";
415             $proto = new $class;
416             $this->_block_types[] = $proto;
417             $this->_regexps[] = $proto->_re;
418         }
419         $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
420     }
421
422     function _getBlock (&$input) {
423         $this->_atSpace = $input->skipSpace();
424
425         if (! ($line = $input->currentLine()) )
426             return false;
427
428         if ($this->_atSpace)
429             $this->_isLoose = true;
430
431         $tight_top = !$this->_atSpace;
432         $re_set = &$this->_regexpset;
433         for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
434             $block = $this->_block_types[$m->regexp_ind];
435             //$input->_debug('>', get_class($block));
436             
437             if ($block->_match($input, $m)) {
438                 //$input->_debug('<', get_class($block));
439                 $tight_bottom = ! $input->skipSpace();
440                 $block->_setTightness($tight_top, $tight_bottom);
441                 return $block;
442             }
443             //$input->_debug('[', "_match failed");
444         }
445
446         trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
447         return false;
448     }
449 }
450
451 class WikiText extends ParsedBlock {
452     function WikiText ($text) {
453         $input = new BlockParser_Input($text);
454         $this->ParsedBlock($input);
455     }
456 }
457
458 class SubBlock extends ParsedBlock {
459     function SubBlock (&$input, $indent_re, $initial_indent = false,
460                        $tag = 'div', $attr = false) {
461         $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
462         $this->ParsedBlock($subinput, $tag, $attr);
463     }
464 }
465
466 class BlockMarkup {
467     var $_re;
468     
469     function _match (&$input, $match) {
470         trigger_error('pure virtual', E_USER_ERROR);
471     }
472
473     function _setTightness ($top, $bot) {
474         $this->_tight_top = $top;
475         $this->_tight_bot = $bot;
476     }
477
478     function merge ($followingBlock, $followsSpace) {
479         return false;
480     }
481
482     function finish () {
483         $this->_element->setTightness($this->_top_tight, $this->_bot_tight);
484         return $this->_element;
485     }
486 }
487
488 class Block_blockquote extends BlockMarkup
489 {
490     var $_depth;
491
492     var $_re = '\ +(?=\S)';
493
494     // FIXME: tight top/bottom.
495     
496     function _match (&$input, $m) {
497         $this->_depth = strlen($m->match);
498         $indent = sprintf("\\ {%d}", $this->_depth);
499         $this->_element = new SubBlock($input, $indent, $m->match,
500                                        'blockquote');
501         return true;
502     }
503     
504     function merge ($nextBlock, $followsSpace) {
505         if (get_class($nextBlock) == get_class($this)) {
506             assert ($nextBlock->_depth < $this->_depth);
507             $nextBlock->_element->unshiftContent($this->_element);
508             $nextBlock->_tight_top = $this->_tight_top;
509             return $nextBlock;
510         }
511         return false;
512     }
513 }
514
515 class Block_list extends BlockMarkup
516 {
517     //var $_tag = 'ol' or 'ul';
518     var $_re = '\ {0,4}
519                 (?: \+
520                   | \\# (?!\[.*\])
521                   | -(?!-)
522                   | [o](?=\ )
523                   | [*] (?! \S[^*]*(?<=\S)[*](?!\S) )
524                 )\ *(?=\S)';
525
526     var $_isLoose = false;
527     var $_content = array();
528
529     function _match (&$input, $m) {
530         // A list as the first content in a list is not allowed.
531         // E.g.:
532         //   *  * Item
533         // Should markup as <ul><li>* Item</li></ul>,
534         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
535         //
536         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
537             return false;
538         }
539         
540         $prefix = $m->match;
541         $indent = sprintf("\\ {%d}", strlen($prefix));
542
543         $bullet = trim($m->match);
544         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
545         $this->_content[] = new SubBlock($input, $indent, $m->match, 'li');
546         return true;
547     }
548
549     function _setTightness($top, $bot) {
550         $li = &$this->_content[0];
551         $li->setTightness($top, $bot);
552     }
553     
554     function merge ($nextBlock, $followsSpace) {
555         if (isa($nextBlock, 'Block_list') && $this->_tag == $nextBlock->_tag) {
556             array_splice($this->_content, count($this->_content), 0,
557                          $nextBlock->_content);
558             if ($followsSpace)
559                 $this->_isLoose = true;
560             return $this;
561         }
562         return false;
563     }
564
565     function finish () {
566         $list = new Tightenable($this->_tag, false, $this->_content);
567         if (!$this->_isLoose && $list->canTighten())
568             $list->tighten();
569         return $list;
570     }
571 }
572
573 class Block_dl extends Block_list
574 {
575     var $_tag = 'dl';
576     var $_re = '\ {0,4}\S.*(?<! ~):\s*$';
577
578     function _match (&$input, $m) {
579         if (!($p = $this->_do_match($input, $m)))
580             return false;
581         list ($term, $defn) = $p;
582         
583         $this->_content[] = new BlockParser_HtmlElement('dt', false, $term);
584         $this->_content[] = $defn;
585         return true;
586     }
587
588     function _setTightness($top, $bot) {
589         $dt = &$this->_content[0];
590         $dd = &$this->_content[1];
591         
592         $dt->setTightness($top, false);
593         $dd->setTightness(false, $bot);
594     }
595
596     function _do_match (&$input, $m) {
597         $pos = $input->getPos();
598
599         $firstIndent = strspn($m->match, ' ');
600         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
601
602         $input->advance();
603         $input->skipSpace();
604         $line = $input->currentLine();
605         
606         if (!$line || !preg_match($pat, $line, $mm)) {
607             $input->setPos($pos);
608             return false;       // No body found.
609         }
610
611         $indent = strlen($mm[0]);
612         $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
613         $defn = new SubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
614         return array($term, $defn);
615     }
616 }
617
618
619
620 class Block_table_dl_defn extends XmlContent
621 {
622     var $nrows;
623     var $ncols;
624     
625     function Block_table_dl_defn ($term, $defn) {
626         $this->XmlContent();
627         if (!is_array($defn))
628             $defn = $defn->getContent();
629
630         $this->_ncols = $this->_ComputeNcols($defn);
631         
632         $this->_nrows = 0;
633         foreach ($defn as $item) {
634             if ($this->_IsASubtable($item))
635                 $this->_addSubtable($item);
636             else
637                 $this->_addToRow($item);
638         }
639         $this->_flushRow();
640
641         $th = HTML::th($term);
642         if ($this->_nrows > 1)
643             $th->setAttr('rowspan', $this->_nrows);
644         $this->_setTerm($th);
645     }
646
647     function _addToRow ($item) {
648         if (empty($this->_accum)) {
649             $this->_accum = HTML::td();
650             if ($this->_ncols > 2)
651                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
652         }
653         $this->_accum->pushContent($item);
654     }
655
656     function _flushRow () {
657         if (!empty($this->_accum)) {
658             $this->pushContent(HTML::tr($this->_accum));
659             $this->_accum = false;
660             $this->_nrows++;
661         }
662     }
663
664     function _addSubtable ($table) {
665         $this->_flushRow();
666         foreach ($table->getContent() as $subdef) {
667             $this->pushContent($subdef);
668             $this->_nrows += $subdef->nrows();
669         }
670     }
671
672     function _setTerm ($th) {
673         $first_row = &$this->_content[0];
674         if (isa($first_row, 'Block_table_dl_defn'))
675             $first_row->_setTerm($th);
676         else
677             $first_row->unshiftContent($th);
678     }
679     
680     function _ComputeNcols ($defn) {
681         $ncols = 2;
682         foreach ($defn as $item) {
683             if ($this->_IsASubtable($item)) {
684                 $row = $this->_FirstDefn($item);
685                 $ncols = max($ncols, $row->ncols() + 1);
686             }
687         }
688         return $ncols;
689     }
690
691     function _IsASubtable ($item) {
692         return isa($item, 'HtmlElement')
693             && $item->getTag() == 'table'
694             && $item->getAttr('class') == 'wiki-dl-table';
695     }
696
697     function _FirstDefn ($subtable) {
698         $defs = $subtable->getContent();
699         return $defs[0];
700     }
701
702     function ncols () {
703         return $this->_ncols;
704     }
705
706     function nrows () {
707         return $this->_nrows;
708     }
709
710     function setWidth ($ncols) {
711         assert($ncols >= $this->_ncols);
712         if ($ncols <= $this->_ncols)
713             return;
714         $rows = &$this->_content;
715         for ($i = 0; $i < count($rows); $i++) {
716             $row = &$rows[$i];
717             if (isa($row, 'Block_table_dl_defn'))
718                 $row->setWidth($ncols - 1);
719             else {
720                 $n = count($row->_content);
721                 $lastcol = &$row->_content[$n - 1];
722                 $lastcol->setAttr('colspan', $ncols - 1);
723             }
724         }
725     }
726 }
727
728 class Block_table_dl extends Block_dl
729 {
730     var $_tag = 'dl-table';     // phony.
731
732     var $_re = '\ {0,4} (?:\S.*)? (?<! ~) \| \s* $';
733
734     function _match (&$input, $m) {
735         if (!($p = $this->_do_match($input, $m)))
736             return false;
737         list ($term, $defn) = $p;
738
739         $this->_content[] = new Block_table_dl_defn($term, $defn);
740         return true;
741     }
742
743     function _setTightness($top, $bot) {
744         // FIXME:  what to do?
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' => 2, // FIXME: CSS?
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 BlockParser_HtmlElement('dt', false,
801                                                                 TransformInline($term));
802             $itemtag = 'dd';
803         }
804
805         $this->_content[] = new SubBlock($input, $indent, $m->match, $itemtag);
806         return true;
807     }
808
809     function _setTightness($top, $bot) {
810         if (count($this->_content) == 1) {
811             $li = &$this->_content[0];
812             $li->setTightness($top, $bot);
813         }
814         else {
815             assert(count($this->_content) == 2);
816             $dt = &$this->_content[0];
817             $dd = &$this->_content[1];
818             $dt->setTightness($top, false);
819             $dd->setTightness(false, $bot);
820         }
821     }
822 }
823
824 class Block_pre extends BlockMarkup
825 {
826     var $_re = '<(?:pre|verbatim)>';
827
828     function _match (&$input, $m) {
829         $endtag = '</' . substr($m->match, 1);
830         $text = array();
831         $pos = $input->getPos();
832
833         $line = $m->postmatch;
834         while (ltrim($line) != $endtag) {
835             $text[] = $line;
836             if (($line = $input->nextLine()) === false) {
837                 $input->setPos($pos);
838                 return false;
839             }
840         }
841         $input->advance();
842         
843         $text = join("\n", $text);
844         
845         // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
846         // in a <pre>.
847         if ($m->match == '<pre>')
848             $text = TransformInline($text);
849
850         $this->_element = new BlockParser_HtmlElement('pref', false, $text);
851         return true;
852     }
853 }
854
855
856 class Block_plugin extends Block_pre
857 {
858     var $_re = '<\?plugin(?:-form)?(?!\S)';
859
860     // FIXME:
861     /* <?plugin Backlinks
862      *       page=ThisPage ?>
863      *
864      * should work. */
865     function _match (&$input, $m) {
866         $pos = $input->getPos();
867         $pi = $m->match . $m->postmatch;
868         while (!preg_match('/(?<!~)\?>\s*$/', $pi)) {
869             if (($line = $input->nextLine()) === false) {
870                 $input->setPos($pos);
871                 return false;
872             }
873             $pi .= "\n$line";
874         }
875         $input->advance();
876
877         global $request;
878         $loader = new WikiPluginLoader;
879         $expansion = $loader->expandPI($pi, $request);
880         $this->_element = new BlockParser_HtmlElement('div', array('class' => 'plugin'),
881                                                       $expansion);
882         return true;
883     }
884 }
885
886 class Block_email_blockquote extends BlockMarkup
887 {
888     // FIXME: move CSS to CSS.
889     var $_attr = array('style' => 'border-left-width: medium; border-left-color: #0f0; border-left-style: ridge; padding-left: 1em; margin-left: 0em; margin-right: 0em;');
890
891     var $_re = '>\ ?';
892     
893     function _match (&$input, $m) {
894         $indent = str_replace(' ', '\\ ', $m->match) . '|>$';
895         $this->_element = new SubBlock($input, $indent, $m->match,
896                                        'blockquote', $this->_attr);
897         return true;
898     }
899 }
900
901 class Block_hr extends BlockMarkup
902 {
903     var $_re = '-{4,}\s*$';
904
905     function _match (&$input, $m) {
906         $input->advance();
907         $this->_element = new BlockParser_HtmlElement('hr');
908         return true;
909     }
910 }
911
912 class Block_heading extends BlockMarkup
913 {
914     var $_re = '!{1,3}';
915     
916     function _match (&$input, $m) {
917         $tag = "h" . (5 - strlen($m->match));
918         $text = TransformInline(trim($m->postmatch));
919         $input->advance();
920
921         $this->_element = new BlockParser_HtmlElement($tag, false, $text);
922         
923         return true;
924     }
925 }
926
927 class Block_p extends BlockMarkup
928 {
929     var $_tag = 'p';
930     var $_re = '\S.*';
931
932     function _match (&$input, $m) {
933         $this->_text = $m->match;
934         $input->advance();
935         return true;
936     }
937     
938     function merge ($nextBlock, $followsSpace) {
939         $class = get_class($nextBlock);
940         if ($class == 'block_p' && !$followsSpace) {
941             $this->_text .= "\n" . $nextBlock->_text;
942             $this->_tight_bot = $nextBlock->_tight_bot;
943             return $this;
944         }
945         return false;
946     }
947             
948     function finish () {
949         $content = TransformInline(trim($this->_text));
950         $p = new TightenableParagraph($content);
951         $p->setTightness($this->_tight_top, $this->_tight_bot);
952         return $p;
953     }
954 }
955
956 ////////////////////////////////////////////////////////////////
957 //
958
959 function TransformText ($text, $markup = 2.0) {
960     if (isa($text, 'WikiDB_PageRevision')) {
961         $rev = $text;
962         $text = $rev->getPackedContent();
963         $markup = $rev->get('markup');
964     }
965
966     if (empty($markup) || $markup < 2.0) {
967         //include_once("lib/transform.php");
968         //return do_transform($text);
969         $text = ConvertOldMarkup($text);
970     }
971     
972     // Expand leading tabs.
973     $text = expand_tabs($text);
974
975     //set_time_limit(3);
976
977     $output = new WikiText($text);
978     return new XmlContent($output->getContent());
979 }
980
981 // (c-file-style: "gnu")
982 // Local Variables:
983 // mode: php
984 // tab-width: 8
985 // c-basic-offset: 4
986 // c-hanging-comment-ender-p: nil
987 // indent-tabs-mode: nil
988 // End:   
989 ?>