]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Custom pages for Fusionforge: FindPage, FullTextSearch, TitleSearch
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php
2 /* Copyright (C) 2002 Geoffrey T. Dairiki <dairiki@dairiki.org>
3  * Copyright (C) 2004,2005 Reini Urban
4  * Copyright (C) 2008-2010 Marc-Etienne Vargenau, Alcatel-Lucent
5  *
6  * This file is part of PhpWiki.
7  *
8  * PhpWiki is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * PhpWiki is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21  */
22 require_once 'lib/CachedMarkup.php';
23 require_once 'lib/InlineParser.php';
24
25 /**
26  * Deal with paragraphs and proper, recursive block indents
27  * for the new style markup (version 2)
28  *
29  * Everything which goes over more than line:
30  * automatic lists, UL, OL, DL, table, blockquote, verbatim,
31  * p, pre, plugin, ...
32  *
33  * FIXME:
34  *  Still to do:
35  *    (old-style) tables
36  * FIXME: unify this with the RegexpSet in InlineParser.
37  *
38  * FIXME: This is very php5 sensitive: It was fixed for 1.3.9,
39  *        but is again broken with the 1.3.11
40  *        allow_call_time_pass_reference clean fixes
41  *
42  * @package Markup
43  * @author: Geoffrey T. Dairiki
44  */
45
46 /**
47  * Return type from RegexpSet::match and RegexpSet::nextMatch.
48  *
49  * @see RegexpSet
50  */
51 class AnchoredRegexpSet_match
52 {
53     /**
54      * The matched text.
55      */
56     public $match;
57
58     /**
59      * The text following the matched text.
60      */
61     public $postmatch;
62
63     /**
64      * Index of the regular expression which matched.
65      */
66     public $regexp_ind;
67 }
68
69 /**
70  * A set of regular expressions.
71  *
72  * This class is probably only useful for InlineTransformer.
73  */
74 class AnchoredRegexpSet
75 {
76     /**
77      * @param $regexps array A list of regular expressions.  The
78      * regular expressions should not include any sub-pattern groups
79      * "(...)".  (Anonymous groups, like "(?:...)", as well as
80      * look-ahead and look-behind assertions are fine.)
81      */
82     function __construct($regexps)
83     {
84         $this->_regexps = $regexps;
85         $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
86     }
87
88     /**
89      * Search text for the next matching regexp from the Regexp Set.
90      *
91      * @param $text string The text to search.
92      *
93      * @return object A RegexpSet_match object, or false if no match.
94      */
95     function match($text)
96     {
97         if (!is_string($text))
98             return false;
99         if (!preg_match($this->_re, $text, $m)) {
100             return false;
101         }
102
103         $match = new AnchoredRegexpSet_match;
104         $match->postmatch = substr($text, strlen($m[0]));
105         $match->match = $m[1];
106         $match->regexp_ind = count($m) - 3;
107         return $match;
108     }
109
110     /**
111      * Search for next matching regexp.
112      *
113      * Here, 'next' has two meanings:
114      *
115      * Match the next regexp(s) in the set, at the same position as the last match.
116      *
117      * If that fails, match the whole RegexpSet, starting after the position of the
118      * previous match.
119      *
120      * @param string $text Text to search.
121      * @param RegexpSet_match $prevMatch
122      *
123      * $prevMatch should be a match object obtained by a previous
124      * match upon the same value of $text.
125      *
126      * @return RegexpSet_match|bool A RegexpSet_match object, or false if no match.
127      */
128     function nextMatch($text, $prevMatch)
129     {
130         // Try to find match at same position.
131         $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
132         if (!$regexps) {
133             return false;
134         }
135
136         $pat = "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
137
138         if (!preg_match($pat, $text, $m)) {
139             return false;
140         }
141
142         $match = new AnchoredRegexpSet_match;
143         $match->postmatch = substr($text, strlen($m[0]));
144         $match->match = $m[1];
145         $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;
146
147         return $match;
148     }
149 }
150
151 class BlockParser_Input
152 {
153
154     function __construct($text)
155     {
156         // Expand leading tabs.
157         // FIXME: do this better.
158         //
159         // We want to ensure the only characters matching \s are ' ' and "\n".
160         //
161         $text = preg_replace('/(?![ \n])\s/', ' ', $text);
162         assert(!preg_match('/(?![ \n])\s/', $text));
163
164         $this->_lines = preg_split('/[^\S\n]*\n/', $text);
165         $this->_pos = 0;
166
167         // Strip leading blank lines.
168         while ($this->_lines and !$this->_lines[0])
169             array_shift($this->_lines);
170         $this->_atSpace = false;
171     }
172
173     function skipSpace()
174     {
175         $nlines = count($this->_lines);
176         while (1) {
177             if ($this->_pos >= $nlines) {
178                 $this->_atSpace = false;
179                 break;
180             }
181             if ($this->_lines[$this->_pos] != '')
182                 break;
183             $this->_pos++;
184             $this->_atSpace = true;
185         }
186         return $this->_atSpace;
187     }
188
189     function currentLine()
190     {
191         if ($this->_pos >= count($this->_lines)) {
192             return false;
193         }
194         return $this->_lines[$this->_pos];
195     }
196
197     function nextLine()
198     {
199         $this->_atSpace = $this->_lines[$this->_pos++] === '';
200         if ($this->_pos >= count($this->_lines)) {
201             return false;
202         }
203         return $this->_lines[$this->_pos];
204     }
205
206     function advance()
207     {
208         $this->_atSpace = ($this->_lines[$this->_pos] === '');
209         $this->_pos++;
210     }
211
212     function getPos()
213     {
214         return array($this->_pos, $this->_atSpace);
215     }
216
217     function setPos($pos)
218     {
219         list($this->_pos, $this->_atSpace) = $pos;
220     }
221
222     function getPrefix()
223     {
224         return '';
225     }
226
227     function getDepth()
228     {
229         return 0;
230     }
231
232     function where()
233     {
234         if ($this->_pos < count($this->_lines))
235             return $this->_lines[$this->_pos];
236         else
237             return "<EOF>";
238     }
239
240     function _debug($tab, $msg)
241     {
242         //return ;
243         $where = $this->where();
244         $tab = str_repeat('____', $this->getDepth()) . $tab;
245         printXML(HTML::div("$tab $msg: at: '",
246             HTML::samp($where),
247             "'"));
248         flush();
249     }
250 }
251
252 class BlockParser_InputSubBlock extends BlockParser_Input
253 {
254     /**
255      * @param BlockParser_Input $input
256      * @param string $prefix_re
257      * @param string $initial_prefix
258      */
259     function __construct(&$input, $prefix_re, $initial_prefix = '')
260     {
261         $this->_input = &$input;
262         $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
263         $this->_atSpace = false;
264
265         if (($line = $input->currentLine()) === false)
266             $this->_line = false;
267         elseif ($initial_prefix) {
268             assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
269             $this->_line = (string)substr($line, strlen($initial_prefix));
270             $this->_atBlank = !ltrim($line);
271         } elseif (preg_match($this->_prefix_pat, $line, $m)) {
272             $this->_line = (string)substr($line, strlen($m[0]));
273             $this->_atBlank = !ltrim($line);
274         } else
275             $this->_line = false;
276     }
277
278     function skipSpace()
279     {
280         // In contrast to the case for top-level blocks,
281         // for sub-blocks, there never appears to be any trailing space.
282         // (The last block in the sub-block should always be of class tight-bottom.)
283         while ($this->_line === '')
284             $this->advance();
285
286         if ($this->_line === false)
287             return $this->_atSpace == 'strong_space';
288         else
289             return $this->_atSpace;
290     }
291
292     function currentLine()
293     {
294         return $this->_line;
295     }
296
297     function nextLine()
298     {
299         if ($this->_line === '')
300             $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
301         else
302             $this->_atSpace = false;
303
304         $line = $this->_input->nextLine();
305         if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
306             $this->_line = (string)substr($line, strlen($m[0]));
307             $this->_atBlank = !ltrim($line);
308         } else
309             $this->_line = false;
310
311         return $this->_line;
312     }
313
314     function advance()
315     {
316         $this->nextLine();
317     }
318
319     function getPos()
320     {
321         return array($this->_line, $this->_atSpace, $this->_input->getPos());
322     }
323
324     function setPos($pos)
325     {
326         $this->_line = $pos[0];
327         $this->_atSpace = $pos[1];
328         $this->_input->setPos($pos[2]);
329     }
330
331     function getPrefix()
332     {
333         assert($this->_line !== false);
334         $line = $this->_input->currentLine();
335         assert($line !== false && strlen($line) >= strlen($this->_line));
336         return substr($line, 0, strlen($line) - strlen($this->_line));
337     }
338
339     function getDepth()
340     {
341         return $this->_input->getDepth() + 1;
342     }
343
344     function where()
345     {
346         return $this->_input->where();
347     }
348 }
349
350 class Block_HtmlElement extends HtmlElement
351 {
352     function __construct($tag /*, ... */)
353     {
354         $this->_init(func_get_args());
355     }
356
357     function setTightness($top, $bottom)
358     {
359     }
360 }
361
362 class ParsedBlock extends Block_HtmlElement
363 {
364     private $_block_types;
365     private $_regexps;
366     private $_regexpset;
367     private $_atSpace;
368
369     function __construct(&$input, $tag = 'div', $attr = array())
370     {
371         parent::__construct($tag, $attr);
372         $this->initBlockTypes();
373         $this->_parse($input);
374     }
375
376     private function _parse(&$input)
377     {
378         // php5 failed to advance the block. php5 copies objects by ref.
379         // nextBlock == block, both are the same objects. So we have to clone it.
380         for ($block = $this->getBlock($input);
381              $block;
382              $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock)) {
383             while ($nextBlock = $this->getBlock($input)) {
384                 // Attempt to merge current with following block.
385                 if (!($merged = $block->merge($nextBlock))) {
386                     break; // can't merge
387                 }
388                 $block = $merged;
389             }
390             $this->pushContent($block->finish());
391         }
392     }
393
394     // FIXME: hackish. This should only be called once.
395     private function initBlockTypes()
396     {
397         // better static or global?
398         static $_regexpset, $_block_types;
399
400         if (!is_object($_regexpset)) {
401             // nowiki_wikicreole must be before template_plugin
402             $Block_types = array
403             ('nowiki_wikicreole', 'template_plugin', 'placeholder', 'oldlists', 'list', 'dl',
404                 'table_dl', 'table_wikicreole', 'table_mediawiki',
405                 'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre',
406                 'email_blockquote', 'wikicreole_indented',
407                 'plugin', 'plugin_wikicreole', 'p');
408             // insert it before p!
409             if (defined('ENABLE_MARKUP_DIVSPAN') and ENABLE_MARKUP_DIVSPAN) {
410                 array_pop($Block_types);
411                 $Block_types[] = 'divspan';
412                 $Block_types[] = 'p';
413             }
414             foreach ($Block_types as $type) {
415                 $class = "Block_$type";
416                 $proto = new $class;
417                 $this->_block_types[] = $proto;
418                 $this->_regexps[] = $proto->_re;
419             }
420             $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
421             $_regexpset = $this->_regexpset;
422             $_block_types = $this->_block_types;
423             unset($Block_types);
424         } else {
425             $this->_regexpset = $_regexpset;
426             $this->_block_types = $_block_types;
427         }
428     }
429
430     private function getBlock(&$input)
431     {
432         $this->_atSpace = $input->skipSpace();
433
434         $line = $input->currentLine();
435         if ($line === false or $line === '') { // allow $line === '0'
436             return false;
437         }
438         $tight_top = !$this->_atSpace;
439         $re_set = &$this->_regexpset;
440         //FIXME: php5 fails to advance here!
441         for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
442             $block = clone($this->_block_types[$m->regexp_ind]);
443             if (DEBUG & _DEBUG_PARSER)
444                 $input->_debug('>', get_class($block));
445
446             if ($block->_match($input, $m)) {
447                 //$block->_text = $line;
448                 if (DEBUG & _DEBUG_PARSER)
449                     $input->_debug('<', get_class($block));
450                 $tight_bottom = !$input->skipSpace();
451                 $block->_setTightness($tight_top, $tight_bottom);
452                 return $block;
453             }
454             if (DEBUG & _DEBUG_PARSER)
455                 $input->_debug('[', "_match failed");
456         }
457         if ($line === false or $line === '') // allow $line === '0'
458             return false;
459
460         trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
461         return false;
462     }
463 }
464
465 class WikiText extends ParsedBlock
466 {
467     function __construct($text)
468     {
469         $input = new BlockParser_Input($text);
470         parent::__construct($input);
471     }
472 }
473
474 class SubBlock extends ParsedBlock
475 {
476     function __construct(&$input, $indent_re, $initial_indent = false,
477                          $tag = 'div', $attr = array())
478     {
479         $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
480         parent::__construct($subinput, $tag, $attr);
481     }
482 }
483
484 /**
485  * TightSubBlock is for use in parsing lists item bodies.
486  *
487  * If the sub-block consists of a single paragraph, it omits
488  * the paragraph element.
489  *
490  * We go to this trouble so that "tight" lists look somewhat reasonable
491  * in older (non-CSS) browsers.  (If you don't do this, then, without
492  * CSS, you only get "loose" lists.
493  */
494 class TightSubBlock extends SubBlock
495 {
496     function __construct(&$input, $indent_re, $initial_indent = false,
497                          $tag = 'div', $attr = array())
498     {
499         parent::__construct($input, $indent_re, $initial_indent, $tag, $attr);
500
501         // If content is a single paragraph, eliminate the paragraph...
502         if (count($this->_content) == 1) {
503             $elem = $this->_content[0];
504             if (is_a($elem, 'XmlElement') and $elem->getTag() == 'p') {
505                 $this->setContent($elem->getContent());
506             }
507         }
508     }
509 }
510
511 abstract class BlockMarkup
512 {
513     public $_re;
514     protected $_element;
515
516     abstract function _match(&$input, $match);
517
518     function _setTightness($top, $bot)
519     {
520     }
521
522     function merge($followingBlock)
523     {
524         return false;
525     }
526
527     function finish()
528     {
529         return $this->_element;
530     }
531 }
532
533 class Block_blockquote extends BlockMarkup
534 {
535     public $_depth;
536     public $_re = '\ +(?=\S)';
537     protected $_element;
538
539     function _match(&$input, $m)
540     {
541         $this->_depth = strlen($m->match);
542         $indent = sprintf("\\ {%d}", $this->_depth);
543         $this->_element = new SubBlock($input, $indent, $m->match,
544             'blockquote');
545         return true;
546     }
547
548     function merge($nextBlock)
549     {
550         if (get_class($nextBlock) == get_class($this)) {
551             assert($nextBlock->_depth < $this->_depth);
552             $nextBlock->_element->unshiftContent($this->_element);
553             if (!empty($this->_tight_top))
554                 $nextBlock->_tight_top = $this->_tight_top;
555             return $nextBlock;
556         }
557         return false;
558     }
559 }
560
561 class Block_list extends BlockMarkup
562 {
563     public $_re = '\ {0,4}
564                 (?: \+
565                   | \\#\ (?!\[.*\])
566                   | -(?!-)
567                   | [o](?=\ )
568                   | [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
569                 )\ *(?=\S)';
570     public $_content = array();
571     public $_tag; //'ol' or 'ul'
572
573     function _match(&$input, $m)
574     {
575         // A list as the first content in a list is not allowed.
576         // E.g.:
577         //   *  * Item
578         // Should markup as <ul><li>* Item</li></ul>,
579         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
580         //
581         if (preg_match('/[*#+-o]/', $input->getPrefix())) {
582             return false;
583         }
584
585         $prefix = $m->match;
586         $indent = sprintf("\\ {%d}", strlen($prefix));
587
588         $bullet = trim($m->match);
589         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
590         $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
591         return true;
592     }
593
594     function _setTightness($top, $bot)
595     {
596         $li = &$this->_content[0];
597         $li->setTightness($top, $bot);
598     }
599
600     function merge($nextBlock)
601     {
602         if (is_a($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
603             array_splice($this->_content, count($this->_content), 0,
604                 $nextBlock->_content);
605             return $this;
606         }
607         return false;
608     }
609
610     function finish()
611     {
612         return new Block_HtmlElement($this->_tag, false, $this->_content);
613     }
614 }
615
616 class Block_dl extends Block_list
617 {
618     public $_tag = 'dl';
619     private $_tight_defn;
620
621     function __construct()
622     {
623         $this->_re = '\ {0,4}\S.*(?<!' . ESCAPE_CHAR . '):\s*$';
624     }
625
626     function _match(&$input, $m)
627     {
628         if (!($p = $this->_do_match($input, $m)))
629             return false;
630         list ($term, $defn, $loose) = $p;
631
632         $this->_content[] = new Block_HtmlElement('dt', false, $term);
633         $this->_content[] = $defn;
634         $this->_tight_defn = !$loose;
635         return true;
636     }
637
638     function _setTightness($top, $bot)
639     {
640         $dt = &$this->_content[0];
641         $dd = &$this->_content[1];
642
643         $dt->setTightness($top, $this->_tight_defn);
644         $dd->setTightness($this->_tight_defn, $bot);
645     }
646
647     function _do_match(&$input, $m)
648     {
649         $pos = $input->getPos();
650
651         $firstIndent = strspn($m->match, ' ');
652         $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
653
654         $input->advance();
655         $loose = $input->skipSpace();
656         $line = $input->currentLine();
657
658         if (!$line || !preg_match($pat, $line, $mm)) {
659             $input->setPos($pos);
660             return false; // No body found.
661         }
662
663         $indent = strlen($mm[0]);
664         $term = TransformInline(rtrim(substr(trim($m->match), 0, -1)));
665         $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
666         return array($term, $defn, $loose);
667     }
668 }
669
670 class Block_table_dl_defn extends XmlContent
671 {
672     public $nrows;
673     public $ncols;
674     private $_accum;
675     private $_tight_top;
676     private $_tight_bot;
677
678     function __construct($term, $defn)
679     {
680         parent::__construct();
681         if (!is_array($defn))
682             $defn = $defn->getContent();
683
684         $this->_next_tight_top = false; // value irrelevant - gets fixed later
685         $this->_ncols = $this->ComputeNcols($defn);
686         $this->_nrows = 0;
687
688         foreach ($defn as $item) {
689             if ($this->IsASubtable($item))
690                 $this->addSubtable($item);
691             else
692                 $this->addToRow($item);
693         }
694         $this->flushRow();
695
696         $th = HTML::th($term);
697         if ($this->_nrows > 1)
698             $th->setAttr('rowspan', $this->_nrows);
699         $this->_setTerm($th);
700     }
701
702     function setTightness($tight_top, $tight_bot)
703     {
704         $this->_tight_top = $tight_top;
705         $this->_tight_bot = $tight_bot;
706     }
707
708     private function addToRow($item)
709     {
710         if (empty($this->_accum)) {
711             $this->_accum = HTML::td();
712             if ($this->_ncols > 2)
713                 $this->_accum->setAttr('colspan', $this->_ncols - 1);
714         }
715         $this->_accum->pushContent($item);
716     }
717
718     private function flushRow($tight_bottom = false)
719     {
720         if (!empty($this->_accum)) {
721             $row = new Block_HtmlElement('tr', false, $this->_accum);
722
723             $row->setTightness($this->_next_tight_top, $tight_bottom);
724             $this->_next_tight_top = $tight_bottom;
725
726             $this->pushContent($row);
727             $this->_accum = false;
728             $this->_nrows++;
729         }
730     }
731
732     private function addSubtable($table)
733     {
734         if (!($table_rows = $table->getContent()))
735             return;
736
737         $this->flushRow($table_rows[0]->_tight_top);
738
739         foreach ($table_rows as $subdef) {
740             $this->pushContent($subdef);
741             $this->_nrows += $subdef->nrows();
742             $this->_next_tight_top = $subdef->_tight_bot;
743         }
744     }
745
746     private function _setTerm($th)
747     {
748         $first_row = &$this->_content[0];
749         if (is_a($first_row, 'Block_table_dl_defn'))
750             $first_row->_setTerm($th);
751         else
752             $first_row->unshiftContent($th);
753     }
754
755     private function ComputeNcols($defn)
756     {
757         $ncols = 2;
758         foreach ($defn as $item) {
759             if ($this->IsASubtable($item)) {
760                 $row = $this->FirstDefn($item);
761                 $ncols = max($ncols, $row->ncols() + 1);
762             }
763         }
764         return $ncols;
765     }
766
767     private function IsASubtable($item)
768     {
769         return is_a($item, 'HtmlElement')
770             && $item->getTag() == 'table'
771             && $item->getAttr('class') == 'wiki-dl-table';
772     }
773
774     private function FirstDefn($subtable)
775     {
776         $defs = $subtable->getContent();
777         return $defs[0];
778     }
779
780     function ncols()
781     {
782         return $this->_ncols;
783     }
784
785     function nrows()
786     {
787         return $this->_nrows;
788     }
789
790     function & firstTR()
791     {
792         $first = &$this->_content[0];
793         if (is_a($first, 'Block_table_dl_defn'))
794             return $first->firstTR();
795         return $first;
796     }
797
798     function & lastTR()
799     {
800         $last = &$this->_content[$this->_nrows - 1];
801         if (is_a($last, 'Block_table_dl_defn'))
802             return $last->lastTR();
803         return $last;
804     }
805
806     function setWidth($ncols)
807     {
808         assert($ncols >= $this->_ncols);
809         if ($ncols <= $this->_ncols)
810             return;
811         $rows = &$this->_content;
812         for ($i = 0; $i < count($rows); $i++) {
813             $row = &$rows[$i];
814             if (is_a($row, 'Block_table_dl_defn'))
815                 $row->setWidth($ncols - 1);
816             else {
817                 $n = count($row->_content);
818                 $lastcol = &$row->_content[$n - 1];
819                 if (!empty($lastcol))
820                     $lastcol->setAttr('colspan', $ncols - 1);
821             }
822         }
823     }
824 }
825
826 class Block_table_dl extends Block_dl
827 {
828     public $_tag = 'dl-table'; // phony.
829
830     function __construct()
831     {
832         $this->_re = '\ {0,4} (?:\S.*)? (?<!' . ESCAPE_CHAR . ') \| \s* $';
833     }
834
835     function _match(&$input, $m)
836     {
837         if (!($p = $this->_do_match($input, $m)))
838             return false;
839         list ($term, $defn, $loose) = $p;
840
841         $this->_content[] = new Block_table_dl_defn($term, $defn);
842         return true;
843     }
844
845     function _setTightness($top, $bot)
846     {
847         $this->_content[0]->setTightness($top, $bot);
848     }
849
850     function finish()
851     {
852         $defs = &$this->_content;
853
854         $ncols = 0;
855         foreach ($defs as $defn)
856             $ncols = max($ncols, $defn->ncols());
857
858         foreach ($defs as $key => $defn)
859             $defs[$key]->setWidth($ncols);
860
861         return HTML::table(array('class' => 'wiki-dl-table'), $defs);
862     }
863 }
864
865 class Block_oldlists extends Block_list
866 {
867     //public $_tag = 'ol', 'ul', or 'dl';
868     public $_re = '(?: [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
869                   | [#]\ (?! \[ .*? \] )
870                   | ; .*? :
871                 ) .*? (?=\S)';
872
873     function _match(&$input, $m)
874     {
875         // FIXME:
876         if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
877             return false;
878         }
879
880         $prefix = $m->match;
881         $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
882         $newindent = sprintf('\\ {%d}', strlen($prefix));
883         $indent = "(?:$oldindent|$newindent)";
884
885         $bullet = $prefix[0];
886         if ($bullet == '*') {
887             $this->_tag = 'ul';
888             $itemtag = 'li';
889         } elseif ($bullet == '#') {
890             $this->_tag = 'ol';
891             $itemtag = 'li';
892         } else {
893             $this->_tag = 'dl';
894             list ($term,) = explode(':', substr($prefix, 1), 2);
895             $term = trim($term);
896             if ($term)
897                 $this->_content[] = new Block_HtmlElement('dt', false,
898                     TransformInline($term));
899             $itemtag = 'dd';
900         }
901
902         $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
903         return true;
904     }
905
906     function _setTightness($top, $bot)
907     {
908         if (count($this->_content) == 1) {
909             $li = &$this->_content[0];
910             $li->setTightness($top, $bot);
911         } else {
912             $dt = &$this->_content[0];
913             $dd = &$this->_content[1];
914             $dt->setTightness($top, false);
915             $dd->setTightness(false, $bot);
916         }
917     }
918 }
919
920 class Block_pre extends BlockMarkup
921 {
922     public $_re = '<(?:pre|verbatim|nowiki|noinclude|includeonly)>';
923
924     function _match(&$input, $m)
925     {
926         $endtag = '</' . substr($m->match, 1);
927         $text = array();
928         $pos = $input->getPos();
929
930         $line = $m->postmatch;
931         while (ltrim($line) != $endtag) {
932             $text[] = $line;
933             if (($line = $input->nextLine()) === false) {
934                 $input->setPos($pos);
935                 return false;
936             }
937         }
938         $input->advance();
939
940         if ($m->match == '<includeonly>') {
941             $this->_element = new Block_HtmlElement('div', false, '');
942             return true;
943         }
944
945         if ($m->match == '<nowiki>')
946             $text = join("<br>\n", $text);
947         else
948             $text = join("\n", $text);
949
950         if ($m->match == '<noinclude>') {
951             $text = TransformText($text);
952             $this->_element = new Block_HtmlElement('div', false, $text);
953         } elseif ($m->match == '<nowiki>') {
954             $text = TransformInlineNowiki($text);
955             $this->_element = new Block_HtmlElement('p', false, $text);
956         } else {
957             $this->_element = new Block_HtmlElement('pre', false, $text);
958         }
959         return true;
960     }
961 }
962
963 // Wikicreole placeholder
964 // <<<placeholder>>>
965 class Block_placeholder extends BlockMarkup
966 {
967     public $_re = '<<<';
968
969     function _match(&$input, $m)
970     {
971         $endtag = '>>>';
972         $text = array();
973         $pos = $input->getPos();
974
975         $line = $m->postmatch;
976         while (ltrim($line) != $endtag) {
977             $text[] = $line;
978             if (($line = $input->nextLine()) === false) {
979                 $input->setPos($pos);
980                 return false;
981             }
982         }
983         $input->advance();
984
985         $text = join("\n", $text);
986         $text = '<<<' . $text . '>>>';
987         $this->_element = new Block_HtmlElement('div', false, $text);
988         return true;
989     }
990 }
991
992 class Block_nowiki_wikicreole extends BlockMarkup
993 {
994     public $_re = '{{{';
995
996     function _match(&$input, $m)
997     {
998         $endtag = '}}}';
999         $text = array();
1000         $pos = $input->getPos();
1001
1002         $line = $m->postmatch;
1003         while (ltrim($line) != $endtag) {
1004             $text[] = $line;
1005             if (($line = $input->nextLine()) === false) {
1006                 $input->setPos($pos);
1007                 return false;
1008             }
1009         }
1010         $input->advance();
1011
1012         $text = join("\n", $text);
1013         $this->_element = new Block_HtmlElement('pre', false, $text);
1014         return true;
1015     }
1016 }
1017
1018 class Block_plugin extends Block_pre
1019 {
1020     public $_re = '<\?plugin(?:-form)?(?!\S)';
1021
1022     // FIXME:
1023     /* <?plugin Backlinks
1024      *       page=ThisPage ?>
1025     /* <?plugin ListPages pages=<!plugin-list Backlinks!>
1026      *                    exclude=<!plugin-list TitleSearch s=T*!> ?>
1027      *
1028      * should all work.
1029      */
1030     function _match(&$input, $m)
1031     {
1032         $pos = $input->getPos();
1033         $pi = $m->match . $m->postmatch;
1034         while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\?>\s*$/', $pi)) {
1035             if (($line = $input->nextLine()) === false) {
1036                 $input->setPos($pos);
1037                 return false;
1038             }
1039             $pi .= "\n$line";
1040         }
1041         $input->advance();
1042
1043         $this->_element = new Cached_PluginInvocation($pi);
1044         return true;
1045     }
1046 }
1047
1048 class Block_plugin_wikicreole extends Block_pre
1049 {
1050     // public $_re = '<<(?!\S)';
1051     public $_re = '<<';
1052
1053     function _match(&$input, $m)
1054     {
1055         $pos = $input->getPos();
1056         $pi = $m->postmatch;
1057         if ($pi[0] == '<') {
1058             return false;
1059         }
1060         $pi = "<?plugin " . $pi;
1061         while (!preg_match('/(?<!' . ESCAPE_CHAR . ')>>\s*$/', $pi)) {
1062             if (($line = $input->nextLine()) === false) {
1063                 $input->setPos($pos);
1064                 return false;
1065             }
1066             $pi .= "\n$line";
1067         }
1068         $input->advance();
1069
1070         $pi = str_replace(">>", "?>", $pi);
1071
1072         $this->_element = new Cached_PluginInvocation($pi);
1073         return true;
1074     }
1075 }
1076
1077 class Block_table_wikicreole extends Block_pre
1078 {
1079     public $_re = '\s*\|';
1080
1081     function _match(&$input, $m)
1082     {
1083         $pos = $input->getPos();
1084         $pi = "|" . $m->postmatch;
1085
1086         $intable = true;
1087         while ($intable) {
1088             if ((($line = $input->nextLine()) === false) && !$intable) {
1089                 $input->setPos($pos);
1090                 return false;
1091             }
1092             if (!$line) {
1093                 $intable = false;
1094                 $trimline = $line;
1095             } else {
1096                 $trimline = trim($line);
1097                 if ($trimline[0] != "|") {
1098                     $intable = false;
1099                 }
1100             }
1101             $pi .= "\n$trimline";
1102         }
1103
1104         $pi = '<' . '?plugin WikicreoleTable ' . $pi . '?' . '>';
1105
1106         $this->_element = new Cached_PluginInvocation($pi);
1107         return true;
1108     }
1109 }
1110
1111 /**
1112  *  Table syntax similar to Mediawiki
1113  *  {|
1114  * => <?plugin MediawikiTable
1115  *  |}
1116  * => ?>
1117  */
1118 class Block_table_mediawiki extends Block_pre
1119 {
1120     public $_re = '{\|';
1121
1122     function _match(&$input, $m)
1123     {
1124         $pos = $input->getPos();
1125         $pi = $m->postmatch;
1126         while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\|}\s*$/', $pi)) {
1127             if (($line = $input->nextLine()) === false) {
1128                 $input->setPos($pos);
1129                 return false;
1130             }
1131             $pi .= "\n$line";
1132         }
1133         $input->advance();
1134
1135         $pi = str_replace("\|}", "", $pi);
1136         $pi = '<' . '?plugin MediawikiTable ' . $pi . '?' . '>';
1137         $this->_element = new Cached_PluginInvocation($pi);
1138         return true;
1139     }
1140 }
1141
1142 /**
1143  *  Template syntax similar to Mediawiki
1144  *  {{template}}
1145  * => < ? plugin Template page=template ? >
1146  *  {{template|var1=value1|var2=value|...}}
1147  * => < ? plugin Template page=template var=value ... ? >
1148  *
1149  * The {{...}} syntax is also used for:
1150  *  - Wikicreole images
1151  *  - videos
1152  */
1153 class Block_template_plugin extends Block_pre
1154 {
1155     public $_re = '{{';
1156
1157     function _match(&$input, $m)
1158     {
1159         // If we find "}}", this is an inline template.
1160         if (strpos($m->postmatch, "}}") !== false) {
1161             return false;
1162         }
1163         $pos = $input->getPos();
1164         $pi = $m->postmatch;
1165         if ($pi[0] == '{') {
1166             return false;
1167         }
1168         while (!preg_match('/(?<!' . ESCAPE_CHAR . ')}}\s*$/', $pi)) {
1169             if (($line = $input->nextLine()) === false) {
1170                 $input->setPos($pos);
1171                 return false;
1172             }
1173             $pi .= "\n$line";
1174         }
1175         $input->advance();
1176
1177         $pi = trim($pi);
1178         $pi = trim($pi, "}}");
1179
1180         if (strpos($pi, "|") === false) {
1181             $imagename = $pi;
1182             $alt = "";
1183         } else {
1184             $imagename = substr($pi, 0, strpos($pi, "|"));
1185             $alt = ltrim(strstr($pi, "|"), "|");
1186         }
1187
1188         // It's not a Mediawiki template, it's a Wikicreole image
1189         if (is_image($imagename)) {
1190             $this->_element = LinkImage(getUploadDataPath() . $imagename, $alt);
1191             return true;
1192         }
1193
1194         // It's a video
1195         if (is_video($imagename)) {
1196             $pi = '<' . '?plugin Video file="' . $pi . '" ?>';
1197             $this->_element = new Cached_PluginInvocation($pi);
1198             return true;
1199         }
1200
1201         $pi = str_replace("\n", "", $pi);
1202
1203         // The argument value might contain a double quote (")
1204         // We have to encode that.
1205         $pi = htmlspecialchars($pi);
1206
1207         $vars = '';
1208
1209         if (preg_match('/^(\S+?)\|(.*)$/', $pi, $_m)) {
1210             $pi = $_m[1];
1211             $vars = '"' . preg_replace('/\|/', '" "', $_m[2]) . '"';
1212             $vars = preg_replace('/"(\S+)=([^"]*)"/', '\\1="\\2"', $vars);
1213         }
1214
1215         // pi may contain a version number
1216         // {{foo?version=5}}
1217         // in that case, output is "page=foo rev=5"
1218         if (strstr($pi, "?")) {
1219             $pi = str_replace("?version=", "\" rev=\"", $pi);
1220         }
1221
1222         if ($vars)
1223             $pi = '<' . '?plugin Template page="' . $pi . '" ' . $vars . ' ?>';
1224         else
1225             $pi = '<' . '?plugin Template page="' . $pi . '" ?>';
1226         $this->_element = new Cached_PluginInvocation($pi);
1227         return true;
1228     }
1229 }
1230
1231 class Block_email_blockquote extends BlockMarkup
1232 {
1233     public $_attr = array('class' => 'mail-style-quote');
1234     public $_re = '>\ ?';
1235
1236     function _match(&$input, $m)
1237     {
1238         //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1239         $indent = $this->_re;
1240         $this->_element = new SubBlock($input, $indent, $m->match, 'blockquote', $this->_attr);
1241         return true;
1242     }
1243 }
1244
1245 class Block_wikicreole_indented extends BlockMarkup
1246 {
1247     public $_attr = array('style' => 'margin-left:2em');
1248     public $_re = ':\ ?';
1249
1250     function _match(&$input, $m)
1251     {
1252         $indent = $this->_re;
1253         $this->_element = new SubBlock($input, $indent, $m->match,
1254             'div', $this->_attr);
1255         return true;
1256     }
1257 }
1258
1259 class Block_hr extends BlockMarkup
1260 {
1261     public $_re = '-{4,}\s*$';
1262
1263     function _match(&$input, $m)
1264     {
1265         $input->advance();
1266         $this->_element = new Block_HtmlElement('hr');
1267         return true;
1268     }
1269 }
1270
1271 class Block_heading extends BlockMarkup
1272 {
1273     public $_re = '!{1,3}';
1274
1275     function _match(&$input, $m)
1276     {
1277         $tag = "h" . (5 - strlen($m->match));
1278         $text = TransformInline(trim($m->postmatch));
1279         $input->advance();
1280
1281         $this->_element = new Block_HtmlElement($tag, false, $text);
1282
1283         return true;
1284     }
1285 }
1286
1287 class Block_heading_wikicreole extends BlockMarkup
1288 {
1289     public $_re = '={2,6}';
1290
1291     function _match(&$input, $m)
1292     {
1293         $tag = "h" . strlen($m->match);
1294         // Remove spaces
1295         $header = trim($m->postmatch);
1296         // Remove '='s at the end so that Mediawiki syntax is recognized
1297         $header = trim($header, "=");
1298         $text = TransformInline(trim($header));
1299         $input->advance();
1300
1301         $this->_element = new Block_HtmlElement($tag, false, $text);
1302
1303         return true;
1304     }
1305 }
1306
1307 class Block_p extends BlockMarkup
1308 {
1309     public $_tag = 'p';
1310     public $_re = '\S.*';
1311     public $_text = '';
1312     private $_tight_bot;
1313     private $_tight_top;
1314
1315     function _match(&$input, $m)
1316     {
1317         $this->_text = $m->match;
1318         $input->advance();
1319         return true;
1320     }
1321
1322     function _setTightness($top, $bot)
1323     {
1324         $this->_tight_top = $top;
1325         $this->_tight_bot = $bot;
1326     }
1327
1328     function merge($nextBlock)
1329     {
1330         $class = get_class($nextBlock);
1331         if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1332             $this->_text .= "\n" . $nextBlock->_text;
1333             $this->_tight_bot = $nextBlock->_tight_bot;
1334             return $this;
1335         }
1336         return false;
1337     }
1338
1339     function finish()
1340     {
1341         $content = TransformInline(trim($this->_text));
1342         $p = new Block_HtmlElement('p', false, $content);
1343         $p->setTightness($this->_tight_top, $this->_tight_bot);
1344         return $p;
1345     }
1346 }
1347
1348 class Block_divspan extends BlockMarkup
1349 {
1350     public $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1351
1352     function _match(&$input, $m)
1353     {
1354         if (substr($m->match, 1, 4) == 'span') {
1355             $tag = 'span';
1356         } else {
1357             $tag = 'div';
1358         }
1359         // without last >
1360         $argstr = substr(trim(substr($m->match, strlen($tag) + 1)), 0, -1);
1361         $pos = $input->getPos();
1362         $pi = $content = $m->postmatch;
1363         while (!preg_match('/^(.*)\<\/' . $tag . '\>(.*)$/i', $pi, $me)) {
1364             if ($pi != $content)
1365                 $content .= "\n$pi";
1366             if (($pi = $input->nextLine()) === false) {
1367                 $input->setPos($pos);
1368                 return false;
1369             }
1370         }
1371         if ($pi != $content)
1372             $content .= $me[1]; // prematch
1373         else
1374             $content = $me[1];
1375         $input->advance();
1376         if (strstr($content, "\n"))
1377             $content = TransformText($content);
1378         else
1379             $content = TransformInline($content);
1380         if (!$argstr)
1381             $args = false;
1382         else {
1383             $args = array();
1384             while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1385                 $k = $m[1];
1386                 $v = $m[2];
1387                 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1388                     $v = $m[1];
1389                     $argstr = $m[2];
1390                 } else {
1391                     preg_match("/^(\s+)(.*)$/", $v, $m);
1392                     $v = $m[1];
1393                     $argstr = $m[2];
1394                 }
1395                 if (trim($k) and trim($v)) $args[$k] = $v;
1396             }
1397         }
1398         $this->_element = new Block_HtmlElement($tag, $args, $content);
1399         return true;
1400     }
1401 }
1402
1403 ////////////////////////////////////////////////////////////////
1404 //
1405
1406 /**
1407  * Transform the text of a page, and return a parse tree.
1408  */
1409 function TransformTextPre($text)
1410 {
1411     if (is_a($text, 'WikiDB_PageRevision')) {
1412         $rev = $text;
1413         $text = $rev->getPackedContent();
1414     }
1415     // Expand leading tabs.
1416     $text = expand_tabs($text);
1417     $output = new WikiText($text);
1418
1419     return $output;
1420 }
1421
1422 /**
1423  * Transform the text of a page, and return an XmlContent,
1424  * suitable for printXml()-ing.
1425  */
1426 function TransformText($text, $basepage = false)
1427 {
1428     $output = TransformTextPre($text);
1429     if ($basepage) {
1430         // This is for immediate consumption.
1431         // We must bind the contents to a base pagename so that
1432         // relative page links can be properly linkified...
1433         return new CacheableMarkup($output->getContent(), $basepage);
1434     }
1435     return new XmlContent($output->getContent());
1436 }
1437
1438 // Local Variables:
1439 // mode: php
1440 // tab-width: 8
1441 // c-basic-offset: 4
1442 // c-hanging-comment-ender-p: nil
1443 // indent-tabs-mode: nil
1444 // End: