]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Added/fixed RCS $Id$s.
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id: BlockParser.php,v 1.8 2002-01-28 03:59:30 dairiki Exp $');
2 /* Copyright (C) 2002, Geoffrey T. Dairiki <dairiki@dairiki.org>
3  *
4  * This file is part of PhpWiki.
5  * 
6  * PhpWiki is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  * 
11  * PhpWiki is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License
17  * along with PhpWiki; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 require_once('lib/HtmlElement.php');
21 //FIXME:
22 require_once('lib/transform.php');
23
24 class InlineTransform
25 extends WikiTransform {
26     function InlineTransform() {
27         global $WikiNameRegexp, $AllowedProtocols, $InterWikiLinkRegexp;
28
29         $this->WikiTransform();
30
31         // register functions
32         // functions are applied in order of registering
33
34         $this->register(WT_SIMPLE_MARKUP, 'wtm_plugin_link');
35  
36         $this->register(WT_TOKENIZER, 'wtt_doublebrackets', '\[\[');
37         //$this->register(WT_TOKENIZER, 'wtt_footnotes', '^\[\d+\]');
38         //$this->register(WT_TOKENIZER, 'wtt_footnoterefs', '\[\d+\]');
39         $this->register(WT_TOKENIZER, 'wtt_bracketlinks', '\[.+?\]');
40         $this->register(WT_TOKENIZER, 'wtt_urls',
41                         "!?\b($AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]");
42
43         if (function_exists('wtt_interwikilinks')) {
44             $this->register(WT_TOKENIZER, 'wtt_interwikilinks',
45                             pcre_fix_posix_classes("!?(?<![[:alnum:]])") .
46                             "$InterWikiLinkRegexp:[^\\s.,;?()]+");
47         }
48         $this->register(WT_TOKENIZER, 'wtt_bumpylinks', "!?$WikiNameRegexp");
49
50         $this->register(WT_SIMPLE_MARKUP, 'wtm_htmlchars');
51         $this->register(WT_SIMPLE_MARKUP, 'wtm_linebreak');
52         $this->register(WT_SIMPLE_MARKUP, 'wtm_bold_italics');
53     }
54 };
55
56 function TransformInline ($text) {
57     // The old transform code does funny things with trailing
58     // white space....
59
60     $trfm = new InlineTransform;
61     preg_match('/\s*$/', $text, $m);
62     $tail = $m[0];
63     // This "\n" -> "\r" hackage is to fool the old transform code
64     // into continuing italics across lines.
65     $in = str_replace("\n", "\r", $text);
66     $out = preg_replace('/\s*$/', '', AsXML($trfm->do_transform('', array($in))));
67     $out = str_replace("\r", "\n", $out);
68     $out .= $tail;
69
70     // DEBUGGING
71     if (false && $out != $text) {
72         echo(" IN <pre>'" . htmlspecialchars($text) . "'</pre><br>\n");
73         echo("OUT <pre>'" . htmlspecialchars($out) . "'</pre><br>\n");
74     }
75     return new RawXml($out);
76 }
77
78
79 ////////////////////////////////////////////////////////////////
80 //
81 //
82 define("BLOCK_NEVER_TIGHTEN", 0);
83 define("BLOCK_NOTIGHTEN_AFTER", 1);
84 define("BLOCK_NOTIGHTEN_BEFORE", 2);
85 define("BLOCK_NOTIGHTEN_EITHER", 3);
86
87 /**
88  * FIXME:
89  *  Still to do:
90  *    (old-style) tables
91  */
92
93 class BlockParser {
94     function parse (&$input, $tighten_mode = BLOCK_NEVER_TIGHTEN) {
95         $content = array();
96         
97         for ($block = BlockParser::_nextBlock($input); $block; $block = $nextBlock) {
98             while ($nextBlock = BlockParser::_nextBlock($input)) {
99                 // Attempt to merge current with following block.
100                 if (! $block->merge($nextBlock))
101                     break;      // can't merge
102             }
103
104             $output = $block->finish($tighten_mode);
105
106             if (is_array($output))
107                 foreach ($output as $x)
108                     $content[] = $x;
109             else
110                 $content[] = $output;
111         }
112         return $content;
113     }
114
115     function _nextBlock (&$input) {
116         global $Block_BlockTypes;
117         
118         if ($input->atEof())
119             return false;
120         
121         foreach ($Block_BlockTypes as $type) {
122             if ($m = $input->match($type->_re)) {
123                 BlockParser::_debug('>', get_class($type), $input);
124                 
125                 $block = $type;
126                 $block->_followsBreak = $input->atBreak();
127                 if (!$block->_parse($input, $m)) {
128                     BlockParser::_debug('[', "_parse failed", $input);
129                     continue;
130                 }
131                 $block->_preceedsBreak = $input->eatSpace();
132                 BlockParser::_debug('<', get_class($type), $input);
133                 return $block;
134             }
135         }
136
137         if ($input->getDepth() == 0) {
138             // We should never get here.
139             //preg_match('/.*/A', substr($this->_text, $this->_pos), $m);// get first line
140             trigger_error("Couldn't match block: '".rawurlencode($m[0])."'", E_USER_NOTICE);
141         }
142         //FIXME:$this->_debug("no match");
143         return false;
144     }
145
146     function _debug ($tab, $msg, $input) {
147         return ;
148         
149         $tab = str_repeat($tab, $input->getDepth() + 1);
150         printXML(HTML::div("$tab $msg: at: '",
151                            HTML::tt($input->where()),
152                            "'"));
153     }
154     
155 }
156
157 class BlockParser_Match {
158     function BlockParser_Match ($match_data) {
159         $this->_m = $match_data;
160     }
161
162     function getPrefix () {
163         return $this->_m[1];
164     }
165
166     function getMatch ($n = 0) {
167         $text = $this->_m[$n + 2];
168         //if (preg_match('/\n./s', $text)) {
169             $prefix = $this->getPrefix();
170             $text = str_replace("\n$prefix", "\n", $text);
171         //}
172         return $text;
173     }
174 }
175
176     
177 class BlockParser_Input {
178
179     function BlockParser_Input ($text) {
180         $this->_text = $text;
181         $this->_pos = 0;
182         $this->_depth = 0;
183         
184         // Expand leading tabs.
185         // FIXME: do this better.
186         //
187         // We want to ensure the only characters matching \s are ' ' and "\n".
188         //
189         $this->_text = preg_replace('/(?![ \n])\s/', ' ', $this->_text);
190         assert(!preg_match('/(?![ \n])\s/', $this->_text));
191         if (!preg_match('/\n$/', $this->_text))
192             $this->_text .= "\n";
193
194         $this->_set_prefix ('');
195         $this->_atBreak = false;
196         $this->eatSpace();
197     }
198
199     function _set_prefix ($prefix, $next_prefix = false) {
200         if ($next_prefix === false)
201             $next_prefix = $prefix;
202
203         $this->_prefix = $prefix;
204         $this->_next_prefix = $next_prefix;
205
206         $this->_regexp_cache = array();
207
208         $blank = "(:?$prefix)?\s*\n";
209         $this->_blank_pat = "/$blank/A";
210         $this->_eof_pat = "/\\Z|(?!$blank|${prefix}.)/A";
211     }
212
213     function atEof () {
214         return preg_match($this->_eof_pat, substr($this->_text, $this->_pos));
215     }
216
217     function match ($regexp) {
218         $cache = &$this->_regexp_cache;
219         if (!isset($cache[$regexp])) {
220             // Fix up any '^'s in pattern (add our prefix)
221             $re = preg_replace('/(?<! [ [ \\\\ ]) \^ /x',
222                                '^' . $this->_next_prefix, $regexp);
223
224             // Fix any match  backreferences (like '\1').
225             $re = preg_replace('/(?<= [^ \\\\ ] [ \\\\ ] )( \\d+ )/ex', "'\\1' + 2", $re);
226
227             $re = "/(" . $this->_prefix . ")($re)/Am";
228             $cache[$regexp] = $re;
229         }
230         else
231             $re = $cache[$regexp];
232         
233         if (preg_match($re, substr($this->_text, $this->_pos), $m)) {
234             return new BlockParser_Match($m);
235         }
236         return false;
237     }
238
239     function accept ($match) {
240         $text = $match->_m[0];
241
242         assert(substr($this->_text, $this->_pos, strlen($text)) == $text);
243         $this->_pos += strlen($text);
244
245         // FIXME:
246         assert(preg_match("/\n$/", $text));
247             
248         if ($this->_next_prefix != $this->_prefix)
249             $this->_set_prefix($this->_next_prefix);
250
251         $this->_atBreak = false;
252         $this->eatSpace();
253     }
254     
255     /**
256      * Consume blank lines.
257      *
258      * @return bool True if any blank lines where comsumed.
259      */
260     function eatSpace () {
261         if (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
262             $this->_pos += strlen($m[0]);
263             if ($this->_next_prefix != $this->_prefix)
264                 $this->_set_prefix($this->_next_prefix);
265             $this->_atBreak = true;
266
267             while (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
268                 $this->_pos += strlen($m[0]);
269             }
270         }
271
272         return $this->_atBreak;
273     }
274     
275     function atBreak () {
276         return $this->_atBreak;
277     }
278
279     function getDepth () {
280         return $this->_depth;
281     }
282
283     // DEBUGGING
284     function where () {
285         if (($m = $this->match('.*\n')))
286             return sprintf('[%s]%s', $m->getPrefix(), $m->getMatch());
287         return '???';
288     }
289     
290     function subBlock ($initial_prefix, $subsequent_prefix = false) {
291         if ($subsequent_prefix === false)
292             $subsequent_prefix = $initial_prefix;
293         
294         return new BlockParser_InputSubBlock ($this, $initial_prefix, $subsequent_prefix);
295     }
296 }
297
298 class BlockParser_InputSubBlock extends BlockParser_Input
299 {
300     function BlockParser_InputSubBlock (&$block, $initial_prefix, $subsequent_prefix) {
301         $this->_text = &$block->_text;
302         $this->_pos = &$block->_pos;
303         $this->_atBreak = &$block->_atBreak;
304
305         $this->_depth = $block->_depth + 1;
306
307         $this->_set_prefix($block->_prefix . $initial_prefix,
308                            $block->_next_prefix . $subsequent_prefix);
309     }
310 }
311
312     
313
314 class Block {
315     var $_tag;
316     var $_attr = false;
317     var $_re;
318     var $_followsBreak = false;
319     var $_preceedsBreak = false;
320     var $_content = array();
321
322         
323     function _parse (&$input, $match) {
324         trigger_error('pure virtual', E_USER_ERROR);
325     }
326
327     function _pushContent ($c) {
328         if (!is_array($c))
329             $c = func_get_args();
330         foreach ($c as $x)
331             $this->_content[] = $x;
332     }
333
334     function isTerminal () {
335         return true;
336     }
337     
338     function merge ($followingBlock) {
339         return false;
340     }
341
342     function finish (/*$tighten*/) {
343         return new HtmlElement($this->_tag, $this->_attr, $this->_content);
344     }
345 }
346
347
348 class CompoundBlock extends Block
349 {
350     function isTerminal () {
351         return false;
352     }
353 }
354
355
356 class Block_blockquote extends CompoundBlock
357 {
358     var $_tag ='blockquote';
359     var $_depth;
360     var $_re = '\ +(?=\S)';
361     
362     function _parse (&$input, $m) {
363         $indent = $m->getMatch();
364         $this->_depth = strlen($indent);
365         $this->_content = BlockParser::parse($input->subBlock($indent),
366                                              BLOCK_NOTIGHTEN_EITHER);
367         return true;
368     }
369
370     function merge ($nextBlock) {
371         if (get_class($nextBlock) != 'block_blockquote')
372             return false;
373         assert ($nextBlock->_depth < $this->_depth);
374         
375         $content = $nextBlock->_content;
376         array_unshift($content, $this->finish());
377         $this->_content = $content;
378         return true;
379     }
380 }
381
382 class Block_list extends CompoundBlock
383 {
384     //var $_tag = 'ol' or 'ul';
385     var $_re = '\ {0,4}([*+#]|-(?!-)|o(?=\ ))\ *(?=\S)';
386
387     function _parse (&$input, $m) {
388         // A list as the first content in a list is not allowed.
389         // E.g.:
390         //   *  * Item
391         // Should markup as <ul><li>* Item</li></ul>,
392         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
393         //
394         if (preg_match('/[-*o+#;]\s*$/', $m->getPrefix()))
395             return false;
396         
397         $prefix = $m->getMatch();
398         $leader = preg_quote($prefix, '/');
399         $indent = sprintf("\\ {%d}", strlen($prefix));
400
401         $bullet = $m->getMatch(1);
402         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
403         
404         $text = $input->subBlock($leader, $indent);
405         $content = BlockParser::parse($text, BLOCK_NOTIGHTEN_AFTER);
406         $this->_pushContent(HTML::li(false, $content));
407         return true;
408     }
409     
410     function merge ($nextBlock) {
411         if (!isa($nextBlock, 'Block_list') || $this->_tag != $nextBlock->_tag)
412             return false;
413
414         $this->_pushContent($nextBlock->_content);
415         return true;
416     }
417 }
418
419
420 class Block_dl extends Block_list
421 {
422     var $_tag = 'dl';
423     var $_re = '(\ {0,4})([^\s!].*):\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
424     //          1-------12--------2                   3-----3
425
426     function _parse (&$input, $m) {
427         $term = TransformInline(rtrim($m->getMatch(2)));
428         $indent = $m->getMatch(3);
429
430         $input->accept($m);
431         
432         $this->_pushContent(HTML::dt(false, $term),
433                             HTML::dd(false,
434                                      BlockParser::parse($input->subBlock($indent),
435                                                         BLOCK_NOTIGHTEN_AFTER)));
436         return true;
437     }
438 }
439
440 class Block_table_dl_row extends HtmlElement
441 {
442     function Block_table_dl_row ($defn) {
443         $this->HtmlElement('tr', /*array('valign' => 'top'), */
444                            HTML::td(false, $defn));
445         $this->_ncols = 2;
446     }
447
448     function setWidth ($ncols) {
449         assert ($ncols >= $this->_ncols);
450         if ($ncols <= $this->_ncols)
451             return;
452         $last_td = &$this->_content[count($this->_content) - 1];
453         $span = max(1, (int)$last_td->getAttr('colspan'));
454         $last_td->setAttr('colspan', $span + $ncols - $this->_ncols);
455         $this->_ncols = $ncols;
456     }
457
458     function setTerm ($term, $rowspan = 1) {
459         if ($term->isEmpty())
460             $term = NBSP;
461         
462         $th = HTML::th(/*array('align' => 'right'),*/ $term);
463         if ($rowspan > 1)
464             $th->setAttr('rowspan', $rowspan);
465         $this->unshiftContent($th);
466     }
467 }
468
469 class Block_table_dl extends Block_list
470 {
471     var $_tag = 'table';
472     var $_attr = array('class' => 'wiki-dl-table',
473                        'border' => 2, // FIXME: CSS?
474                        'cellspacing' => 0,
475                        'cellpadding' => 6);
476     
477
478     var $_re = '(\ {0,4})((?![\s!]).*)?[|]\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
479     //          1-------12-----------2                      3-----3
480
481     function _parse (&$input, $m) {
482         $term = TransformInline(rtrim($m->getMatch(2)));
483         $indent = $m->getMatch(3);
484
485         $input->accept($m);
486         $defn = BlockParser::parse($input->subBlock($indent),
487                                    BLOCK_NOTIGHTEN_AFTER);
488
489         $this->_pushDefinition($term, $defn);
490         return true;
491     }
492
493     function _pushDefinition ($term, $defn) {
494         if (!is_array($defn))
495             $defn = array($defn);
496
497         $rows = array();
498         $grp = array();
499
500         foreach ($defn as $item) {
501             if (! isa($item, 'HtmlElement')
502                 || $item->getTag() != 'table'
503                 || $item->getAttr('class') != 'wiki-dl-table') {
504
505                 $grp[] = $item;
506                 continue;
507             }
508
509             if ($grp) {
510                 $rows[] = new Block_table_dl_row($grp);
511                 $grp = array();
512             }
513
514             $subtable = $item;
515             foreach ($subtable->getContent() as $tr) {
516                 $tr->_ncols++;
517                 $rows[] = $tr;
518             }
519         }
520
521         if ($grp || !$rows)
522             $rows[] = new Block_table_dl_row($grp);
523         
524         $nrows = count($rows);
525         $rows[0]->setTerm($term, $nrows);
526         $this->_pushContent($rows);
527     }
528             
529     function finish () {
530         $rows = &$this->_content;
531
532         $ncols = 0;
533         foreach ($rows as $tr)
534             $ncols = max($ncols, $tr->_ncols);
535         foreach ($rows as $key => $tr)
536             $rows[$key]->setWidth($ncols);
537
538         return parent::finish();
539     }
540 }
541
542 class Block_oldlists extends Block_list
543 {
544     //var $_tag = 'ol', 'ul', or 'dl';
545     var $_re = '(?:([*#])|;(.*):).*?(?=\S)';
546     //             1----1  2--2
547     
548     function _parse (&$input, $m) {
549         if (!preg_match('/[*#;]*$/A', $m->getPrefix()))
550             return false;
551
552         $prefix = $m->getMatch();
553
554         $leader = preg_quote($prefix, '/');
555
556         $oldindent = '[*#;](?=[#*]|;.*:.*?\S)';
557         $newindent = sprintf('\\ {%d}', strlen($prefix));
558         $indent = "(?:$oldindent|$newindent)";
559
560         $bullet = $m->getMatch(1);
561         if ($bullet) {
562             $this->_tag = $bullet == '*' ? 'ul' : 'ol';
563             $item = HTML::li();
564         }
565         else {
566             $this->_tag = 'dl';
567             $term = trim($m->getMatch(2));
568             if ($term)
569                 $this->_pushContent(HTML::dt(false, TransformInline($term)));
570             $item = HTML::dd();
571         }
572         
573         $item->pushContent(BlockParser::parse($input->subBlock($leader, $indent),
574                                               BLOCK_NOTIGHTEN_AFTER));
575         $this->_pushContent($item);
576         return true;
577     }
578 }
579
580 class Block_pre extends Block
581 {
582     var $_tag = 'pre';
583     var $_re = '<(pre|verbatim)>(.*?(?:\s*\n^.*?)*?)(?<!~)<\/\1>\s*?\n';
584     //           1------------1 2------------------2
585
586     function _parse (&$input, $m) {
587         $input->accept($m);
588
589         $text = $m->getMatch(2);
590         $tag = $m->getMatch(1);
591
592         if ($tag == 'pre')
593             $text = TransformInline($text);
594
595         $this->_pushContent($text);
596         return true;
597     }
598 }
599
600 class Block_plugin extends Block
601 {
602     var $_tag = 'div';
603     var $_attr = array('class' => 'plugin');
604     var $_re = '<\?plugin(?:-form)?.*?(?:\n^.*?)*?(?<!~)\?>\s*?\n';
605
606     function _parse (&$input, $m) {
607         global $request;
608         $loader = new WikiPluginLoader;
609         $input->accept($m);
610         $this->_pushContent($loader->expandPI($m->getMatch(), $request));
611         return true;
612     }
613 }
614
615 class Block_hr extends Block
616 {
617     var $_tag = 'hr';
618     var $_re = '-{4,}\s*?\n';
619
620     function _parse (&$input, $m) {
621         $input->accept($m);
622         return true;
623     }
624 }
625
626 class Block_heading extends Block
627 {
628     var $_re = '(!{1,3})(.*)\n';
629     
630     function _parse (&$input, $m) {
631         $input->accept($m);
632         $this->_tag = "h" . (5 - strlen($m->getMatch(1)));
633         $this->_pushContent(TransformInline(trim($m->getMatch(2))));
634         return true;
635     }
636 }
637
638 class Block_p extends Block
639 {
640     var $_tag = 'p';
641     var $_re = '\S.*\n';
642
643     function _parse (&$input, $m) {
644         $this->_text = $m->getMatch();
645         $input->accept($m);
646         return true;
647     }
648     
649     function merge ($nextBlock) {
650         if ($this->_preceedsBreak || get_class($nextBlock) != 'block_p')
651             return false;
652
653         $this->_text .= $nextBlock->_text;
654         $this->_preceedsBreak = $nextBlock->_preceedsBreak;
655         return true;
656     }
657             
658     function finish ($tighten) {
659         $this->_pushContent(TransformInline(trim($this->_text)));
660         
661         if ($this->_followsBreak && ($tighten & BLOCK_NOTIGHTEN_AFTER) != 0)
662             $tighten = 0;
663         elseif ($this->_preceedsBreak && ($tighten & BLOCK_NOTIGHTEN_BEFORE) != 0)
664             $tighten = 0;
665
666         return $tighten ? $this->_content : parent::finish();
667     }
668 }
669
670 class Block_email_blockquote extends CompoundBlock
671 {
672     // FIXME: move CSS to CSS.
673     var $_tag ='blockquote';
674     var $_attr = array('style' => 'border-left-width: medium; border-left-color: #0f0; border-left-style: ridge; padding-left: 1em; margin-left: 0em; margin-right: 0em;');
675     var $_depth;
676     var $_re = '>\ ?';
677     
678     function _parse (&$input, $m) {
679         $prefix = $m->getMatch();
680         $indent = "(?:$prefix|>(?=\s*?\n))";
681         $this->_content = BlockParser::parse($input->subBlock($indent),
682                                              BLOCK_NOTIGHTEN_EITHER);
683         return true;
684     }
685 }
686
687 ////////////////////////////////////////////////////////////////
688 //
689
690
691
692 $GLOBALS['Block_BlockTypes'] = array(new Block_oldlists,
693                                      new Block_list,
694                                      new Block_dl,
695                                      new Block_table_dl,
696                                      new Block_blockquote,
697                                      new Block_heading,
698                                      new Block_hr,
699                                      new Block_pre,
700                                      new Block_email_blockquote,
701                                      new Block_plugin,
702                                      new Block_p);
703
704 // FIXME: This is temporary, too...
705 function NewTransform ($text) {
706
707     set_time_limit(2);
708     
709     // Expand leading tabs.
710     // FIXME: do this better. also move  it...
711     $text = preg_replace('/^\ *[^\ \S\n][^\S\n]*/me', "str_repeat(' ', strlen('\\0'))", $text);
712     assert(!preg_match('/^\ *\t/', $text));
713
714     $input = new BlockParser_Input($text);
715     return BlockParser::parse($input);
716 }
717
718
719 // FIXME: bad name
720 function TransformRevision ($revision) {
721     if ($revision->get('markup') == 'new') {
722         return NewTransform($revision->getPackedContent());
723     }
724     else {
725         return do_transform($revision->getContent());
726     }
727 }
728
729
730 // (c-file-style: "gnu")
731 // Local Variables:
732 // mode: php
733 // tab-width: 8
734 // c-basic-offset: 4
735 // c-hanging-comment-ender-p: nil
736 // indent-tabs-mode: nil
737 // End:   
738 ?>