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