2 /* Copyright (C) 2002 Geoffrey T. Dairiki <dairiki@dairiki.org>
3 * Copyright (C) 2004,2005 Reini Urban
4 * Copyright (C) 2008-2009 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
19 * along with PhpWiki; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 {
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
78 * @param $regexps array A list of regular expressions. The
79 * regular expressions should not include any sub-pattern groups
80 * "(...)". (Anonymous groups, like "(?:...)", as well as
81 * look-ahead and look-behind assertions are fine.)
83 function AnchoredRegexpSet ($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.
95 function match ($text) {
96 if (!is_string($text)) return false;
97 if (! preg_match($this->_re, $text, $m)) {
101 $match = new AnchoredRegexpSet_match;
102 $match->postmatch = substr($text, strlen($m[0]));
103 $match->match = $m[1];
104 $match->regexp_ind = count($m) - 3;
109 * Search for next matching regexp.
111 * Here, 'next' has two meanings:
113 * Match the next regexp(s) in the set, at the same position as the last match.
115 * If that fails, match the whole RegexpSet, starting after the position of the
118 * @param $text string Text to search.
120 * @param $prevMatch A RegexpSet_match object
122 * $prevMatch should be a match object obtained by a previous
123 * match upon the same value of $text.
125 * @return object A RegexpSet_match object, or false if no match.
127 function nextMatch ($text, $prevMatch) {
128 // Try to find match at same position.
129 $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
134 $pat= "/ ( (" . join(')|(', $regexps) . ") ) /Axs";
136 if (! preg_match($pat, $text, $m)) {
140 $match = new AnchoredRegexpSet_match;
141 $match->postmatch = substr($text, strlen($m[0]));
142 $match->match = $m[1];
143 $match->regexp_ind = count($m) - 3 + $prevMatch->regexp_ind + 1;;
150 class BlockParser_Input {
152 function BlockParser_Input ($text) {
154 // Expand leading tabs.
155 // FIXME: do this better.
157 // We want to ensure the only characters matching \s are ' ' and "\n".
159 $text = preg_replace('/(?![ \n])\s/', ' ', $text);
160 assert(!preg_match('/(?![ \n])\s/', $text));
162 $this->_lines = preg_split('/[^\S\n]*\n/', $text);
165 // Strip leading blank lines.
166 while ($this->_lines and ! $this->_lines[0])
167 array_shift($this->_lines);
168 $this->_atSpace = false;
171 function skipSpace () {
172 $nlines = count($this->_lines);
174 if ($this->_pos >= $nlines) {
175 $this->_atSpace = false;
178 if ($this->_lines[$this->_pos] != '')
181 $this->_atSpace = true;
183 return $this->_atSpace;
186 function currentLine () {
187 if ($this->_pos >= count($this->_lines)) {
190 return $this->_lines[$this->_pos];
193 function nextLine () {
194 $this->_atSpace = $this->_lines[$this->_pos++] === '';
195 if ($this->_pos >= count($this->_lines)) {
198 return $this->_lines[$this->_pos];
201 function advance () {
202 $this->_atSpace = ($this->_lines[$this->_pos] === '');
207 return array($this->_pos, $this->_atSpace);
210 function setPos ($pos) {
211 list($this->_pos, $this->_atSpace) = $pos;
214 function getPrefix () {
218 function getDepth () {
223 if ($this->_pos < count($this->_lines))
224 return $this->_lines[$this->_pos];
229 function _debug ($tab, $msg) {
231 $where = $this->where();
232 $tab = str_repeat('____', $this->getDepth() ) . $tab;
233 printXML(HTML::div("$tab $msg: at: '",
240 class BlockParser_InputSubBlock extends BlockParser_Input
242 function BlockParser_InputSubBlock (&$input, $prefix_re, $initial_prefix = false) {
243 $this->_input = &$input;
244 $this->_prefix_pat = "/$prefix_re|\\s*\$/Ax";
245 $this->_atSpace = false;
247 if (($line = $input->currentLine()) === false)
248 $this->_line = false;
249 elseif ($initial_prefix) {
250 assert(substr($line, 0, strlen($initial_prefix)) == $initial_prefix);
251 $this->_line = (string) substr($line, strlen($initial_prefix));
252 $this->_atBlank = ! ltrim($line);
254 elseif (preg_match($this->_prefix_pat, $line, $m)) {
255 $this->_line = (string) substr($line, strlen($m[0]));
256 $this->_atBlank = ! ltrim($line);
259 $this->_line = false;
262 function skipSpace () {
263 // In contrast to the case for top-level blocks,
264 // for sub-blocks, there never appears to be any trailing space.
265 // (The last block in the sub-block should always be of class tight-bottom.)
266 while ($this->_line === '')
269 if ($this->_line === false)
270 return $this->_atSpace == 'strong_space';
272 return $this->_atSpace;
275 function currentLine () {
279 function nextLine () {
280 if ($this->_line === '')
281 $this->_atSpace = $this->_atBlank ? 'weak_space' : 'strong_space';
283 $this->_atSpace = false;
285 $line = $this->_input->nextLine();
286 if ($line !== false && preg_match($this->_prefix_pat, $line, $m)) {
287 $this->_line = (string) substr($line, strlen($m[0]));
288 $this->_atBlank = ! ltrim($line);
291 $this->_line = false;
296 function advance () {
301 return array($this->_line, $this->_atSpace, $this->_input->getPos());
304 function setPos ($pos) {
305 $this->_line = $pos[0];
306 $this->_atSpace = $pos[1];
307 $this->_input->setPos($pos[2]);
310 function getPrefix () {
311 assert ($this->_line !== false);
312 $line = $this->_input->currentLine();
313 assert ($line !== false && strlen($line) >= strlen($this->_line));
314 return substr($line, 0, strlen($line) - strlen($this->_line));
317 function getDepth () {
318 return $this->_input->getDepth() + 1;
322 return $this->_input->where();
327 class Block_HtmlElement extends HtmlElement
329 function Block_HtmlElement($tag /*, ... */) {
330 $this->_init(func_get_args());
333 function setTightness($top, $bottom) {
337 class ParsedBlock extends Block_HtmlElement {
339 function ParsedBlock (&$input, $tag = 'div', $attr = false) {
340 $this->Block_HtmlElement($tag, $attr);
341 $this->_initBlockTypes();
342 $this->_parse($input);
345 function _parse (&$input) {
346 // php5 failed to advance the block. php5 copies objects by ref.
347 // nextBlock == block, both are the same objects. So we have to clone it.
348 for ($block = $this->_getBlock($input);
350 $block = (is_object($nextBlock) ? clone($nextBlock) : $nextBlock))
352 while ($nextBlock = $this->_getBlock($input)) {
353 // Attempt to merge current with following block.
354 if (! ($merged = $block->merge($nextBlock)) ) {
355 break; // can't merge
359 $this->pushContent($block->finish());
363 // FIXME: hackish. This should only be called once.
364 function _initBlockTypes () {
365 // better static or global?
366 static $_regexpset, $_block_types;
368 if (!is_object($_regexpset)) {
370 ('oldlists', 'list', 'dl', 'table_dl', 'table_wikicreole', 'table_mediawiki',
371 'blockquote', 'heading', 'heading_wikicreole', 'hr', 'pre', 'email_blockquote',
372 'plugin', 'plugin_wikicreole', 'p');
373 // insert it before p!
374 if (ENABLE_MARKUP_DIVSPAN) {
375 array_pop($Block_types);
376 $Block_types[] = 'divspan';
377 $Block_types[] = 'p';
379 foreach ($Block_types as $type) {
380 $class = "Block_$type";
382 $this->_block_types[] = $proto;
383 $this->_regexps[] = $proto->_re;
385 $this->_regexpset = new AnchoredRegexpSet($this->_regexps);
386 $_regexpset = $this->_regexpset;
387 $_block_types = $this->_block_types;
390 $this->_regexpset = $_regexpset;
391 $this->_block_types = $_block_types;
395 function _getBlock (&$input) {
396 $this->_atSpace = $input->skipSpace();
398 $line = $input->currentLine();
399 if ($line === false or $line === '') { // allow $line === '0'
402 $tight_top = !$this->_atSpace;
403 $re_set = &$this->_regexpset;
404 //FIXME: php5 fails to advance here!
405 for ($m = $re_set->match($line); $m; $m = $re_set->nextMatch($line, $m)) {
406 $block = clone($this->_block_types[$m->regexp_ind]);
407 if (DEBUG & _DEBUG_PARSER)
408 $input->_debug('>', get_class($block));
410 if ($block->_match($input, $m)) {
411 //$block->_text = $line;
412 if (DEBUG & _DEBUG_PARSER)
413 $input->_debug('<', get_class($block));
414 $tight_bottom = ! $input->skipSpace();
415 $block->_setTightness($tight_top, $tight_bottom);
418 if (DEBUG & _DEBUG_PARSER)
419 $input->_debug('[', "_match failed");
421 if ($line === false or $line === '') // allow $line === '0'
424 trigger_error("Couldn't match block: '$line'", E_USER_NOTICE);
429 class WikiText extends ParsedBlock {
430 function WikiText ($text) {
431 $input = new BlockParser_Input($text);
432 $this->ParsedBlock($input);
436 class SubBlock extends ParsedBlock {
437 function SubBlock (&$input, $indent_re, $initial_indent = false,
438 $tag = 'div', $attr = false) {
439 $subinput = new BlockParser_InputSubBlock($input, $indent_re, $initial_indent);
440 $this->ParsedBlock($subinput, $tag, $attr);
445 * TightSubBlock is for use in parsing lists item bodies.
447 * If the sub-block consists of a single paragraph, it omits
448 * the paragraph element.
450 * We go to this trouble so that "tight" lists look somewhat reasonable
451 * in older (non-CSS) browsers. (If you don't do this, then, without
452 * CSS, you only get "loose" lists.
454 class TightSubBlock extends SubBlock {
455 function TightSubBlock (&$input, $indent_re, $initial_indent = false,
456 $tag = 'div', $attr = false) {
457 $this->SubBlock($input, $indent_re, $initial_indent, $tag, $attr);
459 // If content is a single paragraph, eliminate the paragraph...
460 if (count($this->_content) == 1) {
461 $elem = $this->_content[0];
462 if (isa($elem, 'XmlElement') and $elem->getTag() == 'p') {
463 $this->setContent($elem->getContent());
472 function _match (&$input, $match) {
473 trigger_error('pure virtual', E_USER_ERROR);
476 function _setTightness ($top, $bot) {
477 $this->_element->setTightness($top, $bot);
480 function merge ($followingBlock) {
485 return $this->_element;
489 class Block_blockquote extends BlockMarkup
492 var $_re = '\ +(?=\S)';
494 function _match (&$input, $m) {
495 $this->_depth = strlen($m->match);
496 $indent = sprintf("\\ {%d}", $this->_depth);
497 $this->_element = new SubBlock($input, $indent, $m->match,
502 function merge ($nextBlock) {
503 if (get_class($nextBlock) == get_class($this)) {
504 assert ($nextBlock->_depth < $this->_depth);
505 $nextBlock->_element->unshiftContent($this->_element);
506 if (!empty($this->_tight_top))
507 $nextBlock->_tight_top = $this->_tight_top;
514 class Block_list extends BlockMarkup
516 //var $_tag = 'ol' or 'ul';
522 | [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]) )
524 var $_content = array();
526 function _match (&$input, $m) {
527 // A list as the first content in a list is not allowed.
530 // Should markup as <ul><li>* Item</li></ul>,
531 // not <ul><li><ul><li>Item</li></ul>/li></ul>.
533 if (preg_match('/[*#+-o]/', $input->getPrefix())) {
538 $indent = sprintf("\\ {%d}", strlen($prefix));
540 $bullet = trim($m->match);
541 $this->_tag = $bullet == '#' ? 'ol' : 'ul';
542 $this->_content[] = new TightSubBlock($input, $indent, $m->match, 'li');
546 function _setTightness($top, $bot) {
547 $li = &$this->_content[0];
548 $li->setTightness($top, $bot);
551 function merge ($nextBlock) {
552 if (isa($nextBlock, 'Block_list') and $this->_tag == $nextBlock->_tag) {
553 if ($nextBlock->_content === $this->_content) {
554 trigger_error("Internal Error: no block advance", E_USER_NOTICE);
557 array_splice($this->_content, count($this->_content), 0,
558 $nextBlock->_content);
565 return new Block_HtmlElement($this->_tag, false, $this->_content);
569 class Block_dl extends Block_list
573 function Block_dl () {
574 $this->_re = '\ {0,4}\S.*(?<!'.ESCAPE_CHAR.'):\s*$';
577 function _match (&$input, $m) {
578 if (!($p = $this->_do_match($input, $m)))
580 list ($term, $defn, $loose) = $p;
582 $this->_content[] = new Block_HtmlElement('dt', false, $term);
583 $this->_content[] = $defn;
584 $this->_tight_defn = !$loose;
588 function _setTightness($top, $bot) {
589 $dt = &$this->_content[0];
590 $dd = &$this->_content[1];
592 $dt->setTightness($top, $this->_tight_defn);
593 $dd->setTightness($this->_tight_defn, $bot);
596 function _do_match (&$input, $m) {
597 $pos = $input->getPos();
599 $firstIndent = strspn($m->match, ' ');
600 $pat = sprintf('/\ {%d,%d}(?=\s*\S)/A', $firstIndent + 1, $firstIndent + 5);
603 $loose = $input->skipSpace();
604 $line = $input->currentLine();
606 if (!$line || !preg_match($pat, $line, $mm)) {
607 $input->setPos($pos);
608 return false; // No body found.
611 $indent = strlen($mm[0]);
612 $term = TransformInline(rtrim(substr(trim($m->match),0,-1)));
613 $defn = new TightSubBlock($input, sprintf("\\ {%d}", $indent), false, 'dd');
614 return array($term, $defn, $loose);
620 class Block_table_dl_defn extends XmlContent
625 function Block_table_dl_defn ($term, $defn) {
627 if (!is_array($defn))
628 $defn = $defn->getContent();
630 $this->_next_tight_top = false; // value irrelevant - gets fixed later
631 $this->_ncols = $this->_ComputeNcols($defn);
634 foreach ($defn as $item) {
635 if ($this->_IsASubtable($item))
636 $this->_addSubtable($item);
638 $this->_addToRow($item);
642 $th = HTML::th($term);
643 if ($this->_nrows > 1)
644 $th->setAttr('rowspan', $this->_nrows);
645 $this->_setTerm($th);
648 function setTightness($tight_top, $tight_bot) {
649 $this->_tight_top = $tight_top;
650 $this->_tight_bot = $tight_bot;
651 $first = &$this->firstTR();
652 $last = &$this->lastTR();
653 $first->setInClass('top', $tight_top);
655 $last->setInClass('bottom', $tight_bot);
657 trigger_error(sprintf("no lastTR: %s",AsXML($this->_content[0])), E_USER_WARNING);
661 function _addToRow ($item) {
662 if (empty($this->_accum)) {
663 $this->_accum = HTML::td();
664 if ($this->_ncols > 2)
665 $this->_accum->setAttr('colspan', $this->_ncols - 1);
667 $this->_accum->pushContent($item);
670 function _flushRow ($tight_bottom=false) {
671 if (!empty($this->_accum)) {
672 $row = new Block_HtmlElement('tr', false, $this->_accum);
674 $row->setTightness($this->_next_tight_top, $tight_bottom);
675 $this->_next_tight_top = $tight_bottom;
677 $this->pushContent($row);
678 $this->_accum = false;
683 function _addSubtable ($table) {
684 if (!($table_rows = $table->getContent()))
687 $this->_flushRow($table_rows[0]->_tight_top);
689 foreach ($table_rows as $subdef) {
690 $this->pushContent($subdef);
691 $this->_nrows += $subdef->nrows();
692 $this->_next_tight_top = $subdef->_tight_bot;
696 function _setTerm ($th) {
697 $first_row = &$this->_content[0];
698 if (isa($first_row, 'Block_table_dl_defn'))
699 $first_row->_setTerm($th);
701 $first_row->unshiftContent($th);
704 function _ComputeNcols ($defn) {
706 foreach ($defn as $item) {
707 if ($this->_IsASubtable($item)) {
708 $row = $this->_FirstDefn($item);
709 $ncols = max($ncols, $row->ncols() + 1);
715 function _IsASubtable ($item) {
716 return isa($item, 'HtmlElement')
717 && $item->getTag() == 'table'
718 && $item->getAttr('class') == 'wiki-dl-table';
721 function _FirstDefn ($subtable) {
722 $defs = $subtable->getContent();
727 return $this->_ncols;
731 return $this->_nrows;
734 function & firstTR() {
735 $first = &$this->_content[0];
736 if (isa($first, 'Block_table_dl_defn'))
737 return $first->firstTR();
741 function & lastTR() {
742 $last = &$this->_content[$this->_nrows - 1];
743 if (isa($last, 'Block_table_dl_defn'))
744 return $last->lastTR();
748 function setWidth ($ncols) {
749 assert($ncols >= $this->_ncols);
750 if ($ncols <= $this->_ncols)
752 $rows = &$this->_content;
753 for ($i = 0; $i < count($rows); $i++) {
755 if (isa($row, 'Block_table_dl_defn'))
756 $row->setWidth($ncols - 1);
758 $n = count($row->_content);
759 $lastcol = &$row->_content[$n - 1];
760 if (!empty($lastcol))
761 $lastcol->setAttr('colspan', $ncols - 1);
767 class Block_table_dl extends Block_dl
769 var $_tag = 'dl-table'; // phony.
771 function Block_table_dl() {
772 $this->_re = '\ {0,4} (?:\S.*)? (?<!'.ESCAPE_CHAR.') \| \s* $';
775 function _match (&$input, $m) {
776 if (!($p = $this->_do_match($input, $m)))
778 list ($term, $defn, $loose) = $p;
780 $this->_content[] = new Block_table_dl_defn($term, $defn);
784 function _setTightness($top, $bot) {
785 $this->_content[0]->setTightness($top, $bot);
790 $defs = &$this->_content;
793 foreach ($defs as $defn)
794 $ncols = max($ncols, $defn->ncols());
796 foreach ($defs as $key => $defn)
797 $defs[$key]->setWidth($ncols);
799 return HTML::table(array('class' => 'wiki-dl-table',
807 class Block_oldlists extends Block_list
809 //var $_tag = 'ol', 'ul', or 'dl';
810 var $_re = '(?: [*] (?!(?=\S)[^*]*(?<=\S)[*](?:\\s|[-)}>"\'\\/:.,;!?_*=]))
811 | [#] (?! \[ .*? \] )
815 function _match (&$input, $m) {
817 if (!preg_match('/[*#;]*$/A', $input->getPrefix())) {
823 $oldindent = '[*#;](?=[#*]|;.*:.*\S)';
824 $newindent = sprintf('\\ {%d}', strlen($prefix));
825 $indent = "(?:$oldindent|$newindent)";
827 $bullet = $prefix[0];
828 if ($bullet == '*') {
832 elseif ($bullet == '#') {
838 list ($term,) = explode(':', substr($prefix, 1), 2);
841 $this->_content[] = new Block_HtmlElement('dt', false,
842 TransformInline($term));
846 $this->_content[] = new TightSubBlock($input, $indent, $m->match, $itemtag);
850 function _setTightness($top, $bot) {
851 if (count($this->_content) == 1) {
852 $li = &$this->_content[0];
853 $li->setTightness($top, $bot);
856 // This is where php5 usually brakes.
857 // wrong duplicated <li> contents
858 if (DEBUG and DEBUG & _DEBUG_PARSER and check_php_version(5)) {
859 if (count($this->_content) != 2) {
862 $class = new Reflection_Class('XmlElement');
863 // Print out basic information
865 "===> The %s%s%s %s '%s' [extends %s]\n".
868 " having the modifiers %d [%s]\n",
869 $class->isInternal() ? 'internal' : 'user-defined',
870 $class->isAbstract() ? ' abstract' : '',
871 $class->isFinal() ? ' final' : '',
872 $class->isInterface() ? 'interface' : 'class',
874 var_export($class->getParentClass(), 1),
875 $class->getFileName(),
876 $class->getStartLine(),
877 $class->getEndline(),
878 $class->getModifiers(),
879 implode(' ', Reflection::getModifierNames($class->getModifiers()))
881 // Print class properties
882 printf("---> Properties: %s\n", var_export($class->getProperties(), 1));
884 echo 'count($this->_content): ', count($this->_content),"\n";
885 echo "\$this->_content[0]: "; var_dump ($this->_content[0]);
887 for ($i=1; $i < min(5, count($this->_content)); $i++) {
888 $c =& $this->_content[$i];
889 echo '$this->_content[',$i,"]: \n";
890 echo "_tag: "; var_dump ($c->_tag);
891 echo "_content: "; var_dump ($c->_content);
892 echo "_properties: "; var_dump ($c->_properties);
894 debug_print_backtrace();
895 if (DEBUG & _DEBUG_APD) {
896 if (function_exists("xdebug_get_function_stack")) {
897 var_dump (xdebug_get_function_stack());
903 if (!check_php_version(5))
904 assert(count($this->_content) == 2);
905 $dt = &$this->_content[0];
906 $dd = &$this->_content[1];
907 $dt->setTightness($top, false);
908 $dd->setTightness(false, $bot);
913 class Block_pre extends BlockMarkup
915 var $_re = '<(?:pre|verbatim|nowiki|noinclude)>';
917 function _match (&$input, $m) {
918 $endtag = '</' . substr($m->match, 1);
920 $pos = $input->getPos();
922 $line = $m->postmatch;
923 while (ltrim($line) != $endtag) {
925 if (($line = $input->nextLine()) === false) {
926 $input->setPos($pos);
932 if ($m->match == '<nowiki>')
933 $text = join("<br>\n", $text);
935 $text = join("\n", $text);
937 // FIXME: no <img>, <big>, <small>, <sup>, or <sub>'s allowed
939 if ($m->match == '<pre>') {
940 $text = TransformInline($text);
942 if ($m->match == '<noinclude>') {
943 $text = TransformText($text);
944 $this->_element = new Block_HtmlElement('div', false, $text);
945 } else if ($m->match == '<nowiki>') {
946 $text = TransformInlineNowiki($text);
947 $this->_element = new Block_HtmlElement('p', false, $text);
949 $this->_element = new Block_HtmlElement('pre', false, $text);
955 class Block_plugin extends Block_pre
957 var $_re = '<\?plugin(?:-form)?(?!\S)';
960 /* <?plugin Backlinks
962 /* <?plugin ListPages pages=<!plugin-list Backlinks!>
963 * exclude=<!plugin-list TitleSearch s=T*!> ?>
967 function _match (&$input, $m) {
968 $pos = $input->getPos();
969 $pi = $m->match . $m->postmatch;
970 while (!preg_match('/(?<!'.ESCAPE_CHAR.')\?>\s*$/', $pi)) {
971 if (($line = $input->nextLine()) === false) {
972 $input->setPos($pos);
979 $this->_element = new Cached_PluginInvocation($pi);
984 class Block_plugin_wikicreole extends Block_pre
986 // var $_re = '<<(?!\S)';
989 function _match (&$input, $m) {
990 $pos = $input->getPos();
991 $pi = "<?plugin " . $m->postmatch;
992 while (!preg_match('/(?<!'.ESCAPE_CHAR.')>>\s*$/', $pi)) {
993 if (($line = $input->nextLine()) === false) {
994 $input->setPos($pos);
1001 $pi = str_replace(">>", "?>", $pi);
1003 $this->_element = new Cached_PluginInvocation($pi);
1008 class Block_table_wikicreole extends Block_pre
1012 function _match (&$input, $m) {
1013 $pos = $input->getPos();
1014 $pi = "|" . $m->postmatch;
1018 if (($line = $input->nextLine()) === false) {
1019 $input->setPos($pos);
1026 $trimline = trim($line);
1027 if ($trimline[0] != "|") {
1031 $pi .= "\n$trimline";
1034 $pi = '<'.'?plugin WikicreoleTable ' . $pi . '?'.'>';
1036 $this->_element = new Cached_PluginInvocation($pi);
1041 /** ENABLE_MARKUP_MEDIAWIKI_TABLE
1042 * Table syntax similar to Mediawiki
1044 * => <?plugin MediawikiTable
1048 class Block_table_mediawiki extends Block_pre
1052 function _match (&$input, $m) {
1053 $pos = $input->getPos();
1054 $pi = $m->postmatch;
1055 while (!preg_match('/(?<!'.ESCAPE_CHAR.')\|}\s*$/', $pi)) {
1056 if (($line = $input->nextLine()) === false) {
1057 $input->setPos($pos);
1064 $pi = str_replace("\|}", "", $pi);
1065 $pi = '<'.'?plugin MediawikiTable ' . $pi . '?'.'>';
1066 $this->_element = new Cached_PluginInvocation($pi);
1071 class Block_email_blockquote extends BlockMarkup
1073 var $_attr = array('class' => 'mail-style-quote');
1076 function _match (&$input, $m) {
1077 //$indent = str_replace(' ', '\\ ', $m->match) . '|>$';
1078 $indent = $this->_re;
1079 $this->_element = new SubBlock($input, $indent, $m->match,
1080 'blockquote', $this->_attr);
1085 class Block_hr extends BlockMarkup
1087 var $_re = '-{4,}\s*$';
1089 function _match (&$input, $m) {
1091 $this->_element = new Block_HtmlElement('hr');
1095 function _setTightness($top, $bot) {
1096 // Don't tighten <hr/>s
1100 class Block_heading extends BlockMarkup
1102 var $_re = '!{1,3}';
1104 function _match (&$input, $m) {
1105 $tag = "h" . (5 - strlen($m->match));
1106 $text = TransformInline(trim($m->postmatch));
1109 $this->_element = new Block_HtmlElement($tag, false, $text);
1114 function _setTightness($top, $bot) {
1115 // Don't tighten headers.
1119 class Block_heading_wikicreole extends BlockMarkup
1121 var $_re = '={2,6}';
1123 function _match (&$input, $m) {
1124 $tag = "h" . strlen($m->match);
1126 $header = trim($m->postmatch);
1127 // Remove '='s at the end so that Mediawiki syntax is recognized
1128 $header = trim($header, "=");
1129 $text = TransformInline(trim($header));
1132 $this->_element = new Block_HtmlElement($tag, false, $text);
1137 function _setTightness($top, $bot) {
1138 // Don't tighten headers.
1142 class Block_p extends BlockMarkup
1148 function _match (&$input, $m) {
1149 $this->_text = $m->match;
1154 function _setTightness ($top, $bot) {
1155 $this->_tight_top = $top;
1156 $this->_tight_bot = $bot;
1159 function merge ($nextBlock) {
1160 $class = get_class($nextBlock);
1161 if (strtolower($class) == 'block_p' and $this->_tight_bot) {
1162 $this->_text .= "\n" . $nextBlock->_text;
1163 $this->_tight_bot = $nextBlock->_tight_bot;
1169 function finish () {
1170 $content = TransformInline(trim($this->_text));
1171 $p = new Block_HtmlElement('p', false, $content);
1172 $p->setTightness($this->_tight_top, $this->_tight_bot);
1177 class Block_divspan extends BlockMarkup
1179 var $_re = '<(?im)(?: div|span)(?:[^>]*)?>';
1181 function _match (&$input, $m) {
1182 if (substr($m->match,1,4) == 'span') {
1188 $argstr = substr(trim(substr($m->match,strlen($tag)+1)),0,-1);
1189 $pos = $input->getPos();
1190 $pi = $content = $m->postmatch;
1191 while (!preg_match('/^(.*)\<\/'.$tag.'\>(.*)$/i', $pi, $me)) {
1192 if ($pi != $content)
1193 $content .= "\n$pi";
1194 if (($pi = $input->nextLine()) === false) {
1195 $input->setPos($pos);
1199 if ($pi != $content)
1200 $content .= $me[1]; // prematch
1204 if (strstr($content, "\n"))
1205 $content = TransformText($content);
1207 $content = TransformInline($content);
1212 while (preg_match("/(\w+)=(.+)/", $argstr, $m)) {
1213 $k = $m[1]; $v = $m[2];
1214 if (preg_match("/^\"(.+?)\"(.*)$/", $v, $m)) {
1218 preg_match("/^(\s+)(.*)$/", $v, $m);
1222 if (trim($k) and trim($v)) $args[$k] = $v;
1225 $this->_element = new Block_HtmlElement($tag, $args, $content);
1226 //$this->_element->setTightness($tag == 'span', $tag == 'span');
1229 function _setTightness($top, $bot) {
1230 // Don't tighten user <div|span>
1235 ////////////////////////////////////////////////////////////////
1239 * Transform the text of a page, and return a parse tree.
1241 function TransformTextPre ($text, $markup = 2.0, $basepage=false) {
1242 if (isa($text, 'WikiDB_PageRevision')) {
1244 $text = $rev->getPackedContent();
1245 $markup = $rev->get('markup');
1247 // NEW: default markup is new, to increase stability
1248 if (!empty($markup) && $markup < 2.0) {
1249 $text = ConvertOldMarkup($text);
1252 /*if (!empty($markup) && $markup == 3) {
1253 $text = ConvertFromCreole($text);
1255 // Expand leading tabs.
1256 $text = expand_tabs($text);
1257 //set_time_limit(3);
1258 $output = new WikiText($text);
1264 * Transform the text of a page, and return an XmlContent,
1265 * suitable for printXml()-ing.
1267 function TransformText ($text, $markup = 2.0, $basepage = false) {
1268 $output = TransformTextPre($text, $markup, $basepage);
1270 // This is for immediate consumption.
1271 // We must bind the contents to a base pagename so that
1272 // relative page links can be properly linkified...
1273 return new CacheableMarkup($output->getContent(), $basepage);
1275 return new XmlContent($output->getContent());
1278 // (c-file-style: "gnu")
1282 // c-basic-offset: 4
1283 // c-hanging-comment-ender-p: nil
1284 // indent-tabs-mode: nil