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 |