xref: /dokuwiki/inc/parser/xhtml.php (revision 220c87090a31e0013775f1d7841f92f48c978882)
1<?php
2/**
3 * Renderer for XHTML output
4 *
5 * @author Harry Fuecks <hfuecks@gmail.com>
6 * @author Andreas Gohr <andi@splitbrain.org>
7 */
8
9if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
10
11if ( !defined('DOKU_LF') ) {
12    // Some whitespace to help View > Source
13    define ('DOKU_LF',"\n");
14}
15
16if ( !defined('DOKU_TAB') ) {
17    // Some whitespace to help View > Source
18    define ('DOKU_TAB',"\t");
19}
20
21require_once DOKU_INC . 'inc/parser/renderer.php';
22
23/**
24 * The Renderer
25 */
26class Doku_Renderer_xhtml extends Doku_Renderer {
27
28    // @access public
29    var $doc = '';        // will contain the whole document
30    var $toc = array();   // will contain the Table of Contents
31
32
33    var $headers = array();
34
35    var $footnotes = array();
36
37    var $acronyms = array();
38    var $smileys = array();
39    var $badwords = array();
40    var $entities = array();
41    var $interwiki = array();
42
43    var $lastsec = 0;
44
45    var $store = '';
46
47    function document_start() {
48        //reset some internals
49        $this->toc     = array();
50        $this->headers = array();
51    }
52
53    function document_end() {
54        // add button for last section if any and more than one
55        if($this->lastsec > 1) $this->_secedit($this->lastsec,'');
56
57        if ( count ($this->footnotes) > 0 ) {
58            $this->doc .= '<div class="footnotes">'.DOKU_LF;
59
60            $id = 0;
61            foreach ( $this->footnotes as $footnote ) {
62                $id++;   // the number of the current footnote
63
64                // check its not a placeholder that indicates actual footnote text is elsewhere
65                if (substr($footnote, 0, 5) != "@@FNT") {
66
67                    // open the footnote and set the anchor and backlink
68                    $this->doc .= '<div class="fn">';
69                    $this->doc .= '<a href="#fnt'.$id.'" id="fn'.$id.'" name="fn'.$id.'" class="fn_bot">';
70                    $this->doc .= $id.')</a> '.DOKU_LF;
71
72                    // get any other footnotes that use the same markup
73                    $alt = array_keys($this->footnotes, "@@FNT$id");
74
75                    if (count($alt)) {
76                      foreach ($alt as $ref) {
77                        // set anchor and backlink for the other footnotes
78                        $this->doc .= ', <a href="#fnt'.($ref+1).'" id="fn'.($ref+1).'" name="fn'.($ref+1).'" class="fn_bot">';
79                        $this->doc .= ($ref+1).')</a> '.DOKU_LF;
80                      }
81                    }
82
83                    // add footnote markup and close this footnote
84                    $this->doc .= $footnote;
85                    $this->doc .= '</div>' . DOKU_LF;
86                }
87            }
88            $this->doc .= '</div>'.DOKU_LF;
89        }
90
91        // prepend the TOC
92        if($this->info['toc']){
93            $this->doc = $this->render_TOC().$this->doc;
94        }
95    }
96
97    /**
98     * Return the TOC rendered to XHTML
99     *
100     * @author Andreas Gohr <andi@splitbrain.org>
101     */
102    function render_TOC(){
103        if(count($this->toc) < 3) return '';
104        global $lang;
105        $out  = '<div class="toc">'.DOKU_LF;
106        $out .= '<div class="tocheader toctoggle" id="toc__header">';
107        $out .= $lang['toc'];
108        $out .= '</div>'.DOKU_LF;
109        $out .= '<div id="toc__inside">'.DOKU_LF;
110        $out .= html_buildlist($this->toc,'toc',array($this,'_tocitem'));
111        $out .= '</div>'.DOKU_LF.'</div>'.DOKU_LF;
112        return $out;
113    }
114
115    /**
116     * Callback for html_buildlist
117     */
118    function _tocitem($item){
119        return '<span class="li"><a href="#'.$item['hid'].'" class="toc">'.
120               $this->_xmlEntities($item['title']).'</a></span>';
121    }
122
123/** FIXME deprecated
124
125    function toc_open() {
126        global $lang;
127        $this->doc .= '<div class="toc">'.DOKU_LF;
128        $this->doc .= '<div class="tocheader toctoggle" id="toc__header">';
129        $this->doc .= $lang['toc'];
130        $this->doc .= '</div>'.DOKU_LF;
131        $this->doc .= '<div id="toc__inside">'.DOKU_LF;
132    }
133
134    function tocbranch_open($level) {
135        $this->doc .= '<ul class="toc">'.DOKU_LF;
136    }
137
138    function tocitem_open($level, $empty = FALSE) {
139        if ( !$empty ) {
140            $this->doc .= '<li class="level'.$level.'">';
141        } else {
142            $this->doc .= '<li class="clear">';
143        }
144    }
145
146    function tocelement($level, $title) {
147        $this->doc .= '<span class="li"><a href="#'.$this->_headerToLink($title).'" class="toc">';
148        $this->doc .= $this->_xmlEntities($title);
149        $this->doc .= '</a></span>';
150    }
151
152    function tocitem_close($level) {
153        $this->doc .= '</li>'.DOKU_LF;
154    }
155
156    function tocbranch_close($level) {
157        $this->doc .= '</ul>'.DOKU_LF;
158    }
159
160    function toc_close() {
161        $this->doc .= '</div>'.DOKU_LF.'</div>'.DOKU_LF;
162    }
163
164*/
165
166    function header($text, $level, $pos) {
167        global $conf;
168        //handle section editing
169        if($level <= $conf['maxseclevel']){
170            // add button for last section if any
171            if($this->lastsec) $this->_secedit($this->lastsec,$pos-1);
172            // remember current position
173            $this->lastsec = $pos;
174        }
175
176        // create a unique header id
177        $hid = $this->_headerToLink($text,'true');
178
179        //handle TOC
180        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){
181            // the TOC is one of our standard ul list arrays ;-)
182            $this->toc[] = array( 'hid'   => $hid,
183                                  'title' => $text,
184                                  'type'  => 'ul',
185                                  'level' => $level);
186        }
187
188        // write the header
189        $this->doc .= DOKU_LF.'<h'.$level.'><a name="'.$hid.'" id="'.$hid.'">';
190        $this->doc .= $this->_xmlEntities($text);
191        $this->doc .= "</a></h$level>".DOKU_LF;
192    }
193
194    function section_open($level) {
195        $this->doc .= "<div class=\"level$level\">".DOKU_LF;
196    }
197
198    function section_close() {
199        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
200    }
201
202    function cdata($text) {
203        $this->doc .= $this->_xmlEntities($text);
204    }
205
206    function p_open() {
207        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
208    }
209
210    function p_close() {
211        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
212    }
213
214    function linebreak() {
215        $this->doc .= '<br/>'.DOKU_LF;
216    }
217
218    function hr() {
219        $this->doc .= '<hr />'.DOKU_LF;
220    }
221
222    function strong_open() {
223        $this->doc .= '<strong>';
224    }
225
226    function strong_close() {
227        $this->doc .= '</strong>';
228    }
229
230    function emphasis_open() {
231        $this->doc .= '<em>';
232    }
233
234    function emphasis_close() {
235        $this->doc .= '</em>';
236    }
237
238    function underline_open() {
239        $this->doc .= '<em class="u">';
240    }
241
242    function underline_close() {
243        $this->doc .= '</em>';
244    }
245
246    function monospace_open() {
247        $this->doc .= '<code>';
248    }
249
250    function monospace_close() {
251        $this->doc .= '</code>';
252    }
253
254    function subscript_open() {
255        $this->doc .= '<sub>';
256    }
257
258    function subscript_close() {
259        $this->doc .= '</sub>';
260    }
261
262    function superscript_open() {
263        $this->doc .= '<sup>';
264    }
265
266    function superscript_close() {
267        $this->doc .= '</sup>';
268    }
269
270    function deleted_open() {
271        $this->doc .= '<del>';
272    }
273
274    function deleted_close() {
275        $this->doc .= '</del>';
276    }
277
278    /**
279     * Callback for footnote start syntax
280     *
281     * All following content will go to the footnote instead of
282     * the document. To achieve this the previous rendered content
283     * is moved to $store and $doc is cleared
284     *
285     * @author Andreas Gohr <andi@splitbrain.org>
286     */
287    function footnote_open() {
288
289        // move current content to store and record footnote
290        $this->store = $this->doc;
291        $this->doc   = '';
292    }
293
294    /**
295     * Callback for footnote end syntax
296     *
297     * All rendered content is moved to the $footnotes array and the old
298     * content is restored from $store again
299     *
300     * @author Andreas Gohr
301     */
302    function footnote_close() {
303
304        // recover footnote into the stack and restore old content
305        $footnote = $this->doc;
306        $this->doc = $this->store;
307        $this->store = '';
308
309        // check to see if this footnote has been seen before
310        $i = array_search($footnote, $this->footnotes);
311
312        if ($i === false) {
313            // its a new footnote, add it to the $footnotes array
314            $id = count($this->footnotes)+1;
315            $this->footnotes[count($this->footnotes)] = $footnote;
316        } else {
317            // seen this one before, translate the index to an id and save a placeholder
318            $i++;
319            $id = count($this->footnotes)+1;
320            $this->footnotes[count($this->footnotes)] = "@@FNT".($i);
321        }
322
323        // output the footnote reference and link, incl. onmouseover for insitu footnote popup
324        $this->doc .= '<a href="#fn'.$id.'" name="fnt'.$id.'" id="fnt'.$id.'" class="fn_top" onmouseover="fnt(\''.$id.'\', this, event);">'.$id.')</a>';
325    }
326
327    function listu_open() {
328        $this->doc .= '<ul>'.DOKU_LF;
329    }
330
331    function listu_close() {
332        $this->doc .= '</ul>'.DOKU_LF;
333    }
334
335    function listo_open() {
336        $this->doc .= '<ol>'.DOKU_LF;
337    }
338
339    function listo_close() {
340        $this->doc .= '</ol>'.DOKU_LF;
341    }
342
343    function listitem_open($level) {
344        $this->doc .= '<li class="level'.$level.'">';
345    }
346
347    function listitem_close() {
348        $this->doc .= '</li>'.DOKU_LF;
349    }
350
351    function listcontent_open() {
352        $this->doc .= '<div class="li">';
353    }
354
355    function listcontent_close() {
356        $this->doc .= '</div>'.DOKU_LF;
357    }
358
359    function unformatted($text) {
360        $this->doc .= $this->_xmlEntities($text);
361    }
362
363    /**
364     * Execute PHP code if allowed
365     *
366     * @author Andreas Gohr <andi@splitbrain.org>
367     */
368    function php($text) {
369        global $conf;
370        if($conf['phpok']){
371            ob_start();
372            eval($text);
373            $this->doc .= ob_get_contents();
374            ob_end_clean();
375        }else{
376            $this->file($text);
377        }
378    }
379
380    /**
381     * Insert HTML if allowed
382     *
383     * @author Andreas Gohr <andi@splitbrain.org>
384     */
385    function html($text) {
386        global $conf;
387        if($conf['htmlok']){
388          $this->doc .= $text;
389        }else{
390          $this->file($text);
391        }
392    }
393
394    function preformatted($text) {
395        $this->doc .= '<pre class="code">' . $this->_xmlEntities($text) . '</pre>'. DOKU_LF;
396    }
397
398    function file($text) {
399        $this->doc .= '<pre class="file">' . $this->_xmlEntities($text). '</pre>'. DOKU_LF;
400    }
401
402    function quote_open() {
403        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
404    }
405
406    function quote_close() {
407        $this->doc .= '</div></blockquote>'.DOKU_LF;
408    }
409
410    /**
411     * Callback for code text
412     *
413     * Uses GeSHi to highlight language syntax
414     *
415     * @author Andreas Gohr <andi@splitbrain.org>
416     */
417    function code($text, $language = NULL) {
418        global $conf;
419
420        if ( is_null($language) ) {
421            $this->preformatted($text);
422        } else {
423            //strip leading blank line
424            $text = preg_replace('/^\s*?\n/','',$text);
425            // Handle with Geshi here
426            require_once(DOKU_INC . 'inc/geshi.php');
427            $geshi = new GeSHi($text, strtolower($language), DOKU_INC . 'inc/geshi');
428            $geshi->set_encoding('utf-8');
429            $geshi->enable_classes();
430            $geshi->set_header_type(GESHI_HEADER_PRE);
431            $geshi->set_overall_class("code $language");
432            $geshi->set_link_target($conf['target']['extern']);
433
434            $text = $geshi->parse_code();
435            $this->doc .= $text;
436        }
437    }
438
439    function acronym($acronym) {
440
441        if ( array_key_exists($acronym, $this->acronyms) ) {
442
443            $title = $this->_xmlEntities($this->acronyms[$acronym]);
444
445            $this->doc .= '<acronym title="'.$title
446                .'">'.$this->_xmlEntities($acronym).'</acronym>';
447
448        } else {
449            $this->doc .= $this->_xmlEntities($acronym);
450        }
451    }
452
453    function smiley($smiley) {
454        if ( array_key_exists($smiley, $this->smileys) ) {
455            $title = $this->_xmlEntities($this->smileys[$smiley]);
456            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
457                '" class="middle" alt="'.
458                    $this->_xmlEntities($smiley).'" />';
459        } else {
460            $this->doc .= $this->_xmlEntities($smiley);
461        }
462    }
463
464    /*
465    * not used
466    function wordblock($word) {
467        if ( array_key_exists($word, $this->badwords) ) {
468            $this->doc .= '** BLEEP **';
469        } else {
470            $this->doc .= $this->_xmlEntities($word);
471        }
472    }
473    */
474
475    function entity($entity) {
476        if ( array_key_exists($entity, $this->entities) ) {
477            $this->doc .= $this->entities[$entity];
478        } else {
479            $this->doc .= $this->_xmlEntities($entity);
480        }
481    }
482
483    function multiplyentity($x, $y) {
484        $this->doc .= "$x&times;$y";
485    }
486
487    function singlequoteopening() {
488        $this->doc .= "&lsquo;";
489    }
490
491    function singlequoteclosing() {
492        $this->doc .= "&rsquo;";
493    }
494
495    function doublequoteopening() {
496        $this->doc .= "&ldquo;";
497    }
498
499    function doublequoteclosing() {
500        $this->doc .= "&rdquo;";
501    }
502
503    /**
504    */
505    function camelcaselink($link) {
506      $this->internallink($link,$link);
507    }
508
509
510    function locallink($hash, $name = NULL){
511        global $ID;
512        $name  = $this->_getLinkTitle($name, $hash, $isImage);
513        $hash  = $this->_headerToLink($hash);
514        $title = $ID.' &crarr;';
515        $this->doc .= '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
516        $this->doc .= $name;
517        $this->doc .= '</a>';
518    }
519
520    /**
521     * Render an internal Wiki Link
522     *
523     * $search and $returnonly are not for the renderer but are used
524     * elsewhere - no need to implement them in other renderers
525     *
526     * @author Andreas Gohr <andi@splitbrain.org>
527     */
528    function internallink($id, $name = NULL, $search=NULL,$returnonly=false) {
529        global $conf;
530        global $ID;
531        // default name is based on $id as given
532        $default = $this->_simpleTitle($id);
533        // now first resolve and clean up the $id
534        resolve_pageid(getNS($ID),$id,$exists);
535        $name = $this->_getLinkTitle($name, $default, $isImage, $id);
536        if ( !$isImage ) {
537            if ( $exists ) {
538                $class='wikilink1';
539            } else {
540                $class='wikilink2';
541            }
542        } else {
543            $class='media';
544        }
545
546        //keep hash anchor
547        list($id,$hash) = split('#',$id,2);
548
549        //prepare for formating
550        $link['target'] = $conf['target']['wiki'];
551        $link['style']  = '';
552        $link['pre']    = '';
553        $link['suf']    = '';
554        // highlight link to current page
555        if ($id == $ID) {
556            $link['pre']    = '<span class="curid">';
557            $link['suf']    = '</span>';
558        }
559        $link['more']   = '';
560        $link['class']  = $class;
561        $link['url']    = wl($id);
562        $link['name']   = $name;
563        $link['title']  = $id;
564        //add search string
565        if($search){
566            ($conf['userewrite']) ? $link['url'].='?s=' : $link['url'].='&amp;s=';
567            $link['url'] .= rawurlencode($search);
568        }
569
570        //keep hash
571        if($hash) $link['url'].='#'.$hash;
572
573        //output formatted
574        if($returnonly){
575            return $this->_formatLink($link);
576        }else{
577            $this->doc .= $this->_formatLink($link);
578        }
579    }
580
581    function externallink($url, $name = NULL) {
582        global $conf;
583
584        $name = $this->_getLinkTitle($name, $url, $isImage);
585
586        // add protocol on simple short URLs
587        if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')) $url = 'ftp://'.$url;
588        if(substr($url,0,3) == 'www') $url = 'http://'.$url;
589
590        if ( !$isImage ) {
591            $class='urlextern';
592        } else {
593            $class='media';
594        }
595
596        //prepare for formating
597        $link['target'] = $conf['target']['extern'];
598        $link['style']  = '';
599        $link['pre']    = '';
600        $link['suf']    = '';
601        $link['more']   = '';
602        $link['class']  = $class;
603        $link['url']    = $url;
604        $link['name']   = $name;
605        $link['title']  = $this->_xmlEntities($url);
606        if($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
607
608        //output formatted
609        $this->doc .= $this->_formatLink($link);
610    }
611
612    /**
613    */
614    function interwikilink($match, $name = NULL, $wikiName, $wikiUri) {
615        global $conf;
616
617        $link = array();
618        $link['target'] = $conf['target']['interwiki'];
619        $link['pre']    = '';
620        $link['suf']    = '';
621        $link['more']   = '';
622        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
623
624        //get interwiki URL
625        if ( isset($this->interwiki[$wikiName]) ) {
626            $url = $this->interwiki[$wikiName];
627        } else {
628            // Default to Google I'm feeling lucky
629            $url = 'http://www.google.com/search?q={URL}&amp;btnI=lucky';
630            $wikiName = 'go';
631        }
632
633        if ( !$isImage ) {
634            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$wikiName);
635            $link['class'] = "interwiki iw_$class";
636        } else {
637            $link['class'] = 'media';
638        }
639
640        //do we stay at the same server? Use local target
641        if( strpos($url,DOKU_URL) === 0 ){
642            $link['target'] = $conf['target']['wiki'];
643        }
644
645        //split into hash and url part
646        list($wikiUri,$hash) = explode('#',$wikiUri,2);
647
648        //replace placeholder
649        if(preg_match('#\{(URL|NAME|SCHEME|HOST|PORT|PATH|QUERY)\}#',$url)){
650            //use placeholders
651            $url = str_replace('{URL}',rawurlencode($wikiUri),$url);
652            $url = str_replace('{NAME}',$wikiUri,$url);
653            $parsed = parse_url($wikiUri);
654            if(!$parsed['port']) $parsed['port'] = 80;
655            $url = str_replace('{SCHEME}',$parsed['scheme'],$url);
656            $url = str_replace('{HOST}',$parsed['host'],$url);
657            $url = str_replace('{PORT}',$parsed['port'],$url);
658            $url = str_replace('{PATH}',$parsed['path'],$url);
659            $url = str_replace('{QUERY}',$parsed['query'],$url);
660            $link['url'] = $url;
661        }else{
662            //default
663            $link['url'] = $url.rawurlencode($wikiUri);
664        }
665        if($hash) $link['url'] .= '#'.rawurlencode($hash);
666
667        $link['title'] = htmlspecialchars($link['url']);
668
669        //output formatted
670        $this->doc .= $this->_formatLink($link);
671    }
672
673    /**
674     */
675    function windowssharelink($url, $name = NULL) {
676        global $conf;
677        global $lang;
678        //simple setup
679        $link['target'] = $conf['target']['windows'];
680        $link['pre']    = '';
681        $link['suf']   = '';
682        $link['style']  = '';
683        //Display error on browsers other than IE
684        $link['more'] = 'onclick="if(document.all == null){alert(\''.
685                        $this->_xmlEntities($lang['nosmblinks'],ENT_QUOTES).
686                        '\');}" onkeypress="if(document.all == null){alert(\''.
687                        $this->_xmlEntities($lang['nosmblinks'],ENT_QUOTES).'\');}"';
688
689        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
690        if ( !$isImage ) {
691            $link['class'] = 'windows';
692        } else {
693            $link['class'] = 'media';
694        }
695
696
697        $link['title'] = $this->_xmlEntities($url);
698        $url = str_replace('\\','/',$url);
699        $url = 'file:///'.$url;
700        $link['url'] = $url;
701
702        //output formatted
703        $this->doc .= $this->_formatLink($link);
704    }
705
706    function emaillink($address, $name = NULL) {
707        global $conf;
708        //simple setup
709        $link = array();
710        $link['target'] = '';
711        $link['pre']    = '';
712        $link['suf']   = '';
713        $link['style']  = '';
714        $link['more']   = '';
715
716        //we just test for image here - we need to encode the title our self
717        $this->_getLinkTitle($name, $address, $isImage);
718        if ( !$isImage ) {
719            $link['class']='mail JSnocheck';
720        } else {
721            $link['class']='media JSnocheck';
722        }
723
724        $address = $this->_xmlEntities($address);
725        $address = obfuscate($address);
726        $title   = $address;
727        if(empty($name)){
728            $name = $address;
729        }else{
730            $name = $this->_xmlEntities($name);
731        }
732
733        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
734
735        $link['url']   = 'mailto:'.$address;
736        $link['name']  = $name;
737        $link['title'] = $title;
738
739        //output formatted
740        $this->doc .= $this->_formatLink($link);
741    }
742
743    function internalmedia ($src, $title=NULL, $align=NULL, $width=NULL,
744                            $height=NULL, $cache=NULL, $linking=NULL) {
745        global $conf;
746        global $ID;
747        resolve_mediaid(getNS($ID),$src, $exists);
748
749        $link = array();
750        $link['class']  = 'media';
751        $link['style']  = '';
752        $link['pre']    = '';
753        $link['suf']    = '';
754        $link['more']   = '';
755        $link['target'] = $conf['target']['media'];
756
757        $link['title']  = $this->_xmlEntities($src);
758        list($ext,$mime) = mimetype($src);
759        if(substr($mime,0,5) == 'image'){
760             $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct'));
761         }elseif($mime == 'application/x-shockwave-flash'){
762             // don't link flash movies
763             $noLink = TRUE;
764         }else{
765             // add file icons
766             $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
767             $link['class'] .= ' mediafile mf_'.$class;
768             $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true);
769         }
770         $link['name']   = $this->_media ($src, $title, $align, $width, $height, $cache);
771
772         //output formatted
773         if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
774         else $this->doc .= $this->_formatLink($link);
775    }
776
777    /**
778     * @todo don't add link for flash
779     */
780    function externalmedia ($src, $title=NULL, $align=NULL, $width=NULL,
781                            $height=NULL, $cache=NULL, $linking=NULL) {
782        global $conf;
783
784        $link = array();
785        $link['class']  = 'media';
786        $link['style']  = '';
787        $link['pre']    = '';
788        $link['suf']    = '';
789        $link['more']   = '';
790        $link['target'] = $conf['target']['media'];
791
792        $link['title']  = $this->_xmlEntities($src);
793        $link['url']    = ml($src,array('cache'=>$cache));
794        $link['name']   = $this->_media ($src, $title, $align, $width, $height, $cache);
795
796
797        list($ext,$mime) = mimetype($src);
798        if(substr($mime,0,5) == 'image'){
799             // link only jpeg images
800             // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = TRUE;
801        }elseif($mime == 'application/x-shockwave-flash'){
802             // don't link flash movies
803             $noLink = TRUE;
804        }else{
805             // add file icons
806             $link['class'] .= ' mediafile mf_'.$ext;
807         }
808
809        //output formatted
810        if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
811        else $this->doc .= $this->_formatLink($link);
812    }
813
814    /**
815     * Renders an RSS feed using Magpie
816     *
817     * @author Andreas Gohr <andi@splitbrain.org>
818     */
819    function rss ($url){
820        global $lang;
821        define('MAGPIE_CACHE_ON', false); //we do our own caching
822        define('MAGPIE_DIR', DOKU_INC.'inc/magpie/');
823        define('MAGPIE_OUTPUT_ENCODING','UTF-8'); //return all feeds as UTF-8
824        require_once(MAGPIE_DIR.'/rss_fetch.inc');
825
826        //disable warning while fetching
827        $elvl = error_reporting(E_ERROR);
828        $rss  = fetch_rss($url);
829        error_reporting($elvl);
830
831        $this->doc .= '<ul class="rss">';
832        if($rss){
833            foreach ($rss->items as $item ) {
834                $this->doc .= '<li>';
835                $this->externallink($item['link'],$item['title']);
836                $this->doc .= '</li>';
837            }
838        }else{
839            $this->doc .= '<li>';
840            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
841            $this->externallink($url);
842            $this->doc .= '</li>';
843        }
844        $this->doc .= '</ul>';
845    }
846
847    // $numrows not yet implemented
848    function table_open($maxcols = NULL, $numrows = NULL){
849        $this->doc .= '<table class="inline">'.DOKU_LF;
850    }
851
852    function table_close(){
853        $this->doc .= '</table>'.DOKU_LF.'<br />'.DOKU_LF;
854    }
855
856    function tablerow_open(){
857        $this->doc .= DOKU_TAB . '<tr>' . DOKU_LF . DOKU_TAB . DOKU_TAB;
858    }
859
860    function tablerow_close(){
861        $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF;
862    }
863
864    function tableheader_open($colspan = 1, $align = NULL){
865        $this->doc .= '<th';
866        if ( !is_null($align) ) {
867            $this->doc .= ' class="'.$align.'align"';
868        }
869        if ( $colspan > 1 ) {
870            $this->doc .= ' colspan="'.$colspan.'"';
871        }
872        $this->doc .= '>';
873    }
874
875    function tableheader_close(){
876        $this->doc .= '</th>';
877    }
878
879    function tablecell_open($colspan = 1, $align = NULL){
880        $this->doc .= '<td';
881        if ( !is_null($align) ) {
882            $this->doc .= ' class="'.$align.'align"';
883        }
884        if ( $colspan > 1 ) {
885            $this->doc .= ' colspan="'.$colspan.'"';
886        }
887        $this->doc .= '>';
888    }
889
890    function tablecell_close(){
891        $this->doc .= '</td>';
892    }
893
894    //----------------------------------------------------------
895    // Utils
896
897    /**
898     * Build a link
899     *
900     * Assembles all parts defined in $link returns HTML for the link
901     *
902     * @author Andreas Gohr <andi@splitbrain.org>
903     */
904    function _formatLink($link){
905        //make sure the url is XHTML compliant (skip mailto)
906        if(substr($link['url'],0,7) != 'mailto:'){
907            $link['url'] = str_replace('&','&amp;',$link['url']);
908            $link['url'] = str_replace('&amp;amp;','&amp;',$link['url']);
909        }
910        //remove double encodings in titles
911        $link['title'] = str_replace('&amp;amp;','&amp;',$link['title']);
912
913        // be sure there are no bad chars in url or title
914        // (we can't do this for name because it can contain an img tag)
915        $link['url']   = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22'));
916        $link['title'] = strtr($link['title'],array('>'=>'&gt;','<'=>'&lt;','"'=>'&quot;'));
917
918        $ret  = '';
919        $ret .= $link['pre'];
920        $ret .= '<a href="'.$link['url'].'"';
921        if($link['class'])  $ret .= ' class="'.$link['class'].'"';
922        if($link['target']) $ret .= ' target="'.$link['target'].'"';
923        if($link['title'])  $ret .= ' title="'.$link['title'].'"';
924        if($link['style'])  $ret .= ' style="'.$link['style'].'"';
925        if($link['more'])   $ret .= ' '.$link['more'];
926        $ret .= '>';
927        $ret .= $link['name'];
928        $ret .= '</a>';
929        $ret .= $link['suf'];
930        return $ret;
931    }
932
933    /**
934     * Removes any Namespace from the given name but keeps
935     * casing and special chars
936     *
937     * @author Andreas Gohr <andi@splitbrain.org>
938     */
939    function _simpleTitle($name){
940        global $conf;
941
942        if($conf['useslash']){
943            $nssep = '[:;/]';
944        }else{
945            $nssep = '[:;]';
946        }
947        $name = preg_replace('!.*'.$nssep.'!','',$name);
948        //if there is a hash we use the ancor name only
949        $name = preg_replace('!.*#!','',$name);
950        return $name;
951    }
952
953    /**
954     * Renders internal and external media
955     *
956     * @author Andreas Gohr <andi@splitbrain.org>
957     */
958    function _media ($src, $title=NULL, $align=NULL, $width=NULL,
959                      $height=NULL, $cache=NULL) {
960
961        $ret = '';
962
963        list($ext,$mime) = mimetype($src);
964        if(substr($mime,0,5) == 'image'){
965            //add image tag
966            $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"';
967            $ret .= ' class="media'.$align.'"';
968
969            if (!is_null($title)) {
970                $ret .= ' title="'.$this->_xmlEntities($title).'"';
971                $ret .= ' alt="'.$this->_xmlEntities($title).'"';
972            }elseif($ext == 'jpg' || $ext == 'jpeg'){
973                //try to use the caption from IPTC/EXIF
974                require_once(DOKU_INC.'inc/JpegMeta.php');
975                $jpeg =& new JpegMeta(mediaFN($src));
976                if($jpeg !== false) $cap = $jpeg->getTitle();
977                if($cap){
978                    $ret .= ' title="'.$this->_xmlEntities($cap).'"';
979                    $ret .= ' alt="'.$this->_xmlEntities($cap).'"';
980                }
981            }else{
982                $ret .= ' alt=""';
983            }
984
985            if ( !is_null($width) )
986                $ret .= ' width="'.$this->_xmlEntities($width).'"';
987
988            if ( !is_null($height) )
989                $ret .= ' height="'.$this->_xmlEntities($height).'"';
990
991            $ret .= ' />';
992
993        }elseif($mime == 'application/x-shockwave-flash'){
994            $ret .= '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'.
995                    ' codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"';
996            if ( !is_null($width) ) $ret .= ' width="'.$this->_xmlEntities($width).'"';
997            if ( !is_null($height) ) $ret .= ' height="'.$this->_xmlEntities($height).'"';
998            $ret .= '>'.DOKU_LF;
999            $ret .= '<param name="movie" value="'.ml($src).'" />'.DOKU_LF;
1000            $ret .= '<param name="quality" value="high" />'.DOKU_LF;
1001            $ret .= '<embed src="'.ml($src).'"'.
1002                    ' quality="high"';
1003            if ( !is_null($width) ) $ret .= ' width="'.$this->_xmlEntities($width).'"';
1004            if ( !is_null($height) ) $ret .= ' height="'.$this->_xmlEntities($height).'"';
1005            $ret .= ' type="application/x-shockwave-flash"'.
1006                    ' pluginspage="http://www.macromedia.com/go/getflashplayer"></embed>'.DOKU_LF;
1007            $ret .= '</object>'.DOKU_LF;
1008
1009        }elseif(!is_null($title)){
1010            // well at least we have a title to display
1011            $ret .= $this->_xmlEntities($title);
1012        }else{
1013            // just show the sourcename
1014            $ret .= $this->_xmlEntities(noNS($src));
1015        }
1016
1017        return $ret;
1018    }
1019
1020    function _xmlEntities($string) {
1021        return htmlspecialchars($string);
1022    }
1023
1024    /**
1025     * Creates a linkid from a headline
1026     *
1027     * @param string  $title   The headline title
1028     * @param boolean $create  Create a new unique ID?
1029     * @author Andreas Gohr <andi@splitbrain.org>
1030     */
1031    function _headerToLink($title,$create=false) {
1032        $title = str_replace(':','',cleanID($title,true)); //force ASCII
1033        $title = ltrim($title,'0123456789');
1034        if(empty($title)) $title='section';
1035
1036        if($create){
1037            // make sure tiles are unique
1038            $num = '';
1039            while(in_array($title.$num,$this->headers)){
1040                ($num) ? $num++ : $num = 1;
1041            }
1042            $title = $title.$num;
1043            $this->headers[] = $title;
1044        }
1045
1046        return $title;
1047    }
1048
1049    /**
1050     * Adds code for section editing button
1051     *
1052     * This is just aplaceholder and gets replace by the button if
1053     * section editing is allowed
1054     *
1055     * @author Andreas Gohr <andi@splitbrain.org>
1056     */
1057    function _secedit($f, $t){
1058        $this->doc .= '<!-- SECTION ['.$f.'-'.$t.'] -->';
1059    }
1060
1061    /**
1062     * Construct a title and handle images in titles
1063     *
1064     * @author Harry Fuecks <hfuecks@gmail.com>
1065     */
1066    function _getLinkTitle($title, $default, & $isImage, $id=NULL) {
1067        global $conf;
1068
1069        $isImage = FALSE;
1070        if ( is_null($title) ) {
1071            if ($conf['useheading'] && $id) {
1072                $heading = p_get_first_heading($id);
1073                if ($heading) {
1074                    return $this->_xmlEntities($heading);
1075                }
1076            }
1077            return $this->_xmlEntities($default);
1078        } else if ( is_string($title) ) {
1079            return $this->_xmlEntities($title);
1080        } else if ( is_array($title) ) {
1081            $isImage = TRUE;
1082            return $this->_imageTitle($title);
1083        }
1084    }
1085
1086    /**
1087     * Returns an HTML code for images used in link titles
1088     *
1089     * @todo Resolve namespace on internal images
1090     * @author Andreas Gohr <andi@splitbrain.org>
1091     */
1092    function _imageTitle($img) {
1093        return $this->_media($img['src'],
1094                              $img['title'],
1095                              $img['align'],
1096                              $img['width'],
1097                              $img['height'],
1098                              $img['cache']);
1099    }
1100}
1101
1102//Setup VIM: ex: et ts=4 enc=utf-8 :
1103