xref: /dokuwiki/inc/parser/xhtml.php (revision fc4ff0c349240d12ff91559183e1103ad2c5fa91)
1<?php
2/**
3 * Renderer for XHTML output
4 *
5 * @author Harry Fuecks <hfuecks@gmail.com>
6 * @author Andreas Gohr <andi@splitbrain.org>
7 */
8if(!defined('DOKU_INC')) die('meh.');
9
10if ( !defined('DOKU_LF') ) {
11    // Some whitespace to help View > Source
12    define ('DOKU_LF',"\n");
13}
14
15if ( !defined('DOKU_TAB') ) {
16    // Some whitespace to help View > Source
17    define ('DOKU_TAB',"\t");
18}
19
20/**
21 * The Renderer
22 */
23class Doku_Renderer_xhtml extends Doku_Renderer {
24
25    // @access public
26    var $doc = '';        // will contain the whole document
27    var $toc = array();   // will contain the Table of Contents
28
29    var $sectionedits = array(); // A stack of section edit data
30    private $lastsecid = 0; // last section edit id, used by startSectionEdit
31
32    var $headers = array();
33    /** @var array a list of footnotes, list starts at 1! */
34    var $footnotes = array();
35    var $lastlevel = 0;
36    var $node = array(0,0,0,0,0);
37    var $store = '';
38
39    var $_counter   = array(); // used as global counter, introduced for table classes
40    var $_codeblock = 0; // counts the code and file blocks, used to provide download links
41
42    /**
43     * Register a new edit section range
44     *
45     * @param $type  string The section type identifier
46     * @param $title string The section title
47     * @param $start int    The byte position for the edit start
48     * @return string A marker class for the starting HTML element
49     * @author Adrian Lang <lang@cosmocode.de>
50     */
51    public function startSectionEdit($start, $type, $title = null) {
52        $this->sectionedits[] = array(++$this->lastsecid, $start, $type, $title);
53        return 'sectionedit' . $this->lastsecid;
54    }
55
56    /**
57     * Finish an edit section range
58     *
59     * @param $end int The byte position for the edit end; null for the rest of
60     *                 the page
61     * @author Adrian Lang <lang@cosmocode.de>
62     */
63    public function finishSectionEdit($end = null) {
64        list($id, $start, $type, $title) = array_pop($this->sectionedits);
65        if (!is_null($end) && $end <= $start) {
66            return;
67        }
68        $this->doc .= "<!-- EDIT$id " . strtoupper($type) . ' ';
69        if (!is_null($title)) {
70            $this->doc .= '"' . str_replace('"', '', $title) . '" ';
71        }
72        $this->doc .= "[$start-" . (is_null($end) ? '' : $end) . '] -->';
73    }
74
75    function getFormat(){
76        return 'xhtml';
77    }
78
79
80    function document_start() {
81        //reset some internals
82        $this->toc     = array();
83        $this->headers = array();
84    }
85
86    function document_end() {
87        // Finish open section edits.
88        while (count($this->sectionedits) > 0) {
89            if ($this->sectionedits[count($this->sectionedits) - 1][1] <= 1) {
90                // If there is only one section, do not write a section edit
91                // marker.
92                array_pop($this->sectionedits);
93            } else {
94                $this->finishSectionEdit();
95            }
96        }
97
98        if ( count ($this->footnotes) > 0 ) {
99            $this->doc .= '<div class="footnotes">'.DOKU_LF;
100
101            foreach ( $this->footnotes as $id => $footnote ) {
102                // check its not a placeholder that indicates actual footnote text is elsewhere
103                if (substr($footnote, 0, 5) != "@@FNT") {
104
105                    // open the footnote and set the anchor and backlink
106                    $this->doc .= '<div class="fn">';
107                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
108                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
109
110                    // get any other footnotes that use the same markup
111                    $alt = array_keys($this->footnotes, "@@FNT$id");
112
113                    if (count($alt)) {
114                        foreach ($alt as $ref) {
115                            // set anchor and backlink for the other footnotes
116                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
117                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
118                        }
119                    }
120
121                    // add footnote markup and close this footnote
122                    $this->doc .= $footnote;
123                    $this->doc .= '</div>' . DOKU_LF;
124                }
125            }
126            $this->doc .= '</div>'.DOKU_LF;
127        }
128
129        // Prepare the TOC
130        global $conf;
131        if($this->info['toc'] && is_array($this->toc) && $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']){
132            global $TOC;
133            $TOC = $this->toc;
134        }
135
136        // make sure there are no empty paragraphs
137        $this->doc = preg_replace('#<p>\s*</p>#','',$this->doc);
138    }
139
140    function toc_additem($id, $text, $level) {
141        global $conf;
142
143        //handle TOC
144        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){
145            $this->toc[] = html_mktocitem($id, $text, $level-$conf['toptoclevel']+1);
146        }
147    }
148
149    function header($text, $level, $pos) {
150        global $conf;
151
152        if(!$text) return; //skip empty headlines
153
154        $hid = $this->_headerToLink($text,true);
155
156        //only add items within configured levels
157        $this->toc_additem($hid, $text, $level);
158
159        // adjust $node to reflect hierarchy of levels
160        $this->node[$level-1]++;
161        if ($level < $this->lastlevel) {
162            for ($i = 0; $i < $this->lastlevel-$level; $i++) {
163                $this->node[$this->lastlevel-$i-1] = 0;
164            }
165        }
166        $this->lastlevel = $level;
167
168        if ($level <= $conf['maxseclevel'] &&
169            count($this->sectionedits) > 0 &&
170            $this->sectionedits[count($this->sectionedits) - 1][2] === 'section') {
171            $this->finishSectionEdit($pos - 1);
172        }
173
174        // write the header
175        $this->doc .= DOKU_LF.'<h'.$level;
176        if ($level <= $conf['maxseclevel']) {
177            $this->doc .= ' class="' . $this->startSectionEdit($pos, 'section', $text) . '"';
178        }
179        $this->doc .= ' id="'.$hid.'">';
180        $this->doc .= $this->_xmlEntities($text);
181        $this->doc .= "</h$level>".DOKU_LF;
182    }
183
184    function section_open($level) {
185        $this->doc .= '<div class="level' . $level . '">' . DOKU_LF;
186    }
187
188    function section_close() {
189        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
190    }
191
192    function cdata($text) {
193        $this->doc .= $this->_xmlEntities($text);
194    }
195
196    function p_open() {
197        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
198    }
199
200    function p_close() {
201        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
202    }
203
204    function linebreak() {
205        $this->doc .= '<br/>'.DOKU_LF;
206    }
207
208    function hr() {
209        $this->doc .= '<hr />'.DOKU_LF;
210    }
211
212    function strong_open() {
213        $this->doc .= '<strong>';
214    }
215
216    function strong_close() {
217        $this->doc .= '</strong>';
218    }
219
220    function emphasis_open() {
221        $this->doc .= '<em>';
222    }
223
224    function emphasis_close() {
225        $this->doc .= '</em>';
226    }
227
228    function underline_open() {
229        $this->doc .= '<em class="u">';
230    }
231
232    function underline_close() {
233        $this->doc .= '</em>';
234    }
235
236    function monospace_open() {
237        $this->doc .= '<code>';
238    }
239
240    function monospace_close() {
241        $this->doc .= '</code>';
242    }
243
244    function subscript_open() {
245        $this->doc .= '<sub>';
246    }
247
248    function subscript_close() {
249        $this->doc .= '</sub>';
250    }
251
252    function superscript_open() {
253        $this->doc .= '<sup>';
254    }
255
256    function superscript_close() {
257        $this->doc .= '</sup>';
258    }
259
260    function deleted_open() {
261        $this->doc .= '<del>';
262    }
263
264    function deleted_close() {
265        $this->doc .= '</del>';
266    }
267
268    /**
269     * Callback for footnote start syntax
270     *
271     * All following content will go to the footnote instead of
272     * the document. To achieve this the previous rendered content
273     * is moved to $store and $doc is cleared
274     *
275     * @author Andreas Gohr <andi@splitbrain.org>
276     */
277    function footnote_open() {
278
279        // move current content to store and record footnote
280        $this->store = $this->doc;
281        $this->doc   = '';
282    }
283
284    /**
285     * Callback for footnote end syntax
286     *
287     * All rendered content is moved to the $footnotes array and the old
288     * content is restored from $store again
289     *
290     * @author Andreas Gohr
291     */
292    function footnote_close() {
293        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
294        static $fnid = 0;
295        // assign new footnote id (we start at 1)
296        $fnid++;
297
298        // recover footnote into the stack and restore old content
299        $footnote = $this->doc;
300        $this->doc = $this->store;
301        $this->store = '';
302
303        // check to see if this footnote has been seen before
304        $i = array_search($footnote, $this->footnotes);
305
306        if ($i === false) {
307            // its a new footnote, add it to the $footnotes array
308            $this->footnotes[$fnid] = $footnote;
309        } else {
310            // seen this one before, save a placeholder
311            $this->footnotes[$fnid] = "@@FNT".($i);
312        }
313
314        // output the footnote reference and link
315        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
316    }
317
318    function listu_open() {
319        $this->doc .= '<ul>'.DOKU_LF;
320    }
321
322    function listu_close() {
323        $this->doc .= '</ul>'.DOKU_LF;
324    }
325
326    function listo_open() {
327        $this->doc .= '<ol>'.DOKU_LF;
328    }
329
330    function listo_close() {
331        $this->doc .= '</ol>'.DOKU_LF;
332    }
333
334    function listitem_open($level) {
335        $this->doc .= '<li class="level'.$level.'">';
336    }
337
338    function listitem_close() {
339        $this->doc .= '</li>'.DOKU_LF;
340    }
341
342    function listcontent_open() {
343        $this->doc .= '<div class="li">';
344    }
345
346    function listcontent_close() {
347        $this->doc .= '</div>'.DOKU_LF;
348    }
349
350    function unformatted($text) {
351        $this->doc .= $this->_xmlEntities($text);
352    }
353
354    /**
355     * Execute PHP code if allowed
356     *
357     * @param  string   $text      PHP code that is either executed or printed
358     * @param  string   $wrapper   html element to wrap result if $conf['phpok'] is okff
359     *
360     * @author Andreas Gohr <andi@splitbrain.org>
361     */
362    function php($text, $wrapper='code') {
363        global $conf;
364
365        if($conf['phpok']){
366            ob_start();
367            eval($text);
368            $this->doc .= ob_get_contents();
369            ob_end_clean();
370        } else {
371            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
372        }
373    }
374
375    function phpblock($text) {
376        $this->php($text, 'pre');
377    }
378
379    /**
380     * Insert HTML if allowed
381     *
382     * @param  string   $text      html text
383     * @param  string   $wrapper   html element to wrap result if $conf['htmlok'] is okff
384     *
385     * @author Andreas Gohr <andi@splitbrain.org>
386     */
387    function html($text, $wrapper='code') {
388        global $conf;
389
390        if($conf['htmlok']){
391            $this->doc .= $text;
392        } else {
393            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
394        }
395    }
396
397    function htmlblock($text) {
398        $this->html($text, 'pre');
399    }
400
401    function quote_open() {
402        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
403    }
404
405    function quote_close() {
406        $this->doc .= '</div></blockquote>'.DOKU_LF;
407    }
408
409    function preformatted($text) {
410        $this->doc .= '<pre class="code">' . trim($this->_xmlEntities($text),"\n\r") . '</pre>'. DOKU_LF;
411    }
412
413    function file($text, $language=null, $filename=null) {
414        $this->_highlight('file',$text,$language,$filename);
415    }
416
417    function code($text, $language=null, $filename=null) {
418        $this->_highlight('code',$text,$language,$filename);
419    }
420
421    /**
422     * Use GeSHi to highlight language syntax in code and file blocks
423     *
424     * @author Andreas Gohr <andi@splitbrain.org>
425     */
426    function _highlight($type, $text, $language=null, $filename=null) {
427        global $conf;
428        global $ID;
429        global $lang;
430
431        if($filename){
432            // add icon
433            list($ext) = mimetype($filename,false);
434            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
435            $class = 'mediafile mf_'.$class;
436
437            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
438            $this->doc .= '<dt><a href="'.exportlink($ID,'code',array('codeblock'=>$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
439            $this->doc .= hsc($filename);
440            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
441        }
442
443        if ($text{0} == "\n") {
444            $text = substr($text, 1);
445        }
446        if (substr($text, -1) == "\n") {
447            $text = substr($text, 0, -1);
448        }
449
450        if ( is_null($language) ) {
451            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
452        } else {
453            $class = 'code'; //we always need the code class to make the syntax highlighting apply
454            if($type != 'code') $class .= ' '.$type;
455
456            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF;
457        }
458
459        if($filename){
460            $this->doc .= '</dd></dl>'.DOKU_LF;
461        }
462
463        $this->_codeblock++;
464    }
465
466    function acronym($acronym) {
467
468        if ( array_key_exists($acronym, $this->acronyms) ) {
469
470            $title = $this->_xmlEntities($this->acronyms[$acronym]);
471
472            $this->doc .= '<abbr title="'.$title
473                .'">'.$this->_xmlEntities($acronym).'</abbr>';
474
475        } else {
476            $this->doc .= $this->_xmlEntities($acronym);
477        }
478    }
479
480    function smiley($smiley) {
481        if ( array_key_exists($smiley, $this->smileys) ) {
482            $title = $this->_xmlEntities($this->smileys[$smiley]);
483            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
484                '" class="icon" alt="'.
485                    $this->_xmlEntities($smiley).'" />';
486        } else {
487            $this->doc .= $this->_xmlEntities($smiley);
488        }
489    }
490
491    /*
492    * not used
493    function wordblock($word) {
494        if ( array_key_exists($word, $this->badwords) ) {
495            $this->doc .= '** BLEEP **';
496        } else {
497            $this->doc .= $this->_xmlEntities($word);
498        }
499    }
500    */
501
502    function entity($entity) {
503        if ( array_key_exists($entity, $this->entities) ) {
504            $this->doc .= $this->entities[$entity];
505        } else {
506            $this->doc .= $this->_xmlEntities($entity);
507        }
508    }
509
510    function multiplyentity($x, $y) {
511        $this->doc .= "$x&times;$y";
512    }
513
514    function singlequoteopening() {
515        global $lang;
516        $this->doc .= $lang['singlequoteopening'];
517    }
518
519    function singlequoteclosing() {
520        global $lang;
521        $this->doc .= $lang['singlequoteclosing'];
522    }
523
524    function apostrophe() {
525        global $lang;
526        $this->doc .= $lang['apostrophe'];
527    }
528
529    function doublequoteopening() {
530        global $lang;
531        $this->doc .= $lang['doublequoteopening'];
532    }
533
534    function doublequoteclosing() {
535        global $lang;
536        $this->doc .= $lang['doublequoteclosing'];
537    }
538
539    /**
540     */
541    function camelcaselink($link) {
542        $this->internallink($link,$link);
543    }
544
545
546    function locallink($hash, $name = null){
547        global $ID;
548        $name  = $this->_getLinkTitle($name, $hash, $isImage);
549        $hash  = $this->_headerToLink($hash);
550        $title = $ID.' ↵';
551        $this->doc .= '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
552        $this->doc .= $name;
553        $this->doc .= '</a>';
554    }
555
556    /**
557     * Render an internal Wiki Link
558     *
559     * $search,$returnonly & $linktype are not for the renderer but are used
560     * elsewhere - no need to implement them in other renderers
561     *
562     * @param string $id pageid
563     * @param string|null $name link name
564     * @param string|null $search adds search url param
565     * @param bool $returnonly whether to return html or write to doc attribute
566     * @param string $linktype type to set use of headings
567     * @return void|string writes to doc attribute or returns html depends on $returnonly
568     * @author Andreas Gohr <andi@splitbrain.org>
569     */
570    function internallink($id, $name = null, $search=null,$returnonly=false,$linktype='content') {
571        global $conf;
572        global $ID;
573        global $INFO;
574
575        $params = '';
576        $parts = explode('?', $id, 2);
577        if (count($parts) === 2) {
578            $id = $parts[0];
579            $params = $parts[1];
580        }
581
582        // For empty $id we need to know the current $ID
583        // We need this check because _simpleTitle needs
584        // correct $id and resolve_pageid() use cleanID($id)
585        // (some things could be lost)
586        if ($id === '') {
587            $id = $ID;
588        }
589
590        // default name is based on $id as given
591        $default = $this->_simpleTitle($id);
592
593        // now first resolve and clean up the $id
594        resolve_pageid(getNS($ID),$id,$exists);
595
596        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
597        if ( !$isImage ) {
598            if ( $exists ) {
599                $class='wikilink1';
600            } else {
601                $class='wikilink2';
602                $link['rel']='nofollow';
603            }
604        } else {
605            $class='media';
606        }
607
608        //keep hash anchor
609        @list($id,$hash) = explode('#',$id,2);
610        if(!empty($hash)) $hash = $this->_headerToLink($hash);
611
612        //prepare for formating
613        $link['target'] = $conf['target']['wiki'];
614        $link['style']  = '';
615        $link['pre']    = '';
616        $link['suf']    = '';
617        // highlight link to current page
618        if ($id == $INFO['id']) {
619            $link['pre']    = '<span class="curid">';
620            $link['suf']    = '</span>';
621        }
622        $link['more']   = '';
623        $link['class']  = $class;
624        $link['url']    = wl($id, $params);
625        $link['name']   = $name;
626        $link['title']  = $id;
627        //add search string
628        if($search){
629            ($conf['userewrite']) ? $link['url'].='?' : $link['url'].='&amp;';
630            if(is_array($search)){
631                $search = array_map('rawurlencode',$search);
632                $link['url'] .= 's[]='.join('&amp;s[]=',$search);
633            }else{
634                $link['url'] .= 's='.rawurlencode($search);
635            }
636        }
637
638        //keep hash
639        if($hash) $link['url'].='#'.$hash;
640
641        //output formatted
642        if($returnonly){
643            return $this->_formatLink($link);
644        }else{
645            $this->doc .= $this->_formatLink($link);
646        }
647    }
648
649    function externallink($url, $name = null) {
650        global $conf;
651
652        $name = $this->_getLinkTitle($name, $url, $isImage);
653
654        // url might be an attack vector, only allow registered protocols
655        if(is_null($this->schemes)) $this->schemes = getSchemes();
656        list($scheme) = explode('://',$url);
657        $scheme = strtolower($scheme);
658        if(!in_array($scheme,$this->schemes)) $url = '';
659
660        // is there still an URL?
661        if(!$url){
662            $this->doc .= $name;
663            return;
664        }
665
666        // set class
667        if ( !$isImage ) {
668            $class='urlextern';
669        } else {
670            $class='media';
671        }
672
673        //prepare for formating
674        $link['target'] = $conf['target']['extern'];
675        $link['style']  = '';
676        $link['pre']    = '';
677        $link['suf']    = '';
678        $link['more']   = '';
679        $link['class']  = $class;
680        $link['url']    = $url;
681
682        $link['name']   = $name;
683        $link['title']  = $this->_xmlEntities($url);
684        if($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
685
686        //output formatted
687        $this->doc .= $this->_formatLink($link);
688    }
689
690    /**
691     */
692    function interwikilink($match, $name = null, $wikiName, $wikiUri) {
693        global $conf;
694
695        $link = array();
696        $link['target'] = $conf['target']['interwiki'];
697        $link['pre']    = '';
698        $link['suf']    = '';
699        $link['more']   = '';
700        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
701
702        //get interwiki URL
703        $exists = null;
704        $url = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
705
706        if(!$isImage) {
707            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
708            $link['class'] = "interwiki iw_$class";
709        } else {
710            $link['class'] = 'media';
711        }
712
713        //do we stay at the same server? Use local target
714        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
715            $link['target'] = $conf['target']['wiki'];
716        }
717        if($exists !== null && !$isImage) {
718            if($exists) {
719                $link['class'] .= ' wikilink1';
720            } else {
721                $link['class'] .= ' wikilink2';
722                $link['rel'] = 'nofollow';
723            }
724        }
725
726        $link['url'] = $url;
727        $link['title'] = htmlspecialchars($link['url']);
728
729        //output formatted
730        $this->doc .= $this->_formatLink($link);
731    }
732
733    /**
734     */
735    function windowssharelink($url, $name = null) {
736        global $conf;
737        global $lang;
738        //simple setup
739        $link['target'] = $conf['target']['windows'];
740        $link['pre']    = '';
741        $link['suf']   = '';
742        $link['style']  = '';
743
744        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
745        if ( !$isImage ) {
746            $link['class'] = 'windows';
747        } else {
748            $link['class'] = 'media';
749        }
750
751        $link['title'] = $this->_xmlEntities($url);
752        $url = str_replace('\\','/',$url);
753        $url = 'file:///'.$url;
754        $link['url'] = $url;
755
756        //output formatted
757        $this->doc .= $this->_formatLink($link);
758    }
759
760    function emaillink($address, $name = null) {
761        global $conf;
762        //simple setup
763        $link = array();
764        $link['target'] = '';
765        $link['pre']    = '';
766        $link['suf']   = '';
767        $link['style']  = '';
768        $link['more']   = '';
769
770        $name = $this->_getLinkTitle($name, '', $isImage);
771        if ( !$isImage ) {
772            $link['class']='mail';
773        } else {
774            $link['class']='media';
775        }
776
777        $address = $this->_xmlEntities($address);
778        $address = obfuscate($address);
779        $title   = $address;
780
781        if(empty($name)){
782            $name = $address;
783        }
784
785        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
786
787        $link['url']   = 'mailto:'.$address;
788        $link['name']  = $name;
789        $link['title'] = $title;
790
791        //output formatted
792        $this->doc .= $this->_formatLink($link);
793    }
794
795    function internalmedia ($src, $title=null, $align=null, $width=null,
796                            $height=null, $cache=null, $linking=null, $return=NULL) {
797        global $ID;
798        list($src,$hash) = explode('#',$src,2);
799        resolve_mediaid(getNS($ID),$src, $exists);
800
801        $noLink = false;
802        $render = ($linking == 'linkonly') ? false : true;
803        $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
804
805        list($ext,$mime,$dl) = mimetype($src,false);
806        if(substr($mime,0,5) == 'image' && $render){
807            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct'));
808        }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){
809            // don't link movies
810            $noLink = true;
811        }else{
812            // add file icons
813            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
814            $link['class'] .= ' mediafile mf_'.$class;
815            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true);
816            if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))).')';
817        }
818
819        if($hash) $link['url'] .= '#'.$hash;
820
821        //markup non existing files
822        if (!$exists) {
823            $link['class'] .= ' wikilink2';
824        }
825
826        //output formatted
827        if ($return) {
828            if ($linking == 'nolink' || $noLink) return $link['name'];
829            else return $this->_formatLink($link);
830        } else {
831            if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
832            else $this->doc .= $this->_formatLink($link);
833        }
834    }
835
836    function externalmedia ($src, $title=null, $align=null, $width=null,
837                            $height=null, $cache=null, $linking=null) {
838        list($src,$hash) = explode('#',$src,2);
839        $noLink = false;
840        $render = ($linking == 'linkonly') ? false : true;
841        $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
842
843        $link['url']    = ml($src,array('cache'=>$cache));
844
845        list($ext,$mime,$dl) = mimetype($src,false);
846        if(substr($mime,0,5) == 'image' && $render){
847            // link only jpeg images
848            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
849        }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){
850            // don't link movies
851            $noLink = true;
852        }else{
853            // add file icons
854            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
855            $link['class'] .= ' mediafile mf_'.$class;
856        }
857
858        if($hash) $link['url'] .= '#'.$hash;
859
860        //output formatted
861        if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
862        else $this->doc .= $this->_formatLink($link);
863    }
864
865    /**
866     * Renders an RSS feed
867     *
868     * @author Andreas Gohr <andi@splitbrain.org>
869     */
870    function rss ($url,$params){
871        global $lang;
872        global $conf;
873
874        require_once(DOKU_INC.'inc/FeedParser.php');
875        $feed = new FeedParser();
876        $feed->set_feed_url($url);
877
878        //disable warning while fetching
879        if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); }
880        $rc = $feed->init();
881        if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); }
882
883        //decide on start and end
884        if($params['reverse']){
885            $mod = -1;
886            $start = $feed->get_item_quantity()-1;
887            $end   = $start - ($params['max']);
888            $end   = ($end < -1) ? -1 : $end;
889        }else{
890            $mod   = 1;
891            $start = 0;
892            $end   = $feed->get_item_quantity();
893            $end   = ($end > $params['max']) ? $params['max'] : $end;
894        }
895
896        $this->doc .= '<ul class="rss">';
897        if($rc){
898            for ($x = $start; $x != $end; $x += $mod) {
899                $item = $feed->get_item($x);
900                $this->doc .= '<li><div class="li">';
901                // support feeds without links
902                $lnkurl = $item->get_permalink();
903                if($lnkurl){
904                    // title is escaped by SimplePie, we unescape here because it
905                    // is escaped again in externallink() FS#1705
906                    $this->externallink($item->get_permalink(),
907                                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8'));
908                }else{
909                    $this->doc .= ' '.$item->get_title();
910                }
911                if($params['author']){
912                    $author = $item->get_author(0);
913                    if($author){
914                        $name = $author->get_name();
915                        if(!$name) $name = $author->get_email();
916                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
917                    }
918                }
919                if($params['date']){
920                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
921                }
922                if($params['details']){
923                    $this->doc .= '<div class="detail">';
924                    if($conf['htmlok']){
925                        $this->doc .= $item->get_description();
926                    }else{
927                        $this->doc .= strip_tags($item->get_description());
928                    }
929                    $this->doc .= '</div>';
930                }
931
932                $this->doc .= '</div></li>';
933            }
934        }else{
935            $this->doc .= '<li><div class="li">';
936            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
937            $this->externallink($url);
938            if($conf['allowdebug']){
939                $this->doc .= '<!--'.hsc($feed->error).'-->';
940            }
941            $this->doc .= '</div></li>';
942        }
943        $this->doc .= '</ul>';
944    }
945
946    // $numrows not yet implemented
947    function table_open($maxcols = null, $numrows = null, $pos = null){
948        global $lang;
949        // initialize the row counter used for classes
950        $this->_counter['row_counter'] = 0;
951        $class = 'table';
952        if ($pos !== null) {
953            $class .= ' ' . $this->startSectionEdit($pos, 'table');
954        }
955        $this->doc .= '<div class="' . $class . '"><table class="inline">' .
956                      DOKU_LF;
957    }
958
959    function table_close($pos = null){
960        $this->doc .= '</table></div>'.DOKU_LF;
961        if ($pos !== null) {
962            $this->finishSectionEdit($pos);
963        }
964    }
965
966    function tablethead_open(){
967        $this->doc .= DOKU_TAB . '<thead>' . DOKU_LF;
968    }
969
970    function tablethead_close(){
971        $this->doc .= DOKU_TAB . '</thead>' . DOKU_LF;
972    }
973
974    function tablerow_open(){
975        // initialize the cell counter used for classes
976        $this->_counter['cell_counter'] = 0;
977        $class = 'row' . $this->_counter['row_counter']++;
978        $this->doc .= DOKU_TAB . '<tr class="'.$class.'">' . DOKU_LF . DOKU_TAB . DOKU_TAB;
979    }
980
981    function tablerow_close(){
982        $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF;
983    }
984
985    function tableheader_open($colspan = 1, $align = null, $rowspan = 1){
986        $class = 'class="col' . $this->_counter['cell_counter']++;
987        if ( !is_null($align) ) {
988            $class .= ' '.$align.'align';
989        }
990        $class .= '"';
991        $this->doc .= '<th ' . $class;
992        if ( $colspan > 1 ) {
993            $this->_counter['cell_counter'] += $colspan-1;
994            $this->doc .= ' colspan="'.$colspan.'"';
995        }
996        if ( $rowspan > 1 ) {
997            $this->doc .= ' rowspan="'.$rowspan.'"';
998        }
999        $this->doc .= '>';
1000    }
1001
1002    function tableheader_close(){
1003        $this->doc .= '</th>';
1004    }
1005
1006    function tablecell_open($colspan = 1, $align = null, $rowspan = 1){
1007        $class = 'class="col' . $this->_counter['cell_counter']++;
1008        if ( !is_null($align) ) {
1009            $class .= ' '.$align.'align';
1010        }
1011        $class .= '"';
1012        $this->doc .= '<td '.$class;
1013        if ( $colspan > 1 ) {
1014            $this->_counter['cell_counter'] += $colspan-1;
1015            $this->doc .= ' colspan="'.$colspan.'"';
1016        }
1017        if ( $rowspan > 1 ) {
1018            $this->doc .= ' rowspan="'.$rowspan.'"';
1019        }
1020        $this->doc .= '>';
1021    }
1022
1023    function tablecell_close(){
1024        $this->doc .= '</td>';
1025    }
1026
1027    //----------------------------------------------------------
1028    // Utils
1029
1030    /**
1031     * Build a link
1032     *
1033     * Assembles all parts defined in $link returns HTML for the link
1034     *
1035     * @author Andreas Gohr <andi@splitbrain.org>
1036     */
1037    function _formatLink($link){
1038        //make sure the url is XHTML compliant (skip mailto)
1039        if(substr($link['url'],0,7) != 'mailto:'){
1040            $link['url'] = str_replace('&','&amp;',$link['url']);
1041            $link['url'] = str_replace('&amp;amp;','&amp;',$link['url']);
1042        }
1043        //remove double encodings in titles
1044        $link['title'] = str_replace('&amp;amp;','&amp;',$link['title']);
1045
1046        // be sure there are no bad chars in url or title
1047        // (we can't do this for name because it can contain an img tag)
1048        $link['url']   = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22'));
1049        $link['title'] = strtr($link['title'],array('>'=>'&gt;','<'=>'&lt;','"'=>'&quot;'));
1050
1051        $ret  = '';
1052        $ret .= $link['pre'];
1053        $ret .= '<a href="'.$link['url'].'"';
1054        if(!empty($link['class']))  $ret .= ' class="'.$link['class'].'"';
1055        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1056        if(!empty($link['title']))  $ret .= ' title="'.$link['title'].'"';
1057        if(!empty($link['style']))  $ret .= ' style="'.$link['style'].'"';
1058        if(!empty($link['rel']))    $ret .= ' rel="'.$link['rel'].'"';
1059        if(!empty($link['more']))   $ret .= ' '.$link['more'];
1060        $ret .= '>';
1061        $ret .= $link['name'];
1062        $ret .= '</a>';
1063        $ret .= $link['suf'];
1064        return $ret;
1065    }
1066
1067    /**
1068     * Renders internal and external media
1069     *
1070     * @author Andreas Gohr <andi@splitbrain.org>
1071     */
1072    function _media ($src, $title=null, $align=null, $width=null,
1073                      $height=null, $cache=null, $render = true) {
1074
1075        $ret = '';
1076
1077        list($ext,$mime,$dl) = mimetype($src);
1078        if(substr($mime,0,5) == 'image'){
1079            // first get the $title
1080            if (!is_null($title)) {
1081                $title  = $this->_xmlEntities($title);
1082            }elseif($ext == 'jpg' || $ext == 'jpeg'){
1083                //try to use the caption from IPTC/EXIF
1084                require_once(DOKU_INC.'inc/JpegMeta.php');
1085                $jpeg =new JpegMeta(mediaFN($src));
1086                if($jpeg !== false) $cap = $jpeg->getTitle();
1087                if($cap){
1088                    $title = $this->_xmlEntities($cap);
1089                }
1090            }
1091            if (!$render) {
1092                // if the picture is not supposed to be rendered
1093                // return the title of the picture
1094                if (!$title) {
1095                    // just show the sourcename
1096                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1097                }
1098                return $title;
1099            }
1100            //add image tag
1101            $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"';
1102            $ret .= ' class="media'.$align.'"';
1103
1104            if ($title) {
1105                $ret .= ' title="' . $title . '"';
1106                $ret .= ' alt="'   . $title .'"';
1107            }else{
1108                $ret .= ' alt=""';
1109            }
1110
1111            if ( !is_null($width) )
1112                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1113
1114            if ( !is_null($height) )
1115                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1116
1117            $ret .= ' />';
1118
1119        }elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')){
1120            // first get the $title
1121            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1122            if (!$render) {
1123                // if the file is not supposed to be rendered
1124                // return the title of the file (just the sourcename if there is no title)
1125                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1126            }
1127
1128            $att = array();
1129            $att['class'] = "media$align";
1130            if ($title) {
1131                $att['title'] = $title;
1132            }
1133
1134            if (media_supportedav($mime, 'video')) {
1135                //add video
1136                $ret .= $this->_video($src, $width, $height, $att);
1137            }
1138            if (media_supportedav($mime, 'audio')) {
1139                //add audio
1140                $ret .= $this->_audio($src, $att);
1141            }
1142
1143        }elseif($mime == 'application/x-shockwave-flash'){
1144            if (!$render) {
1145                // if the flash is not supposed to be rendered
1146                // return the title of the flash
1147                if (!$title) {
1148                    // just show the sourcename
1149                    $title = utf8_basename(noNS($src));
1150                }
1151                return $this->_xmlEntities($title);
1152            }
1153
1154            $att = array();
1155            $att['class'] = "media$align";
1156            if($align == 'right') $att['align'] = 'right';
1157            if($align == 'left')  $att['align'] = 'left';
1158            $ret .= html_flashobject(ml($src,array('cache'=>$cache),true,'&'),$width,$height,
1159                                     array('quality' => 'high'),
1160                                     null,
1161                                     $att,
1162                                     $this->_xmlEntities($title));
1163        }elseif($title){
1164            // well at least we have a title to display
1165            $ret .= $this->_xmlEntities($title);
1166        }else{
1167            // just show the sourcename
1168            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1169        }
1170
1171        return $ret;
1172    }
1173
1174    function _xmlEntities($string) {
1175        return htmlspecialchars($string,ENT_QUOTES,'UTF-8');
1176    }
1177
1178    /**
1179     * Creates a linkid from a headline
1180     *
1181     * @param string  $title   The headline title
1182     * @param boolean $create  Create a new unique ID?
1183     * @author Andreas Gohr <andi@splitbrain.org>
1184     */
1185    function _headerToLink($title,$create=false) {
1186        if($create){
1187            return sectionID($title,$this->headers);
1188        }else{
1189            $check = false;
1190            return sectionID($title,$check);
1191        }
1192    }
1193
1194    /**
1195     * Construct a title and handle images in titles
1196     *
1197     * @author Harry Fuecks <hfuecks@gmail.com>
1198     */
1199    function _getLinkTitle($title, $default, & $isImage, $id=null, $linktype='content') {
1200        global $conf;
1201
1202        $isImage = false;
1203        if ( is_array($title) ) {
1204            $isImage = true;
1205            return $this->_imageTitle($title);
1206        } elseif ( is_null($title) || trim($title)=='') {
1207            if (useHeading($linktype) && $id) {
1208                $heading = p_get_first_heading($id);
1209                if ($heading) {
1210                    return $this->_xmlEntities($heading);
1211                }
1212            }
1213            return $this->_xmlEntities($default);
1214        } else {
1215            return $this->_xmlEntities($title);
1216        }
1217    }
1218
1219    /**
1220     * Returns an HTML code for images used in link titles
1221     *
1222     * @todo Resolve namespace on internal images
1223     * @author Andreas Gohr <andi@splitbrain.org>
1224     */
1225    function _imageTitle($img) {
1226        global $ID;
1227
1228        // some fixes on $img['src']
1229        // see internalmedia() and externalmedia()
1230        list($img['src'],$hash) = explode('#',$img['src'],2);
1231        if ($img['type'] == 'internalmedia') {
1232            resolve_mediaid(getNS($ID),$img['src'],$exists);
1233        }
1234
1235        return $this->_media($img['src'],
1236                              $img['title'],
1237                              $img['align'],
1238                              $img['width'],
1239                              $img['height'],
1240                              $img['cache']);
1241    }
1242
1243    /**
1244     * _getMediaLinkConf is a helperfunction to internalmedia() and externalmedia()
1245     * which returns a basic link to a media.
1246     *
1247     * @author Pierre Spring <pierre.spring@liip.ch>
1248     * @param string $src
1249     * @param string $title
1250     * @param string $align
1251     * @param string $width
1252     * @param string $height
1253     * @param string $cache
1254     * @param string $render
1255     * @access protected
1256     * @return array
1257     */
1258    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1259        global $conf;
1260
1261        $link = array();
1262        $link['class']  = 'media';
1263        $link['style']  = '';
1264        $link['pre']    = '';
1265        $link['suf']    = '';
1266        $link['more']   = '';
1267        $link['target'] = $conf['target']['media'];
1268        $link['title']  = $this->_xmlEntities($src);
1269        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1270
1271        return $link;
1272    }
1273
1274
1275    /**
1276     * Embed video(s) in HTML
1277     *
1278     * @author Anika Henke <anika@selfthinker.org>
1279     *
1280     * @param string $src      - ID of video to embed
1281     * @param int $width       - width of the video in pixels
1282     * @param int $height      - height of the video in pixels
1283     * @param array $atts      - additional attributes for the <video> tag
1284     * @return string
1285     */
1286    function _video($src,$width,$height,$atts=null){
1287        // prepare width and height
1288        if(is_null($atts)) $atts = array();
1289        $atts['width']  = (int) $width;
1290        $atts['height'] = (int) $height;
1291        if(!$atts['width'])  $atts['width']  = 320;
1292        if(!$atts['height']) $atts['height'] = 240;
1293
1294        // prepare alternative formats
1295        $extensions = array('webm', 'ogv', 'mp4');
1296        $alternatives = media_alternativefiles($src, $extensions);
1297        $poster = media_alternativefiles($src, array('jpg', 'png'), true);
1298        $posterUrl = '';
1299        if (!empty($poster)) {
1300            $posterUrl = ml(reset($poster),array('cache'=>$cache),true,'&');
1301        }
1302
1303        $out = '';
1304        // open video tag
1305        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1306        if ($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1307        $out .= '>'.NL;
1308        $fallback = '';
1309
1310        // output source for each alternative video format
1311        foreach($alternatives as $mime => $file) {
1312            $url = ml($file,array('cache'=>$cache),true,'&');
1313            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1314
1315            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1316            // alternative content (just a link to the file)
1317            $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true);
1318        }
1319
1320        // finish
1321        $out .= $fallback;
1322        $out .= '</video>'.NL;
1323        return $out;
1324    }
1325
1326    /**
1327     * Embed audio in HTML
1328     *
1329     * @author Anika Henke <anika@selfthinker.org>
1330     *
1331     * @param string $src      - ID of audio to embed
1332     * @param array $atts      - additional attributes for the <audio> tag
1333     * @return string
1334     */
1335    function _audio($src,$atts=null){
1336
1337        // prepare alternative formats
1338        $extensions = array('ogg', 'mp3', 'wav');
1339        $alternatives = media_alternativefiles($src, $extensions);
1340
1341        $out = '';
1342        // open audio tag
1343        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1344        $fallback = '';
1345
1346        // output source for each alternative audio format
1347        foreach($alternatives as $mime => $file) {
1348            $url = ml($file,array('cache'=>$cache),true,'&');
1349            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1350
1351            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1352            // alternative content (just a link to the file)
1353            $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true);
1354        }
1355
1356        // finish
1357        $out .= $fallback;
1358        $out .= '</audio>'.NL;
1359        return $out;
1360    }
1361
1362}
1363
1364//Setup VIM: ex: et ts=4 :
1365