]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/BlockParser.php
Another refactor of the OOP HTML/XML generation code. (Sorry.)
[SourceForge/phpwiki.git] / lib / BlockParser.php
1 <?php rcs_id('$Id: BlockParser.php,v 1.9 2002-01-28 18:49:08 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 = HTML();
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             $content->pushContent($block->finish($tighten_mode));
105         }
106         return $content;
107     }
108
109     function _nextBlock (&$input) {
110         global $Block_BlockTypes;
111         
112         if ($input->atEof())
113             return false;
114         
115         foreach ($Block_BlockTypes as $type) {
116             if ($m = $input->match($type->_re)) {
117                 BlockParser::_debug('>', get_class($type), $input);
118                 
119                 $block = $type;
120                 $block->_followsBreak = $input->atBreak();
121                 if (!$block->_parse($input, $m)) {
122                     BlockParser::_debug('[', "_parse failed", $input);
123                     continue;
124                 }
125                 $block->_preceedsBreak = $input->eatSpace();
126                 BlockParser::_debug('<', get_class($type), $input);
127                 return $block;
128             }
129         }
130
131         if ($input->getDepth() == 0) {
132             // We should never get here.
133             //preg_match('/.*/A', substr($this->_text, $this->_pos), $m);// get first line
134             trigger_error("Couldn't match block: '".rawurlencode($m[0])."'", E_USER_NOTICE);
135         }
136         //FIXME:$this->_debug("no match");
137         return false;
138     }
139
140     function _debug ($tab, $msg, $input) {
141         return ;
142         
143         $tab = str_repeat($tab, $input->getDepth() + 1);
144         printXML(HTML::div("$tab $msg: at: '",
145                            HTML::tt($input->where()),
146                            "'"));
147     }
148     
149 }
150
151 class BlockParser_Match {
152     function BlockParser_Match ($match_data) {
153         $this->_m = $match_data;
154     }
155
156     function getPrefix () {
157         return $this->_m[1];
158     }
159
160     function getMatch ($n = 0) {
161         $text = $this->_m[$n + 2];
162         //if (preg_match('/\n./s', $text)) {
163             $prefix = $this->getPrefix();
164             $text = str_replace("\n$prefix", "\n", $text);
165         //}
166         return $text;
167     }
168 }
169
170     
171 class BlockParser_Input {
172
173     function BlockParser_Input ($text) {
174         $this->_text = $text;
175         $this->_pos = 0;
176         $this->_depth = 0;
177         
178         // Expand leading tabs.
179         // FIXME: do this better.
180         //
181         // We want to ensure the only characters matching \s are ' ' and "\n".
182         //
183         $this->_text = preg_replace('/(?![ \n])\s/', ' ', $this->_text);
184         assert(!preg_match('/(?![ \n])\s/', $this->_text));
185         if (!preg_match('/\n$/', $this->_text))
186             $this->_text .= "\n";
187
188         $this->_set_prefix ('');
189         $this->_atBreak = false;
190         $this->eatSpace();
191     }
192
193     function _set_prefix ($prefix, $next_prefix = false) {
194         if ($next_prefix === false)
195             $next_prefix = $prefix;
196
197         $this->_prefix = $prefix;
198         $this->_next_prefix = $next_prefix;
199
200         $this->_regexp_cache = array();
201
202         $blank = "(:?$prefix)?\s*\n";
203         $this->_blank_pat = "/$blank/A";
204         $this->_eof_pat = "/\\Z|(?!$blank|${prefix}.)/A";
205     }
206
207     function atEof () {
208         return preg_match($this->_eof_pat, substr($this->_text, $this->_pos));
209     }
210
211     function match ($regexp) {
212         $cache = &$this->_regexp_cache;
213         if (!isset($cache[$regexp])) {
214             // Fix up any '^'s in pattern (add our prefix)
215             $re = preg_replace('/(?<! [ [ \\\\ ]) \^ /x',
216                                '^' . $this->_next_prefix, $regexp);
217
218             // Fix any match  backreferences (like '\1').
219             $re = preg_replace('/(?<= [^ \\\\ ] [ \\\\ ] )( \\d+ )/ex', "'\\1' + 2", $re);
220
221             $re = "/(" . $this->_prefix . ")($re)/Am";
222             $cache[$regexp] = $re;
223         }
224         else
225             $re = $cache[$regexp];
226         
227         if (preg_match($re, substr($this->_text, $this->_pos), $m)) {
228             return new BlockParser_Match($m);
229         }
230         return false;
231     }
232
233     function accept ($match) {
234         $text = $match->_m[0];
235
236         assert(substr($this->_text, $this->_pos, strlen($text)) == $text);
237         $this->_pos += strlen($text);
238
239         // FIXME:
240         assert(preg_match("/\n$/", $text));
241             
242         if ($this->_next_prefix != $this->_prefix)
243             $this->_set_prefix($this->_next_prefix);
244
245         $this->_atBreak = false;
246         $this->eatSpace();
247     }
248     
249     /**
250      * Consume blank lines.
251      *
252      * @return bool True if any blank lines where comsumed.
253      */
254     function eatSpace () {
255         if (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
256             $this->_pos += strlen($m[0]);
257             if ($this->_next_prefix != $this->_prefix)
258                 $this->_set_prefix($this->_next_prefix);
259             $this->_atBreak = true;
260
261             while (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
262                 $this->_pos += strlen($m[0]);
263             }
264         }
265
266         return $this->_atBreak;
267     }
268     
269     function atBreak () {
270         return $this->_atBreak;
271     }
272
273     function getDepth () {
274         return $this->_depth;
275     }
276
277     // DEBUGGING
278     function where () {
279         if (($m = $this->match('.*\n')))
280             return sprintf('[%s]%s', $m->getPrefix(), $m->getMatch());
281         return '???';
282     }
283     
284     function subBlock ($initial_prefix, $subsequent_prefix = false) {
285         if ($subsequent_prefix === false)
286             $subsequent_prefix = $initial_prefix;
287         
288         return new BlockParser_InputSubBlock ($this, $initial_prefix, $subsequent_prefix);
289     }
290 }
291
292 class BlockParser_InputSubBlock extends BlockParser_Input
293 {
294     function BlockParser_InputSubBlock (&$block, $initial_prefix, $subsequent_prefix) {
295         $this->_text = &$block->_text;
296         $this->_pos = &$block->_pos;
297         $this->_atBreak = &$block->_atBreak;
298
299         $this->_depth = $block->_depth + 1;
300
301         $this->_set_prefix($block->_prefix . $initial_prefix,
302                            $block->_next_prefix . $subsequent_prefix);
303     }
304 }
305
306     
307
308 class Block {
309     var $_tag;
310     var $_attr = false;
311     var $_re;
312     var $_followsBreak = false;
313     var $_preceedsBreak = false;
314     var $_content = array();
315
316         
317     function _parse (&$input, $match) {
318         trigger_error('pure virtual', E_USER_ERROR);
319     }
320
321     function _pushContent ($c) {
322         if (!is_array($c))
323             $c = func_get_args();
324         foreach ($c as $x)
325             $this->_content[] = $x;
326     }
327
328     function isTerminal () {
329         return true;
330     }
331     
332     function merge ($followingBlock) {
333         return false;
334     }
335
336     function finish (/*$tighten*/) {
337         return new HtmlElement($this->_tag, $this->_attr, $this->_content);
338     }
339 }
340
341
342 class CompoundBlock extends Block
343 {
344     function isTerminal () {
345         return false;
346     }
347 }
348
349
350 class Block_blockquote extends CompoundBlock
351 {
352     var $_tag ='blockquote';
353     var $_depth;
354     var $_re = '\ +(?=\S)';
355     
356     function _parse (&$input, $m) {
357         $indent = $m->getMatch();
358         $this->_depth = strlen($indent);
359         $this->_content = BlockParser::parse($input->subBlock($indent),
360                                              BLOCK_NOTIGHTEN_EITHER);
361         return true;
362     }
363
364     function merge ($nextBlock) {
365         if (get_class($nextBlock) != 'block_blockquote')
366             return false;
367         assert ($nextBlock->_depth < $this->_depth);
368         
369         $content = $nextBlock->_content;
370         array_unshift($content, $this->finish());
371         $this->_content = $content;
372         return true;
373     }
374 }
375
376 class Block_list extends CompoundBlock
377 {
378     //var $_tag = 'ol' or 'ul';
379     var $_re = '\ {0,4}([*+#]|-(?!-)|o(?=\ ))\ *(?=\S)';
380
381     function _parse (&$input, $m) {
382         // A list as the first content in a list is not allowed.
383         // E.g.:
384         //   *  * Item
385         // Should markup as <ul><li>* Item</li></ul>,
386         // not <ul><li><ul><li>Item</li></ul>/li></ul>.
387         //
388         if (preg_match('/[-*o+#;]\s*$/', $m->getPrefix()))
389             return false;
390         
391         $prefix = $m->getMatch();
392         $leader = preg_quote($prefix, '/');
393         $indent = sprintf("\\ {%d}", strlen($prefix));
394
395         $bullet = $m->getMatch(1);
396         $this->_tag = $bullet == '#' ? 'ol' : 'ul';
397         
398         $text = $input->subBlock($leader, $indent);
399         $content = BlockParser::parse($text, BLOCK_NOTIGHTEN_AFTER);
400         $this->_pushContent(HTML::li(false, $content));
401         return true;
402     }
403     
404     function merge ($nextBlock) {
405         if (!isa($nextBlock, 'Block_list') || $this->_tag != $nextBlock->_tag)
406             return false;
407
408         $this->_pushContent($nextBlock->_content);
409         return true;
410     }
411 }
412
413
414 class Block_dl extends Block_list
415 {
416     var $_tag = 'dl';
417     var $_re = '(\ {0,4})([^\s!].*):\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
418     //          1-------12--------2                   3-----3
419
420     function _parse (&$input, $m) {
421         $term = TransformInline(rtrim($m->getMatch(2)));
422         $indent = $m->getMatch(3);
423
424         $input->accept($m);
425         
426         $this->_pushContent(HTML::dt(false, $term),
427                             HTML::dd(false,
428                                      BlockParser::parse($input->subBlock($indent),
429                                                         BLOCK_NOTIGHTEN_AFTER)));
430         return true;
431     }
432 }
433
434 class Block_table_dl_row extends HtmlElement
435 {
436     function Block_table_dl_row ($defn) {
437         $this->HtmlElement('tr', /*array('valign' => 'top'), */
438                            HTML::td(false, $defn));
439         $this->_ncols = 2;
440     }
441
442     function setWidth ($ncols) {
443         assert ($ncols >= $this->_ncols);
444         if ($ncols <= $this->_ncols)
445             return;
446         $last_td = &$this->_content[count($this->_content) - 1];
447         $span = max(1, (int)$last_td->getAttr('colspan'));
448         $last_td->setAttr('colspan', $span + $ncols - $this->_ncols);
449         $this->_ncols = $ncols;
450     }
451
452     function setTerm ($term, $rowspan = 1) {
453         if ($term->isEmpty())
454             $term = NBSP;
455         
456         $th = HTML::th(/*array('align' => 'right'),*/ $term);
457         if ($rowspan > 1)
458             $th->setAttr('rowspan', $rowspan);
459         $this->unshiftContent($th);
460     }
461 }
462
463 class Block_table_dl extends Block_list
464 {
465     var $_tag = 'table';
466     var $_attr = array('class' => 'wiki-dl-table',
467                        'border' => 2, // FIXME: CSS?
468                        'cellspacing' => 0,
469                        'cellpadding' => 6);
470     
471
472     var $_re = '(\ {0,4})((?![\s!]).*)?[|]\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
473     //          1-------12-----------2                      3-----3
474
475     function _parse (&$input, $m) {
476         $term = TransformInline(rtrim($m->getMatch(2)));
477         $indent = $m->getMatch(3);
478
479         $input->accept($m);
480         $defn = BlockParser::parse($input->subBlock($indent),
481                                    BLOCK_NOTIGHTEN_AFTER);
482
483         $this->_pushDefinition($term, $defn);
484         return true;
485     }
486
487     function _pushDefinition ($term, $defn) {
488         if (!is_array($defn))
489             $defn = array($defn);
490
491         $rows = array();
492         $grp = array();
493
494         foreach ($defn as $item) {
495             if (! isa($item, 'HtmlElement')
496                 || $item->getTag() != 'table'
497                 || $item->getAttr('class') != 'wiki-dl-table') {
498
499                 $grp[] = $item;
500                 continue;
501             }
502
503             if ($grp) {
504                 $rows[] = new Block_table_dl_row($grp);
505                 $grp = array();
506             }
507
508             $subtable = $item;
509             foreach ($subtable->getContent() as $tr) {
510                 $tr->_ncols++;
511                 $rows[] = $tr;
512             }
513         }
514
515         if ($grp || !$rows)
516             $rows[] = new Block_table_dl_row($grp);
517         
518         $nrows = count($rows);
519         $rows[0]->setTerm($term, $nrows);
520         $this->_pushContent($rows);
521     }
522             
523     function finish () {
524         $rows = &$this->_content;
525
526         $ncols = 0;
527         foreach ($rows as $tr)
528             $ncols = max($ncols, $tr->_ncols);
529         foreach ($rows as $key => $tr)
530             $rows[$key]->setWidth($ncols);
531
532         return parent::finish();
533     }
534 }
535
536 class Block_oldlists extends Block_list
537 {
538     //var $_tag = 'ol', 'ul', or 'dl';
539     var $_re = '(?:([*#])|;(.*):).*?(?=\S)';
540     //             1----1  2--2
541     
542     function _parse (&$input, $m) {
543         if (!preg_match('/[*#;]*$/A', $m->getPrefix()))
544             return false;
545
546         $prefix = $m->getMatch();
547
548         $leader = preg_quote($prefix, '/');
549
550         $oldindent = '[*#;](?=[#*]|;.*:.*?\S)';
551         $newindent = sprintf('\\ {%d}', strlen($prefix));
552         $indent = "(?:$oldindent|$newindent)";
553
554         $bullet = $m->getMatch(1);
555         if ($bullet) {
556             $this->_tag = $bullet == '*' ? 'ul' : 'ol';
557             $item = HTML::li();
558         }
559         else {
560             $this->_tag = 'dl';
561             $term = trim($m->getMatch(2));
562             if ($term)
563                 $this->_pushContent(HTML::dt(false, TransformInline($term)));
564             $item = HTML::dd();
565         }
566         
567         $item->pushContent(BlockParser::parse($input->subBlock($leader, $indent),
568                                               BLOCK_NOTIGHTEN_AFTER));
569         $this->_pushContent($item);
570         return true;
571     }
572 }
573
574 class Block_pre extends Block
575 {
576     var $_tag = 'pre';
577     var $_re = '<(pre|verbatim)>(.*?(?:\s*\n^.*?)*?)(?<!~)<\/\1>\s*?\n';
578     //           1------------1 2------------------2
579
580     function _parse (&$input, $m) {
581         $input->accept($m);
582
583         $text = $m->getMatch(2);
584         $tag = $m->getMatch(1);
585
586         if ($tag == 'pre')
587             $text = TransformInline($text);
588
589         $this->_pushContent($text);
590         return true;
591     }
592 }
593
594 class Block_plugin extends Block
595 {
596     var $_tag = 'div';
597     var $_attr = array('class' => 'plugin');
598     var $_re = '<\?plugin(?:-form)?.*?(?:\n^.*?)*?(?<!~)\?>\s*?\n';
599
600     function _parse (&$input, $m) {
601         global $request;
602         $loader = new WikiPluginLoader;
603         $input->accept($m);
604         $this->_pushContent($loader->expandPI($m->getMatch(), $request));
605         return true;
606     }
607 }
608
609 class Block_hr extends Block
610 {
611     var $_tag = 'hr';
612     var $_re = '-{4,}\s*?\n';
613
614     function _parse (&$input, $m) {
615         $input->accept($m);
616         return true;
617     }
618 }
619
620 class Block_heading extends Block
621 {
622     var $_re = '(!{1,3})(.*)\n';
623     
624     function _parse (&$input, $m) {
625         $input->accept($m);
626         $this->_tag = "h" . (5 - strlen($m->getMatch(1)));
627         $this->_pushContent(TransformInline(trim($m->getMatch(2))));
628         return true;
629     }
630 }
631
632 class Block_p extends Block
633 {
634     var $_tag = 'p';
635     var $_re = '\S.*\n';
636
637     function _parse (&$input, $m) {
638         $this->_text = $m->getMatch();
639         $input->accept($m);
640         return true;
641     }
642     
643     function merge ($nextBlock) {
644         if ($this->_preceedsBreak || get_class($nextBlock) != 'block_p')
645             return false;
646
647         $this->_text .= $nextBlock->_text;
648         $this->_preceedsBreak = $nextBlock->_preceedsBreak;
649         return true;
650     }
651             
652     function finish ($tighten) {
653         $this->_pushContent(TransformInline(trim($this->_text)));
654         
655         if ($this->_followsBreak && ($tighten & BLOCK_NOTIGHTEN_AFTER) != 0)
656             $tighten = 0;
657         elseif ($this->_preceedsBreak && ($tighten & BLOCK_NOTIGHTEN_BEFORE) != 0)
658             $tighten = 0;
659
660         return $tighten ? $this->_content : parent::finish();
661     }
662 }
663
664 class Block_email_blockquote extends CompoundBlock
665 {
666     // FIXME: move CSS to CSS.
667     var $_tag ='blockquote';
668     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;');
669     var $_depth;
670     var $_re = '>\ ?';
671     
672     function _parse (&$input, $m) {
673         $prefix = $m->getMatch();
674         $indent = "(?:$prefix|>(?=\s*?\n))";
675         $this->_content = BlockParser::parse($input->subBlock($indent),
676                                              BLOCK_NOTIGHTEN_EITHER);
677         return true;
678     }
679 }
680
681 ////////////////////////////////////////////////////////////////
682 //
683
684
685
686 $GLOBALS['Block_BlockTypes'] = array(new Block_oldlists,
687                                      new Block_list,
688                                      new Block_dl,
689                                      new Block_table_dl,
690                                      new Block_blockquote,
691                                      new Block_heading,
692                                      new Block_hr,
693                                      new Block_pre,
694                                      new Block_email_blockquote,
695                                      new Block_plugin,
696                                      new Block_p);
697
698 // FIXME: This is temporary, too...
699 function NewTransform ($text) {
700
701     set_time_limit(2);
702     
703     // Expand leading tabs.
704     // FIXME: do this better. also move  it...
705     $text = preg_replace('/^\ *[^\ \S\n][^\S\n]*/me', "str_repeat(' ', strlen('\\0'))", $text);
706     assert(!preg_match('/^\ *\t/', $text));
707
708     $input = new BlockParser_Input($text);
709     return BlockParser::parse($input);
710 }
711
712
713 // FIXME: bad name
714 function TransformRevision ($revision) {
715     if ($revision->get('markup') == 'new') {
716         return NewTransform($revision->getPackedContent());
717     }
718     else {
719         return do_transform($revision->getContent());
720     }
721 }
722
723
724 // (c-file-style: "gnu")
725 // Local Variables:
726 // mode: php
727 // tab-width: 8
728 // c-basic-offset: 4
729 // c-hanging-comment-ender-p: nil
730 // indent-tabs-mode: nil
731 // End:   
732 ?>