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