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