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