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