*
* This file is part of PhpWiki.
*
* PhpWiki is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* PhpWiki is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PhpWiki; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
require_once('lib/HtmlElement.php');
require_once('lib/InlineParser.php');
require_once('lib/transform.php');
/*
class InlineTransform
extends WikiTransform {
function InlineTransform() {
global $WikiNameRegexp, $AllowedProtocols, $InterWikiLinkRegexp;
$this->WikiTransform();
// register functions
// functions are applied in order of registering
$this->register(WT_SIMPLE_MARKUP, 'wtm_plugin_link');
$this->register(WT_TOKENIZER, 'wtt_doublebrackets', '\[\[');
//$this->register(WT_TOKENIZER, 'wtt_footnotes', '^\[\d+\]');
//$this->register(WT_TOKENIZER, 'wtt_footnoterefs', '\[\d+\]');
$this->register(WT_TOKENIZER, 'wtt_bracketlinks', '\[.+?\]');
$this->register(WT_TOKENIZER, 'wtt_urls',
"!?\b($AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]");
if (function_exists('wtt_interwikilinks')) {
$this->register(WT_TOKENIZER, 'wtt_interwikilinks',
pcre_fix_posix_classes("!?(?register(WT_TOKENIZER, 'wtt_bumpylinks', "!?$WikiNameRegexp");
$this->register(WT_SIMPLE_MARKUP, 'wtm_htmlchars');
$this->register(WT_SIMPLE_MARKUP, 'wtm_linebreak');
$this->register(WT_SIMPLE_MARKUP, 'wtm_bold_italics');
}
};
function TransformInline ($text) {
// The old transform code does funny things with trailing
// white space....
$trfm = new InlineTransform;
preg_match('/\s*$/', $text, $m);
$tail = $m[0];
// This "\n" -> "\r" hackage is to fool the old transform code
// into continuing italics across lines.
$in = str_replace("\n", "\r", $text);
$out = preg_replace('/\s*$/', '', AsXML($trfm->do_transform('', array($in))));
$out = str_replace("\r", "\n", $out);
$out .= $tail;
// DEBUGGING
if (false && $out != $text) {
echo(" IN
'" . htmlspecialchars($text) . "'
\n");
echo("OUT '" . htmlspecialchars($out) . "'
\n");
}
return new RawXml($out);
}
*/
////////////////////////////////////////////////////////////////
//
//
define("BLOCK_NEVER_TIGHTEN", 0);
define("BLOCK_NOTIGHTEN_AFTER", 1);
define("BLOCK_NOTIGHTEN_BEFORE", 2);
define("BLOCK_NOTIGHTEN_EITHER", 3);
/**
* FIXME:
* Still to do:
* (old-style) tables
*/
class BlockParser {
function parse (&$input, $tighten_mode = BLOCK_NEVER_TIGHTEN) {
$content = HTML();
for ($block = BlockParser::_nextBlock($input); $block; $block = $nextBlock) {
while ($nextBlock = BlockParser::_nextBlock($input)) {
// Attempt to merge current with following block.
if (! $block->merge($nextBlock))
break; // can't merge
}
$content->pushContent($block->finish($tighten_mode));
}
return $content;
}
function _nextBlock (&$input) {
global $Block_BlockTypes;
if ($input->atEof())
return false;
foreach ($Block_BlockTypes as $type) {
if ($m = $input->match($type->_re)) {
BlockParser::_debug('>', get_class($type), $input);
$block = $type;
$block->_followsBreak = $input->atBreak();
if (!$block->_parse($input, $m)) {
BlockParser::_debug('[', "_parse failed", $input);
continue;
}
$block->_preceedsBreak = $input->eatSpace();
BlockParser::_debug('<', get_class($type), $input);
return $block;
}
}
if ($input->getDepth() == 0) {
// We should never get here.
//preg_match('/.*/A', substr($this->_text, $this->_pos), $m);// get first line
trigger_error("Couldn't match block: '".rawurlencode($m[0])."'", E_USER_NOTICE);
}
//FIXME:$this->_debug("no match");
return false;
}
function _debug ($tab, $msg, $input) {
return ;
$tab = str_repeat($tab, $input->getDepth() + 1);
printXML(HTML::div("$tab $msg: at: '",
HTML::tt($input->where()),
"'"));
}
}
class BlockParser_Match {
function BlockParser_Match ($match_data) {
$this->_m = $match_data;
}
function getPrefix () {
return $this->_m[1];
}
function getMatch ($n = 0) {
$text = $this->_m[$n + 2];
//if (preg_match('/\n./s', $text)) {
$prefix = $this->getPrefix();
$text = str_replace("\n$prefix", "\n", $text);
//}
return $text;
}
}
class BlockParser_Input {
function BlockParser_Input ($text) {
$this->_text = $text;
$this->_pos = 0;
$this->_depth = 0;
// Expand leading tabs.
// FIXME: do this better.
//
// We want to ensure the only characters matching \s are ' ' and "\n".
//
$this->_text = preg_replace('/(?![ \n])\s/', ' ', $this->_text);
assert(!preg_match('/(?![ \n])\s/', $this->_text));
if (!preg_match('/\n$/', $this->_text))
$this->_text .= "\n";
$this->_set_prefix ('');
$this->_atBreak = false;
$this->eatSpace();
}
function _set_prefix ($prefix, $next_prefix = false) {
if ($next_prefix === false)
$next_prefix = $prefix;
$this->_prefix = $prefix;
$this->_next_prefix = $next_prefix;
$this->_regexp_cache = array();
$blank = "(:?$prefix)?\s*\n";
$this->_blank_pat = "/$blank/A";
$this->_eof_pat = "/\\Z|(?!$blank|${prefix}.)/A";
}
function atEof () {
return preg_match($this->_eof_pat, substr($this->_text, $this->_pos));
}
function match ($regexp) {
$cache = &$this->_regexp_cache;
if (!isset($cache[$regexp])) {
// Fix up any '^'s in pattern (add our prefix)
$re = preg_replace('/(?_next_prefix, $regexp);
// Fix any match backreferences (like '\1').
$re = preg_replace('/(?<= [^ \\\\ ] [ \\\\ ] )( \\d+ )/ex', "'\\1' + 2", $re);
$re = "/(" . $this->_prefix . ")($re)/Am";
$cache[$regexp] = $re;
}
else
$re = $cache[$regexp];
if (preg_match($re, substr($this->_text, $this->_pos), $m)) {
return new BlockParser_Match($m);
}
return false;
}
function accept ($match) {
$text = $match->_m[0];
assert(substr($this->_text, $this->_pos, strlen($text)) == $text);
$this->_pos += strlen($text);
// FIXME:
assert(preg_match("/\n$/", $text));
if ($this->_next_prefix != $this->_prefix)
$this->_set_prefix($this->_next_prefix);
$this->_atBreak = false;
$this->eatSpace();
}
/**
* Consume blank lines.
*
* @return bool True if any blank lines where comsumed.
*/
function eatSpace () {
if (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
$this->_pos += strlen($m[0]);
if ($this->_next_prefix != $this->_prefix)
$this->_set_prefix($this->_next_prefix);
$this->_atBreak = true;
while (preg_match($this->_blank_pat, substr($this->_text, $this->_pos), $m)) {
$this->_pos += strlen($m[0]);
}
}
return $this->_atBreak;
}
function atBreak () {
return $this->_atBreak;
}
function getDepth () {
return $this->_depth;
}
// DEBUGGING
function where () {
if (($m = $this->match('.*\n')))
return sprintf('[%s]%s', $m->getPrefix(), $m->getMatch());
return '???';
}
function subBlock ($initial_prefix, $subsequent_prefix = false) {
if ($subsequent_prefix === false)
$subsequent_prefix = $initial_prefix;
return new BlockParser_InputSubBlock ($this, $initial_prefix, $subsequent_prefix);
}
}
class BlockParser_InputSubBlock extends BlockParser_Input
{
function BlockParser_InputSubBlock (&$block, $initial_prefix, $subsequent_prefix) {
$this->_text = &$block->_text;
$this->_pos = &$block->_pos;
$this->_atBreak = &$block->_atBreak;
$this->_depth = $block->_depth + 1;
$this->_set_prefix($block->_prefix . $initial_prefix,
$block->_next_prefix . $subsequent_prefix);
}
}
class Block {
var $_tag;
var $_attr = false;
var $_re;
var $_followsBreak = false;
var $_preceedsBreak = false;
var $_content = array();
function _parse (&$input, $match) {
trigger_error('pure virtual', E_USER_ERROR);
}
function _pushContent ($c) {
if (!is_array($c))
$c = func_get_args();
foreach ($c as $x)
$this->_content[] = $x;
}
function isTerminal () {
return true;
}
function merge ($followingBlock) {
return false;
}
function finish (/*$tighten*/) {
return new HtmlElement($this->_tag, $this->_attr, $this->_content);
}
}
class CompoundBlock extends Block
{
function isTerminal () {
return false;
}
}
class Block_blockquote extends CompoundBlock
{
var $_tag ='blockquote';
var $_depth;
var $_re = '\ +(?=\S)';
function _parse (&$input, $m) {
$indent = $m->getMatch();
$this->_depth = strlen($indent);
$this->_content[] = BlockParser::parse($input->subBlock($indent),
BLOCK_NOTIGHTEN_EITHER);
return true;
}
function merge ($nextBlock) {
if (get_class($nextBlock) != 'block_blockquote')
return false;
assert ($nextBlock->_depth < $this->_depth);
$content = $nextBlock->_content;
array_unshift($content, $this->finish());
$this->_content = $content;
return true;
}
}
class Block_list extends CompoundBlock
{
//var $_tag = 'ol' or 'ul';
var $_re = '\ {0,4}([*+#]|-(?!-)|o(?=\ ))\ *(?=\S)';
function _parse (&$input, $m) {
// A list as the first content in a list is not allowed.
// E.g.:
// * * Item
// Should markup as ,
// not .
//
if (preg_match('/[-*o+#;]\s*$/', $m->getPrefix()))
return false;
$prefix = $m->getMatch();
$leader = preg_quote($prefix, '/');
$indent = sprintf("\\ {%d}", strlen($prefix));
$bullet = $m->getMatch(1);
$this->_tag = $bullet == '#' ? 'ol' : 'ul';
$text = $input->subBlock($leader, $indent);
$content = BlockParser::parse($text, BLOCK_NOTIGHTEN_AFTER);
$this->_pushContent(HTML::li(false, $content));
return true;
}
function merge ($nextBlock) {
if (!isa($nextBlock, 'Block_list') || $this->_tag != $nextBlock->_tag)
return false;
$this->_pushContent($nextBlock->_content);
return true;
}
}
class Block_dl extends Block_list
{
var $_tag = 'dl';
var $_re = '(\ {0,4})([^\s!].*):\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
// 1-------12--------2 3-----3
function _parse (&$input, $m) {
$term = TransformInline(rtrim($m->getMatch(2)));
$indent = $m->getMatch(3);
$input->accept($m);
$this->_pushContent(HTML::dt(false, $term),
HTML::dd(false,
BlockParser::parse($input->subBlock($indent),
BLOCK_NOTIGHTEN_AFTER)));
return true;
}
}
class Block_table_dl_defn extends XmlContent
{
var $nrows;
var $ncols;
function Block_table_dl_defn ($term, $defn) {
$this->XmlContent();
if (!is_array($defn))
$defn = $defn->getContent();
$this->_ncols = $this->_ComputeNcols($defn);
$this->_nrows = 0;
foreach ($defn as $item) {
if ($this->_IsASubtable($item))
$this->_addSubtable($item);
else
$this->_addToRow($item);
}
$this->_flushRow();
$th = HTML::th($term);
if ($this->_nrows > 1)
$th->setAttr('rowspan', $this->_nrows);
$this->_setTerm($th);
}
function _addToRow ($item) {
if (empty($this->_accum)) {
$this->_accum = HTML::td();
if ($this->_ncols > 2)
$this->_accum->setAttr('colspan', $this->_ncols - 1);
}
$this->_accum->pushContent($item);
}
function _flushRow () {
if (!empty($this->_accum)) {
$this->pushContent(HTML::tr($this->_accum));
$this->_accum = false;
$this->_nrows++;
}
}
function _addSubtable ($table) {
$this->_flushRow();
foreach ($table->getContent() as $subdef) {
$this->pushContent($subdef);
$this->_nrows += $subdef->nrows();
}
}
function _setTerm ($th) {
$first_row = &$this->_content[0];
if (isa($first_row, 'Block_table_dl_defn'))
$first_row->_setTerm($th);
else
$first_row->unshiftContent($th);
}
function _ComputeNcols ($defn) {
$ncols = 2;
foreach ($defn as $item) {
if ($this->_IsASubtable($item)) {
$row = $this->_FirstDefn($item);
$ncols = max($ncols, $row->ncols() + 1);
}
}
return $ncols;
}
function _IsASubtable ($item) {
return isa($item, 'HtmlElement')
&& $item->getTag() == 'table'
&& $item->getAttr('class') == 'wiki-dl-table';
}
function _FirstDefn ($subtable) {
$defs = $subtable->getContent();
return $defs[0];
}
function ncols () {
return $this->_ncols;
}
function nrows () {
return $this->_nrows;
}
function setWidth ($ncols) {
assert($ncols >= $this->_ncols);
if ($ncols <= $this->_ncols)
return;
$rows = &$this->_content;
for ($i = 0; $i < count($rows); $i++) {
$row = &$rows[$i];
if (isa($row, 'Block_table_dl_defn'))
$row->setWidth($ncols - 1);
else {
$n = count($row->_content);
$lastcol = &$row->_content[$n - 1];
$lastcol->setAttr('colspan', $ncols - 1);
}
}
}
}
class Block_table_dl extends Block_list
{
var $_tag = 'table';
var $_attr = array('class' => 'wiki-dl-table',
'border' => 2, // FIXME: CSS?
'cellspacing' => 0,
'cellpadding' => 6);
var $_re = '(\ {0,4})((?![\s!]).*)?[|]\s*?\n(?=(?:\s*^)+(\1\ +)\S)';
// 1-------12-----------2 3-----3
function _parse (&$input, $m) {
$term = TransformInline(rtrim($m->getMatch(2)));
$indent = $m->getMatch(3);
$input->accept($m);
$defn = BlockParser::parse($input->subBlock($indent),
BLOCK_NOTIGHTEN_AFTER);
$this->_pushContent(new Block_table_dl_defn($term, $defn));
return true;
}
function finish () {
$defs = &$this->_content;
$ncols = 0;
foreach ($defs as $defn)
$ncols = max($ncols, $defn->ncols());
foreach ($defs as $key => $defn)
$defs[$key]->setWidth($ncols);
return parent::finish();
}
}
class Block_oldlists extends Block_list
{
//var $_tag = 'ol', 'ul', or 'dl';
var $_re = '(?:([*#])|;(.*):).*?(?=\S)';
// 1----1 2--2
function _parse (&$input, $m) {
if (!preg_match('/[*#;]*$/A', $m->getPrefix()))
return false;
$prefix = $m->getMatch();
$leader = preg_quote($prefix, '/');
$oldindent = '[*#;](?=[#*]|;.*:.*?\S)';
$newindent = sprintf('\\ {%d}', strlen($prefix));
$indent = "(?:$oldindent|$newindent)";
$bullet = $m->getMatch(1);
if ($bullet) {
$this->_tag = $bullet == '*' ? 'ul' : 'ol';
$item = HTML::li();
}
else {
$this->_tag = 'dl';
$term = trim($m->getMatch(2));
if ($term)
$this->_pushContent(HTML::dt(false, TransformInline($term)));
$item = HTML::dd();
}
$item->pushContent(BlockParser::parse($input->subBlock($leader, $indent),
BLOCK_NOTIGHTEN_AFTER));
$this->_pushContent($item);
return true;
}
}
class Block_pre extends Block
{
var $_tag = 'pre';
var $_re = '<(pre|verbatim)>(.*?(?:\s*\n^.*?)*?)(?\s*?\n';
// 1------------1 2------------------2
function _parse (&$input, $m) {
$input->accept($m);
$text = $m->getMatch(2);
$tag = $m->getMatch(1);
if ($tag == 'pre')
$text = TransformInline($text);
$this->_pushContent($text);
return true;
}
}
class Block_plugin extends Block
{
var $_tag = 'div';
var $_attr = array('class' => 'plugin');
var $_re = '<\?plugin(?:-form)?.*?(?:\n^.*?)*?(?\s*?\n';
function _parse (&$input, $m) {
global $request;
$loader = new WikiPluginLoader;
$input->accept($m);
$this->_pushContent($loader->expandPI($m->getMatch(), $request));
return true;
}
}
class Block_hr extends Block
{
var $_tag = 'hr';
var $_re = '-{4,}\s*?\n';
function _parse (&$input, $m) {
$input->accept($m);
return true;
}
}
class Block_heading extends Block
{
var $_re = '(!{1,3})(.*)\n';
function _parse (&$input, $m) {
$input->accept($m);
$this->_tag = "h" . (5 - strlen($m->getMatch(1)));
$this->_pushContent(TransformInline(trim($m->getMatch(2))));
return true;
}
}
class Block_p extends Block
{
var $_tag = 'p';
var $_re = '\S.*\n';
function _parse (&$input, $m) {
$this->_text = $m->getMatch();
$input->accept($m);
return true;
}
function merge ($nextBlock) {
if ($this->_preceedsBreak || get_class($nextBlock) != 'block_p')
return false;
$this->_text .= $nextBlock->_text;
$this->_preceedsBreak = $nextBlock->_preceedsBreak;
return true;
}
function finish ($tighten) {
$this->_pushContent(TransformInline(trim($this->_text)));
if ($this->_followsBreak && ($tighten & BLOCK_NOTIGHTEN_AFTER) != 0)
$tighten = 0;
elseif ($this->_preceedsBreak && ($tighten & BLOCK_NOTIGHTEN_BEFORE) != 0)
$tighten = 0;
return $tighten ? $this->_content : parent::finish();
}
}
class Block_email_blockquote extends CompoundBlock
{
// FIXME: move CSS to CSS.
var $_tag ='blockquote';
var $_attr = array('style' => 'border-left-width: medium; border-left-color: #0f0; border-left-style: ridge; padding-left: 1em; margin-left: 0em; margin-right: 0em;');
var $_depth;
var $_re = '>\ ?';
function _parse (&$input, $m) {
$prefix = $m->getMatch();
$indent = "(?:$prefix|>(?=\s*?\n))";
$this->_content[] = BlockParser::parse($input->subBlock($indent),
BLOCK_NOTIGHTEN_EITHER);
return true;
}
}
////////////////////////////////////////////////////////////////
//
$GLOBALS['Block_BlockTypes'] = array(new Block_oldlists,
new Block_list,
new Block_dl,
new Block_table_dl,
new Block_blockquote,
new Block_heading,
new Block_hr,
new Block_pre,
new Block_email_blockquote,
new Block_plugin,
new Block_p);
// FIXME: This is temporary, too...
function NewTransform ($text) {
set_time_limit(2);
// Expand leading tabs.
// FIXME: do this better. also move it...
$text = preg_replace('/^\ *[^\ \S\n][^\S\n]*/me', "str_repeat(' ', strlen('\\0'))", $text);
assert(!preg_match('/^\ *\t/', $text));
$input = new BlockParser_Input($text);
return BlockParser::parse($input);
}
// FIXME: bad name
function TransformRevision ($revision) {
if ($revision->get('markup') == 'new') {
return NewTransform($revision->getPackedContent());
}
else {
return do_transform($revision->getContent());
}
}
// (c-file-style: "gnu")
// Local Variables:
// mode: php
// tab-width: 8
// c-basic-offset: 4
// c-hanging-comment-ender-p: nil
// indent-tabs-mode: nil
// End:
?>