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