Mereged updates from DokuWiki 38
[sudaraka-org:dokuwiki-mods.git] / inc / parserutils.php
1 <?php
2 /**
3  * Utilities for accessing the parser
4  *
5  * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6  * @author     Harry Fuecks <hfuecks@gmail.com>
7  * @author     Andreas Gohr <andi@splitbrain.org>
8  */
9
10 if(!defined('DOKU_INC')) die('meh.');
11
12 /**
13  * How many pages shall be rendered for getting metadata during one request
14  * at maximum? Note that this limit isn't respected when METADATA_RENDER_UNLIMITED
15  * is passed as render parameter to p_get_metadata.
16  */
17 if (!defined('P_GET_METADATA_RENDER_LIMIT')) define('P_GET_METADATA_RENDER_LIMIT', 5);
18
19 /** Don't render metadata even if it is outdated or doesn't exist */
20 define('METADATA_DONT_RENDER', 0);
21 /**
22  * Render metadata when the page is really newer or the metadata doesn't exist.
23  * Uses just a simple check, but should work pretty well for loading simple
24  * metadata values like the page title and avoids rendering a lot of pages in
25  * one request. The P_GET_METADATA_RENDER_LIMIT is used in this mode.
26  * Use this if it is unlikely that the metadata value you are requesting
27  * does depend e.g. on pages that are included in the current page using
28  * the include plugin (this is very likely the case for the page title, but
29  * not for relation references).
30  */
31 define('METADATA_RENDER_USING_SIMPLE_CACHE', 1);
32 /**
33  * Render metadata using the metadata cache logic. The P_GET_METADATA_RENDER_LIMIT
34  * is used in this mode. Use this mode when you are requesting more complex
35  * metadata. Although this will cause rendering more often it might actually have
36  * the effect that less current metadata is returned as it is more likely than in
37  * the simple cache mode that metadata needs to be rendered for all pages at once
38  * which means that when the metadata for the page is requested that actually needs
39  * to be updated the limit might have been reached already.
40  */
41 define('METADATA_RENDER_USING_CACHE', 2);
42 /**
43  * Render metadata without limiting the number of pages for which metadata is
44  * rendered. Use this mode with care, normally it should only be used in places
45  * like the indexer or in cli scripts where the execution time normally isn't
46  * limited. This can be combined with the simple cache using
47  * METADATA_RENDER_USING_CACHE | METADATA_RENDER_UNLIMITED.
48  */
49 define('METADATA_RENDER_UNLIMITED', 4);
50
51 /**
52  * Returns the parsed Wikitext in XHTML for the given id and revision.
53  *
54  * If $excuse is true an explanation is returned if the file
55  * wasn't found
56  *
57  * @author Andreas Gohr <andi@splitbrain.org>
58  */
59 function p_wiki_xhtml($id, $rev='', $excuse=true){
60     $file = wikiFN($id,$rev);
61     $ret  = '';
62
63     //ensure $id is in global $ID (needed for parsing)
64     global $ID;
65     $keep = $ID;
66     $ID   = $id;
67
68     if($rev){
69         if(@file_exists($file)){
70             $ret = p_render('xhtml',p_get_instructions(io_readWikiPage($file,$id,$rev)),$info); //no caching on old revisions
71         }elseif($excuse){
72             $ret = p_locale_xhtml('norev');
73         }
74     }else{
75         if(@file_exists($file)){
76             $ret = p_cached_output($file,'xhtml',$id);
77         }elseif($excuse){
78             $ret = p_locale_xhtml('newpage');
79         }
80     }
81
82     //restore ID (just in case)
83     $ID = $keep;
84
85     return $ret;
86 }
87
88 /**
89  * Returns starting summary for a page (e.g. the first few
90  * paragraphs), marked up in XHTML.
91  *
92  * If $excuse is true an explanation is returned if the file
93  * wasn't found
94  *
95  * @param string $id wiki page id
96  * @param string $title populated with page title from heading or page id
97  * @param string $rev revision string
98  * @param bool   $excuse if an excuse shall be renderer when no content is found
99  * @return string xhtml code
100  * @deprecated
101  * @author Harry Fuecks <hfuecks@gmail.com>
102  */
103 function p_wiki_xhtml_summary($id, &$title, $rev='', $excuse=true){
104     $file = wikiFN($id,$rev);
105     $ret  = '';
106     $ins  = null;
107
108     //ensure $id is in global $ID (needed for parsing)
109     global $ID;
110     $keep = $ID;
111     $ID   = $id;
112
113     if($rev){
114         if(@file_exists($file)){
115             //no caching on old revisions
116             $ins = p_get_instructions(io_readWikiPage($file,$id,$rev));
117         }elseif($excuse){
118             $ret = p_locale_xhtml('norev');
119             //restore ID (just in case)
120             $ID = $keep;
121             return $ret;
122         }
123
124     }else{
125
126         if(@file_exists($file)){
127             // The XHTML for a summary is not cached so use the instruction cache
128             $ins = p_cached_instructions($file);
129         }elseif($excuse){
130             $ret = p_locale_xhtml('newpage');
131             //restore ID (just in case)
132             $ID = $keep;
133             return $ret;
134         }
135     }
136
137     $ret = p_render('xhtmlsummary',$ins,$info);
138
139     if ( $info['sum_pagetitle'] ) {
140         $title = $info['sum_pagetitle'];
141     } else {
142         $title = $id;
143     }
144
145     $ID = $keep;
146     return $ret;
147 }
148
149 /**
150  * Returns the specified local text in parsed format
151  *
152  * @author Andreas Gohr <andi@splitbrain.org>
153  */
154 function p_locale_xhtml($id){
155     //fetch parsed locale
156     $html = p_cached_output(localeFN($id));
157     return $html;
158 }
159
160 /**
161  *     *** DEPRECATED ***
162  *
163  * use p_cached_output()
164  *
165  * Returns the given file parsed to XHTML
166  *
167  * Uses and creates a cachefile
168  *
169  * @deprecated
170  * @author Andreas Gohr <andi@splitbrain.org>
171  * @todo   rewrite to use mode instead of hardcoded XHTML
172  */
173 function p_cached_xhtml($file){
174     return p_cached_output($file);
175 }
176
177 /**
178  * Returns the given file parsed into the requested output format
179  *
180  * @author Andreas Gohr <andi@splitbrain.org>
181  * @author Chris Smith <chris@jalakai.co.uk>
182  */
183 function p_cached_output($file, $format='xhtml', $id='') {
184     global $conf;
185
186     $cache = new cache_renderer($id, $file, $format);
187     if ($cache->useCache()) {
188         $parsed = $cache->retrieveCache(false);
189         if($conf['allowdebug'] && $format=='xhtml') $parsed .= "\n<!-- cachefile {$cache->cache} used -->\n";
190     } else {
191         $parsed = p_render($format, p_cached_instructions($file,false,$id), $info);
192
193         if ($info['cache']) {
194             $cache->storeCache($parsed);               //save cachefile
195             if($conf['allowdebug'] && $format=='xhtml') $parsed .= "\n<!-- no cachefile used, but created {$cache->cache} -->\n";
196         }else{
197             $cache->removeCache();                     //try to delete cachefile
198             if($conf['allowdebug'] && $format=='xhtml') $parsed .= "\n<!-- no cachefile used, caching forbidden -->\n";
199         }
200     }
201
202     return $parsed;
203 }
204
205 /**
206  * Returns the render instructions for a file
207  *
208  * Uses and creates a serialized cache file
209  *
210  * @author Andreas Gohr <andi@splitbrain.org>
211  */
212 function p_cached_instructions($file,$cacheonly=false,$id='') {
213     static $run = null;
214     if(is_null($run)) $run = array();
215
216     $cache = new cache_instructions($id, $file);
217
218     if ($cacheonly || $cache->useCache() || (isset($run[$file]) && !defined('DOKU_UNITTEST'))) {
219         return $cache->retrieveCache();
220     } else if (@file_exists($file)) {
221         // no cache - do some work
222         $ins = p_get_instructions(io_readWikiPage($file,$id));
223         if ($cache->storeCache($ins)) {
224             $run[$file] = true; // we won't rebuild these instructions in the same run again
225         } else {
226             msg('Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.',-1);
227         }
228         return $ins;
229     }
230
231     return null;
232 }
233
234 /**
235  * turns a page into a list of instructions
236  *
237  * @author Harry Fuecks <hfuecks@gmail.com>
238  * @author Andreas Gohr <andi@splitbrain.org>
239  */
240 function p_get_instructions($text){
241
242     $modes = p_get_parsermodes();
243
244     // Create the parser
245     $Parser = new Doku_Parser();
246
247     // Add the Handler
248     $Parser->Handler = new Doku_Handler();
249
250     //add modes to parser
251     foreach($modes as $mode){
252         $Parser->addMode($mode['mode'],$mode['obj']);
253     }
254
255     // Do the parsing
256     trigger_event('PARSER_WIKITEXT_PREPROCESS', $text);
257     $p = $Parser->parse($text);
258     //  dbg($p);
259     return $p;
260 }
261
262 /**
263  * returns the metadata of a page
264  *
265  * @param string $id The id of the page the metadata should be returned from
266  * @param string $key The key of the metdata value that shall be read (by default everything) - separate hierarchies by " " like "date created"
267  * @param int $render If the page should be rendererd - possible values:
268  *     METADATA_DONT_RENDER, METADATA_RENDER_USING_SIMPLE_CACHE, METADATA_RENDER_USING_CACHE
269  *     METADATA_RENDER_UNLIMITED (also combined with the previous two options),
270  *     default: METADATA_RENDER_USING_CACHE
271  * @return mixed The requested metadata fields
272  *
273  * @author Esther Brunner <esther@kaffeehaus.ch>
274  * @author Michael Hamann <michael@content-space.de>
275  */
276 function p_get_metadata($id, $key='', $render=METADATA_RENDER_USING_CACHE){
277     global $ID;
278     static $render_count = 0;
279     // track pages that have already been rendered in order to avoid rendering the same page
280     // again
281     static $rendered_pages = array();
282
283     // cache the current page
284     // Benchmarking shows the current page's metadata is generally the only page metadata
285     // accessed several times. This may catch a few other pages, but that shouldn't be an issue.
286     $cache = ($ID == $id);
287     $meta = p_read_metadata($id, $cache);
288
289     if (!is_numeric($render)) {
290         if ($render) {
291             $render = METADATA_RENDER_USING_SIMPLE_CACHE;
292         } else {
293             $render = METADATA_DONT_RENDER;
294         }
295     }
296
297     // prevent recursive calls in the cache
298     static $recursion = false;
299     if (!$recursion && $render != METADATA_DONT_RENDER && !isset($rendered_pages[$id])&& page_exists($id)){
300         $recursion = true;
301
302         $cachefile = new cache_renderer($id, wikiFN($id), 'metadata');
303
304         $do_render = false;
305         if ($render & METADATA_RENDER_UNLIMITED || $render_count < P_GET_METADATA_RENDER_LIMIT) {
306             if ($render & METADATA_RENDER_USING_SIMPLE_CACHE) {
307                 $pagefn = wikiFN($id);
308                 $metafn = metaFN($id, '.meta');
309                 if (!@file_exists($metafn) || @filemtime($pagefn) > @filemtime($cachefile->cache)) {
310                     $do_render = true;
311                 }
312             } elseif (!$cachefile->useCache()){
313                 $do_render = true;
314             }
315         }
316         if ($do_render) {
317             if (!defined('DOKU_UNITTEST')) {
318                 ++$render_count;
319                 $rendered_pages[$id] = true;
320             }
321             $old_meta = $meta;
322             $meta = p_render_metadata($id, $meta);
323             // only update the file when the metadata has been changed
324             if ($meta == $old_meta || p_save_metadata($id, $meta)) {
325                 // store a timestamp in order to make sure that the cachefile is touched
326                 // this timestamp is also stored when the meta data is still the same
327                 $cachefile->storeCache(time());
328             } else {
329                 msg('Unable to save metadata file. Hint: disk full; file permissions; safe_mode setting.',-1);
330             }
331         }
332
333         $recursion = false;
334     }
335
336     $val = $meta['current'];
337
338     // filter by $key
339     foreach(preg_split('/\s+/', $key, 2, PREG_SPLIT_NO_EMPTY) as $cur_key) {
340         if (!isset($val[$cur_key])) {
341             return null;
342         }
343         $val = $val[$cur_key];
344     }
345     return $val;
346 }
347
348 /**
349  * sets metadata elements of a page
350  *
351  * @see http://www.dokuwiki.org/devel:metadata#functions_to_get_and_set_metadata
352  *
353  * @param String  $id         is the ID of a wiki page
354  * @param Array   $data       is an array with key ⇒ value pairs to be set in the metadata
355  * @param Boolean $render     whether or not the page metadata should be generated with the renderer
356  * @param Boolean $persistent indicates whether or not the particular metadata value will persist through
357  *                            the next metadata rendering.
358  * @return boolean true on success
359  *
360  * @author Esther Brunner <esther@kaffeehaus.ch>
361  * @author Michael Hamann <michael@content-space.de>
362  */
363 function p_set_metadata($id, $data, $render=false, $persistent=true){
364     if (!is_array($data)) return false;
365
366     global $ID, $METADATA_RENDERERS;
367
368     // if there is currently a renderer change the data in the renderer instead
369     if (isset($METADATA_RENDERERS[$id])) {
370         $orig =& $METADATA_RENDERERS[$id];
371         $meta = $orig;
372     } else {
373         // cache the current page
374         $cache = ($ID == $id);
375         $orig = p_read_metadata($id, $cache);
376
377         // render metadata first?
378         $meta = $render ? p_render_metadata($id, $orig) : $orig;
379     }
380
381     // now add the passed metadata
382     $protected = array('description', 'date', 'contributor');
383     foreach ($data as $key => $value){
384
385         // be careful with sub-arrays of $meta['relation']
386         if ($key == 'relation'){
387
388             foreach ($value as $subkey => $subvalue){
389                 if(isset($meta['current'][$key][$subkey]) && is_array($meta['current'][$key][$subkey])) {
390                     $meta['current'][$key][$subkey] = array_merge($meta['current'][$key][$subkey], (array)$subvalue);
391                 } else {
392                     $meta['current'][$key][$subkey] = $subvalue;
393                 }
394                 if($persistent) {
395                     if(isset($meta['persistent'][$key][$subkey]) && is_array($meta['persistent'][$key][$subkey])) {
396                         $meta['persistent'][$key][$subkey] = array_merge($meta['persistent'][$key][$subkey], (array)$subvalue);
397                     } else {
398                         $meta['persistent'][$key][$subkey] = $subvalue;
399                     }
400                 }
401             }
402
403             // be careful with some senisitive arrays of $meta
404         } elseif (in_array($key, $protected)){
405
406             // these keys, must have subkeys - a legitimate value must be an array
407             if (is_array($value)) {
408                 $meta['current'][$key] = !empty($meta['current'][$key]) ? array_merge((array)$meta['current'][$key],$value) : $value;
409
410                 if ($persistent) {
411                     $meta['persistent'][$key] = !empty($meta['persistent'][$key]) ? array_merge((array)$meta['persistent'][$key],$value) : $value;
412                 }
413             }
414
415             // no special treatment for the rest
416         } else {
417             $meta['current'][$key] = $value;
418             if ($persistent) $meta['persistent'][$key] = $value;
419         }
420     }
421
422     // save only if metadata changed
423     if ($meta == $orig) return true;
424
425     if (isset($METADATA_RENDERERS[$id])) {
426         // set both keys individually as the renderer has references to the individual keys
427         $METADATA_RENDERERS[$id]['current']    = $meta['current'];
428         $METADATA_RENDERERS[$id]['persistent'] = $meta['persistent'];
429         return true;
430     } else {
431         return p_save_metadata($id, $meta);
432     }
433 }
434
435 /**
436  * Purges the non-persistant part of the meta data
437  * used on page deletion
438  *
439  * @author Michael Klier <chi@chimeric.de>
440  */
441 function p_purge_metadata($id) {
442     $meta = p_read_metadata($id);
443     foreach($meta['current'] as $key => $value) {
444         if(is_array($meta[$key])) {
445             $meta['current'][$key] = array();
446         } else {
447             $meta['current'][$key] = '';
448         }
449
450     }
451     return p_save_metadata($id, $meta);
452 }
453
454 /**
455  * read the metadata from source/cache for $id
456  * (internal use only - called by p_get_metadata & p_set_metadata)
457  *
458  * @author   Christopher Smith <chris@jalakai.co.uk>
459  *
460  * @param    string   $id      absolute wiki page id
461  * @param    bool     $cache   whether or not to cache metadata in memory
462  *                             (only use for metadata likely to be accessed several times)
463  *
464  * @return   array             metadata
465  */
466 function p_read_metadata($id,$cache=false) {
467     global $cache_metadata;
468
469     if (isset($cache_metadata[(string)$id])) return $cache_metadata[(string)$id];
470
471     $file = metaFN($id, '.meta');
472     $meta = @file_exists($file) ? unserialize(io_readFile($file, false)) : array('current'=>array(),'persistent'=>array());
473
474     if ($cache) {
475         $cache_metadata[(string)$id] = $meta;
476     }
477
478     return $meta;
479 }
480
481 /**
482  * This is the backend function to save a metadata array to a file
483  *
484  * @param    string   $id      absolute wiki page id
485  * @param    array    $meta    metadata
486  *
487  * @return   bool              success / fail
488  */
489 function p_save_metadata($id, $meta) {
490     // sync cached copies, including $INFO metadata
491     global $cache_metadata, $INFO;
492
493     if (isset($cache_metadata[$id])) $cache_metadata[$id] = $meta;
494     if (!empty($INFO) && ($id == $INFO['id'])) { $INFO['meta'] = $meta['current']; }
495
496     return io_saveFile(metaFN($id, '.meta'), serialize($meta));
497 }
498
499 /**
500  * renders the metadata of a page
501  *
502  * @author Esther Brunner <esther@kaffeehaus.ch>
503  */
504 function p_render_metadata($id, $orig){
505     // make sure the correct ID is in global ID
506     global $ID, $METADATA_RENDERERS;
507
508     // avoid recursive rendering processes for the same id
509     if (isset($METADATA_RENDERERS[$id]))
510         return $orig;
511
512     // store the original metadata in the global $METADATA_RENDERERS so p_set_metadata can use it
513     $METADATA_RENDERERS[$id] =& $orig;
514
515     $keep = $ID;
516     $ID   = $id;
517
518     // add an extra key for the event - to tell event handlers the page whose metadata this is
519     $orig['page'] = $id;
520     $evt = new Doku_Event('PARSER_METADATA_RENDER', $orig);
521     if ($evt->advise_before()) {
522
523         require_once DOKU_INC."inc/parser/metadata.php";
524
525         // get instructions
526         $instructions = p_cached_instructions(wikiFN($id),false,$id);
527         if(is_null($instructions)){
528             $ID = $keep;
529             unset($METADATA_RENDERERS[$id]);
530             return null; // something went wrong with the instructions
531         }
532
533         // set up the renderer
534         $renderer = new Doku_Renderer_metadata();
535         $renderer->meta =& $orig['current'];
536         $renderer->persistent =& $orig['persistent'];
537
538         // loop through the instructions
539         foreach ($instructions as $instruction){
540             // execute the callback against the renderer
541             call_user_func_array(array(&$renderer, $instruction[0]), (array) $instruction[1]);
542         }
543
544         $evt->result = array('current'=>&$renderer->meta,'persistent'=>&$renderer->persistent);
545     }
546     $evt->advise_after();
547
548     // clean up
549     $ID = $keep;
550     unset($METADATA_RENDERERS[$id]);
551     return $evt->result;
552 }
553
554 /**
555  * returns all available parser syntax modes in correct order
556  *
557  * @author Andreas Gohr <andi@splitbrain.org>
558  */
559 function p_get_parsermodes(){
560     global $conf;
561
562     //reuse old data
563     static $modes = null;
564     if($modes != null && !defined('DOKU_UNITTEST')){
565         return $modes;
566     }
567
568     //import parser classes and mode definitions
569     require_once DOKU_INC . 'inc/parser/parser.php';
570
571     // we now collect all syntax modes and their objects, then they will
572     // be sorted and added to the parser in correct order
573     $modes = array();
574
575     // add syntax plugins
576     $pluginlist = plugin_list('syntax');
577     if(count($pluginlist)){
578         global $PARSER_MODES;
579         $obj = null;
580         foreach($pluginlist as $p){
581             /** @var DokuWiki_Syntax_Plugin $obj */
582             if(!$obj =& plugin_load('syntax',$p)) continue; //attempt to load plugin into $obj
583             $PARSER_MODES[$obj->getType()][] = "plugin_$p"; //register mode type
584             //add to modes
585             $modes[] = array(
586                     'sort' => $obj->getSort(),
587                     'mode' => "plugin_$p",
588                     'obj'  => $obj,
589                     );
590             unset($obj); //remove the reference
591         }
592     }
593
594     // add default modes
595     $std_modes = array('listblock','preformatted','notoc','nocache',
596             'header','table','linebreak','footnote','hr',
597             'unformatted','php','html','code','file','quote',
598             'internallink','rss','media','externallink',
599             'emaillink','windowssharelink','eol');
600     if($conf['typography']){
601         $std_modes[] = 'quotes';
602         $std_modes[] = 'multiplyentity';
603     }
604     foreach($std_modes as $m){
605         $class = "Doku_Parser_Mode_$m";
606         $obj   = new $class();
607         $modes[] = array(
608                 'sort' => $obj->getSort(),
609                 'mode' => $m,
610                 'obj'  => $obj
611                 );
612     }
613
614     // add formatting modes
615     $fmt_modes = array('strong','emphasis','underline','monospace',
616             'subscript','superscript','deleted');
617     foreach($fmt_modes as $m){
618         $obj   = new Doku_Parser_Mode_formatting($m);
619         $modes[] = array(
620                 'sort' => $obj->getSort(),
621                 'mode' => $m,
622                 'obj'  => $obj
623                 );
624     }
625
626     // add modes which need files
627     $obj     = new Doku_Parser_Mode_smiley(array_keys(getSmileys()));
628     $modes[] = array('sort' => $obj->getSort(), 'mode' => 'smiley','obj'  => $obj );
629     $obj     = new Doku_Parser_Mode_acronym(array_keys(getAcronyms()));
630     $modes[] = array('sort' => $obj->getSort(), 'mode' => 'acronym','obj'  => $obj );
631     $obj     = new Doku_Parser_Mode_entity(array_keys(getEntities()));
632     $modes[] = array('sort' => $obj->getSort(), 'mode' => 'entity','obj'  => $obj );
633
634     // add optional camelcase mode
635     if($conf['camelcase']){
636         $obj     = new Doku_Parser_Mode_camelcaselink();
637         $modes[] = array('sort' => $obj->getSort(), 'mode' => 'camelcaselink','obj'  => $obj );
638     }
639
640     //sort modes
641     usort($modes,'p_sort_modes');
642
643     return $modes;
644 }
645
646 /**
647  * Callback function for usort
648  *
649  * @author Andreas Gohr <andi@splitbrain.org>
650  */
651 function p_sort_modes($a, $b){
652     if($a['sort'] == $b['sort']) return 0;
653     return ($a['sort'] < $b['sort']) ? -1 : 1;
654 }
655
656 /**
657  * Renders a list of instruction to the specified output mode
658  *
659  * In the $info array is information from the renderer returned
660  *
661  * @author Harry Fuecks <hfuecks@gmail.com>
662  * @author Andreas Gohr <andi@splitbrain.org>
663  */
664 function p_render($mode,$instructions,&$info){
665     if(is_null($instructions)) return '';
666
667     $Renderer =& p_get_renderer($mode);
668     if (is_null($Renderer)) return null;
669
670     $Renderer->reset();
671
672     $Renderer->smileys = getSmileys();
673     $Renderer->entities = getEntities();
674     $Renderer->acronyms = getAcronyms();
675     $Renderer->interwiki = getInterwiki();
676
677     // Loop through the instructions
678     foreach ( $instructions as $instruction ) {
679         // Execute the callback against the Renderer
680         if(method_exists($Renderer, $instruction[0])){
681             call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
682         }
683     }
684
685     //set info array
686     $info = $Renderer->info;
687
688     // Post process and return the output
689     $data = array($mode,& $Renderer->doc);
690     trigger_event('RENDERER_CONTENT_POSTPROCESS',$data);
691     return $Renderer->doc;
692 }
693
694 /**
695  * @param $mode string Mode of the renderer to get
696  * @return null|Doku_Renderer The renderer
697  */
698 function & p_get_renderer($mode) {
699     /** @var Doku_Plugin_Controller $plugin_controller */
700     global $conf, $plugin_controller;
701
702     $rname = !empty($conf['renderer_'.$mode]) ? $conf['renderer_'.$mode] : $mode;
703     $rclass = "Doku_Renderer_$rname";
704
705     if( class_exists($rclass) ) {
706         return new $rclass();
707     }
708
709     // try default renderer first:
710     $file = DOKU_INC."inc/parser/$rname.php";
711     if(@file_exists($file)){
712         require_once $file;
713
714         if ( !class_exists($rclass) ) {
715             trigger_error("Unable to resolve render class $rclass",E_USER_WARNING);
716             msg("Renderer '$rname' for $mode not valid",-1);
717             return null;
718         }
719         $Renderer = new $rclass();
720     }else{
721         // Maybe a plugin/component is available?
722         list($plugin, $component) = $plugin_controller->_splitName($rname);
723         if (!$plugin_controller->isdisabled($plugin)){
724             $Renderer =& $plugin_controller->load('renderer',$rname);
725         }
726
727         if(!isset($Renderer) || is_null($Renderer)){
728             msg("No renderer '$rname' found for mode '$mode'",-1);
729             return null;
730         }
731     }
732
733     return $Renderer;
734 }
735
736 /**
737  * Gets the first heading from a file
738  *
739  * @param   string   $id       dokuwiki page id
740  * @param   int      $render   rerender if first heading not known
741  *                             default: METADATA_RENDER_USING_SIMPLE_CACHE
742  *                             Possible values: METADATA_DONT_RENDER,
743  *                                              METADATA_RENDER_USING_SIMPLE_CACHE,
744  *                                              METADATA_RENDER_USING_CACHE,
745  *                                              METADATA_RENDER_UNLIMITED
746  *
747  * @return string|null The first heading
748  * @author Andreas Gohr <andi@splitbrain.org>
749  * @author Michael Hamann <michael@content-space.de>
750  */
751 function p_get_first_heading($id, $render=METADATA_RENDER_USING_SIMPLE_CACHE){
752     return p_get_metadata(cleanID($id),'title',$render);
753 }
754
755 /**
756  * Wrapper for GeSHi Code Highlighter, provides caching of its output
757  *
758  * @param  string   $code       source code to be highlighted
759  * @param  string   $language   language to provide highlighting
760  * @param  string   $wrapper    html element to wrap the returned highlighted text
761  *
762  * @return string xhtml code
763  * @author Christopher Smith <chris@jalakai.co.uk>
764  * @author Andreas Gohr <andi@splitbrain.org>
765  */
766 function p_xhtml_cached_geshi($code, $language, $wrapper='pre') {
767     global $conf, $config_cascade, $INPUT;
768     $language = strtolower($language);
769
770     // remove any leading or trailing blank lines
771     $code = preg_replace('/^\s*?\n|\s*?\n$/','',$code);
772
773     $cache = getCacheName($language.$code,".code");
774     $ctime = @filemtime($cache);
775     if($ctime && !$INPUT->bool('purge') &&
776             $ctime > filemtime(DOKU_INC.'inc/geshi.php') &&                 // geshi changed
777             $ctime > @filemtime(DOKU_INC.'inc/geshi/'.$language.'.php') &&  // language syntax definition changed
778             $ctime > filemtime(reset($config_cascade['main']['default']))){ // dokuwiki changed
779         $highlighted_code = io_readFile($cache, false);
780
781     } else {
782
783         $geshi = new GeSHi($code, $language, DOKU_INC . 'inc/geshi');
784         $geshi->set_encoding('utf-8');
785         $geshi->enable_classes();
786                 $geshi->set_overall_class('geshi');
787         $geshi->set_header_type(GESHI_HEADER_PRE_VALID);
788         $geshi->set_link_target($conf['target']['extern']);
789
790                 if(in_array($language, array('cli'))) $geshi->enable_line_numbers(GESHI_NO_LINE_NUMBERS);
791                 else $geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
792
793         // remove GeSHi's wrapper element (we'll replace it with our own later)
794         // we need to use a GeSHi wrapper to avoid <BR> throughout the highlighted text
795         //$highlighted_code = trim(preg_replace('!^<pre[^>]*>|</pre>$!','',$geshi->parse_code()),"\n\r");
796                 $highlighted_code = $geshi->parse_code() . '<style type="text/css">' . $geshi->get_stylesheet() . '</style>';
797         io_saveFile($cache,$highlighted_code);
798     }
799
800     // add a wrapper element if required
801     //if ($wrapper) {
802         //return "<$wrapper class=\"code $language\">$highlighted_code</$wrapper>";
803     //} else {
804         return $highlighted_code;
805     //}
806 }
807