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