--- 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 <pmjones@php.net>
- * @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 <dan@enanocms.org>
+ * @copyright (C) 2009 Enano CMS Project
+ * @license GNU General Public License, version 2 or later <http://www.gnu.org/licenses/gpl-2.0.html>
*/
-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', '<p>Could not find file '.$file.' in include_path</p>');
- }
- fclose($fp);
- include_once($file);
- if (!class_exists($class)) {
- die_semicritical('Wiki formatting engine error', '<p>Class '.$class.' does not exist after including '.$file.'</p>');
- }
- }
-
- 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('<p>' . $name . ':</p><pre>'.htmlspecialchars($this->source).'</pre>');
}
}
}
-
- 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']) ) ? '<pre>' . htmlspecialchars($text) . '</pre>' : $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;
+ }
}
-?>