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