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