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($link, $title = NULL) { 386 387 echo '<a'; 388 389 $title = $this->__getLinkTitle($title,$link, $isImage); 390 391 resolve_pageid($link,$exists); 392 393 if ( !$isImage ) { 394 if ( $exists ) { 395 echo ' class="wikilink1"'; 396 } else { 397 echo ' class="wikilink2"'; 398 } 399 } else { 400 echo ' class="media"'; 401 } 402 403 echo ' href="http://wiki.splitbrain.org/'.$this->__xmlEntities($link).'"'; 404 405 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 406 407 echo $title; 408 409 echo '</a>'; 410 } 411 412 413 /** 414 * @TODO Should list assume blacklist check already made? 415 * @TODO External link icon 416 * @TODO correct attributes 417 */ 418 function externallink($link, $title = NULL) { 419 420 echo '<a'; 421 422 $title = $this->__getLinkTitle($title, $link, $isImage); 423 424 if ( !$isImage ) { 425 echo ' class="urlextern"'; 426 } else { 427 echo ' class="media"'; 428 } 429 430 echo ' target="_blank" href="'.$this->__xmlEntities($link).'"'; 431 432 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 433 434 echo $title; 435 436 echo '</a>'; 437 } 438 439 /** 440 * @TODO Remove hard coded link to splitbrain.org on style 441 */ 442 function interwikilink($link, $title = NULL, $wikiName, $wikiUri) { 443 444 // RESOLVE THE URL 445 if ( isset($this->interwiki[$wikiName]) ) { 446 447 $wikiUriEnc = urlencode($wikiUri); 448 449 if ( strstr($this->interwiki[$wikiName],'{URL}' ) !== FALSE ) { 450 451 $url = str_replace('{URL}', $wikiUriEnc, $this->interwiki[$wikiName] ); 452 453 } else if ( strstr($this->interwiki[$wikiName],'{NAME}' ) !== FALSE ) { 454 455 $url = str_replace('{NAME}', $wikiUriEnc, $this->interwiki[$wikiName] ); 456 457 } else { 458 459 $url = $this->interwiki[$wikiName] . urlencode($wikiUri); 460 461 } 462 463 } else { 464 // Default to Google I'm feeling lucky 465 $url = 'http://www.google.com/search?q='.urlencode($wikiUri).'&btnI=lucky'; 466 } 467 468 // BUILD THE LINK 469 echo '<a'; 470 471 $title = $this->__getLinkTitle($title, $wikiUri, $isImage); 472 473 if ( !$isImage ) { 474 echo ' class="interwiki"'; 475 } else { 476 echo ' class="media"'; 477 } 478 479 echo ' href="'.$this->__xmlEntities($url).'"'; 480 481 if ( FALSE !== ( $type = interwikiImgExists($wikiName) ) ) { 482 echo ' style="background: transparent url(http://wiki.splitbrain.org/interwiki/'. 483 $wikiName.'.'.$type.') 0px 1px no-repeat;"'; 484 } 485 486 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 487 488 echo $title; 489 490 echo '</a>'; 491 } 492 493 /** 494 * @TODO Correct the CSS class for files? (not windows) 495 * @TODO Remove hard coded URL to splitbrain.org 496 */ 497 function filelink($link, $title = NULL) { 498 echo '<a'; 499 500 $title = $this->__getLinkTitle($title, $link, $isImage); 501 502 if ( !$isImage ) { 503 echo ' class="windows"'; 504 } else { 505 echo ' class="media"'; 506 } 507 508 echo ' href="'.$this->__xmlEntities($link).'"'; 509 510 echo ' style="background: transparent url(http://wiki.splitbrain.org/images/windows.gif) 0px 1px no-repeat;"'; 511 512 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 513 514 echo $title; 515 516 echo '</a>'; 517 } 518 519 /** 520 * @TODO Remove hard coded URL to splitbrain.org 521 * @TODO Add error message for non-IE users 522 */ 523 function windowssharelink($link, $title = NULL) { 524 echo '<a'; 525 526 $title = $this->__getLinkTitle($title, $link, $isImage); 527 528 if ( !$isImage ) { 529 echo ' class="windows"'; 530 } else { 531 echo ' class="media"'; 532 } 533 534 $link = str_replace('\\','/',$link); 535 $link = 'file:///'.$link; 536 echo ' href="'.$this->__xmlEntities($link).'"'; 537 538 echo ' style="background: transparent url(http://wiki.splitbrain.org/images/windows.gif) 0px 1px no-repeat;"'; 539 540 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 541 542 echo $title; 543 544 echo '</a>'; 545 } 546 547 /** 548 * @TODO Protect email address from harvesters 549 * @TODO Remove hard coded link to splitbrain.org 550 */ 551 function email($address, $title = NULL) { 552 echo '<a'; 553 554 $title = $this->__getLinkTitle($title, $address, $isImage); 555 556 if ( !$isImage ) { 557 echo ' class="mail"'; 558 } else { 559 echo ' class="media"'; 560 } 561 562 echo ' href="mailto:'.$this->__xmlEntities($address).'"'; 563 564 echo ' style="background: transparent url(http://wiki.splitbrain.org/images/mail_icon.gif) 0px 1px no-repeat;"'; 565 566 echo ' onclick="return svchk()" onkeypress="return svchk()">'; 567 568 echo $title; 569 570 echo '</a>'; 571 572 } 573 574 /** 575 * @TODO Resolve namespaces 576 * @TODO Add image caching 577 * @TODO Remove hard coded link to splitbrain.org 578 */ 579 function internalmedia ( 580 $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL 581 ) { 582 583 // Sort out the namespace here... 584 if ( strpos($src,':') ) { 585 $src = explode(':',$src); 586 $src = $src[1]; 587 } 588 echo '<img class="media" src="http://wiki.splitbrain.org/media/wiki/'.$this->__xmlEntities($src).'"'; 589 590 if ( !is_null($title) ) { 591 echo ' title="'.$this->__xmlEntities($title).'"'; 592 } 593 594 if ( !is_null($align) ) { 595 echo ' align="'.$align.'"'; 596 } 597 598 if ( !is_null($width) ) { 599 echo ' width="'.$this->__xmlEntities($width).'"'; 600 } 601 602 if ( !is_null($height) ) { 603 echo ' height="'.$this->__xmlEntities($height).'"'; 604 } 605 606 echo '/>'; 607 608 } 609 610 /** 611 * @TODO Add image caching 612 */ 613 function externalmedia ( 614 $src,$title=NULL,$align=NULL,$width=NULL,$height=NULL,$cache=NULL 615 ) { 616 617 echo '<img class="media" src="'.$this->__xmlEntities($src).'"'; 618 619 if ( !is_null($title) ) { 620 echo ' title="'.$this->__xmlEntities($title).'"'; 621 } 622 623 if ( !is_null($align) ) { 624 echo ' align="'.$align.'"'; 625 } 626 627 if ( !is_null($width) ) { 628 echo ' width="'.$this->__xmlEntities($width).'"'; 629 } 630 631 if ( !is_null($height) ) { 632 echo ' height="'.$this->__xmlEntities($height).'"'; 633 } 634 635 echo '/>'; 636 } 637 638 // $numrows not yet implemented 639 function table_open($maxcols = NULL, $numrows = NULL){ 640 echo '<table class="inline">'.DOKU_LF; 641 } 642 643 function table_close(){ 644 echo '</table>'.DOKU_LF.'<br />'.DOKU_LF; 645 } 646 647 function tablerow_open(){ 648 echo DOKU_TAB . '<tr>' . DOKU_LF . DOKU_TAB . DOKU_TAB; 649 } 650 651 function tablerow_close(){ 652 echo DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF; 653 } 654 655 function tableheader_open($colspan = 1, $align = NULL){ 656 echo '<th'; 657 if ( !is_null($align) ) { 658 echo ' class="'.$align.'align"'; 659 } 660 if ( $colspan > 1 ) { 661 echo ' colspan="'.$colspan.'"'; 662 } 663 echo '>'; 664 } 665 666 function tableheader_close(){ 667 echo '</th>'; 668 } 669 670 function tablecell_open($colspan = 1, $align = NULL){ 671 echo '<td'; 672 if ( !is_null($align) ) { 673 echo ' class="'.$align.'align"'; 674 } 675 if ( $colspan > 1 ) { 676 echo ' colspan="'.$colspan.'"'; 677 } 678 echo '>'; 679 } 680 681 function tablecell_close(){ 682 echo '</td>'; 683 } 684 685 //---------------------------------------------------------- 686 // Utils 687 688 function __newFootnoteId() { 689 static $id = 1; 690 return $id++; 691 } 692 693 function __xmlEntities($string) { 694 return htmlspecialchars($string); 695 } 696 697 /** 698 * @TODO Tuning needed - e.g. utf8 strtolower ? 699 */ 700 function __headerToLink($title) { 701 return preg_replace('/\W/','_',trim($title)); 702 } 703 704 function __getLinkTitle($title, $default, & $isImage) { 705 $isImage = FALSE; 706 707 if ( is_null($title) ) { 708 709 return $this->__xmlEntities($default); 710 711 } else if ( is_string($title) ) { 712 713 return $this->__xmlEntities($title); 714 715 } else if ( is_array($title) ) { 716 717 $isImage = TRUE; 718 return $this->__imageTitle($title); 719 720 } 721 } 722 723 /** 724 * @TODO Resolve namespace on internal images 725 * @TODO Remove hard coded url to splitbrain.org 726 * @TODO Image caching 727 */ 728 function __imageTitle($img) { 729 730 if ( $img['type'] == 'internalmedia' ) { 731 732 // Resolve here... 733 if ( strpos($img['src'],':') ) { 734 $src = explode(':',$img['src']); 735 $src = $src[1]; 736 } else { 737 $src = $img['src']; 738 } 739 740 $imgStr = '<img class="media" src="http://wiki.splitbrain.org/media/wiki/'.$this->__xmlEntities($src).'"'; 741 742 } else { 743 744 $imgStr = '<img class="media" src="'.$this->__xmlEntities($img['src']).'"'; 745 746 } 747 748 if ( !is_null($img['title']) ) { 749 $imgStr .= ' alt="'.$this->__xmlEntities($img['title']).'"'; 750 } else { 751 $imgStr .= ' alt=""'; 752 } 753 754 if ( !is_null($img['align']) ) { 755 $imgStr .= ' align="'.$img['align'].'"'; 756 } 757 758 if ( !is_null($img['width']) ) { 759 $imgStr .= ' width="'.$this->__xmlEntities($img['width']).'"'; 760 } 761 762 if ( !is_null($img['height']) ) { 763 $imgStr .= ' height="'.$this->__xmlEntities($img['height']).'"'; 764 } 765 766 $imgStr .= '/>'; 767 768 return $imgStr; 769 } 770} 771 772/** 773* Test whether there's an image to display with this interwiki link 774*/ 775function interwikiImgExists($name) { 776 777 static $exists = array(); 778 779 if ( array_key_exists($name,$exists) ) { 780 return $exists[$name]; 781 } 782 783 if( @file_exists( DOKU. 'interwiki/'.$name.'.png') ) { 784 $exists[$name] = 'png'; 785 } else if ( @file_exists( DOKU . 'interwiki/'.$name.'.gif') ) { 786 $exists[$name] = 'gif'; 787 } else { 788 $exists[$name] = FALSE; 789 } 790 791 return $exists[$name]; 792} 793 794/** 795 * For determining whether to use CSS class "wikilink1" or "wikilink2" 796 * @todo use configinstead of DOKU_DATA 797 * @deprecated -> resolve_pagename should be used 798 */ 799function wikiPageExists($name) { 800 801 static $pages = array(); 802 803 if ( array_key_exists($name,$pages) ) { 804 return $pages[$name]; 805 } 806 807 $file = str_replace(':','/',$name).'.txt'; 808 809 if ( @file_exists( DOKU_DATA . $file ) ) { 810 $pages[$name] = TRUE; 811 } else { 812 $pages[$name] = FALSE; 813 } 814 815 return $pages[$name]; 816} 817 818