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