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