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