1 <?php // $Id: BlockParser.php 7964 2011-03-05 17:05:30Z vargenau $
2 /* Copyright (C) 2002 Geoffrey T. Dairiki <dairiki@dairiki.org>
3 * Copyright (C) 2004,2005 Reini Urban
4 * Copyright (C) 2008-2010 Marc-Etienne Vargenau, Alcatel-Lucent
6 * This file is part of PhpWiki.
8 * PhpWiki is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * PhpWiki is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 //require_once('lib/HtmlElement.php');
24 require_once('lib/CachedMarkup.php');
25 require_once('lib/InlineParser.php');
28 * Deal with paragraphs and proper, recursive block indents
29 * for the new style markup (version 2)
31 * Everything which goes over more than line:
32 * automatic lists, UL, OL, DL, table, blockquote, verbatim,
38 * FIXME: unify this with the RegexpSet in InlineParser.
40 * FIXME: This is very php5 sensitive: It was fixed for 1.3.9,
41 * but is again broken with the 1.3.11
42 * allow_call_time_pass_reference clean fixes
45 * @author: Geoffrey T. Dairiki
49 * Return type from RegexpSet::match and RegexpSet::nextMatch.
53 class AnchoredRegexpSet_match {
60 * The text following the matched text.
65 * Index of the regular expression which matched.
71 * A set of regular expressions.
73 * This class is probably only useful for InlineTransformer.
75 class AnchoredRegexpSet
79 * @param $regexps array A list of regular expressions. The
80 * regular expressions should not include any sub-pattern groups
81 * "(...)". (Anonymous groups, like "(?:...)", as well as
82 * look-ahead and look-behind assertions are fine.)
84 function AnchoredRegexpSet ($regexps) {
85 $this->_regexps = $regexps;
86 $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
90 * Search text for the next matching regexp from the Regexp Set.
92 * @param $text string The text to search.
94 * @return object A RegexpSet_match object, or false if no match.
96 function match ($text) {
97 if (!is_string($text)) return false;
98 if (! preg_match($this->_re, $text, $m)) {
102 $match = new AnchoredRegexpSet_match;
103 $match->postmatch = substr($text, strlen($m[0]));
104 $match->match = $m[1];
105 $match->regexp_ind = count($m) - 3;
110 * Search for next matching regexp.
112 * Here, 'next' has two meanings:
114 * Match the next regexp(s) in the set, at the same position as the last match.
116 * If that fails, match the whole RegexpSet, starting after the position of the
119 * @param $text string Text to search.
121 * @param $prevMatch A RegexpSet_match object
123 * $prevMatch should be a match object obtained by a previous
124 * match upon the same value of $text.
126 * @return object A RegexpSet_match object, or false if no match.
128 function nextMatch ($text, $prevMatch) {
129 // Try to find match at same position.
130 $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
135 $pat= "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
137 if (! preg_match($pat, $text, $m)) {
141 $match = new AnchoredRegexpSet_match;
142 $match->postmatch = substr($text, strlen($m[0]));
143 $match->match = $m[1];
144 $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;;
151 class BlockParser_Input {
153 function BlockParser_Input ($text) {
155 // Expand leading tabs.
156 // FIXME: do this better.
158 // We want to ensure the only characters matching \s are ' ' and "\n".
160 $text = preg_replace('/(?![ \n])\s/', ' ', $text);
161 assert(!preg_match('/(?![ \n])\s/', $text));
163 $this->_lines = preg_split('/[^\S\n]*\n/', $text);
166 // Strip leading blank lines.
167 while ($this->_lines and ! $this->_lines[0])
168 array_shift($this->_lines);
169 $this->_atSpace = false;
172 function skipSpace () {
173 $nlines = count($this->_lines);
175 if ($this->_pos >= $nlines) {
176 $this->_atSpace = false;
179 if ($this->_lines[$this->_pos] != '')
182 $this->_atSpace = true;
184 return $this->_atSpace;
187 function currentLine () {
188 if ($this->_pos >= count($this->_lines)) {
191 return $this->_lines[$this->_pos];
194 function nextLine () {
195 $this->_atSpace = $this->_lines[$this->_pos++] === '';
196 if ($this->_pos >= count($this->_lines)) {
199 return $this->_lines[$this->_pos];
202 function advance () {
203 $this->_atSpace = ($this->_lines[$this->_pos] === '');
208 return array($this->_pos, $this->_atSpace);
211 function setPos ($pos) {
212 list($this->_pos, $this->_atSpace) = $pos;
215 function getPrefix () {
219 function getDepth () {
224 if ($this->_pos < count($this->_lines))
225 return $this->_lines[$this->_pos];
230 function _debug ($tab, $msg) {
232 $where = $this->where();
233 $tab = str_repeat('____', $this->getDepth() ) . $tab;
234 printXML(HTML::div("$tab $msg: at: '",
241 class BlockParser_InputSubBlock extends BlockParser_Input
243 function BlockParser_InputSubBlock (&$input, $prefix_re, $initial_prefix = false) {
244 $this->_input = &$input;
245 $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
246 $this->_atSpace = false;
248 if (($line = $input->currentLine()) === false)
249 $this->_line = false;
250 elseif ($initial_prefix) {
251 assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
252 $this->_line = (string) substr($line, strlen($initial_prefix));
253 $this->_atBlank = ! ltrim($line);
255 elseif (preg_match($this->_prefix_pat, $line, $m)) {
256 $this->_line = (string) substr($line, strlen($m[0]));
257 $this->_atBlank = ! ltrim($line);
260 $this->_line = false;
263 function skipSpace () {
264 // In contrast to the case for top-level blocks,
265 // for sub-blocks, there never appears to be any trailing space.
266 // (The last block in the sub-block should always be of class tight-bottom.)
267 while ($this->_line === '')
270 if ($this->_line === false)
271 return $this->_atSpace == 'strong_space';
273 return $this->_atSpace;
276 function currentLine () {
280 function nextLine () {
281 if ($this->_line === '')
282 $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
284 $this->_atSpace = false;
286 $line = $this->_input->nextLine();
287 if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
288 $this->_line = (string) substr($line, strlen($m[0]));
289 $this->_atBlank = ! ltrim($line);
292 $this->_line = false;
297 function advance () {
302 return array($this->_line, $this->_atSpace, $this->_input->getPos());
305 function setPos ($pos) {
306 $this->_line = $pos[0];
307 $this->_atSpace = $pos[1];
308 $this->_input->setPos($pos[2]);
311 function getPrefix () {
312 assert ($this->_line !== false);
313 $line = $this->_input->currentLine();
314 assert ($line !== false && strlen($line) >= strlen($this->_line));
315 return substr($line, 0, strlen($line) - strlen($this->_line));
318 function getDepth () {
319 return $this->_input->getDepth() + 1;
323 return $this->_input->where();
328 class Block_HtmlElement extends HtmlElement
330 function Block_HtmlElement($tag /*, ... */) {
331 $this->_init(func_get_args());
334 function setTightness($top, $bottom) {
338 class ParsedBlock extends Block_HtmlElement {
340 function ParsedBlock (&$input, $tag = 'div', $attr = false) {
341 $this->Block_HtmlElement($tag, $attr);
342 $this->_initBlockTypes();
343 $this->_parse($input);
346 function _parse (&$input) {
347 // php5 failed to advance the block. php5 copies objects by ref.
348 // nextBlock == block, both are the same objects. So we have to clone it.
349 for ($block = $this->_getBlock($input);
351 $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock))
353 while ($nextBlock = $this->_getBlock($input)) {
354 // Attempt to merge current with following block.
355 if (! ($merged = $block->merge($nextBlock)) ) {
356 break; // can't merge
360 $this->pushContent($block->finish());
364 // FIXME: hackish. This should only be called once.
365 function _initBlockTypes () {
366 // better static or global?
367 static $_regexpset, $_block_types;
369 if (!is_object($_regexpset)) {
370 // nowiki_wikicreole must be before template_plugin
372 ('nowiki_wikicreole', 'template_plugin', 'placeholder', 'oldlists', 'list', 'dl',
373 'table_dl', 'table_wikicreole', 'table_mediawiki',
374 'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre',
375 'email_blockquote', 'wikicreole_indented',
376 'plugin', 'plugin_wikicreole', 'p');
377 // insert it before p!
378 if (ENABLE_MARKUP_DIVSPAN) {
379 array_pop($Block_types);
380 $Block_types[] = 'divspan';
381 $Block_types[] = 'p';
383 foreach ($Block_types as $type) {
384 $class = "Block_$type";
386 $this->_block_types[] = $proto;
387 $this->_regexps[] = $proto->_re;
389 $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
390 $_regexpset = $this->_regexpset;
391 $_block_types = $this->_block_types;
394 $this->_regexpset = $_regexpset;
395 $this->_block_types = $_block_types;
399 function _getBlock (&$input) {
400 $this->_atSpace = $input->skipSpace();
402 $line = $input->currentLine();
403 if ($line === false or $line === '') { // allow $line === '0'
406 $tight_top = !$this->_atSpace;
407 $re_set = &$this->_regexpset;
408 //FIXME: php5 fails to advance here!
409 for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
410 $block = clone($this->_block_types[$m->regexp_ind]);
411 if (DEBUG & _DEBUG_PARSER)
412 $input->_debug('>', get_class($block));
414 if ($block->_match($input, $m)) {
415 //$block->_text = $line;
416 if (DEBUG & _DEBUG_PARSER)
417 $input->_debug('<', get_class($block));
418 $tight_bottom = ! $input->skipSpace();
419 $block->_setTightness($tight_top, $tight_bottom);
422 if (DEBUG & _DEBUG_PARSER)
423 $input->_debug('[', "_match failed");
425 if ($line === false or $line === '') // allow $line === '0'
428 trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
433 class WikiText extends ParsedBlock {
434 function WikiText ($text) {
435 $input = new BlockParser_Input($text);
436 $this->ParsedBlock($input);
440 class SubBlock extends ParsedBlock {
441 function SubBlock (&$input, $indent_re, $initial_indent = false,
442 $tag = 'div', $attr = false) {
443 $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
444 $this->ParsedBlock($subinput, $tag, $attr);
449 * TightSubBlock is for use in parsing lists item bodies.
451 * If the sub-block consists of a single paragraph, it omits
452 * the paragraph element.
454 * We go to this trouble so that "tight" lists look somewhat reasonable
455 * in older (non-CSS) browsers. (If you don't do this, then, without
456 * CSS, you only get "loose" lists.
458 class TightSubBlock extends SubBlock {
459 function TightSubBlock (&$input, $indent_re, $initial_indent = false,
460 $tag = 'div', $attr = false) {
461 $this->SubBlock($input, $indent_re, $initial_indent, $tag, $attr);
463 // If content is a single paragraph, eliminate the paragraph...
464 if (count($this->_content) == 1) {
465 $elem = $this->_content[0];
466 if (isa($elem, 'XmlElement') and $elem->getTag() == 'p') {
467 $this->setContent($elem->getContent());
476 function _match (&$input, $match) {
477 trigger_error('pure virtual', E_USER_ERROR);
480 function _setTightness ($top, $bot) {
483 function merge ($followingBlock) {
488 return $this->_element;
492 class Block_blockquote extends BlockMarkup
495 var $_re = '\ +(?=\S)';
497 function _match (&$input, $m) {
498 $this->_depth = strlen($m->match);
499 $indent = sprintf("\\ {%d}", $this->_depth);
500 $this->_element = new SubBlock($input, $indent, $m->match,
505 function merge ($nextBlock) {
506 if (get_class($nextBlock) == get_class($this)) {
507 assert ($nextBlock->_depth < $this->_depth);
508 $nextBlock->_element->unshiftContent($this->_element);
509 if (!empty($this->_tight_top))
510 $nextBlock->_tight_top = $this->_tight_top;
517 class Block_list extends BlockMarkup
519 //var $_tag = 'ol' or 'ul';
525 | [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
527 var $_content = array();
529 function _match (&$input, $m) {
530 // A list as the first content in a list is not allowed.
533 // Should markup as <ul><li>* Item</li></ul>,
534 // not <ul><li><ul><li>Item</li></ul>/li></ul>.
536 if (preg_match('/[*#+-o]/', $input->getPrefix())) {
541 $indent = sprintf("\\ {%d}", strlen($prefix));
543 $bullet = trim($m->match);
544 $this->_tag = $bullet == '#' ? 'ol' : 'ul';
545 $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
549 function _setTightness($top, $bot) {
550 $li = &$this->_content[0];
551 $li->setTightness($top, $bot);
554 function merge ($nextBlock) {
555 if (isa($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
556 array_splice($this->_content, count($this->_content), 0,
557 $nextBlock->_content);
564 return new Block_HtmlElement($this->_tag, false, $this->_content);
568 class Block_dl extends Block_list
572 function Block_dl () {
573 $this->_re = '\ {0,4}\S.*(?<!'.ESCAPE_CHAR.'):\s*$';
576 function _match (&$input, $m) {
577 if (!($p = $this->_do_match($input, $m)))
579 list ($term, $defn, $loose) = $p;
581 $this->_content[] = new Block_HtmlElement('dt', false, $term);
582 $this->_content[] = $defn;
583 $this->_tight_defn = !$loose;
587 function _setTightness($top, $bot) {
588 $dt = &$this->_content[0];
589 $dd = &$this->_content[1];
591 $dt->setTightness($top, $this->_tight_defn);
592 $dd->setTightness($this->_tight_defn, $bot);
595 function _do_match (&$input, $m) {
596 $pos = $input->getPos();
598 $firstIndent = strspn($m->match, ' ');
599 $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
602 $loose = $input->skipSpace();
603 $line = $input->currentLine();
605 if (!$line || !preg_match($pat, $line, $mm)) {
606 $input->setPos($pos);
607 return false; // No body found.
610 $indent = strlen($mm[0]);
611 $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
612 $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
613 return array($term, $defn, $loose);
619 class Block_table_dl_defn extends XmlContent
624 function Block_table_dl_defn ($term, $defn) {
626 if (!is_array($defn))
627 $defn = $defn->getContent();
629 $this->_next_tight_top = false; // value irrelevant - gets fixed later
630 $this->_ncols = $this->_ComputeNcols($defn);
633 foreach ($defn as $item) {
634 if ($this->_IsASubtable($item))
635 $this->_addSubtable($item);
637 $this->_addToRow($item);
641 $th = HTML::th($term);
642 if ($this->_nrows > 1)
643 $th->setAttr('rowspan', $this->_nrows);
644 $this->_setTerm($th);
647 function setTightness($tight_top, $tight_bot) {
648 $this->_tight_top = $tight_top;
649 $this->_tight_bot = $tight_bot;
652 function _addToRow ($item) {
653 if (empty($this->_accum)) {
654 $this->_accum = HTML::td();
655 if ($this->_ncols > 2)
656 $this->_accum->setAttr('colspan', $this->_ncols - 1);
658 $this->_accum->pushContent($item);
661 function _flushRow ($tight_bottom=false) {
662 if (!empty($this->_accum)) {
663 $row = new Block_HtmlElement('tr', false, $this->_accum);
665 $row->setTightness($this->_next_tight_top, $tight_bottom);
666 $this->_next_tight_top = $tight_bottom;
668 $this->pushContent($row);
669 $this->_accum = false;
674 function _addSubtable ($table) {
675 if (!($table_rows = $table->getContent()))
678 $this->_flushRow($table_rows[0]->_tight_top);
680 foreach ($table_rows as $subdef) {
681 $this->pushContent($subdef);
682 $this->_nrows += $subdef->nrows();
683 $this->_next_tight_top = $subdef->_tight_bot;
687 function _setTerm ($th) {
688 $first_row = &$this->_content[0];
689 if (isa($first_row, 'Block_table_dl_defn'))
690 $first_row->_setTerm($th);
692 $first_row->unshiftContent($th);
695 function _ComputeNcols ($defn) {
697 foreach ($defn as $item) {
698 if ($this->_IsASubtable($item)) {
699 $row = $this->_FirstDefn($item);
700 $ncols = max($ncols, $row->ncols() + 1);
706 function _IsASubtable ($item) {
707 return isa($item, 'HtmlElement')
708 && $item->getTag() == 'table'
709 && $item->getAttr('class') == 'wiki-dl-table';
712 function _FirstDefn ($subtable) {
713 $defs = $subtable->getContent();
718 return $this->_ncols;
722 return $this->_nrows;
725 function & firstTR() {
726 $first = &$this->_content[0];
727 if (isa($first, 'Block_table_dl_defn'))
728 return $first->firstTR();
732 function & lastTR() {
733 $last = &$this->_content[$this->_nrows - 1];
734 if (isa($last, 'Block_table_dl_defn'))
735 return $last->lastTR();
739 function setWidth ($ncols) {
740 assert($ncols >= $this->_ncols);
741 if ($ncols <= $this->_ncols)
743 $rows = &$this->_content;
744 for ($i = 0; $i < count($rows); $i++) {
746 if (isa($row, 'Block_table_dl_defn'))
747 $row->setWidth($ncols - 1);
749 $n = count($row->_content);
750 $lastcol = &$row->_content[$n - 1];
751 if (!empty($lastcol))
752 $lastcol->setAttr('colspan', $ncols - 1);
758 class Block_table_dl extends Block_dl
760 var $_tag = 'dl-table'; // phony.
762 function Block_table_dl() {
763 $this->_re = '\ {0,4} (?:\S.*)? (?<!'.ESCAPE_CHAR.') \| \s* $';
766 function _match (&$input, $m) {
767 if (!($p = $this->_do_match($input, $m)))
769 list ($term, $defn, $loose) = $p;
771 $this->_content[] = new Block_table_dl_defn($term, $defn);
775 function _setTightness($top, $bot) {
776 $this->_content[0]->setTightness($top, $bot);
781 $defs = &$this->_content;
784 foreach ($defs as $defn)
785 $ncols = max($ncols, $defn->ncols());
787 foreach ($defs as $key => $defn)
788 $defs[$key]->setWidth($ncols);
790 return HTML::table(array('class' => 'wiki-dl-table',
798 class Block_oldlists extends Block_list
800 //var $_tag = 'ol', 'ul', or 'dl';
801 var $_re = '(?: [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
802 | [#]\ (?! \[ .*? \] )
806 function _match (&$input, $m) {
808 if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
814 $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
815 $newindent = sprintf('\\ {%d}', strlen($prefix));
816 $indent = "(?:$oldindent|$newindent)";
818 $bullet = $prefix[0];
819 if ($bullet == '*') {
823 elseif ($bullet == '#') {
829 list ($term,) = explode(':', substr($prefix, 1), 2);
832 $this->_content[] = new Block_HtmlElement('dt', false,
833 TransformInline($term));
837 $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
841 function _setTightness($top, $bot) {
842 if (count($this->_content) == 1) {
843 $li = &$this->_content[0];
844 $li->setTightness($top, $bot);
847 // This is where php5 usually brakes.
848 // wrong duplicated <li> contents
849 if (DEBUG and DEBUG & _DEBUG_PARSER and check_php_version(5)) {
850 if (count($this->_content) != 2) {
853 $class = new Reflection_Class('XmlElement');
854 // Print out basic information
856 "===> The %s%s%s %s '%s' [extends %s]\n".
859 " having the modifiers %d [%s]\n",
860 $class->isInternal() ? 'internal' : 'user-defined',
861 $class->isAbstract() ? ' abstract' : '',
862 $class->isFinal() ? ' final' : '',
863 $class->isInterface() ? 'interface' : 'class',
865 var_export($class->getParentClass(), 1),
866 $class->getFileName(),
867 $class->getStartLine(),
868 $class->getEndline(),
869 $class->getModifiers(),
870 implode(' ', Reflection::getModifierNames($class->getModifiers()))
872 // Print class properties
873 printf("---> Properties: %s\n", var_export($class->getProperties(), 1));
875 echo 'count($this->_content): ', count($this->_content),"\n";
876 echo "\$this->_content[0]: "; var_dump ($this->_content[0]);
878 for ($i=1; $i < min(5, count($this->_content)); $i++) {
879 $c =& $this->_content[$i];
880 echo '$this->_content[',$i,"]: \n";
881 echo "_tag: "; var_dump ($c->_tag);
882 echo "_content: "; var_dump ($c->_content);
883 echo "_properties: "; var_dump ($c->_properties);
885 debug_print_backtrace();
886 if (DEBUG & _DEBUG_APD) {
887 if (function_exists("xdebug_get_function_stack")) {
888 var_dump (xdebug_get_function_stack());
894 if (!check_php_version(5))
895 assert(count($this->_content) == 2);
896 $dt = &$this->_content[0];
897 $dd = &$this->_content[1];
898 $dt->setTightness($top, false);
899 $dd->setTightness(false, $bot);
904 class Block_pre extends BlockMarkup
906 var $_re = '<(?:pre|verbatim|nowiki|noinclude)>';
908 function _match (&$input, $m) {
909 $endtag = '</' . substr($m->match, 1);
911 $pos = $input->getPos();
913 $line = $m->postmatch;
914 while (ltrim($line) != $endtag) {
916 if (($line = $input->nextLine()) === false) {
917 $input->setPos($pos);
923 if ($m->match == '<nowiki>')
924 $text = join("<br>\n", $text);
926 $text = join("\n", $text);
928 // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
930 if ($m->match == '<pre>') {
931 $text = TransformInline($text);
933 if ($m->match == '<noinclude>') {
934 $text = TransformText($text);
935 $this->_element = new Block_HtmlElement('div', false, $text);
936 } else if ($m->match == '<nowiki>') {
937 $text = TransformInlineNowiki($text);
938 $this->_element = new Block_HtmlElement('p', false, $text);
940 $this->_element = new Block_HtmlElement('pre', false, $text);
946 // Wikicreole placeholder
948 class Block_placeholder extends BlockMarkup
952 function _match (&$input, $m) {
955 $pos = $input->getPos();
957 $line = $m->postmatch;
958 while (ltrim($line) != $endtag) {
960 if (($line = $input->nextLine()) === false) {
961 $input->setPos($pos);
967 $text = join("\n", $text);
968 $text = '<<<' . $text . '>>>';
969 $this->_element = new Block_HtmlElement('div', false, $text);
974 class Block_nowiki_wikicreole extends BlockMarkup
978 function _match (&$input, $m) {
981 $pos = $input->getPos();
983 $line = $m->postmatch;
984 while (ltrim($line) != $endtag) {
986 if (($line = $input->nextLine()) === false) {
987 $input->setPos($pos);
993 $text = join("\n", $text);
994 $this->_element = new Block_HtmlElement('pre', false, $text);
999 class Block_plugin extends Block_pre
1001 var $_re = '<\?plugin(?:-form)?(?!\S)';
1004 /* <?plugin Backlinks
1006 /* <?plugin ListPages pages=<!plugin-list Backlinks!>
1007 * exclude=<!plugin-list TitleSearch s=T*!> ?>
1011 function _match (&$input, $m) {
1012 $pos = $input->getPos();
1013 $pi = $m->match . $m->postmatch;
1014 while (!preg_match('/(?<!'.ESCAPE_CHAR.')\?>\s*$/', $pi)) {
1015 if (($line = $input->nextLine()) === false) {
1016 $input->setPos($pos);
1023 $this->_element = new Cached_PluginInvocation($pi);
1028 class Block_plugin_wikicreole extends Block_pre
1030 // var $_re = '<<(?!\S)';
1033 function _match (&$input, $m) {
1034 $pos = $input->getPos();
1035 $pi = $m->postmatch;
1036 if ($pi[0] == '<') {
1039 $pi = "<?plugin " . $pi;
1040 while (!preg_match('/(?<!'.ESCAPE_CHAR.')>>\s*$/', $pi)) {
1041 if (($line = $input->nextLine()) === false) {
1042 $input->setPos($pos);
1049 $pi = str_replace(">>", "?>", $pi);
1051 $this->_element = new Cached_PluginInvocation($pi);
1056 class Block_table_wikicreole extends Block_pre
1060 function _match (&$input, $m) {
1061 $pos = $input->getPos();
1062 $pi = "|" . $m->postmatch;
1066 if ((($line = $input->nextLine()) === false) && !$intable) {
1067 $input->setPos($pos);
1074 $trimline = trim($line);
1075 if ($trimline[0] != "|") {
1079 $pi .= "\n$trimline";
1082 $pi = '<'.'?plugin WikicreoleTable ' . $pi . '?'.'>';
1084 $this->_element = new Cached_PluginInvocation($pi);
1090 * Table syntax similar to Mediawiki
1092 * => <?plugin MediawikiTable
1096 class Block_table_mediawiki extends Block_pre
1100 function _match (&$input, $m) {
1101 $pos = $input->getPos();
1102 $pi = $m->postmatch;
1103 while (!preg_match('/(?<!'.ESCAPE_CHAR.')\|}\s*$/', $pi)) {
1104 if (($line = $input->nextLine()) === false) {
1105 $input->setPos($pos);
1112 $pi = str_replace("\|}", "", $pi);
1113 $pi = '<'.'?plugin MediawikiTable ' . $pi . '?'.'>';
1114 $this->_element = new Cached_PluginInvocation($pi);
1120 * Template syntax similar to Mediawiki
1122 * => < ? plugin Template page=template ? >
1123 * {{template|var1=value1|var2=value|...}}
1124 * => < ? plugin Template page=template var=value ... ? >
1126 * The {{...}} syntax is also used for:
1127 * - Wikicreole images
1130 class Block_template_plugin extends Block_pre
1134 function _match (&$input, $m) {
1135 // If we find "}}", this is an inline template.
1136 if (strpos($m->postmatch, "}}") !== false) {
1139 $pos = $input->getPos();
1140 $pi = $m->postmatch;
1141 if ($pi[0] == '{') {
1144 while (!preg_match('/(?<!'.ESCAPE_CHAR.')}}\s*$/', $pi)) {
1145 if (($line = $input->nextLine()) === false) {
1146 $input->setPos($pos);
1154 $pi = trim($pi, "}}");
1156 if (strpos($pi, "|") === false) {
1160 $imagename = substr($pi, 0, strpos($pi, "|"));
1161 $alt = ltrim(strstr($pi, "|"), "|");
1164 // It's not a Mediawiki template, it's a Wikicreole image
1165 if (is_image($imagename)) {
1166 $this->_element = LinkImage(getUploadDataPath() . $imagename, $alt);
1171 if (is_video($imagename)) {
1172 $pi = '<'.'?plugin Video file="' . $pi . '" ?>';
1173 $this->_element = new Cached_PluginInvocation($pi);
1177 $pi = str_replace("\n", "", $pi);
1179 // The argument value might contain a double quote (")
1180 // We have to encode that.
1181 $pi = htmlspecialchars($pi);
1185 if (preg_match('/^(\S+?)\|(.*)$/', $pi, $_m)) {
1187 $vars = '"' . preg_replace('/\|/', '" "', $_m[2]) . '"';
1188 $vars = preg_replace('/"(\S+)=([^"]*)"/', '\\1="\\2"', $vars);
1191 // pi may contain a version number
1192 // {{foo?version=5}}
1193 // in that case, output is "page=foo rev=5"
1194 if (strstr($pi, "?")) {
1195 $pi = str_replace("?version=", "\" rev=\"", $pi);
1199 $pi = '<'.'?plugin Template page="'.$pi.'" '.$vars . ' ?>';
1201 $pi = '<'.'?plugin Template page="' . $pi . '" ?>';
1202 $this->_element = new Cached_PluginInvocation($pi);
1207 class Block_email_blockquote extends BlockMarkup
1209 var $_attr = array('class' => 'mail-style-quote');
1212 function _match (&$input, $m) {
1213 //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1214 $indent = $this->_re;
1215 $this->_element = new SubBlock($input, $indent, $m->match,
1216 'blockquote', $this->_attr);
1221 class Block_wikicreole_indented extends BlockMarkup
1223 var $_attr = array('style' => 'margin-left:2em');
1226 function _match (&$input, $m) {
1227 $indent = $this->_re;
1228 $this->_element = new SubBlock($input, $indent, $m->match,
1229 'div', $this->_attr);
1234 class Block_hr extends BlockMarkup
1236 var $_re = '-{4,}\s*$';
1238 function _match (&$input, $m) {
1240 $this->_element = new Block_HtmlElement('hr');
1245 class Block_heading extends BlockMarkup
1247 var $_re = '!{1,3}';
1249 function _match (&$input, $m) {
1250 $tag = "h" . (5 - strlen($m->match));
1251 $text = TransformInline(trim($m->postmatch));
1254 $this->_element = new Block_HtmlElement($tag, false, $text);
1260 class Block_heading_wikicreole extends BlockMarkup
1262 var $_re = '={2,6}';
1264 function _match (&$input, $m) {
1265 $tag = "h" . strlen($m->match);
1267 $header = trim($m->postmatch);
1268 // Remove '='s at the end so that Mediawiki syntax is recognized
1269 $header = trim($header, "=");
1270 $text = TransformInline(trim($header));
1273 $this->_element = new Block_HtmlElement($tag, false, $text);
1279 class Block_p extends BlockMarkup
1285 function _match (&$input, $m) {
1286 $this->_text = $m->match;
1291 function _setTightness ($top, $bot) {
1292 $this->_tight_top = $top;
1293 $this->_tight_bot = $bot;
1296 function merge ($nextBlock) {
1297 $class = get_class($nextBlock);
1298 if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1299 $this->_text .= "\n" . $nextBlock->_text;
1300 $this->_tight_bot = $nextBlock->_tight_bot;
1306 function finish () {
1307 $content = TransformInline(trim($this->_text));
1308 $p = new Block_HtmlElement('p', false, $content);
1309 $p->setTightness($this->_tight_top, $this->_tight_bot);
1314 class Block_divspan extends BlockMarkup
1316 var $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1318 function _match (&$input, $m) {
1319 if (substr($m->match,1,4) == 'span') {
1325 $argstr = substr(trim(substr($m->match,strlen($tag)+1)),0,-1);
1326 $pos = $input->getPos();
1327 $pi = $content = $m->postmatch;
1328 while (!preg_match('/^(.*)\<\/'.$tag.'\>(.*)$/i', $pi, $me)) {
1329 if ($pi != $content)
1330 $content .= "\n$pi";
1331 if (($pi = $input->nextLine()) === false) {
1332 $input->setPos($pos);
1336 if ($pi != $content)
1337 $content .= $me[1]; // prematch
1341 if (strstr($content, "\n"))
1342 $content = TransformText($content);
1344 $content = TransformInline($content);
1349 while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1350 $k = $m[1]; $v = $m[2];
1351 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1355 preg_match("/^(\s+)(.*)$/", $v, $m);
1359 if (trim($k) and trim($v)) $args[$k] = $v;
1362 $this->_element = new Block_HtmlElement($tag, $args, $content);
1368 ////////////////////////////////////////////////////////////////
1372 * Transform the text of a page, and return a parse tree.
1374 function TransformTextPre ($text, $markup = 2.0, $basepage=false) {
1375 if (isa($text, 'WikiDB_PageRevision')) {
1377 $text = $rev->getPackedContent();
1378 $markup = $rev->get('markup');
1380 // NEW: default markup is new, to increase stability
1381 if (!empty($markup) && $markup < 2.0) {
1382 $text = ConvertOldMarkup($text);
1385 /*if (!empty($markup) && $markup == 3) {
1386 $text = ConvertFromCreole($text);
1388 // Expand leading tabs.
1389 $text = expand_tabs($text);
1390 //set_time_limit(3);
1391 $output = new WikiText($text);
1397 * Transform the text of a page, and return an XmlContent,
1398 * suitable for printXml()-ing.
1400 function TransformText ($text, $markup = 2.0, $basepage = false) {
1401 $output = TransformTextPre($text, $markup, $basepage);
1403 // This is for immediate consumption.
1404 // We must bind the contents to a base pagename so that
1405 // relative page links can be properly linkified...
1406 return new CacheableMarkup($output->getContent(), $basepage);
1408 return new XmlContent($output->getContent());
1414 // c-basic-offset: 4
1415 // c-hanging-comment-ender-p: nil
1416 // indent-tabs-mode: nil