* * 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'); //////////////////////////////////////////////////////////////// // // 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[^*]*(?<=\S)[*](?!\S)))\ *(?=\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[^*]*(?<=\S)[*](?!\S))|[#])|;(.*):).*?(?=\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); // FIXME: no , , , , or 's allowed // in a
.
        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:   
?>