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/CachedMarkup.php';
23 require_once 'lib/InlineParser.php';
26 * Deal with paragraphs and proper, recursive block indents
27 * for the new style markup (version 2)
29 * Everything which goes over more than line:
30 * automatic lists, UL, OL, DL, table, blockquote, verbatim,
36 * FIXME: unify this with the RegexpSet in InlineParser.
38 * FIXME: This is very php5 sensitive: It was fixed for 1.3.9,
39 * but is again broken with the 1.3.11
40 * allow_call_time_pass_reference clean fixes
43 * @author: Geoffrey T. Dairiki
47 * Return type from RegexpSet::match and RegexpSet::nextMatch.
51 class AnchoredRegexpSet_match
59 * The text following the matched text.
64 * Index of the regular expression which matched.
70 * A set of regular expressions.
72 * This class is probably only useful for InlineTransformer.
74 class AnchoredRegexpSet
77 * @param $regexps array A list of regular expressions. The
78 * regular expressions should not include any sub-pattern groups
79 * "(...)". (Anonymous groups, like "(?:...)", as well as
80 * look-ahead and look-behind assertions are fine.)
82 function __construct($regexps)
84 $this->_regexps = $regexps;
85 $this->_re = "/((" . join(")|(", $regexps) . "))/Ax";
89 * Search text for the next matching regexp from the Regexp Set.
91 * @param $text string The text to search.
93 * @return object A RegexpSet_match object, or false if no match.
97 if (!is_string($text))
99 if (!preg_match($this->_re, $text, $m)) {
103 $match = new AnchoredRegexpSet_match;
104 $match->postmatch = substr($text, strlen($m[0]));
105 $match->match = $m[1];
106 $match->regexp_ind = count($m) - 3;
111 * Search for next matching regexp.
113 * Here, 'next' has two meanings:
115 * Match the next regexp(s) in the set, at the same position as the last match.
117 * If that fails, match the whole RegexpSet, starting after the position of the
120 * @param string $text Text to search.
121 * @param RegexpSet_match $prevMatch
123 * $prevMatch should be a match object obtained by a previous
124 * match upon the same value of $text.
126 * @return RegexpSet_match|bool A RegexpSet_match object, or false if no match.
128 function nextMatch($text, $prevMatch)
130 // Try to find match at same position.
131 $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
136 $pat = "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
138 if (!preg_match($pat, $text, $m)) {
142 $match = new AnchoredRegexpSet_match;
143 $match->postmatch = substr($text, strlen($m[0]));
144 $match->match = $m[1];
145 $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;
151 class BlockParser_Input
154 function __construct($text)
156 // Expand leading tabs.
157 // FIXME: do this better.
159 // We want to ensure the only characters matching \s are ' ' and "\n".
161 $text = preg_replace('/(?![ \n])\s/', ' ', $text);
162 assert(!preg_match('/(?![ \n])\s/', $text));
164 $this->_lines = preg_split('/[^\S\n]*\n/', $text);
167 // Strip leading blank lines.
168 while ($this->_lines and !$this->_lines[0])
169 array_shift($this->_lines);
170 $this->_atSpace = false;
175 $nlines = count($this->_lines);
177 if ($this->_pos >= $nlines) {
178 $this->_atSpace = false;
181 if ($this->_lines[$this->_pos] != '')
184 $this->_atSpace = true;
186 return $this->_atSpace;
189 function currentLine()
191 if ($this->_pos >= count($this->_lines)) {
194 return $this->_lines[$this->_pos];
199 $this->_atSpace = $this->_lines[$this->_pos++] === '';
200 if ($this->_pos >= count($this->_lines)) {
203 return $this->_lines[$this->_pos];
208 $this->_atSpace = ($this->_lines[$this->_pos] === '');
214 return array($this->_pos, $this->_atSpace);
217 function setPos($pos)
219 list($this->_pos, $this->_atSpace) = $pos;
234 if ($this->_pos < count($this->_lines))
235 return $this->_lines[$this->_pos];
240 function _debug($tab, $msg)
243 $where = $this->where();
244 $tab = str_repeat('____', $this->getDepth()) . $tab;
245 printXML(HTML::div("$tab $msg: at: '",
252 class BlockParser_InputSubBlock extends BlockParser_Input
255 * @param BlockParser_Input $input
256 * @param string $prefix_re
257 * @param string $initial_prefix
259 function __construct(&$input, $prefix_re, $initial_prefix = '')
261 $this->_input = &$input;
262 $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
263 $this->_atSpace = false;
265 if (($line = $input->currentLine()) === false)
266 $this->_line = false;
267 elseif ($initial_prefix) {
268 assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
269 $this->_line = (string)substr($line, strlen($initial_prefix));
270 $this->_atBlank = !ltrim($line);
271 } elseif (preg_match($this->_prefix_pat, $line, $m)) {
272 $this->_line = (string)substr($line, strlen($m[0]));
273 $this->_atBlank = !ltrim($line);
275 $this->_line = false;
280 // In contrast to the case for top-level blocks,
281 // for sub-blocks, there never appears to be any trailing space.
282 // (The last block in the sub-block should always be of class tight-bottom.)
283 while ($this->_line === '')
286 if ($this->_line === false)
287 return $this->_atSpace == 'strong_space';
289 return $this->_atSpace;
292 function currentLine()
299 if ($this->_line === '')
300 $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
302 $this->_atSpace = false;
304 $line = $this->_input->nextLine();
305 if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
306 $this->_line = (string)substr($line, strlen($m[0]));
307 $this->_atBlank = !ltrim($line);
309 $this->_line = false;
321 return array($this->_line, $this->_atSpace, $this->_input->getPos());
324 function setPos($pos)
326 $this->_line = $pos[0];
327 $this->_atSpace = $pos[1];
328 $this->_input->setPos($pos[2]);
333 assert($this->_line !== false);
334 $line = $this->_input->currentLine();
335 assert($line !== false && strlen($line) >= strlen($this->_line));
336 return substr($line, 0, strlen($line) - strlen($this->_line));
341 return $this->_input->getDepth() + 1;
346 return $this->_input->where();
350 class Block_HtmlElement extends HtmlElement
352 function __construct($tag /*, ... */)
354 $this->_init(func_get_args());
357 function setTightness($top, $bottom)
362 class ParsedBlock extends Block_HtmlElement
364 private $_block_types;
369 function __construct(&$input, $tag = 'div', $attr = array())
371 parent::__construct($tag, $attr);
372 $this->initBlockTypes();
373 $this->_parse($input);
376 private function _parse(&$input)
378 // php5 failed to advance the block. php5 copies objects by ref.
379 // nextBlock == block, both are the same objects. So we have to clone it.
380 for ($block = $this->getBlock($input);
382 $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock)) {
383 while ($nextBlock = $this->getBlock($input)) {
384 // Attempt to merge current with following block.
385 if (!($merged = $block->merge($nextBlock))) {
386 break; // can't merge
390 $this->pushContent($block->finish());
394 // FIXME: hackish. This should only be called once.
395 private function initBlockTypes()
397 // better static or global?
398 static $_regexpset, $_block_types;
400 if (!is_object($_regexpset)) {
401 // nowiki_wikicreole must be before template_plugin
403 ('nowiki_wikicreole', 'template_plugin', 'placeholder', 'oldlists', 'list', 'dl',
404 'table_dl', 'table_wikicreole', 'table_mediawiki',
405 'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre',
406 'email_blockquote', 'wikicreole_indented',
407 'plugin', 'plugin_wikicreole', 'p');
408 // insert it before p!
409 if (defined('ENABLE_MARKUP_DIVSPAN') and ENABLE_MARKUP_DIVSPAN) {
410 array_pop($Block_types);
411 $Block_types[] = 'divspan';
412 $Block_types[] = 'p';
414 foreach ($Block_types as $type) {
415 $class = "Block_$type";
417 $this->_block_types[] = $proto;
418 $this->_regexps[] = $proto->_re;
420 $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
421 $_regexpset = $this->_regexpset;
422 $_block_types = $this->_block_types;
425 $this->_regexpset = $_regexpset;
426 $this->_block_types = $_block_types;
430 private function getBlock(&$input)
432 $this->_atSpace = $input->skipSpace();
434 $line = $input->currentLine();
435 if ($line === false or $line === '') { // allow $line === '0'
438 $tight_top = !$this->_atSpace;
439 $re_set = &$this->_regexpset;
440 //FIXME: php5 fails to advance here!
441 for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
442 $block = clone($this->_block_types[$m->regexp_ind]);
443 if (DEBUG & _DEBUG_PARSER)
444 $input->_debug('>', get_class($block));
446 if ($block->_match($input, $m)) {
447 //$block->_text = $line;
448 if (DEBUG & _DEBUG_PARSER)
449 $input->_debug('<', get_class($block));
450 $tight_bottom = !$input->skipSpace();
451 $block->_setTightness($tight_top, $tight_bottom);
454 if (DEBUG & _DEBUG_PARSER)
455 $input->_debug('[', "_match failed");
457 if ($line === false or $line === '') // allow $line === '0'
460 trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
465 class WikiText extends ParsedBlock
467 function __construct($text)
469 $input = new BlockParser_Input($text);
470 parent::__construct($input);
474 class SubBlock extends ParsedBlock
476 function __construct(&$input, $indent_re, $initial_indent = false,
477 $tag = 'div', $attr = array())
479 $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
480 parent::__construct($subinput, $tag, $attr);
485 * TightSubBlock is for use in parsing lists item bodies.
487 * If the sub-block consists of a single paragraph, it omits
488 * the paragraph element.
490 * We go to this trouble so that "tight" lists look somewhat reasonable
491 * in older (non-CSS) browsers. (If you don't do this, then, without
492 * CSS, you only get "loose" lists.
494 class TightSubBlock extends SubBlock
496 function __construct(&$input, $indent_re, $initial_indent = false,
497 $tag = 'div', $attr = array())
499 parent::__construct($input, $indent_re, $initial_indent, $tag, $attr);
501 // If content is a single paragraph, eliminate the paragraph...
502 if (count($this->_content) == 1) {
503 $elem = $this->_content[0];
504 if (is_a($elem, 'XmlElement') and $elem->getTag() == 'p') {
505 $this->setContent($elem->getContent());
511 abstract class BlockMarkup
516 abstract function _match(&$input, $match);
518 function _setTightness($top, $bot)
522 function merge($followingBlock)
529 return $this->_element;
533 class Block_blockquote extends BlockMarkup
536 public $_re = '\ +(?=\S)';
539 function _match(&$input, $m)
541 $this->_depth = strlen($m->match);
542 $indent = sprintf("\\ {%d}", $this->_depth);
543 $this->_element = new SubBlock($input, $indent, $m->match,
548 function merge($nextBlock)
550 if (get_class($nextBlock) == get_class($this)) {
551 assert($nextBlock->_depth < $this->_depth);
552 $nextBlock->_element->unshiftContent($this->_element);
553 if (!empty($this->_tight_top))
554 $nextBlock->_tight_top = $this->_tight_top;
561 class Block_list extends BlockMarkup
563 public $_re = '\ {0,4}
568 | [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
570 public $_content = array();
571 public $_tag; //'ol' or 'ul'
573 function _match(&$input, $m)
575 // A list as the first content in a list is not allowed.
578 // Should markup as <ul><li>* Item</li></ul>,
579 // not <ul><li><ul><li>Item</li></ul>/li></ul>.
581 if (preg_match('/[*#+-o]/', $input->getPrefix())) {
586 $indent = sprintf("\\ {%d}", strlen($prefix));
588 $bullet = trim($m->match);
589 $this->_tag = $bullet == '#' ? 'ol' : 'ul';
590 $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
594 function _setTightness($top, $bot)
596 $li = &$this->_content[0];
597 $li->setTightness($top, $bot);
600 function merge($nextBlock)
602 if (is_a($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
603 array_splice($this->_content, count($this->_content), 0,
604 $nextBlock->_content);
612 return new Block_HtmlElement($this->_tag, false, $this->_content);
616 class Block_dl extends Block_list
619 private $_tight_defn;
621 function __construct()
623 $this->_re = '\ {0,4}\S.*(?<!' . ESCAPE_CHAR . '):\s*$';
626 function _match(&$input, $m)
628 if (!($p = $this->_do_match($input, $m)))
630 list ($term, $defn, $loose) = $p;
632 $this->_content[] = new Block_HtmlElement('dt', false, $term);
633 $this->_content[] = $defn;
634 $this->_tight_defn = !$loose;
638 function _setTightness($top, $bot)
640 $dt = &$this->_content[0];
641 $dd = &$this->_content[1];
643 $dt->setTightness($top, $this->_tight_defn);
644 $dd->setTightness($this->_tight_defn, $bot);
647 function _do_match(&$input, $m)
649 $pos = $input->getPos();
651 $firstIndent = strspn($m->match, ' ');
652 $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
655 $loose = $input->skipSpace();
656 $line = $input->currentLine();
658 if (!$line || !preg_match($pat, $line, $mm)) {
659 $input->setPos($pos);
660 return false; // No body found.
663 $indent = strlen($mm[0]);
664 $term = TransformInline(rtrim(substr(trim($m->match), 0, -1)));
665 $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
666 return array($term, $defn, $loose);
670 class Block_table_dl_defn extends XmlContent
678 function __construct($term, $defn)
680 parent::__construct();
681 if (!is_array($defn))
682 $defn = $defn->getContent();
684 $this->_next_tight_top = false; // value irrelevant - gets fixed later
685 $this->_ncols = $this->ComputeNcols($defn);
688 foreach ($defn as $item) {
689 if ($this->IsASubtable($item))
690 $this->addSubtable($item);
692 $this->addToRow($item);
696 $th = HTML::th($term);
697 if ($this->_nrows > 1)
698 $th->setAttr('rowspan', $this->_nrows);
699 $this->_setTerm($th);
702 function setTightness($tight_top, $tight_bot)
704 $this->_tight_top = $tight_top;
705 $this->_tight_bot = $tight_bot;
708 private function addToRow($item)
710 if (empty($this->_accum)) {
711 $this->_accum = HTML::td();
712 if ($this->_ncols > 2)
713 $this->_accum->setAttr('colspan', $this->_ncols - 1);
715 $this->_accum->pushContent($item);
718 private function flushRow($tight_bottom = false)
720 if (!empty($this->_accum)) {
721 $row = new Block_HtmlElement('tr', false, $this->_accum);
723 $row->setTightness($this->_next_tight_top, $tight_bottom);
724 $this->_next_tight_top = $tight_bottom;
726 $this->pushContent($row);
727 $this->_accum = false;
732 private function addSubtable($table)
734 if (!($table_rows = $table->getContent()))
737 $this->flushRow($table_rows[0]->_tight_top);
739 foreach ($table_rows as $subdef) {
740 $this->pushContent($subdef);
741 $this->_nrows += $subdef->nrows();
742 $this->_next_tight_top = $subdef->_tight_bot;
746 private function _setTerm($th)
748 $first_row = &$this->_content[0];
749 if (is_a($first_row, 'Block_table_dl_defn'))
750 $first_row->_setTerm($th);
752 $first_row->unshiftContent($th);
755 private function ComputeNcols($defn)
758 foreach ($defn as $item) {
759 if ($this->IsASubtable($item)) {
760 $row = $this->FirstDefn($item);
761 $ncols = max($ncols, $row->ncols() + 1);
767 private function IsASubtable($item)
769 return is_a($item, 'HtmlElement')
770 && $item->getTag() == 'table'
771 && $item->getAttr('class') == 'wiki-dl-table';
774 private function FirstDefn($subtable)
776 $defs = $subtable->getContent();
782 return $this->_ncols;
787 return $this->_nrows;
792 $first = &$this->_content[0];
793 if (is_a($first, 'Block_table_dl_defn'))
794 return $first->firstTR();
800 $last = &$this->_content[$this->_nrows - 1];
801 if (is_a($last, 'Block_table_dl_defn'))
802 return $last->lastTR();
806 function setWidth($ncols)
808 assert($ncols >= $this->_ncols);
809 if ($ncols <= $this->_ncols)
811 $rows = &$this->_content;
812 for ($i = 0; $i < count($rows); $i++) {
814 if (is_a($row, 'Block_table_dl_defn'))
815 $row->setWidth($ncols - 1);
817 $n = count($row->_content);
818 $lastcol = &$row->_content[$n - 1];
819 if (!empty($lastcol))
820 $lastcol->setAttr('colspan', $ncols - 1);
826 class Block_table_dl extends Block_dl
828 public $_tag = 'dl-table'; // phony.
830 function __construct()
832 $this->_re = '\ {0,4} (?:\S.*)? (?<!' . ESCAPE_CHAR . ') \| \s* $';
835 function _match(&$input, $m)
837 if (!($p = $this->_do_match($input, $m)))
839 list ($term, $defn, $loose) = $p;
841 $this->_content[] = new Block_table_dl_defn($term, $defn);
845 function _setTightness($top, $bot)
847 $this->_content[0]->setTightness($top, $bot);
852 $defs = &$this->_content;
855 foreach ($defs as $defn)
856 $ncols = max($ncols, $defn->ncols());
858 foreach ($defs as $key => $defn)
859 $defs[$key]->setWidth($ncols);
861 return HTML::table(array('class' => 'wiki-dl-table'), $defs);
865 class Block_oldlists extends Block_list
867 //public $_tag = 'ol', 'ul', or 'dl';
868 public $_re = '(?: [*]\ (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
869 | [#]\ (?! \[ .*? \] )
873 function _match(&$input, $m)
876 if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
881 $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
882 $newindent = sprintf('\\ {%d}', strlen($prefix));
883 $indent = "(?:$oldindent|$newindent)";
885 $bullet = $prefix[0];
886 if ($bullet == '*') {
889 } elseif ($bullet == '#') {
894 list ($term,) = explode(':', substr($prefix, 1), 2);
897 $this->_content[] = new Block_HtmlElement('dt', false,
898 TransformInline($term));
902 $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
906 function _setTightness($top, $bot)
908 if (count($this->_content) == 1) {
909 $li = &$this->_content[0];
910 $li->setTightness($top, $bot);
912 $dt = &$this->_content[0];
913 $dd = &$this->_content[1];
914 $dt->setTightness($top, false);
915 $dd->setTightness(false, $bot);
920 class Block_pre extends BlockMarkup
922 public $_re = '<(?:pre|verbatim|nowiki|noinclude|includeonly)>';
924 function _match(&$input, $m)
926 $endtag = '</' . substr($m->match, 1);
928 $pos = $input->getPos();
930 $line = $m->postmatch;
931 while (ltrim($line) != $endtag) {
933 if (($line = $input->nextLine()) === false) {
934 $input->setPos($pos);
940 if ($m->match == '<includeonly>') {
941 $this->_element = new Block_HtmlElement('div', false, '');
945 if ($m->match == '<nowiki>')
946 $text = join("<br>\n", $text);
948 $text = join("\n", $text);
950 if ($m->match == '<noinclude>') {
951 $text = TransformText($text);
952 $this->_element = new Block_HtmlElement('div', false, $text);
953 } elseif ($m->match == '<nowiki>') {
954 $text = TransformInlineNowiki($text);
955 $this->_element = new Block_HtmlElement('p', false, $text);
957 $this->_element = new Block_HtmlElement('pre', false, $text);
963 // Wikicreole placeholder
965 class Block_placeholder extends BlockMarkup
969 function _match(&$input, $m)
973 $pos = $input->getPos();
975 $line = $m->postmatch;
976 while (ltrim($line) != $endtag) {
978 if (($line = $input->nextLine()) === false) {
979 $input->setPos($pos);
985 $text = join("\n", $text);
986 $text = '<<<' . $text . '>>>';
987 $this->_element = new Block_HtmlElement('div', false, $text);
992 class Block_nowiki_wikicreole extends BlockMarkup
996 function _match(&$input, $m)
1000 $pos = $input->getPos();
1002 $line = $m->postmatch;
1003 while (ltrim($line) != $endtag) {
1005 if (($line = $input->nextLine()) === false) {
1006 $input->setPos($pos);
1012 $text = join("\n", $text);
1013 $this->_element = new Block_HtmlElement('pre', false, $text);
1018 class Block_plugin extends Block_pre
1020 public $_re = '<\?plugin(?:-form)?(?!\S)';
1023 /* <?plugin Backlinks
1025 /* <?plugin ListPages pages=<!plugin-list Backlinks!>
1026 * exclude=<!plugin-list TitleSearch s=T*!> ?>
1030 function _match(&$input, $m)
1032 $pos = $input->getPos();
1033 $pi = $m->match . $m->postmatch;
1034 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\?>\s*$/', $pi)) {
1035 if (($line = $input->nextLine()) === false) {
1036 $input->setPos($pos);
1043 $this->_element = new Cached_PluginInvocation($pi);
1048 class Block_plugin_wikicreole extends Block_pre
1050 // public $_re = '<<(?!\S)';
1053 function _match(&$input, $m)
1055 $pos = $input->getPos();
1056 $pi = $m->postmatch;
1057 if ($pi[0] == '<') {
1060 $pi = "<?plugin " . $pi;
1061 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')>>\s*$/', $pi)) {
1062 if (($line = $input->nextLine()) === false) {
1063 $input->setPos($pos);
1070 $pi = str_replace(">>", "?>", $pi);
1072 $this->_element = new Cached_PluginInvocation($pi);
1077 class Block_table_wikicreole extends Block_pre
1079 public $_re = '\s*\|';
1081 function _match(&$input, $m)
1083 $pos = $input->getPos();
1084 $pi = "|" . $m->postmatch;
1088 if ((($line = $input->nextLine()) === false) && !$intable) {
1089 $input->setPos($pos);
1096 $trimline = trim($line);
1097 if ($trimline[0] != "|") {
1101 $pi .= "\n$trimline";
1104 $pi = '<' . '?plugin WikicreoleTable ' . $pi . '?' . '>';
1106 $this->_element = new Cached_PluginInvocation($pi);
1112 * Table syntax similar to Mediawiki
1114 * => <?plugin MediawikiTable
1118 class Block_table_mediawiki extends Block_pre
1120 public $_re = '{\|';
1122 function _match(&$input, $m)
1124 $pos = $input->getPos();
1125 $pi = $m->postmatch;
1126 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')\|}\s*$/', $pi)) {
1127 if (($line = $input->nextLine()) === false) {
1128 $input->setPos($pos);
1135 $pi = str_replace("\|}", "", $pi);
1136 $pi = '<' . '?plugin MediawikiTable ' . $pi . '?' . '>';
1137 $this->_element = new Cached_PluginInvocation($pi);
1143 * Template syntax similar to Mediawiki
1145 * => < ? plugin Template page=template ? >
1146 * {{template|var1=value1|var2=value|...}}
1147 * => < ? plugin Template page=template var=value ... ? >
1149 * The {{...}} syntax is also used for:
1150 * - Wikicreole images
1153 class Block_template_plugin extends Block_pre
1157 function _match(&$input, $m)
1159 // If we find "}}", this is an inline template.
1160 if (strpos($m->postmatch, "}}") !== false) {
1163 $pos = $input->getPos();
1164 $pi = $m->postmatch;
1165 if ($pi[0] == '{') {
1168 while (!preg_match('/(?<!' . ESCAPE_CHAR . ')}}\s*$/', $pi)) {
1169 if (($line = $input->nextLine()) === false) {
1170 $input->setPos($pos);
1178 $pi = trim($pi, "}}");
1180 if (strpos($pi, "|") === false) {
1184 $imagename = substr($pi, 0, strpos($pi, "|"));
1185 $alt = ltrim(strstr($pi, "|"), "|");
1188 // It's not a Mediawiki template, it's a Wikicreole image
1189 if (is_image($imagename)) {
1190 $this->_element = LinkImage(getUploadDataPath() . $imagename, $alt);
1195 if (is_video($imagename)) {
1196 if ((strpos($imagename, 'http://') === 0)
1197 || (strpos($imagename, 'https://') === 0)) {
1198 $pi = '<' . '?plugin Video url="' . $pi . '" ?>';
1200 $pi = '<' . '?plugin Video file="' . $pi . '" ?>';
1202 $this->_element = new Cached_PluginInvocation($pi);
1206 $pi = str_replace("\n", "", $pi);
1208 // The argument value might contain a double quote (")
1209 // We have to encode that.
1210 $pi = htmlspecialchars($pi);
1214 if (preg_match('/^(\S+?)\|(.*)$/', $pi, $_m)) {
1216 $vars = '"' . preg_replace('/\|/', '" "', $_m[2]) . '"';
1217 $vars = preg_replace('/"(\S+)=([^"]*)"/', '\\1="\\2"', $vars);
1220 // pi may contain a version number
1221 // {{foo?version=5}}
1222 // in that case, output is "page=foo rev=5"
1223 if (strstr($pi, "?")) {
1224 $pi = str_replace("?version=", "\" rev=\"", $pi);
1228 $pi = '<' . '?plugin Template page="' . $pi . '" ' . $vars . ' ?>';
1230 $pi = '<' . '?plugin Template page="' . $pi . '" ?>';
1231 $this->_element = new Cached_PluginInvocation($pi);
1236 class Block_email_blockquote extends BlockMarkup
1238 public $_attr = array('class' => 'mail-style-quote');
1239 public $_re = '>\ ?';
1241 function _match(&$input, $m)
1243 //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1244 $indent = $this->_re;
1245 $this->_element = new SubBlock($input, $indent, $m->match, 'blockquote', $this->_attr);
1250 class Block_wikicreole_indented extends BlockMarkup
1252 public $_attr = array('style' => 'margin-left:2em');
1253 public $_re = ':\ ?';
1255 function _match(&$input, $m)
1257 $indent = $this->_re;
1258 $this->_element = new SubBlock($input, $indent, $m->match,
1259 'div', $this->_attr);
1264 class Block_hr extends BlockMarkup
1266 public $_re = '-{4,}\s*$';
1268 function _match(&$input, $m)
1271 $this->_element = new Block_HtmlElement('hr');
1276 class Block_heading extends BlockMarkup
1278 public $_re = '!{1,3}';
1280 function _match(&$input, $m)
1282 $tag = "h" . (5 - strlen($m->match));
1283 $text = TransformInline(trim($m->postmatch));
1286 $this->_element = new Block_HtmlElement($tag, false, $text);
1292 class Block_heading_wikicreole extends BlockMarkup
1294 public $_re = '={2,6}';
1296 function _match(&$input, $m)
1298 $tag = "h" . strlen($m->match);
1300 $header = trim($m->postmatch);
1301 // Remove '='s at the end so that Mediawiki syntax is recognized
1302 $header = trim($header, "=");
1303 $text = TransformInline(trim($header));
1306 $this->_element = new Block_HtmlElement($tag, false, $text);
1312 class Block_p extends BlockMarkup
1315 public $_re = '\S.*';
1317 private $_tight_bot;
1318 private $_tight_top;
1320 function _match(&$input, $m)
1322 $this->_text = $m->match;
1327 function _setTightness($top, $bot)
1329 $this->_tight_top = $top;
1330 $this->_tight_bot = $bot;
1333 function merge($nextBlock)
1335 $class = get_class($nextBlock);
1336 if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1337 $this->_text .= "\n" . $nextBlock->_text;
1338 $this->_tight_bot = $nextBlock->_tight_bot;
1346 $content = TransformInline(trim($this->_text));
1347 $p = new Block_HtmlElement('p', false, $content);
1348 $p->setTightness($this->_tight_top, $this->_tight_bot);
1353 class Block_divspan extends BlockMarkup
1355 public $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1357 function _match(&$input, $m)
1359 if (substr($m->match, 1, 4) == 'span') {
1365 $argstr = substr(trim(substr($m->match, strlen($tag) + 1)), 0, -1);
1366 $pos = $input->getPos();
1367 $pi = $content = $m->postmatch;
1368 while (!preg_match('/^(.*)\<\/' . $tag . '\>(.*)$/i', $pi, $me)) {
1369 if ($pi != $content)
1370 $content .= "\n$pi";
1371 if (($pi = $input->nextLine()) === false) {
1372 $input->setPos($pos);
1376 if ($pi != $content)
1377 $content .= $me[1]; // prematch
1381 if (strstr($content, "\n"))
1382 $content = TransformText($content);
1384 $content = TransformInline($content);
1389 while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1392 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1396 preg_match("/^(\s+)(.*)$/", $v, $m);
1400 if (trim($k) and trim($v)) $args[$k] = $v;
1403 $this->_element = new Block_HtmlElement($tag, $args, $content);
1408 ////////////////////////////////////////////////////////////////
1412 * Transform the text of a page, and return a parse tree.
1414 function TransformTextPre($text)
1416 if (is_a($text, 'WikiDB_PageRevision')) {
1418 $text = $rev->getPackedContent();
1420 // Expand leading tabs.
1421 $text = expand_tabs($text);
1422 $output = new WikiText($text);
1428 * Transform the text of a page, and return an XmlContent,
1429 * suitable for printXml()-ing.
1431 function TransformText($text, $basepage = false)
1433 $output = TransformTextPre($text);
1435 // This is for immediate consumption.
1436 // We must bind the contents to a base pagename so that
1437 // relative page links can be properly linkified...
1438 return new CacheableMarkup($output->getContent(), $basepage);
1440 return new XmlContent($output->getContent());
1446 // c-basic-offset: 4
1447 // c-hanging-comment-ender-p: nil
1448 // indent-tabs-mode: nil