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