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