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