PageRenderTime 24ms CodeModel.GetById 13ms app.highlight 7ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/wikiformat.php

https://code.google.com/p/enanocms/
PHP | 400 lines | 218 code | 56 blank | 126 comment | 48 complexity | f234b9fd26aa1d57f90fe7dfb204f49d MD5 | raw file
  1<?php
  2
  3/*
  4 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
  5 * Copyright (C) 2006-2009 Dan Fuhry
  6 *
  7 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
  8 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
  9 *
 10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 11 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 12 */
 13
 14/**
 15 * Framework for parsing and rendering various formats. In Enano by default, this is MediaWiki-style wikitext being
 16 * rendered to XHTML, but this framework allows other formats to be supported as well.
 17 *
 18 * @package Enano
 19 * @subpackage Content
 20 * @author Dan Fuhry <dan@enanocms.org>
 21 * @copyright (C) 2009 Enano CMS Project
 22 * @license GNU General Public License, version 2 or later <http://www.gnu.org/licenses/gpl-2.0.html>
 23 */
 24
 25class Carpenter
 26{
 27	/**
 28 	* Parser token
 29 	* @const string
 30 	*/
 31	
 32	const PARSER_TOKEN = "\xFF";
 33	
 34	/**
 35 	* Parsing engine
 36 	* @var string
 37 	*/
 38	
 39	private $parser = 'mediawiki';
 40	
 41	/**
 42 	* Rendering engine
 43 	* @var string
 44 	*/
 45	
 46	private $renderer = 'xhtml';
 47	
 48	/**
 49 	* Rendering flags
 50 	*/
 51	
 52	public $flags = RENDER_WIKI_DEFAULT;
 53	
 54	/**
 55 	* List of rendering rules
 56 	* @var array
 57 	*/
 58	
 59	private $rules = array(
 60			'lang',
 61			'templates',
 62			'blockquote',
 63			'code',
 64			'tables',
 65			'heading',
 66			'hr',
 67			// note: can't be named list ("list" is a PHP language construct)
 68			'multilist',
 69			'bold',
 70			'italic',
 71			'underline',
 72			'image',
 73			'externalnotext',
 74			'externalwithtext',
 75			'mailtowithtext',
 76			'mailtonotext',
 77			'internallink',
 78			'paragraph',
 79			'blockquotepost'
 80		);
 81	
 82	/**
 83 	* List of render hooks
 84 	* @var array
 85 	*/
 86	
 87	private $hooks = array();
 88	
 89	/* private $rules = array('prefilter', 'delimiter', 'code', 'function', 'html', 'raw', 'include', 'embed', 'anchor',
 90 					'heading', 'toc', 'horiz', 'break', 'blockquote', 'list', 'deflist', 'table', 'image',
 91 					'phplookup', 'center', 'newline', 'paragraph', 'url', 'freelink', 'interwiki',
 92 					'wikilink', 'colortext', 'strong', 'bold', 'emphasis', 'italic', 'underline', 'tt',
 93 					'superscript', 'subscript', 'revise', 'tighten'); */
 94	
 95	/**
 96 	* Render the text!
 97 	* @param string Text to render
 98 	* @return string
 99 	*/
100	
101	public function render($text)
102	{
103		$parser_class = "Carpenter_Parse_" . ucwords($this->parser);
104		$renderer_class = "Carpenter_Render_" . ucwords($this->renderer);
105		
106		// empty? (don't remove this. the parser will shit bricks later about rules returning empty strings)
107		if ( trim($text) === '' )
108			return $text;
109		
110		// include files, if we haven't already
111		if ( !class_exists($parser_class) )
112		{
113			require_once( ENANO_ROOT . "/includes/wikiengine/parse_{$this->parser}.php");
114		}
115		
116		if ( !class_exists($renderer_class) )
117		{
118			require_once( ENANO_ROOT . "/includes/wikiengine/render_{$this->renderer}.php");
119		}
120		
121		$parser = new $parser_class;
122		$renderer = new $renderer_class;
123		
124		// run prehooks
125		foreach ( $this->hooks as $hook )
126		{
127			if ( $hook['when'] === PO_FIRST )
128			{
129				$text = call_user_func($hook['callback'], $text);
130				if ( !is_string($text) || empty($text) )
131				{
132					trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING);
133					// *sigh*
134					$text = '';
135				}
136			}
137		}
138		
139		// perform render
140		foreach ( $this->rules as $rule )
141		{
142			// run prehooks
143			foreach ( $this->hooks as $hook )
144			{
145				if ( $hook['when'] === PO_BEFORE && $hook['rule'] === $rule )
146				{
147					$text = call_user_func($hook['callback'], $text);
148					if ( !is_string($text) || empty($text) )
149					{
150						trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING);
151						// *sigh*
152						$text = '';
153					}
154				}
155			}
156			
157			// execute rule
158			$text_before = $text;
159			$text = $this->perform_render_step($text, $rule, $parser, $renderer);
160			if ( empty($text) )
161			{
162				trigger_error("Wikitext was completely empty after rule \"$rule\"; restoring backup", E_USER_WARNING);
163				$text = $text_before;
164			}
165			unset($text_before);
166			
167			// run posthooks
168			foreach ( $this->hooks as $hook )
169			{
170				if ( $hook['when'] === PO_AFTER && $hook['rule'] === $rule )
171				{
172					$text = call_user_func($hook['callback'], $text);
173					if ( !is_string($text) || empty($text) )
174					{
175						trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING);
176						// *sigh*
177						$text = '';
178					}
179				}
180			}
181			
182			RenderMan::tag_strip_push('final', $text, $final_stripdata);
183		}
184		
185		RenderMan::tag_unstrip('final', $text, $final_stripdata);
186		
187		// run posthooks
188		foreach ( $this->hooks as $hook )
189		{
190			if ( $hook['when'] === PO_LAST )
191			{
192				$text = call_user_func($hook['callback'], $text);
193				if ( !is_string($text) || empty($text) )
194				{
195					trigger_error("Hook returned empty/invalid text: " . print_r($hook['callback'], true), E_USER_WARNING);
196					// *sigh*
197					$text = '';
198				}
199			}
200		}
201		
202		return (( defined('ENANO_DEBUG') && isset($_GET['parserdebug']) ) ? '<pre>' . htmlspecialchars($text) . '</pre>' : $text) . "\n\n";
203	}
204	
205	/**
206 	* Performs a step in the rendering process.
207 	* @param string Text to render
208 	* @param string Rule to execute
209 	* @param object Parser instance
210 	* @param object Renderer instance
211 	* @return string
212 	* @access private
213 	*/
214	
215	private function perform_render_step($text, $rule, $parser, $renderer)
216	{
217		// First look for a direct function
218		if ( function_exists("parser_{$this->parser}_{$this->renderer}_{$rule}") )
219		{
220			return call_user_func("parser_{$this->parser}_{$this->renderer}_{$rule}", $text, $this->flags);
221		}
222		
223		// We don't have that, so start looking for other ways or means of doing this
224		if ( method_exists($parser, $rule) && method_exists($renderer, $rule) )
225		{
226			// Both the parser and render have callbacks they want to use.
227			$pieces = $parser->$rule($text);
228			$text = call_user_func(array($renderer, $rule), $text, $pieces);
229		}
230		else if ( method_exists($parser, $rule) && !method_exists($renderer, $rule) && isset($renderer->rules[$rule]) )
231		{
232			// The parser has a callback, but the renderer does not
233			$pieces = $parser->$rule($text);
234			$text = $this->generic_render($text, $pieces, $renderer->rules[$rule]);
235		}
236		else if ( !method_exists($parser, $rule) && isset($parser->rules[$rule]) && method_exists($renderer, $rule) )
237		{
238			// The parser has no callback, but the renderer does
239			$text = preg_replace_callback($parser->rules[$rule], array($renderer, $rule), $text);
240		}
241		else if ( isset($parser->rules[$rule]) && isset($renderer->rules[$rule]) )
242		{
243			// This is a straight-up regex only rule
244			$text = preg_replace($parser->rules[$rule], $renderer->rules[$rule], $text);
245		}
246		else
247		{
248			// Either the renderer or parser does not support this rule, ignore it
249		}
250		
251		return $text;
252	}
253	
254	/**
255 	* Generic renderer
256 	* @access protected
257 	*/
258	
259	protected function generic_render($text, $pieces, $rule)
260	{
261		foreach ( $pieces as $i => $piece )
262		{
263			$replacement = $rule;
264			
265			// if the piece is an array, replace $1, $2, etc. in the rule with each value in the piece
266			if ( is_array($piece) )
267			{
268				$j = 0;
269				foreach ( $piece as $part )
270				{
271					$j++;
272					$replacement = str_replace(array("\\$j", "\${$j}"), $part, $replacement);
273				}
274			}
275			// else, just replace \\1 or $1 in the rule with the piece
276			else
277			{
278				$replacement = str_replace(array("\\1", "\$1"), $piece, $replacement);
279			}
280			
281			$text = str_replace(self::generate_token($i), $replacement, $text);
282		}
283		
284		return $text;
285	}
286	
287	/**
288 	* Add a hook into the parser.
289 	* @param callback Function to call
290 	* @param int PO_* constant
291 	* @param string If PO_{BEFORE,AFTER} used, rule
292 	*/
293	
294	public function hook($callback, $when, $rule = false)
295	{
296		if ( !is_int($when) )
297			return null;
298		if ( ($when == PO_BEFORE || $when == PO_AFTER) && !is_string($rule) )
299			return null;
300		if ( ( is_string($callback) && !function_exists($callback) ) || ( is_array($callback) && !method_exists($callback[0], $callback[1]) ) || ( !is_string($callback) && !is_array($callback) ) )
301		{
302			trigger_error("Attempt to hook with undefined function/method " . print_r($callback, true), E_USER_ERROR);
303			return null;
304		}
305		
306		$this->hooks[] = array(
307				'callback' => $callback,
308				'when'     => $when,
309				'rule'     => $rule
310			);
311	}
312	
313	/**
314 	* Disable a render stage
315 	* @param string stage
316 	* @return null
317 	*/
318	
319	public function disable_rule($rule)
320	{
321		foreach ( $this->rules as $i => $current_rule )
322		{
323			if ( $current_rule === $rule )
324			{
325				unset($this->rules[$i]);
326				return null;
327			}
328		}
329		return null;
330	}
331	
332	/**
333 	* Disables all rules.
334 	* @return null
335 	*/
336	
337	public function disable_all_rules()
338	{
339		$this->rules = array();
340		return null;
341	}
342	
343	/**
344 	* Enables a rule
345 	* @param string rule
346 	* @return null
347 	*/
348 	
349	public function enable_rule($rule)
350	{
351		$this->rules[] = $rule;
352		return null;
353	}
354	
355	/**
356 	* Make a rule exclusive (the only one called)
357 	* @param string stage
358 	* @return null
359 	*/
360	
361	public function exclusive_rule($rule)
362	{
363		if ( is_string($rule) )
364			$this->rules = array($rule);
365		
366		return null;
367	}
368	
369	/**
370 	* Generate a token
371 	* @param int Token index
372 	* @return string
373 	* @static
374 	*/
375	
376	public static function generate_token($i)
377	{
378		return self::PARSER_TOKEN . $i . self::PARSER_TOKEN;
379	}
380	
381	/**
382 	* Tokenize string
383 	* @param string
384 	* @param array Matches
385 	* @return string
386 	* @static
387 	*/
388	
389	public static function tokenize($text, $matches)
390	{
391		$matches = array_values($matches);
392		foreach ( $matches as $i => $match )
393		{
394			$text = str_replace_once($match, self::generate_token($i), $text);
395		}
396		
397		return $text;
398	}
399}
400