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