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 PhpWiki; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 //require_once('lib/HtmlElement.php');
23 require_once 'lib/CachedMarkup.php';
24 require_once 'lib/InlineParser.php';
27 * Deal with paragraphs and proper, recursive block indents
28 * for the new style markup (version 2)
30 * Everything which goes over more than line:
31 * automatic lists, UL, OL, DL, table, blockquote, verbatim,
37 * FIXME: unify this with the RegexpSet in InlineParser.
39 * FIXME: This is very php5 sensitive: It was fixed for 1.3.9,
40 * but is again broken with the 1.3.11
41 * allow_call_time_pass_reference clean fixes
44 * @author: Geoffrey T. Dairiki
48 * Return type from RegexpSet::match and RegexpSet::nextMatch.
52 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)
86 $this->_regexps = $regexps;
87 $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
91 * Search text for the next matching regexp from the Regexp Set.
93 * @param $text string The text to search.
95 * @return object A RegexpSet_match object, or false if no match.
99 if (!is_string($text)) return false;
100 if (!preg_match($this->_re, $text, $m)) {
104 $match = new AnchoredRegexpSet_match;
105 $match->postmatch = substr($text, strlen($m[0]));
106 $match->match = $m[1];
107 $match->regexp_ind = count($m) - 3;
112 * Search for next matching regexp.
114 * Here, 'next' has two meanings:
116 * Match the next regexp(s) in the set, at the same position as the last match.
118 * If that fails, match the whole RegexpSet, starting after the position of the
121 * @param $text string Text to search.
123 * @param $prevMatch A RegexpSet_match object
125 * $prevMatch should be a match object obtained by a previous
126 * match upon the same value of $text.
128 * @return object A RegexpSet_match object, or false if no match.
130 function nextMatch($text, $prevMatch)
132 // Try to find match at same position.
133 $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
138 $pat = "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
140 if (!preg_match($pat, $text, $m)) {
144 $match = new AnchoredRegexpSet_match;
145 $match->postmatch = substr($text, strlen($m[0]));
146 $match->match = $m[1];
147 $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;
153 class BlockParser_Input
156 function BlockParser_Input($text)
159 // Expand leading tabs.
160 // FIXME: do this better.
162 // We want to ensure the only characters matching \s are ' ' and "\n".
164 $text = preg_replace('/(?![ \n])\s/', ' ', $text);
165 assert(!preg_match('/(?![ \n])\s/', $text));
167 $this->_lines = preg_split('/[^\S\n]*\n/', $text);
170 // Strip leading blank lines.
171 while ($this->_lines and !$this->_lines[0])
172 array_shift($this->_lines);
173 $this->_atSpace = false;
178 $nlines = count($this->_lines);
180 if ($this->_pos >= $nlines) {
181 $this->_atSpace = false;
184 if ($this->_lines[$this->_pos] != '')
187 $this->_atSpace = true;
189 return $this->_atSpace;
192 function currentLine()
194 if ($this->_pos >= count($this->_lines)) {
197 return $this->_lines[$this->_pos];
202 $this->_atSpace = $this->_lines[$this->_pos++] === '';
203 if ($this->_pos >= count($this->_lines)) {
206 return $this->_lines[$this->_pos];
211 $this->_atSpace = ($this->_lines[$this->_pos] === '');
217 return array($this->_pos, $this->_atSpace);
220 function setPos($pos)
222 list($this->_pos, $this->_atSpace) = $pos;
237 if ($this->_pos < count($this->_lines))
238 return $this->_lines[$this->_pos];
243 function _debug($tab, $msg)
246 $where = $this->where();
247 $tab = str_repeat('____', $this->getDepth()) . $tab;
248 printXML(HTML::div("$tab $msg: at: '",
255 class BlockParser_InputSubBlock extends BlockParser_Input
257 function BlockParser_InputSubBlock(&$input, $prefix_re, $initial_prefix = false)
259 $this->_input = &$input;
260 $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
261 $this->_atSpace = false;
263 if (($line = $input->currentLine()) === false)
264 $this->_line = false;
265 elseif ($initial_prefix) {
266 assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
267 $this->_line = (string)substr($line, strlen($initial_prefix));
268 $this->_atBlank = !ltrim($line);
269 } elseif (preg_match($this->_prefix_pat, $line, $m)) {
270 $this->_line = (string)substr($line, strlen($m[0]));
271 $this->_atBlank = !ltrim($line);
273 $this->_line = false;
278 // In contrast to the case for top-level blocks,
279 // for sub-blocks, there never appears to be any trailing space.
280 // (The last block in the sub-block should always be of class tight-bottom.)
281 while ($this->_line === '')
284 if ($this->_line === false)
285 return $this->_atSpace == 'strong_space';
287 return $this->_atSpace;
290 function currentLine()
297 if ($this->_line === '')
298 $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
300 $this->_atSpace = false;
302 $line = $this->_input->nextLine();
303 if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
304 $this->_line = (string)substr($line, strlen($m[0]));
305 $this->_atBlank = !ltrim($line);
307 $this->_line = false;
319 return array($this->_line, $this->_atSpace, $this->_input->getPos());
322 function setPos($pos)
324 $this->_line = $pos[0];
325 $this->_atSpace = $pos[1];
326 $this->_input->setPos($pos[2]);
331 assert($this->_line !== false);
332 $line = $this->_input->currentLine();
333 assert($line !== false && strlen($line) >= strlen($this->_line));
334 return substr($line, 0, strlen($line) - strlen($this->_line));
339 return $this->_input->getDepth() + 1;
344 return $this->_input->where();
348 class Block_HtmlElement extends HtmlElement
350 function Block_HtmlElement($tag /*, ... */)
352 $this->_init(func_get_args());
355 function setTightness($top, $bottom)
360 class ParsedBlock extends Block_HtmlElement
363 function ParsedBlock(&$input, $tag = 'div', $attr = false)
365 $this->Block_HtmlElement($tag, $attr);
366 $this->_initBlockTypes();
367 $this->_parse($input);
370 function _parse(&$input)
372 // php5 failed to advance the block. php5 copies objects by ref.
373 // nextBlock == block, both are the same objects. So we have to clone it.
374 for ($block = $this->_getBlock($input);
376 $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock)) {
377 while ($nextBlock = $this->_getBlock($input)) {
378 // Attempt to merge current with following block.
379 if (!($merged = $block->merge($nextBlock))) {
380 break; // can't merge
384 $this->pushContent($block->finish());
388 // FIXME: hackish. This should only be called once.
389 function _initBlockTypes()
391 // better static or global?
392 static $_regexpset, $_block_types;
394 if (!is_object($_regexpset)) {
395 // nowiki_wikicreole must be before template_plugin
397 ('nowiki_wikicreole', 'template_plugin', 'placeholder', 'oldlists', 'list', 'dl',
398 'table_dl', 'table_wikicreole', 'table_mediawiki',
399 'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre',
400 'email_blockquote', 'wikicreole_indented',
401 'plugin', 'plugin_wikicreole', 'p');
402 // insert it before p!
403 if (ENABLE_MARKUP_DIVSPAN) {
404 array_pop($Block_types);
405 $Block_types[] = 'divspan';
406 $Block_types[] = 'p';
408 foreach ($Block_types as $type) {
409 $class = "Block_$type";
411 $this->_block_types[] = $proto;
412 $this->_regexps[] = $proto->_re;
414 $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
415 $_regexpset = $this->_regexpset;
416 $_block_types = $this->_block_types;
419 $this->_regexpset = $_regexpset;
420 $this->_block_types = $_block_types;
424 function _getBlock(&$input)
426 $this->_atSpace = $input->skipSpace();
428 $line = $input->currentLine();
429 if ($line === false or $line === '') { // allow $line === '0'
432 $tight_top = !$this->_atSpace;
433 $re_set = &$this->_regexpset;
434 //FIXME: php5 fails to advance here!
435 for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
436 $block = clone($this->_block_types[$m->regexp_ind]);
437 if (DEBUG & _DEBUG_PARSER)
438 $input->_debug('>', get_class($block));
440 if ($block->_match($input, $m)) {
441 //$block->_text = $line;
442 if (DEBUG & _DEBUG_PARSER)
443 $input->_debug('<', get_class($block));
444 $tight_bottom = !$input->skipSpace();
445 $block->_setTightness($tight_top, $tight_bottom);
448 if (DEBUG & _DEBUG_PARSER)
449 $input->_debug('[', "_match failed");
451 if ($line === false or $line === '') // allow $line === '0'
454 trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
459 class WikiText extends ParsedBlock
461 function WikiText($text)
463 $input = new BlockParser_Input($text);
464 $this->ParsedBlock($input);
468 class SubBlock extends ParsedBlock
470 function SubBlock(&$input, $indent_re, $initial_indent = false,
471 $tag = 'div', $attr = false)
473 $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
474 $this->ParsedBlock($subinput, $tag, $attr);
479 * TightSubBlock is for use in parsing lists item bodies.
481 * If the sub-block consists of a single paragraph, it omits
482 * the paragraph element.
484 * We go to this trouble so that "tight" lists look somewhat reasonable
485 * in older (non-CSS) browsers. (If you don't do this, then, without
486 * CSS, you only get "loose" lists.
488 class TightSubBlock extends SubBlock
490 function TightSubBlock(&$input, $indent_re, $initial_indent = false,
491 $tag = 'div', $attr = false)
493 $this->SubBlock($input, $indent_re, $initial_indent, $tag, $attr);
495 // If content is a single paragraph, eliminate the paragraph...
496 if (count($this->_content) == 1) {
497 $elem = $this->_content[0];
498 if (isa($elem, 'XmlElement') and $elem->getTag() == 'p') {
499 $this->setContent($elem->getContent());
509 function _match(&$input, $match)
511 trigger_error('pure virtual', E_USER_ERROR);
514 function _setTightness($top, $bot)
518 function merge($followingBlock)
525 return $this->_element;
529 class Block_blockquote extends BlockMarkup
532 var $_re = '\ +(?=\S)';
534 function _match(&$input, $m)
536 $this->_depth = strlen($m->match);
537 $indent = sprintf("\\ {%d}", $this->_depth);
538 $this->_element = new SubBlock($input, $indent, $m->match,
543 function merge($nextBlock)
545 if (get_class($nextBlock) == get_class($this)) {
546 assert($nextBlock->_depth < $this->_depth);
547 $nextBlock->_element->unshiftContent($this->_element);
548 if (!empty($this->_tight_top))
549 $nextBlock->_tight_top = $this->_tight_top;
556 class Block_list extends BlockMarkup
558 //var $_tag = 'ol' or 'ul';
564 | [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
566 var $_content = array();
568 function _match(&$input, $m)
570 // A list as the first content in a list is not allowed.
573 // Should markup as <ul><li>* Item</li></ul>,
574 // not <ul><li><ul><li>Item</li></ul>/li></ul>.
576 if (preg_match('/[*#+-o]/', $input->getPrefix())) {
581 $indent = sprintf("\\ {%d}", strlen($prefix));
583 $bullet = trim($m->match);
584 $this->_tag = $bullet == '#' ? 'ol' : 'ul';
585 $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
589 function _setTightness($top, $bot)
591 $li = &$this->_content[0];
592 $li->setTightness($top, $bot);
595 function merge($nextBlock)
597 if (isa($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
598 array_splice($this->_content, count($this->_content), 0,
599 $nextBlock->_content);
607 return new Block_HtmlElement($this->_tag, false, $this->_content);
611 class Block_dl extends Block_list
617 $this->_re = '\ {0,4}\S.*(?<!' . ESCAPE_CHAR . '):\s*$';
620 function _match(&$input, $m)
622 if (!($p = $this->_do_match($input, $m)))
624 list ($term, $defn, $loose) = $p;
626 $this->_content[] = new Block_HtmlElement('dt', false, $term);
627 $this->_content[] = $defn;
628 $this->_tight_defn = !$loose;
632 function _setTightness($top, $bot)
634 $dt = &$this->_content[0];
635 $dd = &$this->_content[1];
637 $dt->setTightness($top, $this->_tight_defn);
638 $dd->setTightness($this->_tight_defn, $bot);
641 function _do_match(&$input, $m)
643 $pos = $input->getPos();
645 $firstIndent = strspn($m->match, ' ');
646 $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
649 $loose = $input->skipSpace();
650 $line = $input->currentLine();
652 if (!$line || !preg_match($pat, $line, $mm)) {
653 $input->setPos($pos);
654 return false; // No body found.
657 $indent = strlen($mm[0]);
658 $term = TransformInline(rtrim(substr(trim($m->match), 0, -1)));
659 $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
660 return array($term, $defn, $loose);
664 class Block_table_dl_defn extends XmlContent
669 function Block_table_dl_defn($term, $defn)
672 if (!is_array($defn))
673 $defn = $defn->getContent();
675 $this->_next_tight_top = false; // value irrelevant - gets fixed later
676 $this->_ncols = $this->_ComputeNcols($defn);
679 foreach ($defn as $item) {
680 if ($this->_IsASubtable($item))
681 $this->_addSubtable($item);
683 $this->_addToRow($item);
687 $th = HTML::th($term);
688 if ($this->_nrows > 1)
689 $th->setAttr('rowspan', $this->_nrows);
690 $this->_setTerm($th);
693 function setTightness($tight_top, $tight_bot)
695 $this->_tight_top = $tight_top;
696 $this->_tight_bot = $tight_bot;
699 function _addToRow($item)
701 if (empty($this->_accum)) {
702 $this->_accum = HTML::td();
703 if ($this->_ncols > 2)
704 $this->_accum->setAttr('colspan', $this->_ncols - 1);
706 $this->_accum->pushContent($item);
709 function _flushRow($tight_bottom = false)
711 if (!empty($this->_accum)) {
712 $row = new Block_HtmlElement('tr', false, $this->_accum);
714 $row->setTightness($this->_next_tight_top, $tight_bottom);
715 $this->_next_tight_top = $tight_bottom;
717 $this->pushContent($row);
718 $this->_accum = false;
723 function _addSubtable($table)
725 if (!($table_rows = $table->getContent()))
728 $this->_flushRow($table_rows[0]->_tight_top);
730 foreach ($table_rows as $subdef) {
731 $this->pushContent($subdef);
732 $this->_nrows += $subdef->nrows();
733 $this->_next_tight_top = $subdef->_tight_bot;
737 function _setTerm($th)
739 $first_row = &$this->_content[0];
740 if (isa($first_row, 'Block_table_dl_defn'))
741 $first_row->_setTerm($th);
743 $first_row->unshiftContent($th);
746 function _ComputeNcols($defn)
749 foreach ($defn as $item) {
750 if ($this->_IsASubtable($item)) {
751 $row = $this->_FirstDefn($item);
752 $ncols = max($ncols, $row->ncols() + 1);
758 function _IsASubtable($item)
760 return isa($item, 'HtmlElement')
761 && $item->getTag() == 'table'
762 && $item->getAttr('class') == 'wiki-dl-table';
765 function _FirstDefn($subtable)
767 $defs = $subtable->getContent();
773 return $this->_ncols;
778 return $this->_nrows;
783 $first = &$this->_content[0];
784 if (isa($first, 'Block_table_dl_defn'))
785 return $first->firstTR();
791 $last = &$this->_content[$this->_nrows - 1];
792 if (isa($last, 'Block_table_dl_defn'))
793 return $last->lastTR();
797 function setWidth($ncols)
799 assert($ncols >= $this->_ncols);
800 if ($ncols <= $this->_ncols)
802 $rows = &$this->_content;
803 for ($i = 0; $i < count($rows); $i++) {
805 if (isa($row, 'Block_table_dl_defn'))
806 $row->setWidth($ncols - 1);
808 $n = count($row->_content);
809 $lastcol = &$row->_content[$n - 1];
810 if (!empty($lastcol))
811 $lastcol->setAttr('colspan', $ncols - 1);
817 class Block_table_dl extends Block_dl
819 var $_tag = 'dl-table'; // phony.
821 function Block_table_dl()
823 $this->_re = '\ {0,4} (?:\S.*)? (?<!' . ESCAPE_CHAR . ') \| \s* $';
826 function _match(&$input, $m)
828 if (!($p = $this->_do_match($input, $m)))
830 list ($term, $defn, $loose) = $p;
832 $this->_content[] = new Block_table_dl_defn($term, $defn);
836 function _setTightness($top, $bot)
838 $this->_content[0]->setTightness($top, $bot);
844 $defs = &$this->_content;
847 foreach ($defs as $defn)
848 $ncols = max($ncols, $defn->ncols());
850 foreach ($defs as $key => $defn)
851 $defs[$key]->setWidth($ncols);
853 return HTML::table(array('class' => 'wiki-dl-table',
861 class Block_oldlists extends Block_list
863 //var $_tag = 'ol', 'ul', or 'dl';
864 var $_re = '(?: [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
865 | [#]\ (?! \[ .*? \] )
869 function _match(&$input, $m)
872 if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
877 $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
878 $newindent = sprintf('\\ {%d}', strlen($prefix));
879 $indent = "(?:$oldindent|$newindent)";
881 $bullet = $prefix[0];
882 if ($bullet == '*') {
885 } elseif ($bullet == '#') {
890 list ($term,) = explode(':', substr($prefix, 1), 2);
893 $this->_content[] = new Block_HtmlElement('dt', false,
894 TransformInline($term));
898 $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
902 function _setTightness($top, $bot)
904 if (count($this->_content) == 1) {
905 $li = &$this->_content[0];
906 $li->setTightness($top, $bot);
908 // This is where php5 usually brakes.
909 // wrong duplicated <li> contents
910 if (DEBUG and DEBUG & _DEBUG_PARSER and check_php_version(5)) {
911 if (count($this->_content) != 2) {
914 $class = new Reflection_Class('XmlElement');
915 // Print out basic information
917 "===> The %s%s%s %s '%s' [extends %s]\n".
920 " having the modifiers %d [%s]\n",
921 $class->isInternal() ? 'internal' : 'user-defined',
922 $class->isAbstract() ? ' abstract' : '',
923 $class->isFinal() ? ' final' : '',
924 $class->isInterface() ? 'interface' : 'class',
926 var_export($class->getParentClass(), 1),
927 $class->getFileName(),
928 $class->getStartLine(),
929 $class->getEndline(),
930 $class->getModifiers(),
931 implode(' ', Reflection::getModifierNames($class->getModifiers()))
933 // Print class properties
934 printf("---> Properties: %s\n", var_export($class->getProperties(), 1));
936 echo 'count($this->_content): ', count($this->_content), "\n";
937 echo "\$this->_content[0]: ";
938 var_dump($this->_content[0]);
940 for ($i = 1; $i < min(5, count($this->_content)); $i++) {
941 $c =& $this->_content[$i];
942 echo '$this->_content[', $i, "]: \n";
946 var_dump($c->_content);
947 echo "_properties: ";
948 var_dump($c->_properties);
950 debug_print_backtrace();
951 if (DEBUG & _DEBUG_APD) {
952 if (function_exists("xdebug_get_function_stack")) {
953 var_dump(xdebug_get_function_stack());
959 if (!check_php_version(5))
960 assert(count($this->_content) == 2);
961 $dt = &$this->_content[0];
962 $dd = &$this->_content[1];
963 $dt->setTightness($top, false);
964 $dd->setTightness(false, $bot);
969 class Block_pre extends BlockMarkup
971 var $_re = '<(?:pre|verbatim|nowiki|noinclude)>';
973 function _match(&$input, $m)
975 $endtag = '</' . substr($m->match, 1);
977 $pos = $input->getPos();
979 $line = $m->postmatch;
980 while (ltrim($line) != $endtag) {
982 if (($line = $input->nextLine()) === false) {
983 $input->setPos($pos);
989 if ($m->match == '<nowiki>')
990 $text = join("<br>\n", $text);
992 $text = join("\n", $text);
994 // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
996 if ($m->match == '<pre>') {
997 $text = TransformInline($text);
999 if ($m->match == '<noinclude>') {
1000 $text = TransformText($text);
1001 $this->_element = new Block_HtmlElement('div', false, $text);
1002 } elseif ($m->match == '<nowiki>') {
1003 $text = TransformInlineNowiki($text);
1004 $this->_element = new Block_HtmlElement('p', false, $text);
1006 $this->_element = new Block_HtmlElement('pre', false, $text);
1012 // Wikicreole placeholder
1013 // <<<placeholder>>>
1014 class Block_placeholder extends BlockMarkup
1018 function _match(&$input, $m)
1022 $pos = $input->getPos();
1024 $line = $m->postmatch;
1025 while (ltrim($line) != $endtag) {
1027 if (($line = $input->nextLine()) === false) {
1028 $input->setPos($pos);
1034 $text = join("\n", $text);
1035 $text = '<<<' . $text . '>>>';
1036 $this->_element = new Block_HtmlElement('div', false, $text);
1041 class Block_nowiki_wikicreole extends BlockMarkup
1045 function _match(&$input, $m)
1049 $pos = $input->getPos();
1051 $line = $m->postmatch;
1052 while (ltrim($line) != $endtag) {
1054 if (($line = $input->nextLine()) === false) {
1055 $input->setPos($pos);
1061 $text = join("\n", $text);
1062 $this->_element = new Block_HtmlElement('pre', false, $text);
1067 class Block_plugin extends Block_pre
1069 var $_re = '<\?plugin(?:-form)?(?!\S)';
1072 /* <?plugin Backlinks
1074 /* <?plugin ListPages pages=<!plugin-list Backlinks!>
1075 * exclude=<!plugin-list TitleSearch s=T*!> ?>
1079 function _match(&$input, $m)
1081 $pos = $input->getPos();
1082 $pi = $m->match . $m->postmatch;
1083 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\?>\s*$/', $pi)) {
1084 if (($line = $input->nextLine()) === false) {
1085 $input->setPos($pos);
1092 $this->_element = new Cached_PluginInvocation($pi);
1097 class Block_plugin_wikicreole extends Block_pre
1099 // var $_re = '<<(?!\S)';
1102 function _match(&$input, $m)
1104 $pos = $input->getPos();
1105 $pi = $m->postmatch;
1106 if ($pi[0] == '<') {
1109 $pi = "<?plugin " . $pi;
1110 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')>>\s*$/', $pi)) {
1111 if (($line = $input->nextLine()) === false) {
1112 $input->setPos($pos);
1119 $pi = str_replace(">>", "?>", $pi);
1121 $this->_element = new Cached_PluginInvocation($pi);
1126 class Block_table_wikicreole extends Block_pre
1130 function _match(&$input, $m)
1132 $pos = $input->getPos();
1133 $pi = "|" . $m->postmatch;
1137 if ((($line = $input->nextLine()) === false) && !$intable) {
1138 $input->setPos($pos);
1145 $trimline = trim($line);
1146 if ($trimline[0] != "|") {
1150 $pi .= "\n$trimline";
1153 $pi = '<' . '?plugin WikicreoleTable ' . $pi . '?' . '>';
1155 $this->_element = new Cached_PluginInvocation($pi);
1161 * Table syntax similar to Mediawiki
1163 * => <?plugin MediawikiTable
1167 class Block_table_mediawiki extends Block_pre
1171 function _match(&$input, $m)
1173 $pos = $input->getPos();
1174 $pi = $m->postmatch;
1175 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\|}\s*$/', $pi)) {
1176 if (($line = $input->nextLine()) === false) {
1177 $input->setPos($pos);
1184 $pi = str_replace("\|}", "", $pi);
1185 $pi = '<' . '?plugin MediawikiTable ' . $pi . '?' . '>';
1186 $this->_element = new Cached_PluginInvocation($pi);
1192 * Template syntax similar to Mediawiki
1194 * => < ? plugin Template page=template ? >
1195 * {{template|var1=value1|var2=value|...}}
1196 * => < ? plugin Template page=template var=value ... ? >
1198 * The {{...}} syntax is also used for:
1199 * - Wikicreole images
1202 class Block_template_plugin extends Block_pre
1206 function _match(&$input, $m)
1208 // If we find "}}", this is an inline template.
1209 if (strpos($m->postmatch, "}}") !== false) {
1212 $pos = $input->getPos();
1213 $pi = $m->postmatch;
1214 if ($pi[0] == '{') {
1217 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')}}\s*$/', $pi)) {
1218 if (($line = $input->nextLine()) === false) {
1219 $input->setPos($pos);
1227 $pi = trim($pi, "}}");
1229 if (strpos($pi, "|") === false) {
1233 $imagename = substr($pi, 0, strpos($pi, "|"));
1234 $alt = ltrim(strstr($pi, "|"), "|");
1237 // It's not a Mediawiki template, it's a Wikicreole image
1238 if (is_image($imagename)) {
1239 $this->_element = LinkImage(getUploadDataPath() . $imagename, $alt);
1244 if (is_video($imagename)) {
1245 $pi = '<' . '?plugin Video file="' . $pi . '" ?>';
1246 $this->_element = new Cached_PluginInvocation($pi);
1250 $pi = str_replace("\n", "", $pi);
1252 // The argument value might contain a double quote (")
1253 // We have to encode that.
1254 $pi = htmlspecialchars($pi);
1258 if (preg_match('/^(\S+?)\|(.*)$/', $pi, $_m)) {
1260 $vars = '"' . preg_replace('/\|/', '" "', $_m[2]) . '"';
1261 $vars = preg_replace('/"(\S+)=([^"]*)"/', '\\1="\\2"', $vars);
1264 // pi may contain a version number
1265 // {{foo?version=5}}
1266 // in that case, output is "page=foo rev=5"
1267 if (strstr($pi, "?")) {
1268 $pi = str_replace("?version=", "\" rev=\"", $pi);
1272 $pi = '<' . '?plugin Template page="' . $pi . '" ' . $vars . ' ?>';
1274 $pi = '<' . '?plugin Template page="' . $pi . '" ?>';
1275 $this->_element = new Cached_PluginInvocation($pi);
1280 class Block_email_blockquote extends BlockMarkup
1282 var $_attr = array('class' => 'mail-style-quote');
1285 function _match(&$input, $m)
1287 //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1288 $indent = $this->_re;
1289 $this->_element = new SubBlock($input, $indent, $m->match,
1290 'blockquote', $this->_attr);
1295 class Block_wikicreole_indented extends BlockMarkup
1297 var $_attr = array('style' => 'margin-left:2em');
1300 function _match(&$input, $m)
1302 $indent = $this->_re;
1303 $this->_element = new SubBlock($input, $indent, $m->match,
1304 'div', $this->_attr);
1309 class Block_hr extends BlockMarkup
1311 var $_re = '-{4,}\s*$';
1313 function _match(&$input, $m)
1316 $this->_element = new Block_HtmlElement('hr');
1321 class Block_heading extends BlockMarkup
1323 var $_re = '!{1,3}';
1325 function _match(&$input, $m)
1327 $tag = "h" . (5 - strlen($m->match));
1328 $text = TransformInline(trim($m->postmatch));
1331 $this->_element = new Block_HtmlElement($tag, false, $text);
1337 class Block_heading_wikicreole extends BlockMarkup
1339 var $_re = '={2,6}';
1341 function _match(&$input, $m)
1343 $tag = "h" . strlen($m->match);
1345 $header = trim($m->postmatch);
1346 // Remove '='s at the end so that Mediawiki syntax is recognized
1347 $header = trim($header, "=");
1348 $text = TransformInline(trim($header));
1351 $this->_element = new Block_HtmlElement($tag, false, $text);
1357 class Block_p extends BlockMarkup
1363 function _match(&$input, $m)
1365 $this->_text = $m->match;
1370 function _setTightness($top, $bot)
1372 $this->_tight_top = $top;
1373 $this->_tight_bot = $bot;
1376 function merge($nextBlock)
1378 $class = get_class($nextBlock);
1379 if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1380 $this->_text .= "\n" . $nextBlock->_text;
1381 $this->_tight_bot = $nextBlock->_tight_bot;
1389 $content = TransformInline(trim($this->_text));
1390 $p = new Block_HtmlElement('p', false, $content);
1391 $p->setTightness($this->_tight_top, $this->_tight_bot);
1396 class Block_divspan extends BlockMarkup
1398 var $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1400 function _match(&$input, $m)
1402 if (substr($m->match, 1, 4) == 'span') {
1408 $argstr = substr(trim(substr($m->match, strlen($tag) + 1)), 0, -1);
1409 $pos = $input->getPos();
1410 $pi = $content = $m->postmatch;
1411 while (!preg_match('/^(.*)\<\/' . $tag . '\>(.*)$/i', $pi, $me)) {
1412 if ($pi != $content)
1413 $content .= "\n$pi";
1414 if (($pi = $input->nextLine()) === false) {
1415 $input->setPos($pos);
1419 if ($pi != $content)
1420 $content .= $me[1]; // prematch
1424 if (strstr($content, "\n"))
1425 $content = TransformText($content);
1427 $content = TransformInline($content);
1432 while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1435 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1439 preg_match("/^(\s+)(.*)$/", $v, $m);
1443 if (trim($k) and trim($v)) $args[$k] = $v;
1446 $this->_element = new Block_HtmlElement($tag, $args, $content);
1452 ////////////////////////////////////////////////////////////////
1456 * Transform the text of a page, and return a parse tree.
1458 function TransformTextPre($text, $markup = 2.0, $basepage = false)
1460 if (isa($text, 'WikiDB_PageRevision')) {
1462 $text = $rev->getPackedContent();
1463 $markup = $rev->get('markup');
1465 // NEW: default markup is new, to increase stability
1466 if (!empty($markup) && $markup < 2.0) {
1467 $text = ConvertOldMarkup($text);
1469 // Expand leading tabs.
1470 $text = expand_tabs($text);
1471 $output = new WikiText($text);
1477 * Transform the text of a page, and return an XmlContent,
1478 * suitable for printXml()-ing.
1480 function TransformText($text, $markup = 2.0, $basepage = false)
1482 $output = TransformTextPre($text, $markup, $basepage);
1484 // This is for immediate consumption.
1485 // We must bind the contents to a base pagename so that
1486 // relative page links can be properly linkified...
1487 return new CacheableMarkup($output->getContent(), $basepage);
1489 return new XmlContent($output->getContent());
1495 // c-basic-offset: 4
1496 // c-hanging-comment-ender-p: nil
1497 // indent-tabs-mode: nil