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