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