xref: /dokuwiki/inc/parser/xhtml.php (revision 426d8ff4dbde756dd678712847a0fc783edb75ce)
1<?php
2if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
3
4if ( !defined('DOKU_LF') ) {
5    // Some whitespace to help View > Source
6    define ('DOKU_LF',"\n");
7}
8
9if ( !defined('DOKU_TAB') ) {
10    // Some whitespace to help View > Source
11    define ('DOKU_TAB',"\t");
12}
13
14require_once DOKU_INC . 'inc/parser/renderer.php';
15
16/**
17* @TODO Probably useful for have constant for linefeed formatting
18*/
19class Doku_Renderer_XHTML extends Doku_Renderer {
20
21    var $doc = '';
22
23    var $headers = array();
24
25    var $footnotes = array();
26
27    var $footnoteIdStack = array();
28
29    var $acronyms = array();
30    var $smileys = array();
31    var $badwords = array();
32    var $entities = array();
33    var $interwiki = array();
34
35    var $lastsec = 0;
36
37    function document_start() {
38        ob_start();
39    }
40
41    function document_end() {
42				// add button for last section if any
43        if($this->lastsec) $this->__secedit($this->lastsec,'');
44
45        if ( count ($this->footnotes) > 0 ) {
46            echo '<div class="footnotes">'.DOKU_LF;
47            foreach ( $this->footnotes as $footnote ) {
48                echo $footnote;
49            }
50            echo '</div>'.DOKU_LF;
51        }
52
53        $this->doc .= ob_get_contents();
54        ob_end_clean();
55
56    }
57
58    function toc_open() {
59        echo '<div class="toc">'.DOKU_LF;
60        echo '<div class="tocheader">Table of Contents <script type="text/javascript">showTocToggle("+","-")</script></div>'.DOKU_LF;
61        echo '<div id="tocinside">'.DOKU_LF;
62    }
63
64    function tocbranch_open($level) {
65        echo '<ul class="toc">'.DOKU_LF;
66    }
67
68    function tocitem_open($level, $empty = FALSE) {
69        if ( !$empty ) {
70            echo '<li class="level'.$level.'">';
71        } else {
72            echo '<li class="clear">';
73        }
74    }
75
76    function tocelement($level, $title) {
77        echo '<span class="li"><a href="#'.$this->__headerToLink($title).'" class="toc">';
78        echo $this->__xmlEntities($title);
79        echo '</a></span>';
80    }
81
82    function tocitem_close($level) {
83        echo '</li>'.DOKU_LF;
84    }
85
86    function tocbranch_close($level) {
87        echo '</ul>'.DOKU_LF;
88    }
89
90    function toc_close() {
91        echo '</div>'.DOKU_LF.'</div>'.DOKU_LF;
92    }
93
94    function header($text, $level, $pos) {
95        global $conf;
96				//handle section editing
97        if($level <= $conf['maxseclevel']){
98            // add button for last section if any
99            if($this->lastsec) $this->__secedit($this->lastsec,$pos-1);
100						// remember current position
101	          $this->lastsec = $pos;
102				}
103
104        echo DOKU_LF.'<a name="'.$this->__headerToLink($text).'"></a><h'.$level.'>';
105        echo $this->__xmlEntities($text);
106        echo "</h$level>".DOKU_LF;
107    }
108
109    function section_open($level) {
110        echo "<div class=\"level$level\">".DOKU_LF;
111    }
112
113    function section_close() {
114        echo DOKU_LF.'</div>'.DOKU_LF;
115    }
116
117    function cdata($text) {
118        echo $this->__xmlEntities($text);
119    }
120
121    function p_open() {
122        echo DOKU_LF.'<p>'.DOKU_LF;
123    }
124
125    function p_close() {
126        echo DOKU_LF.'</p>'.DOKU_LF;
127    }
128
129    function linebreak() {
130        echo '<br/>'.DOKU_LF;
131    }
132
133    function hr() {
134        echo '<hr noshade="noshade" size="1" />'.DOKU_LF;
135    }
136
137    function strong_open() {
138        echo '<strong>';
139    }
140
141    function strong_close() {
142        echo '</strong>';
143    }
144
145    function emphasis_open() {
146        echo '<em>';
147    }
148
149    function emphasis_close() {
150        echo '</em>';
151    }
152
153    function underline_open() {
154        echo '<u>';
155    }
156
157    function underline_close() {
158        echo '</u>';
159    }
160
161    function monospace_open() {
162        echo '<code>';
163    }
164
165    function monospace_close() {
166        echo '</code>';
167    }
168
169    function subscript_open() {
170        echo '<sub>';
171    }
172
173    function subscript_close() {
174        echo '</sub>';
175    }
176
177    function superscript_open() {
178        echo '<sup>';
179    }
180
181    function superscript_close() {
182        echo '</sup>';
183    }
184
185    function deleted_open() {
186        echo '<del>';
187    }
188
189    function deleted_close() {
190        echo '</del>';
191    }
192
193    function footnote_open() {
194        $id = $this->__newFootnoteId();
195        echo '<a href="#fn'.$id.'" name="fnt'.$id.'" class="fn_top">'.$id.')</a>';
196        $this->footnoteIdStack[] = $id;
197        ob_start();
198    }
199
200    function footnote_close() {
201        $contents = ob_get_contents();
202        ob_end_clean();
203        $id = array_pop($this->footnoteIdStack);
204
205        $contents = '<div class="fn"><a href="#fnt'.
206            $id.'" name="fn'.$id.'" class="fn_bot">'.
207                $id.')</a> ' .DOKU_LF .$contents. "\n" . '</div>' . DOKU_LF;
208        $this->footnotes[$id] = $contents;
209    }
210
211    function listu_open() {
212        echo '<ul>'.DOKU_LF;
213    }
214
215    function listu_close() {
216        echo '</ul>'.DOKU_LF;
217    }
218
219    function listo_open() {
220        echo '<ol>'.DOKU_LF;
221    }
222
223    function listo_close() {
224        echo '</ol>'.DOKU_LF;
225    }
226
227    function listitem_open($level) {
228        echo '<li class="level'.$level.'">';
229    }
230
231    function listitem_close() {
232        echo '</li>'.DOKU_LF;
233    }
234
235    function listcontent_open() {
236        echo '<span class="li">';
237    }
238
239    function listcontent_close() {
240        echo '</span>'.DOKU_LF;
241    }
242
243    function unformatted($text) {
244        echo $this->__xmlEntities($text);
245    }
246
247    /**
248    */
249    function php($text) {
250				global $conf;
251				if($conf['phpok']){
252						eval($text);
253        }else{
254					  $this->file($text);
255				}
256    }
257
258    /**
259    */
260    function html($text) {
261				global $conf;
262        if($conf['htmlok']){
263					echo $text;
264				}else{
265	        $this->file($text);
266				}
267    }
268
269    function preformatted($text) {
270        echo '<pre class="code">' . $this->__xmlEntities($text) . '</pre>'. DOKU_LF;
271    }
272
273    function file($text) {
274        echo '<pre class="file">' . $this->__xmlEntities($text). '</pre>'. DOKU_LF;
275    }
276
277    /**
278    * @TODO Shouldn't this output <blockquote??
279    */
280    function quote_open() {
281        echo '<div class="quote">'.DOKU_LF;
282    }
283
284    /**
285    * @TODO Shouldn't this output </blockquote>?
286    */
287    function quote_close() {
288        echo '</div>'.DOKU_LF;
289    }
290
291    /**
292    */
293    function code($text, $language = NULL) {
294				global $conf;
295
296        if ( is_null($language) ) {
297            $this->preformatted($text);
298        } else {
299            // Handle with Geshi here FIXME: strip first beginning newline
300            require_once(DOKU_INC . 'inc/geshi.php');
301            $geshi = new GeSHi($text, strtolower($language), DOKU_INC . 'inc/geshi');
302            $geshi->enable_classes();
303            $geshi->set_header_type(GESHI_HEADER_PRE);
304            $geshi->set_overall_class('code');
305						$geshi->set_link_target($conf['target']['extern']);
306
307            $text = $geshi->parse_code();
308            echo $text;
309        }
310    }
311
312    function acronym($acronym) {
313
314        if ( array_key_exists($acronym, $this->acronyms) ) {
315
316            $title = $this->__xmlEntities($this->acronyms[$acronym]);
317
318            echo '<acronym title="'.$title
319                .'">'.$this->__xmlEntities($acronym).'</acronym>';
320
321        } else {
322            echo $this->__xmlEntities($acronym);
323        }
324    }
325
326    /**
327    */
328    function smiley($smiley) {
329        if ( array_key_exists($smiley, $this->smileys) ) {
330            $title = $this->__xmlEntities($this->smileys[$smiley]);
331            echo '<img src="'.DOKU_BASE.'smileys/'.$this->smileys[$smiley].
332                '" align="middle" alt="'.
333                    $this->__xmlEntities($smiley).'" />';
334        } else {
335            echo $this->__xmlEntities($smiley);
336        }
337    }
338
339    /**
340    * not used
341    function wordblock($word) {
342        if ( array_key_exists($word, $this->badwords) ) {
343            echo '** BLEEP **';
344        } else {
345            echo $this->__xmlEntities($word);
346        }
347    }
348    */
349
350    function entity($entity) {
351        if ( array_key_exists($entity, $this->entities) ) {
352            echo $this->entities[$entity];
353        } else {
354            echo $this->__xmlEntities($entity);
355        }
356    }
357
358    function multiplyentity($x, $y) {
359        echo "$x&times;$y";
360    }
361
362    function singlequoteopening() {
363        echo "&lsquo;";
364    }
365
366    function singlequoteclosing() {
367        echo "&rsquo;";
368    }
369
370    function doublequoteopening() {
371        echo "&ldquo;";
372    }
373
374    function doublequoteclosing() {
375        echo "&rdquo;";
376    }
377
378    /**
379    */
380    function camelcaselink($link) {
381    	$this->internallink($link,$link);
382    }
383
384    /**
385    * @TODO Support media
386    * @TODO correct attributes
387    */
388    function internallink($id, $name = NULL) {
389				global $conf;
390
391        $name = $this->__getLinkTitle($name, $this->__simpleTitle($id), $isImage);
392        resolve_pageid($id,$exists);
393
394        if ( !$isImage ) {
395            if ( $exists ) {
396                $class='wikilink1';
397            } else {
398                $class='wikilink2';
399            }
400        } else {
401            $class='media';
402        }
403
404				//prepare for formating
405        $link['target'] = $conf['target']['wiki'];
406        $link['style']  = '';
407        $link['pre']    = '';
408        $link['suf']    = '';
409        $link['more']   = 'onclick="return svchk()" onkeypress="return svchk()"';
410        $link['class']  = $class;
411        $link['url']    = wl($id);
412        $link['name']   = $name;
413        $link['title']  = $id;
414
415        //output formatted
416        echo $this->__formatLink($link);
417    }
418
419
420    /**
421    * @TODO Should list assume blacklist check already made?
422    * @TODO External link icon
423    * @TODO correct attributes
424    */
425    function externallink($link, $title = NULL) {
426
427        echo '<a';
428
429        $title = $this->__getLinkTitle($title, $link, $isImage);
430
431        if ( !$isImage ) {
432            echo ' class="urlextern"';
433        } else {
434            echo ' class="media"';
435        }
436
437        echo ' target="_blank" href="'.$this->__xmlEntities($link).'"';
438
439        echo ' onclick="return svchk()" onkeypress="return svchk()">';
440
441        echo $title;
442
443        echo '</a>';
444    }
445
446    /**
447    * @TODO Remove hard coded link to splitbrain.org on style
448    */
449    function interwikilink($link, $title = NULL, $wikiName, $wikiUri) {
450
451        // RESOLVE THE URL
452        if ( isset($this->interwiki[$wikiName]) ) {
453
454            $wikiUriEnc = urlencode($wikiUri);
455
456            if ( strstr($this->interwiki[$wikiName],'{URL}' ) !== FALSE ) {
457
458                $url = str_replace('{URL}', $wikiUriEnc, $this->interwiki[$wikiName] );
459
460            } else if ( strstr($this->interwiki[$wikiName],'{NAME}' ) !== FALSE ) {
461
462                $url = str_replace('{NAME}', $wikiUriEnc, $this->interwiki[$wikiName] );
463
464            } else {
465
466                $url = $this->interwiki[$wikiName] . urlencode($wikiUri);
467
468            }
469
470        } else {
471            // Default to Google I'm feeling lucky
472            $url = 'http://www.google.com/search?q='.urlencode($wikiUri).'&amp;btnI=lucky';
473        }
474
475        // BUILD THE LINK
476        echo '<a';
477
478        $title = $this->__getLinkTitle($title, $wikiUri, $isImage);
479
480        if ( !$isImage ) {
481            echo ' class="interwiki"';
482        } else {
483            echo ' class="media"';
484        }
485
486        echo ' href="'.$this->__xmlEntities($url).'"';
487
488        if ( FALSE !== ( $type = interwikiImgExists($wikiName) ) ) {
489            echo ' style="background: transparent url(http://wiki.splitbrain.org/interwiki/'.
490                $wikiName.'.'.$type.') 0px 1px no-repeat;"';
491        }
492
493        echo ' onclick="return svchk()" onkeypress="return svchk()">';
494
495        echo $title;
496
497        echo '</a>';
498    }
499
500    /**
501    * @TODO Correct the CSS class for files? (not windows)
502    * @TODO Remove hard coded URL to splitbrain.org
503    */
504    function filelink($link, $title = NULL) {
505        echo '<a';
506
507        $title = $this->__getLinkTitle($title, $link, $isImage);
508
509        if ( !$isImage ) {
510            echo ' class="windows"';
511        } else {
512            echo ' class="media"';
513        }
514
515        echo ' href="'.$this->__xmlEntities($link).'"';
516
517        echo ' style="background: transparent url(http://wiki.splitbrain.org/images/windows.gif) 0px 1px no-repeat;"';
518
519        echo ' onclick="return svchk()" onkeypress="return svchk()">';
520
521        echo $title;
522
523        echo '</a>';
524    }
525
526    /**
527    * @TODO Remove hard coded URL to splitbrain.org
528    * @TODO Add error message for non-IE users
529    */
530    function windowssharelink($link, $title = NULL) {
531        echo '<a';
532
533        $title = $this->__getLinkTitle($title, $link, $isImage);
534
535        if ( !$isImage ) {
536            echo ' class="windows"';
537        } else {
538            echo ' class="media"';
539        }
540
541        $link = str_replace('\\','/',$link);
542        $link = 'file:///'.$link;
543        echo ' href="'.$this->__xmlEntities($link).'"';
544
545        echo ' style="background: transparent url(http://wiki.splitbrain.org/images/windows.gif) 0px 1px no-repeat;"';
546
547        echo ' onclick="return svchk()" onkeypress="return svchk()">';
548
549        echo $title;
550
551        echo '</a>';
552    }
553
554    /**
555    * @TODO Protect email address from harvesters
556    * @TODO Remove hard coded link to splitbrain.org
557    */
558    function email($address, $title = NULL) {
559        echo '<a';
560
561        $title = $this->__getLinkTitle($title, $address, $isImage);
562
563        if ( !$isImage ) {
564            echo ' class="mail"';
565        } else {
566            echo ' class="media"';
567        }
568
569        echo ' href="mailto:'.$this->__xmlEntities($address).'"';
570
571        echo ' style="background: transparent url(http://wiki.splitbrain.org/images/mail_icon.gif) 0px 1px no-repeat;"';
572
573        echo ' onclick="return svchk()" onkeypress="return svchk()">';
574
575        echo $title;
576
577        echo '</a>';
578
579    }
580
581    /**
582    * @TODO Resolve namespaces
583    * @TODO Add image caching
584    * @TODO Remove hard coded link to splitbrain.org
585    */
586    function internalmedia (
587        $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL
588        ) {
589
590        // Sort out the namespace here...
591        if ( strpos($src,':') ) {
592            $src = explode(':',$src);
593            $src = $src[1];
594        }
595        echo '<img class="media" src="http://wiki.splitbrain.org/media/wiki/'.$this->__xmlEntities($src).'"';
596
597        if ( !is_null($title) ) {
598            echo ' title="'.$this->__xmlEntities($title).'"';
599        }
600
601        if ( !is_null($align) ) {
602            echo ' align="'.$align.'"';
603        }
604
605        if ( !is_null($width) ) {
606            echo ' width="'.$this->__xmlEntities($width).'"';
607        }
608
609        if ( !is_null($height) ) {
610            echo ' height="'.$this->__xmlEntities($height).'"';
611        }
612
613        echo '/>';
614
615    }
616
617    /**
618    * @TODO Add image caching
619    */
620    function externalmedia (
621        $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL
622        ) {
623
624        echo '<img class="media" src="'.$this->__xmlEntities($src).'"';
625
626        if ( !is_null($title) ) {
627            echo ' title="'.$this->__xmlEntities($title).'"';
628        }
629
630        if ( !is_null($align) ) {
631            echo ' align="'.$align.'"';
632        }
633
634        if ( !is_null($width) ) {
635            echo ' width="'.$this->__xmlEntities($width).'"';
636        }
637
638        if ( !is_null($height) ) {
639            echo ' height="'.$this->__xmlEntities($height).'"';
640        }
641
642        echo '/>';
643    }
644
645    // $numrows not yet implemented
646    function table_open($maxcols = NULL, $numrows = NULL){
647        echo '<table class="inline">'.DOKU_LF;
648    }
649
650    function table_close(){
651        echo '</table>'.DOKU_LF.'<br />'.DOKU_LF;
652    }
653
654    function tablerow_open(){
655        echo DOKU_TAB . '<tr>' . DOKU_LF . DOKU_TAB . DOKU_TAB;
656    }
657
658    function tablerow_close(){
659        echo DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF;
660    }
661
662    function tableheader_open($colspan = 1, $align = NULL){
663        echo '<th';
664        if ( !is_null($align) ) {
665            echo ' class="'.$align.'align"';
666        }
667        if ( $colspan > 1 ) {
668            echo ' colspan="'.$colspan.'"';
669        }
670        echo '>';
671    }
672
673    function tableheader_close(){
674        echo '</th>';
675    }
676
677    function tablecell_open($colspan = 1, $align = NULL){
678        echo '<td';
679        if ( !is_null($align) ) {
680            echo ' class="'.$align.'align"';
681        }
682        if ( $colspan > 1 ) {
683            echo ' colspan="'.$colspan.'"';
684        }
685        echo '>';
686    }
687
688    function tablecell_close(){
689        echo '</td>';
690    }
691
692    //----------------------------------------------------------
693    // Utils
694
695    /**
696     * Assembles all parts defined by the link formater below
697     * Returns HTML for the link
698     *
699     * @author Andreas Gohr <andi@splitbrain.org>
700     */
701    function __formatLink($link){
702      //make sure the url is XHTML compliant (skip mailto)
703      if(substr($link['url'],0,7) != 'mailto:'){
704        $link['url'] = str_replace('&','&amp;',$link['url']);
705        $link['url'] = str_replace('&amp;amp;','&amp;',$link['url']);
706      }
707      //remove double encodings in titles
708      $link['title'] = str_replace('&amp;amp;','&amp;',$link['title']);
709
710      $ret  = '';
711      $ret .= $link['pre'];
712      $ret .= '<a href="'.$link['url'].'"';
713      if($link['class'])  $ret .= ' class="'.$link['class'].'"';
714      if($link['target']) $ret .= ' target="'.$link['target'].'"';
715      if($link['title'])  $ret .= ' title="'.$link['title'].'"';
716      if($link['style'])  $ret .= ' style="'.$link['style'].'"';
717      if($link['more'])   $ret .= ' '.$link['more'];
718      $ret .= '>';
719      $ret .= $link['name'];
720      $ret .= '</a>';
721      $ret .= $link['suf'];
722      return $ret;
723    }
724
725    /**
726     * Removes any Namespace from the given name but keeps
727     * casing and special chars
728     *
729     * @author Andreas Gohr <andi@splitbrain.org>
730     */
731    function __simpleTitle($name){
732			global $conf;
733      if($conf['useslash']){
734        $nssep = '[:;/]';
735      }else{
736        $nssep = '[:;]';
737      }
738      return preg_replace('!.*'.$nssep.'!','',$name);
739    }
740
741
742    function __newFootnoteId() {
743        static $id = 1;
744        return $id++;
745    }
746
747    function __xmlEntities($string) {
748        return htmlspecialchars($string);
749    }
750
751    /**
752    * @TODO Tuning needed - e.g. utf8 strtolower ?
753    */
754    function __headerToLink($title) {
755        return preg_replace('/\W/','_',trim($title));
756    }
757
758    /**
759     * Adds code for section editing button
760     */
761		function __secedit($f, $t){
762        print '<!-- SECTION ['.$f.'-'.$t.'] -->';
763		}
764
765    function __getLinkTitle($title, $default, & $isImage) {
766        $isImage = FALSE;
767
768        if ( is_null($title) ) {
769            return $this->__xmlEntities($default);
770
771        } else if ( is_string($title) ) {
772
773            return $this->__xmlEntities($title);
774
775        } else if ( is_array($title) ) {
776
777            $isImage = TRUE;
778            return $this->__imageTitle($title);
779
780        }
781    }
782
783    /**
784    * @TODO Resolve namespace on internal images
785    * @TODO Remove hard coded url to splitbrain.org
786    * @TODO Image caching
787    */
788    function __imageTitle($img) {
789
790        if ( $img['type'] == 'internalmedia' ) {
791
792            // Resolve here...
793            if ( strpos($img['src'],':') ) {
794                $src = explode(':',$img['src']);
795                $src = $src[1];
796            } else {
797                $src = $img['src'];
798            }
799
800            $imgStr = '<img class="media" src="http://wiki.splitbrain.org/media/wiki/'.$this->__xmlEntities($src).'"';
801
802        } else {
803
804            $imgStr = '<img class="media" src="'.$this->__xmlEntities($img['src']).'"';
805
806        }
807
808        if ( !is_null($img['title']) ) {
809            $imgStr .= ' alt="'.$this->__xmlEntities($img['title']).'"';
810        } else {
811            $imgStr .= ' alt=""';
812        }
813
814        if ( !is_null($img['align']) ) {
815            $imgStr .= ' align="'.$img['align'].'"';
816        }
817
818        if ( !is_null($img['width']) ) {
819            $imgStr .= ' width="'.$this->__xmlEntities($img['width']).'"';
820        }
821
822        if ( !is_null($img['height']) ) {
823            $imgStr .= ' height="'.$this->__xmlEntities($img['height']).'"';
824        }
825
826        $imgStr .= '/>';
827
828        return $imgStr;
829    }
830}
831
832/**
833* Test whether there's an image to display with this interwiki link
834*/
835function interwikiImgExists($name) {
836
837    static $exists = array();
838
839    if ( array_key_exists($name,$exists) ) {
840        return $exists[$name];
841    }
842
843    if( @file_exists( DOKU. 'interwiki/'.$name.'.png') ) {
844        $exists[$name] = 'png';
845    } else if ( @file_exists( DOKU . 'interwiki/'.$name.'.gif') ) {
846        $exists[$name] = 'gif';
847    } else {
848        $exists[$name] = FALSE;
849    }
850
851    return $exists[$name];
852}
853
854/**
855 * For determining whether to use CSS class "wikilink1" or "wikilink2"
856 * @todo use configinstead of DOKU_DATA
857 * @deprecated -> resolve_pagename should be used
858 */
859function wikiPageExists($name) {
860msg("deprecated wikiPageExists called",-1);
861    static $pages = array();
862
863    if ( array_key_exists($name,$pages) ) {
864        return $pages[$name];
865    }
866
867    $file = str_replace(':','/',$name).'.txt';
868
869    if ( @file_exists( DOKU_DATA . $file ) ) {
870        $pages[$name] = TRUE;
871    } else {
872        $pages[$name] = FALSE;
873    }
874
875    return $pages[$name];
876}
877
878
879//Setup VIM: ex: et ts=2 enc=utf-8 :
880