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×$y"; 360 } 361 362 function singlequoteopening() { 363 echo "‘"; 364 } 365 366 function singlequoteclosing() { 367 echo "’"; 368 } 369 370 function doublequoteopening() { 371 echo "“"; 372 } 373 374 function doublequoteclosing() { 375 echo "”"; 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).'&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('&','&',$link['url']); 705 $link['url'] = str_replace('&amp;','&',$link['url']); 706 } 707 //remove double encodings in titles 708 $link['title'] = str_replace('&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