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