diff -r f0431eb8161e -r 98c052fc3337 includes/wikiformat.php --- a/includes/wikiformat.php Sun Jun 21 00:16:21 2009 -0400 +++ b/includes/wikiformat.php Sun Jun 21 00:20:32 2009 -0400 @@ -13,633 +13,312 @@ */ /** - * Parse structured wiki text and render into arbitrary formats such as XHTML. - * - * PHP versions 4 and 5 + * Framework for parsing and rendering various formats. In Enano by default, this is MediaWiki-style wikitext being + * rendered to XHTML, but this framework allows other formats to be supported as well. * - * @category Text - * @package Text_Wiki - * @author Paul M. Jones - * @license http://www.gnu.org/licenses/lgpl.html - * @version CVS: $Id: Wiki.php,v 1.44 2006/03/02 04:04:59 justinpatrin Exp $ - * @link http://wiki.ciaweb.net/yawiki/index.php?area=Text_Wiki - * - * This code was modified for use in Enano. The Text_Wiki engine is licensed - * under the GNU Lesser General Public License; see - * http://www.gnu.org/licenses/lgpl.html for details. - * + * @package Enano + * @subpackage Content + * @author Dan Fuhry + * @copyright (C) 2009 Enano CMS Project + * @license GNU General Public License, version 2 or later */ -require_once ENANO_ROOT.'/includes/wikiengine/Parse.php'; -require_once ENANO_ROOT.'/includes/wikiengine/Render.php'; - -class Text_Wiki { - - var $rules = array( - 'Prefilter', - 'Delimiter', - 'Code', - 'Function', - 'Html', - 'Raw', - 'Include', - 'Embed', - 'Anchor', - 'Heading', - 'Toc', - 'Horiz', - 'Break', - 'Blockquote', - 'List', - 'Deflist', - 'Table', - 'Image', - 'Phplookup', - 'Center', - 'Newline', - 'Paragraph', - 'Url', - 'Freelink', - 'Interwiki', - 'Wikilink', - 'Colortext', - 'Strong', - 'Bold', - 'Emphasis', - 'Italic', - 'Underline', - 'Tt', - 'Superscript', - 'Subscript', - 'Revise', - 'Tighten' +class Carpenter +{ + /** + * Parser token + * @const string + */ + + const PARSER_TOKEN = "\xFF"; + + /** + * Parsing engine + * @var string + */ + + private $parser = 'mediawiki'; + + /** + * Rendering engine + * @var string + */ + + private $renderer = 'xhtml'; + + /** + * Rendering flags + */ + + public $flags = RENDER_WIKI_DEFAULT; + + /** + * List of rendering rules + * @var array + */ + + private $rules = array( + 'lang', + 'templates', + 'tables', + 'heading', + // note: can't be named list ("list" is a PHP language construct) + 'multilist', + 'bold', + 'italic', + 'underline', + 'externalwithtext', + 'externalnotext', + 'image', + 'internallink', + 'paragraph' ); - - var $disable = array( - 'Html', - 'Include', - 'Embed', - 'Tighten', - 'Image', - 'Wikilink' - ); - - var $parseConf = array(); - - var $renderConf = array( - 'Docbook' => array(), - 'Latex' => array(), - 'Pdf' => array(), - 'Plain' => array(), - 'Rtf' => array(), - 'Xhtml' => array() - ); - - var $formatConf = array( - 'Docbook' => array(), - 'Latex' => array(), - 'Pdf' => array(), - 'Plain' => array(), - 'Rtf' => array(), - 'Xhtml' => array() - ); - var $delim = "\xFF"; - var $tokens = array(); - var $_countRulesTokens = array(); - var $source = ''; - var $parseObj = array(); - var $renderObj = array(); - var $formatObj = array(); - var $path = array( - 'parse' => array(), - 'render' => array() - ); - var $_dirSep = DIRECTORY_SEPARATOR; - function Text_Wiki($rules = null) + + /** + * List of render hooks + * @var array + */ + + private $hooks = array(); + + /* private $rules = array('prefilter', 'delimiter', 'code', 'function', 'html', 'raw', 'include', 'embed', 'anchor', + 'heading', 'toc', 'horiz', 'break', 'blockquote', 'list', 'deflist', 'table', 'image', + 'phplookup', 'center', 'newline', 'paragraph', 'url', 'freelink', 'interwiki', + 'wikilink', 'colortext', 'strong', 'bold', 'emphasis', 'italic', 'underline', 'tt', + 'superscript', 'subscript', 'revise', 'tighten'); */ + + /** + * Render the text! + * @param string Text to render + * @return string + */ + + public function render($text) + { + $parser_class = "Carpenter_Parse_" . ucwords($this->parser); + $renderer_class = "Carpenter_Render_" . ucwords($this->renderer); + + // include files, if we haven't already + if ( !class_exists($parser_class) ) + { + require_once( ENANO_ROOT . "/includes/wikiengine/parse_{$this->parser}.php"); + } + + if ( !class_exists($renderer_class) ) { - if (is_array($rules)) { - $this->rules = $rules; + require_once( ENANO_ROOT . "/includes/wikiengine/render_{$this->renderer}.php"); + } + + $parser = new $parser_class; + $renderer = new $renderer_class; + + // run prehooks + foreach ( $this->hooks as $hook ) + { + if ( $hook['when'] === PO_FIRST ) + { + $text = call_user_func($hook['callback'], $text); + if ( !is_string($text) || empty($text) ) + { + trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING); + // *sigh* + $text = ''; } - - global $plugins; - // This code can be run from the installer, so in some cases $plugins - // isn't initted. (Bug in 1.1.2, fixed for 1.1.3) - if ( is_object($plugins) ) + } + } + + // perform render + foreach ( $this->rules as $rule ) + { + // run prehooks + foreach ( $this->hooks as $hook ) + { + if ( $hook['when'] === PO_BEFORE && $hook['rule'] === $rule ) { - $code = $plugins->setHook('text_wiki_construct'); - foreach ( $code as $cmd ) + $text = call_user_func($hook['callback'], $text); + if ( !is_string($text) || empty($text) ) { - eval($cmd); + trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING); + // *sigh* + $text = ''; } } - - $this->addPath( - 'parse', - $this->fixPath(ENANO_ROOT) . 'includes/wikiengine/Parse/Default/' - ); - $this->addPath( - 'render', - $this->fixPath(ENANO_ROOT) . 'includes/wikiengine/Render/' - ); - - } - - public static function singleton($parser = 'Default', $rules = null) - { - static $only = array(); - if (!isset($only[$parser])) { - $ret = Text_Wiki::factory($parser, $rules); - if (!$ret) { - return $ret; - } - $only[$parser] =& $ret; - } - return $only[$parser]; - } - - public static function factory($parser = 'Default', $rules = null) - { - $d=getcwd(); - chdir(ENANO_ROOT); - - $class = 'Text_Wiki_' . $parser; - $c2 = $parser; - $file = ENANO_ROOT . '/includes/wikiengine/' . str_replace('_', '/', $c2).'.php'; - if (!class_exists($class)) { - $fp = @fopen($file, 'r', true); - if ($fp === false) { - die_semicritical('Wiki formatting engine error', '

Could not find file '.$file.' in include_path

'); - } - fclose($fp); - include_once($file); - if (!class_exists($class)) { - die_semicritical('Wiki formatting engine error', '

Class '.$class.' does not exist after including '.$file.'

'); - } - } - - chdir($d); - - $obj = new $class($rules); - return $obj; - } - - function setParseConf($rule, $arg1, $arg2 = null) - { - $rule = ucwords(strtolower($rule)); - - if (! isset($this->parseConf[$rule])) { - $this->parseConf[$rule] = array(); - } - - if (is_array($arg1)) { - $this->parseConf[$rule] = $arg1; - } else { - $this->parseConf[$rule][$arg1] = $arg2; - } - } - - function getParseConf($rule, $key = null) - { - $rule = ucwords(strtolower($rule)); - - if (! isset($this->parseConf[$rule])) { - return null; - } - - if (is_null($key)) { - return $this->parseConf[$rule]; - } - - if (isset($this->parseConf[$rule][$key])) { - return $this->parseConf[$rule][$key]; - } else { - return null; - } - } - - function setRenderConf($format, $rule, $arg1, $arg2 = null) - { - $format = ucwords(strtolower($format)); - $rule = ucwords(strtolower($rule)); - - if (! isset($this->renderConf[$format])) { - $this->renderConf[$format] = array(); - } - - if (! isset($this->renderConf[$format][$rule])) { - $this->renderConf[$format][$rule] = array(); - } - - if (is_array($arg1)) { - $this->renderConf[$format][$rule] = $arg1; - } else { - $this->renderConf[$format][$rule][$arg1] = $arg2; - } - } - - function getRenderConf($format, $rule, $key = null) - { - $format = ucwords(strtolower($format)); - $rule = ucwords(strtolower($rule)); - - if (! isset($this->renderConf[$format]) || - ! isset($this->renderConf[$format][$rule])) { - return null; - } - - if (is_null($key)) { - return $this->renderConf[$format][$rule]; - } - - if (isset($this->renderConf[$format][$rule][$key])) { - return $this->renderConf[$format][$rule][$key]; - } else { - return null; - } - - } - - function setFormatConf($format, $arg1, $arg2 = null) - { - if (! is_array($this->formatConf[$format])) { - $this->formatConf[$format] = array(); } - - if (is_array($arg1)) { - $this->formatConf[$format] = $arg1; - } else { - $this->formatConf[$format][$arg1] = $arg2; - } - } - - function getFormatConf($format, $key = null) - { - if (! isset($this->formatConf[$format])) { - return null; - } - - if (is_null($key)) { - return $this->formatConf[$format]; - } - - if (isset($this->formatConf[$format][$key])) { - return $this->formatConf[$format][$key]; - } else { - return null; - } - } - - function insertRule($name, $tgt = null) - { - $name = ucwords(strtolower($name)); - if (! is_null($tgt)) { - $tgt = ucwords(strtolower($tgt)); - } - if (in_array($name, $this->rules)) { - return null; - } - - if (! is_null($tgt) && $tgt != '' && - ! in_array($tgt, $this->rules)) { - return false; - } - - if (is_null($tgt)) { - $this->rules[] = $name; - return true; - } - - if ($tgt == '') { - array_unshift($this->rules, $name); - return true; - } - - $tmp = $this->rules; - $this->rules = array(); - - foreach ($tmp as $val) { - $this->rules[] = $val; - if ($val == $tgt) { - $this->rules[] = $name; - } - } - - return true; - } - - function deleteRule($name) - { - $name = ucwords(strtolower($name)); - $key = array_search($name, $this->rules); - if ($key !== false) { - unset($this->rules[$key]); - } - } - - function changeRule($old, $new) - { - $old = ucwords(strtolower($old)); - $new = ucwords(strtolower($new)); - $key = array_search($old, $this->rules); - if ($key !== false) { - $this->deleteRule($new); - $this->rules[$key] = $new; - } - } - - function enableRule($name) - { - $name = ucwords(strtolower($name)); - $key = array_search($name, $this->disable); - if ($key !== false) { - unset($this->disable[$key]); - } - } - - function disableRule($name) - { - $name = ucwords(strtolower($name)); - $key = array_search($name, $this->disable); - if ($key === false) { - $this->disable[] = $name; - } - } - - function transform($text, $format = 'Xhtml') - { - $this->parse($text); - return $this->render($format); - } - - function parse($text) - { - $this->source = $text; - - $this->tokens = array(); - $this->_countRulesTokens = array(); - - foreach ($this->rules as $name) { - if (! in_array($name, $this->disable)) { - $this->loadParseObj($name); - - if (is_object($this->parseObj[$name])) { - $this->parseObj[$name]->parse(); + + // execute rule + $text = $this->perform_render_step($text, $rule, $parser, $renderer); + + // run posthooks + foreach ( $this->hooks as $hook ) + { + if ( $hook['when'] === PO_AFTER && $hook['rule'] === $rule ) + { + $text = call_user_func($hook['callback'], $text); + if ( !is_string($text) || empty($text) ) + { + trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING); + // *sigh* + $text = ''; } - // For debugging - // echo('

' . $name . ':

'.htmlspecialchars($this->source).'
'); } } } - - function render($format = 'Xhtml') + + // run posthooks + foreach ( $this->hooks as $hook ) { - $format = ucwords(strtolower($format)); - - $output = ''; - - $in_delim = false; - - $key = ''; - - $result = $this->loadFormatObj($format); - if ($this->isError($result)) { - return $result; - } - - if (is_object($this->formatObj[$format])) { - $output .= $this->formatObj[$format]->pre(); - } - - foreach (array_keys($this->_countRulesTokens) as $rule) { - $this->loadRenderObj($format, $rule); - } - - $k = strlen($this->source); - for ($i = 0; $i < $k; $i++) { - - $char = $this->source{$i}; - - if ($in_delim) { - - if ($char == $this->delim) { - - $key = (int)$key; - $rule = $this->tokens[$key][0]; - $opts = $this->tokens[$key][1]; - $output .= $this->renderObj[$rule]->token($opts); - $in_delim = false; - - } else { - - $key .= $char; - - } - - } else { - - if ($char == $this->delim) { - $key = ''; - $in_delim = true; - } else { - $output .= $char; - } + if ( $hook['when'] === PO_LAST ) + { + $text = call_user_func($hook['callback'], $text); + if ( !is_string($text) || empty($text) ) + { + trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING); + // *sigh* + $text = ''; } } - - if (is_object($this->formatObj[$format])) { - $output .= $this->formatObj[$format]->post(); - } - - return $output; } - - function getSource() - { - return $this->source; - } - - function getTokens($rules = null) - { - if (is_null($rules)) { - return $this->tokens; - } else { - settype($rules, 'array'); - $result = array(); - foreach ($this->tokens as $key => $val) { - if (in_array($val[0], $rules)) { - $result[$key] = $val; - } - } - return $result; - } - } - - function addToken($rule, $options = array(), $id_only = false) + + return (( defined('ENANO_DEBUG') && isset($_GET['parserdebug']) ) ? '
' . htmlspecialchars($text) . '
' : $text) . "\n\n"; + } + + /** + * Performs a step in the rendering process. + * @param string Text to render + * @param string Rule to execute + * @param object Parser instance + * @param object Renderer instance + * @return string + * @access private + */ + + private function perform_render_step($text, $rule, $parser, $renderer) + { + // First look for a direct function + if ( function_exists("parser_{$this->parser}_{$this->renderer}_{$rule}") ) { - static $id; - if (! isset($id)) { - $id = 0; - } else { - $id ++; - } - - settype($options, 'array'); - - $this->tokens[$id] = array( - 0 => $rule, - 1 => $options - ); - if (!isset($this->_countRulesTokens[$rule])) { - $this->_countRulesTokens[$rule] = 1; - } else { - ++$this->_countRulesTokens[$rule]; - } - - if ($id_only) { - return $id; - } else { - return $this->delim . $id . $this->delim; - } + return call_user_func("parser_{$this->parser}_{$this->renderer}_{$rule}", $text, $this->flags); } - - function setToken($id, $rule, $options = array()) + + // We don't have that, so start looking for other ways or means of doing this + if ( method_exists($parser, $rule) && method_exists($renderer, $rule) ) + { + // Both the parser and render have callbacks they want to use. + $pieces = $parser->$rule($text); + $text = call_user_func(array($renderer, $rule), $text, $pieces); + } + else if ( method_exists($parser, $rule) && !method_exists($renderer, $rule) && isset($renderer->rules[$rule]) ) { - $oldRule = $this->tokens[$id][0]; - $this->tokens[$id] = array( - 0 => $rule, - 1 => $options - ); - if ($rule != $oldRule) { - if (!($this->_countRulesTokens[$oldRule]--)) { - unset($this->_countRulesTokens[$oldRule]); - } - if (!isset($this->_countRulesTokens[$rule])) { - $this->_countRulesTokens[$rule] = 1; - } else { - ++$this->_countRulesTokens[$rule]; - } - } + // The parser has a callback, but the renderer does not + $pieces = $parser->$rule($text); + $text = $this->generic_render($text, $pieces, $renderer->rules[$rule]); } - - function loadParseObj($rule) + else if ( !method_exists($parser, $rule) && isset($parser->rules[$rule]) && method_exists($renderer, $rule) ) { - $rule = ucwords(strtolower($rule)); - $file = $rule . '.php'; - $class = "Text_Wiki_Parse_$rule"; - - if (! class_exists($class)) { - $loc = $this->findFile('parse', $file); - if ($loc) { - include_once $loc; - } else { - $this->parseObj[$rule] = null; - return $this->error( - "Parse rule '$rule' not found" - ); - } - } - - $this->parseObj[$rule] = new $class($this); - + // The parser has no callback, but the renderer does + $text = preg_replace_callback($parser->rules[$rule], array($renderer, $rule), $text); + } + else if ( isset($parser->rules[$rule]) && isset($renderer->rules[$rule]) ) + { + // This is a straight-up regex only rule + $text = preg_replace($parser->rules[$rule], $renderer->rules[$rule], $text); + } + else + { + // Either the renderer or parser does not support this rule, ignore it } - - function loadRenderObj($format, $rule) - { - $format = ucwords(strtolower($format)); - $rule = ucwords(strtolower($rule)); - $file = "$format/$rule.php"; - $class = "Text_Wiki_Render_$format" . "_$rule"; - - if (! class_exists($class)) { - $loc = $this->findFile('render', $file); - if ($loc) { - include_once $loc; - } else { - return $this->error( - "Render rule '$rule' in format '$format' not found" - ); - } - } - - $this->renderObj[$rule] = new $class($this); - } - - function loadFormatObj($format) - { - $format = ucwords(strtolower($format)); - $file = $format . '.php'; - $class = "Text_Wiki_Render_$format"; - - if (! class_exists($class)) { - $loc = $this->findFile('render', $file); - if ($loc) { - include_once $loc; - } else { - return $this->error( - "Rendering format class '$class' not found" - ); - } - } - - $this->formatObj[$format] = new $class($this); - } - - function addPath($type, $dir) + + return $text; + } + + /** + * Generic renderer + * @access protected + */ + + protected function generic_render($text, $pieces, $rule) + { + foreach ( $pieces as $i => $piece ) { - $dir = $this->fixPath($dir); - if (! isset($this->path[$type])) { - $this->path[$type] = array($dir); - } else { - array_unshift($this->path[$type], $dir); + $replacement = $rule; + + // if the piece is an array, replace $1, $2, etc. in the rule with each value in the piece + if ( is_array($piece) ) + { + $j = 0; + foreach ( $piece as $part ) + { + $j++; + $replacement = str_replace(array("\\$j", "\${$j}"), $part, $replacement); } + } + // else, just replace \\1 or $1 in the rule with the piece + else + { + $replacement = str_replace(array("\\1", "\$1"), $piece, $replacement); + } + + $text = str_replace(self::generate_token($i), $replacement, $text); } - - function getPath($type = null) - { - if (is_null($type)) { - return $this->path; - } elseif (! isset($this->path[$type])) { - return array(); - } else { - return $this->path[$type]; - } - } - - function findFile($type, $file) + + return $text; + } + + /** + * Add a hook into the parser. + * @param callback Function to call + * @param int PO_* constant + * @param string If PO_{BEFORE,AFTER} used, rule + */ + + public function hook($callback, $when, $rule = false) + { + if ( !is_int($when) ) + return null; + if ( ($when == PO_BEFORE || $when == PO_AFTER) && !is_string($rule) ) + return null; + if ( ( is_string($callback) && !function_exists($callback) ) || ( is_array($callback) && !method_exists($callback[0], $callback[1]) ) || ( !is_string($callback) && !is_array($callback) ) ) { - $set = $this->getPath($type); - - foreach ($set as $path) { - $fullname = $path . $file; - if (file_exists($fullname) && is_readable($fullname)) { - return $fullname; - } - } - - return false; + trigger_error("Attempt to hook with undefined function/method " . print_r($callback, true), E_USER_ERROR); + return null; } - - function fixPath($path) + + $this->hooks[] = array( + 'callback' => $callback, + 'when' => $when, + 'rule' => $rule + ); + } + + /** + * Generate a token + * @param int Token index + * @return string + * @static + */ + + public static function generate_token($i) + { + return self::PARSER_TOKEN . $i . self::PARSER_TOKEN; + } + + /** + * Tokenize string + * @param string + * @param array Matches + * @return string + * @static + */ + + public static function tokenize($text, $matches) + { + $matches = array_values($matches); + foreach ( $matches as $i => $match ) { - $len = strlen($this->_dirSep); - - if (! empty($path) && - substr($path, -1 * $len, $len) != $this->_dirSep) { - return $path . $this->_dirSep; - } else { - return $path; - } + $text = str_replace_once($match, self::generate_token($i), $text); } - - function &error($message) - { - die($message); - } - - function isError(&$obj) - { - return ( @get_class($obj) == 'PEAR_Error' ); - } + + return $text; + } } -?>