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