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