xref: /dokuwiki/inc/parser/xhtml.php (revision 71324fa7507690cb9078385f401e3ce772221fb8)
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        $url = $this->_resolveInterWiki($wikiName,$wikiUri);
704
705        if ( !$isImage ) {
706            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$wikiName);
707            $link['class'] = "interwiki iw_$class";
708        } else {
709            $link['class'] = 'media';
710        }
711
712        //do we stay at the same server? Use local target
713        if( strpos($url,DOKU_URL) === 0 ){
714            $link['target'] = $conf['target']['wiki'];
715        }
716
717        $link['url'] = $url;
718        $link['title'] = htmlspecialchars($link['url']);
719
720        //output formatted
721        $this->doc .= $this->_formatLink($link);
722    }
723
724    /**
725     */
726    function windowssharelink($url, $name = null) {
727        global $conf;
728        global $lang;
729        //simple setup
730        $link['target'] = $conf['target']['windows'];
731        $link['pre']    = '';
732        $link['suf']   = '';
733        $link['style']  = '';
734
735        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
736        if ( !$isImage ) {
737            $link['class'] = 'windows';
738        } else {
739            $link['class'] = 'media';
740        }
741
742        $link['title'] = $this->_xmlEntities($url);
743        $url = str_replace('\\','/',$url);
744        $url = 'file:///'.$url;
745        $link['url'] = $url;
746
747        //output formatted
748        $this->doc .= $this->_formatLink($link);
749    }
750
751    function emaillink($address, $name = null) {
752        global $conf;
753        //simple setup
754        $link = array();
755        $link['target'] = '';
756        $link['pre']    = '';
757        $link['suf']   = '';
758        $link['style']  = '';
759        $link['more']   = '';
760
761        $name = $this->_getLinkTitle($name, '', $isImage);
762        if ( !$isImage ) {
763            $link['class']='mail';
764        } else {
765            $link['class']='media';
766        }
767
768        $address = $this->_xmlEntities($address);
769        $address = obfuscate($address);
770        $title   = $address;
771
772        if(empty($name)){
773            $name = $address;
774        }
775
776        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
777
778        $link['url']   = 'mailto:'.$address;
779        $link['name']  = $name;
780        $link['title'] = $title;
781
782        //output formatted
783        $this->doc .= $this->_formatLink($link);
784    }
785
786    function internalmedia ($src, $title=null, $align=null, $width=null,
787                            $height=null, $cache=null, $linking=null, $return=NULL) {
788        global $ID;
789        list($src,$hash) = explode('#',$src,2);
790        resolve_mediaid(getNS($ID),$src, $exists);
791
792        $noLink = false;
793        $render = ($linking == 'linkonly') ? false : true;
794        $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
795
796        list($ext,$mime,$dl) = mimetype($src,false);
797        if(substr($mime,0,5) == 'image' && $render){
798            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct'));
799        }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){
800            // don't link movies
801            $noLink = true;
802        }else{
803            // add file icons
804            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
805            $link['class'] .= ' mediafile mf_'.$class;
806            $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true);
807            if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))).')';
808        }
809
810        if($hash) $link['url'] .= '#'.$hash;
811
812        //markup non existing files
813        if (!$exists) {
814            $link['class'] .= ' wikilink2';
815        }
816
817        //output formatted
818        if ($return) {
819            if ($linking == 'nolink' || $noLink) return $link['name'];
820            else return $this->_formatLink($link);
821        } else {
822            if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
823            else $this->doc .= $this->_formatLink($link);
824        }
825    }
826
827    function externalmedia ($src, $title=null, $align=null, $width=null,
828                            $height=null, $cache=null, $linking=null) {
829        list($src,$hash) = explode('#',$src,2);
830        $noLink = false;
831        $render = ($linking == 'linkonly') ? false : true;
832        $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
833
834        $link['url']    = ml($src,array('cache'=>$cache));
835
836        list($ext,$mime,$dl) = mimetype($src,false);
837        if(substr($mime,0,5) == 'image' && $render){
838            // link only jpeg images
839            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
840        }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){
841            // don't link movies
842            $noLink = true;
843        }else{
844            // add file icons
845            $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext);
846            $link['class'] .= ' mediafile mf_'.$class;
847        }
848
849        if($hash) $link['url'] .= '#'.$hash;
850
851        //output formatted
852        if ($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
853        else $this->doc .= $this->_formatLink($link);
854    }
855
856    /**
857     * Renders an RSS feed
858     *
859     * @author Andreas Gohr <andi@splitbrain.org>
860     */
861    function rss ($url,$params){
862        global $lang;
863        global $conf;
864
865        require_once(DOKU_INC.'inc/FeedParser.php');
866        $feed = new FeedParser();
867        $feed->set_feed_url($url);
868
869        //disable warning while fetching
870        if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); }
871        $rc = $feed->init();
872        if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); }
873
874        //decide on start and end
875        if($params['reverse']){
876            $mod = -1;
877            $start = $feed->get_item_quantity()-1;
878            $end   = $start - ($params['max']);
879            $end   = ($end < -1) ? -1 : $end;
880        }else{
881            $mod   = 1;
882            $start = 0;
883            $end   = $feed->get_item_quantity();
884            $end   = ($end > $params['max']) ? $params['max'] : $end;
885        }
886
887        $this->doc .= '<ul class="rss">';
888        if($rc){
889            for ($x = $start; $x != $end; $x += $mod) {
890                $item = $feed->get_item($x);
891                $this->doc .= '<li><div class="li">';
892                // support feeds without links
893                $lnkurl = $item->get_permalink();
894                if($lnkurl){
895                    // title is escaped by SimplePie, we unescape here because it
896                    // is escaped again in externallink() FS#1705
897                    $this->externallink($item->get_permalink(),
898                                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8'));
899                }else{
900                    $this->doc .= ' '.$item->get_title();
901                }
902                if($params['author']){
903                    $author = $item->get_author(0);
904                    if($author){
905                        $name = $author->get_name();
906                        if(!$name) $name = $author->get_email();
907                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
908                    }
909                }
910                if($params['date']){
911                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
912                }
913                if($params['details']){
914                    $this->doc .= '<div class="detail">';
915                    if($conf['htmlok']){
916                        $this->doc .= $item->get_description();
917                    }else{
918                        $this->doc .= strip_tags($item->get_description());
919                    }
920                    $this->doc .= '</div>';
921                }
922
923                $this->doc .= '</div></li>';
924            }
925        }else{
926            $this->doc .= '<li><div class="li">';
927            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
928            $this->externallink($url);
929            if($conf['allowdebug']){
930                $this->doc .= '<!--'.hsc($feed->error).'-->';
931            }
932            $this->doc .= '</div></li>';
933        }
934        $this->doc .= '</ul>';
935    }
936
937    // $numrows not yet implemented
938    function table_open($maxcols = null, $numrows = null, $pos = null){
939        global $lang;
940        // initialize the row counter used for classes
941        $this->_counter['row_counter'] = 0;
942        $class = 'table';
943        if ($pos !== null) {
944            $class .= ' ' . $this->startSectionEdit($pos, 'table');
945        }
946        $this->doc .= '<div class="' . $class . '"><table class="inline">' .
947                      DOKU_LF;
948    }
949
950    function table_close($pos = null){
951        $this->doc .= '</table></div>'.DOKU_LF;
952        if ($pos !== null) {
953            $this->finishSectionEdit($pos);
954        }
955    }
956
957    function tablerow_open(){
958        // initialize the cell counter used for classes
959        $this->_counter['cell_counter'] = 0;
960        $class = 'row' . $this->_counter['row_counter']++;
961        $this->doc .= DOKU_TAB . '<tr class="'.$class.'">' . DOKU_LF . DOKU_TAB . DOKU_TAB;
962    }
963
964    function tablerow_close(){
965        $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF;
966    }
967
968    function tableheader_open($colspan = 1, $align = null, $rowspan = 1){
969        $class = 'class="col' . $this->_counter['cell_counter']++;
970        if ( !is_null($align) ) {
971            $class .= ' '.$align.'align';
972        }
973        $class .= '"';
974        $this->doc .= '<th ' . $class;
975        if ( $colspan > 1 ) {
976            $this->_counter['cell_counter'] += $colspan-1;
977            $this->doc .= ' colspan="'.$colspan.'"';
978        }
979        if ( $rowspan > 1 ) {
980            $this->doc .= ' rowspan="'.$rowspan.'"';
981        }
982        $this->doc .= '>';
983    }
984
985    function tableheader_close(){
986        $this->doc .= '</th>';
987    }
988
989    function tablecell_open($colspan = 1, $align = null, $rowspan = 1){
990        $class = 'class="col' . $this->_counter['cell_counter']++;
991        if ( !is_null($align) ) {
992            $class .= ' '.$align.'align';
993        }
994        $class .= '"';
995        $this->doc .= '<td '.$class;
996        if ( $colspan > 1 ) {
997            $this->_counter['cell_counter'] += $colspan-1;
998            $this->doc .= ' colspan="'.$colspan.'"';
999        }
1000        if ( $rowspan > 1 ) {
1001            $this->doc .= ' rowspan="'.$rowspan.'"';
1002        }
1003        $this->doc .= '>';
1004    }
1005
1006    function tablecell_close(){
1007        $this->doc .= '</td>';
1008    }
1009
1010    //----------------------------------------------------------
1011    // Utils
1012
1013    /**
1014     * Build a link
1015     *
1016     * Assembles all parts defined in $link returns HTML for the link
1017     *
1018     * @author Andreas Gohr <andi@splitbrain.org>
1019     */
1020    function _formatLink($link){
1021        //make sure the url is XHTML compliant (skip mailto)
1022        if(substr($link['url'],0,7) != 'mailto:'){
1023            $link['url'] = str_replace('&','&amp;',$link['url']);
1024            $link['url'] = str_replace('&amp;amp;','&amp;',$link['url']);
1025        }
1026        //remove double encodings in titles
1027        $link['title'] = str_replace('&amp;amp;','&amp;',$link['title']);
1028
1029        // be sure there are no bad chars in url or title
1030        // (we can't do this for name because it can contain an img tag)
1031        $link['url']   = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22'));
1032        $link['title'] = strtr($link['title'],array('>'=>'&gt;','<'=>'&lt;','"'=>'&quot;'));
1033
1034        $ret  = '';
1035        $ret .= $link['pre'];
1036        $ret .= '<a href="'.$link['url'].'"';
1037        if(!empty($link['class']))  $ret .= ' class="'.$link['class'].'"';
1038        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1039        if(!empty($link['title']))  $ret .= ' title="'.$link['title'].'"';
1040        if(!empty($link['style']))  $ret .= ' style="'.$link['style'].'"';
1041        if(!empty($link['rel']))    $ret .= ' rel="'.$link['rel'].'"';
1042        if(!empty($link['more']))   $ret .= ' '.$link['more'];
1043        $ret .= '>';
1044        $ret .= $link['name'];
1045        $ret .= '</a>';
1046        $ret .= $link['suf'];
1047        return $ret;
1048    }
1049
1050    /**
1051     * Renders internal and external media
1052     *
1053     * @author Andreas Gohr <andi@splitbrain.org>
1054     */
1055    function _media ($src, $title=null, $align=null, $width=null,
1056                      $height=null, $cache=null, $render = true) {
1057
1058        $ret = '';
1059
1060        list($ext,$mime,$dl) = mimetype($src);
1061        if(substr($mime,0,5) == 'image'){
1062            // first get the $title
1063            if (!is_null($title)) {
1064                $title  = $this->_xmlEntities($title);
1065            }elseif($ext == 'jpg' || $ext == 'jpeg'){
1066                //try to use the caption from IPTC/EXIF
1067                require_once(DOKU_INC.'inc/JpegMeta.php');
1068                $jpeg =new JpegMeta(mediaFN($src));
1069                if($jpeg !== false) $cap = $jpeg->getTitle();
1070                if($cap){
1071                    $title = $this->_xmlEntities($cap);
1072                }
1073            }
1074            if (!$render) {
1075                // if the picture is not supposed to be rendered
1076                // return the title of the picture
1077                if (!$title) {
1078                    // just show the sourcename
1079                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1080                }
1081                return $title;
1082            }
1083            //add image tag
1084            $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"';
1085            $ret .= ' class="media'.$align.'"';
1086
1087            if ($title) {
1088                $ret .= ' title="' . $title . '"';
1089                $ret .= ' alt="'   . $title .'"';
1090            }else{
1091                $ret .= ' alt=""';
1092            }
1093
1094            if ( !is_null($width) )
1095                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1096
1097            if ( !is_null($height) )
1098                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1099
1100            $ret .= ' />';
1101
1102        }elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')){
1103            // first get the $title
1104            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1105            if (!$render) {
1106                // if the file is not supposed to be rendered
1107                // return the title of the file (just the sourcename if there is no title)
1108                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1109            }
1110
1111            $att = array();
1112            $att['class'] = "media$align";
1113            if ($title) {
1114                $att['title'] = $title;
1115            }
1116
1117            if (media_supportedav($mime, 'video')) {
1118                //add video
1119                $ret .= $this->_video($src, $width, $height, $att);
1120            }
1121            if (media_supportedav($mime, 'audio')) {
1122                //add audio
1123                $ret .= $this->_audio($src, $att);
1124            }
1125
1126        }elseif($mime == 'application/x-shockwave-flash'){
1127            if (!$render) {
1128                // if the flash is not supposed to be rendered
1129                // return the title of the flash
1130                if (!$title) {
1131                    // just show the sourcename
1132                    $title = utf8_basename(noNS($src));
1133                }
1134                return $this->_xmlEntities($title);
1135            }
1136
1137            $att = array();
1138            $att['class'] = "media$align";
1139            if($align == 'right') $att['align'] = 'right';
1140            if($align == 'left')  $att['align'] = 'left';
1141            $ret .= html_flashobject(ml($src,array('cache'=>$cache),true,'&'),$width,$height,
1142                                     array('quality' => 'high'),
1143                                     null,
1144                                     $att,
1145                                     $this->_xmlEntities($title));
1146        }elseif($title){
1147            // well at least we have a title to display
1148            $ret .= $this->_xmlEntities($title);
1149        }else{
1150            // just show the sourcename
1151            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1152        }
1153
1154        return $ret;
1155    }
1156
1157    function _xmlEntities($string) {
1158        return htmlspecialchars($string,ENT_QUOTES,'UTF-8');
1159    }
1160
1161    /**
1162     * Creates a linkid from a headline
1163     *
1164     * @param string  $title   The headline title
1165     * @param boolean $create  Create a new unique ID?
1166     * @author Andreas Gohr <andi@splitbrain.org>
1167     */
1168    function _headerToLink($title,$create=false) {
1169        if($create){
1170            return sectionID($title,$this->headers);
1171        }else{
1172            $check = false;
1173            return sectionID($title,$check);
1174        }
1175    }
1176
1177    /**
1178     * Construct a title and handle images in titles
1179     *
1180     * @author Harry Fuecks <hfuecks@gmail.com>
1181     */
1182    function _getLinkTitle($title, $default, & $isImage, $id=null, $linktype='content') {
1183        global $conf;
1184
1185        $isImage = false;
1186        if ( is_array($title) ) {
1187            $isImage = true;
1188            return $this->_imageTitle($title);
1189        } elseif ( is_null($title) || trim($title)=='') {
1190            if (useHeading($linktype) && $id) {
1191                $heading = p_get_first_heading($id);
1192                if ($heading) {
1193                    return $this->_xmlEntities($heading);
1194                }
1195            }
1196            return $this->_xmlEntities($default);
1197        } else {
1198            return $this->_xmlEntities($title);
1199        }
1200    }
1201
1202    /**
1203     * Returns an HTML code for images used in link titles
1204     *
1205     * @todo Resolve namespace on internal images
1206     * @author Andreas Gohr <andi@splitbrain.org>
1207     */
1208    function _imageTitle($img) {
1209        global $ID;
1210
1211        // some fixes on $img['src']
1212        // see internalmedia() and externalmedia()
1213        list($img['src'],$hash) = explode('#',$img['src'],2);
1214        if ($img['type'] == 'internalmedia') {
1215            resolve_mediaid(getNS($ID),$img['src'],$exists);
1216        }
1217
1218        return $this->_media($img['src'],
1219                              $img['title'],
1220                              $img['align'],
1221                              $img['width'],
1222                              $img['height'],
1223                              $img['cache']);
1224    }
1225
1226    /**
1227     * _getMediaLinkConf is a helperfunction to internalmedia() and externalmedia()
1228     * which returns a basic link to a media.
1229     *
1230     * @author Pierre Spring <pierre.spring@liip.ch>
1231     * @param string $src
1232     * @param string $title
1233     * @param string $align
1234     * @param string $width
1235     * @param string $height
1236     * @param string $cache
1237     * @param string $render
1238     * @access protected
1239     * @return array
1240     */
1241    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1242        global $conf;
1243
1244        $link = array();
1245        $link['class']  = 'media';
1246        $link['style']  = '';
1247        $link['pre']    = '';
1248        $link['suf']    = '';
1249        $link['more']   = '';
1250        $link['target'] = $conf['target']['media'];
1251        $link['title']  = $this->_xmlEntities($src);
1252        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1253
1254        return $link;
1255    }
1256
1257
1258    /**
1259     * Embed video(s) in HTML
1260     *
1261     * @author Anika Henke <anika@selfthinker.org>
1262     *
1263     * @param string $src      - ID of video to embed
1264     * @param int $width       - width of the video in pixels
1265     * @param int $height      - height of the video in pixels
1266     * @param array $atts      - additional attributes for the <video> tag
1267     * @return string
1268     */
1269    function _video($src,$width,$height,$atts=null){
1270        // prepare width and height
1271        if(is_null($atts)) $atts = array();
1272        $atts['width']  = (int) $width;
1273        $atts['height'] = (int) $height;
1274        if(!$atts['width'])  $atts['width']  = 320;
1275        if(!$atts['height']) $atts['height'] = 240;
1276
1277        // prepare alternative formats
1278        $extensions = array('webm', 'ogv', 'mp4');
1279        $alternatives = media_alternativefiles($src, $extensions);
1280        $poster = media_alternativefiles($src, array('jpg', 'png'), true);
1281        $posterUrl = '';
1282        if (!empty($poster)) {
1283            $posterUrl = ml(reset($poster),array('cache'=>$cache),true,'&');
1284        }
1285
1286        $out = '';
1287        // open video tag
1288        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1289        if ($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1290        $out .= '>'.NL;
1291        $fallback = '';
1292
1293        // output source for each alternative video format
1294        foreach($alternatives as $mime => $file) {
1295            $url = ml($file,array('cache'=>$cache),true,'&');
1296            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1297
1298            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1299            // alternative content (just a link to the file)
1300            $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true);
1301        }
1302
1303        // finish
1304        $out .= $fallback;
1305        $out .= '</video>'.NL;
1306        return $out;
1307    }
1308
1309    /**
1310     * Embed audio in HTML
1311     *
1312     * @author Anika Henke <anika@selfthinker.org>
1313     *
1314     * @param string $src      - ID of audio to embed
1315     * @param array $atts      - additional attributes for the <audio> tag
1316     * @return string
1317     */
1318    function _audio($src,$atts=null){
1319
1320        // prepare alternative formats
1321        $extensions = array('ogg', 'mp3', 'wav');
1322        $alternatives = media_alternativefiles($src, $extensions);
1323
1324        $out = '';
1325        // open audio tag
1326        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1327        $fallback = '';
1328
1329        // output source for each alternative audio format
1330        foreach($alternatives as $mime => $file) {
1331            $url = ml($file,array('cache'=>$cache),true,'&');
1332            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1333
1334            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1335            // alternative content (just a link to the file)
1336            $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true);
1337        }
1338
1339        // finish
1340        $out .= $fallback;
1341        $out .= '</audio>'.NL;
1342        return $out;
1343    }
1344
1345}
1346
1347//Setup VIM: ex: et ts=4 :
1348