includes/wikiformat.php
author Dan Fuhry <dan@enanocms.org>
Wed, 29 Dec 2010 13:25:32 -0500
changeset 1332 12286b3ee214
parent 1306 e00489a30c72
permissions -rw-r--r--
Added some more hooks to the page editing pipeline. It should now be possible to add controls to the page editor, send the data from them out to the server, and process them on the server side.

<?php

/*
 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
 * Copyright (C) 2006-2009 Dan Fuhry
 *
 * This program is Free Software; you can redistribute 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.
 *
 * This program 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 details.
 */

/**
 * 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.
 *
 * @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>
 */

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',
			'blockquote',
			'code',
			'tables',
			'heading',
			'hr',
			// note: can't be named list ("list" is a PHP language construct)
			'multilist',
			'bold',
			'italic',
			'underline',
			'image',
			'externalnotext',
			'externalwithtext',
			'mailtowithtext',
			'mailtonotext',
			'internallink',
			'paragraph',
			'blockquotepost'
		);
	
	/**
 	* 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);
		
		// empty? (don't remove this. the parser will shit bricks later about rules returning empty strings)
		if ( trim($text) === '' )
			return $text;
		
		// 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) )
		{
			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 = '';
				}
			}
		}
		
		// perform render
		foreach ( $this->rules as $rule )
		{
			// run prehooks
			foreach ( $this->hooks as $hook )
			{
				if ( $hook['when'] === PO_BEFORE && $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 = '';
					}
				}
			}
			
			// execute rule
			$text_before = $text;
			$text = $this->perform_render_step($text, $rule, $parser, $renderer);
			if ( empty($text) )
			{
				trigger_error("Wikitext was completely empty after rule \"$rule\"; restoring backup", E_USER_WARNING);
				$text = $text_before;
			}
			unset($text_before);
			
			// 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 = '';
					}
				}
			}
			
			RenderMan::tag_strip_push('final', $text, $final_stripdata);
		}
		
		RenderMan::tag_unstrip('final', $text, $final_stripdata);
		
		// run posthooks
		foreach ( $this->hooks as $hook )
		{
			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 = '';
				}
			}
		}
		
		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}") )
		{
			return call_user_func("parser_{$this->parser}_{$this->renderer}_{$rule}", $text, $this->flags);
		}
		
		// 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]) )
		{
			// The parser has a callback, but the renderer does not
			$pieces = $parser->$rule($text);
			$text = $this->generic_render($text, $pieces, $renderer->rules[$rule]);
		}
		else if ( !method_exists($parser, $rule) && isset($parser->rules[$rule]) && method_exists($renderer, $rule) )
		{
			// 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
		}
		
		return $text;
	}
	
	/**
 	* Generic renderer
 	* @access protected
 	*/
	
	protected function generic_render($text, $pieces, $rule)
	{
		foreach ( $pieces as $i => $piece )
		{
			$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);
		}
		
		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) ) )
		{
			trigger_error("Attempt to hook with undefined function/method " . print_r($callback, true), E_USER_ERROR);
			return null;
		}
		
		$this->hooks[] = array(
				'callback' => $callback,
				'when'     => $when,
				'rule'     => $rule
			);
	}
	
	/**
 	* Disable a render stage
 	* @param string stage
 	* @return null
 	*/
	
	public function disable_rule($rule)
	{
		foreach ( $this->rules as $i => $current_rule )
		{
			if ( $current_rule === $rule )
			{
				unset($this->rules[$i]);
				return null;
			}
		}
		return null;
	}
	
	/**
 	* Disables all rules.
 	* @return null
 	*/
	
	public function disable_all_rules()
	{
		$this->rules = array();
		return null;
	}
	
	/**
 	* Enables a rule
 	* @param string rule
 	* @return null
 	*/
 	
	public function enable_rule($rule)
	{
		$this->rules[] = $rule;
		return null;
	}
	
	/**
 	* Make a rule exclusive (the only one called)
 	* @param string stage
 	* @return null
 	*/
	
	public function exclusive_rule($rule)
	{
		if ( is_string($rule) )
			$this->rules = array($rule);
		
		return null;
	}
	
	/**
 	* 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 )
		{
			$text = str_replace_once($match, self::generate_token($i), $text);
		}
		
		return $text;
	}
}