1<?php 2if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); 3 4class Doku_Handler { 5 6 var $Renderer = NULL; 7 8 var $CallWriter = NULL; 9 10 var $calls = array(); 11 12 var $meta = array( 13 'section' => FALSE, 14 'toc' => TRUE, 15 ); 16 17 function Doku_Handler() { 18 $this->CallWriter = & new Doku_Handler_CallWriter($this); 19 } 20 21 function __addCall($handler, $args, $pos) { 22 $call = array($handler,$args, $pos); 23 $this->CallWriter->writeCall($call); 24 } 25 26 function __finalize(){ 27 if ( $this->meta['section'] ) { 28 $S = & new Doku_Handler_Section(); 29 $this->calls = $S->process($this->calls); 30 } 31 32 $B = & new Doku_Handler_Block(); 33 $this->calls = $B->process($this->calls); 34 35 if ( $this->meta['toc'] ) { 36 $T = & new Doku_Handler_Toc(); 37 $this->calls = $T->process($this->calls); 38 } 39 40 array_unshift($this->calls,array('document_start',array(),0)); 41 $last_call = end($this->calls); 42 array_push($this->calls,array('document_end',array(),$last_call[2])); 43 } 44 45 function fetch() { 46 $call = each($this->calls); 47 if ( $call ) { 48 return $call['value']; 49 } 50 return FALSE; 51 } 52 53 function base($match, $state, $pos) { 54 switch ( $state ) { 55 case DOKU_LEXER_UNMATCHED: 56 $this->__addCall('cdata',array($match), $pos); 57 return TRUE; 58 break; 59 60 } 61 } 62 63 function header($match, $state, $pos) { 64 $match = trim($match); 65 $levels = array( 66 '======'=>1, 67 '====='=>2, 68 '===='=>3, 69 '==='=>4, 70 '=='=>5, 71 ); 72 $hsplit = preg_split( '/(={2,})/u', $match,-1, 73 PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); 74 75 // Locate the level - default to level 1 if no match (title contains == signs) 76 if ( isset($hsplit[0]) && array_key_exists($hsplit[0], $levels) ) { 77 $level = $levels[$hsplit[0]]; 78 } else { 79 $level = 1; 80 } 81 82 // Strip of the marker for the header, based on the level - the rest is the title 83 $iLevels = array_flip($levels); 84 $markerLen = strlen($iLevels[$level]); 85 $title = substr($match, $markerLen, strlen($match)-($markerLen*2)); 86 87 $this->__addCall('header',array($title,$level), $pos); 88 $this->meta['section'] = TRUE; 89 return TRUE; 90 } 91 92 function notoc($match, $state, $pos) { 93 $this->meta['toc'] = FALSE; 94 return TRUE; 95 } 96 97 function linebreak($match, $state, $pos) { 98 $this->__addCall('linebreak',array(),$pos); 99 return TRUE; 100 } 101 102 function eol($match, $state, $pos) { 103 $this->__addCall('eol',array(),$pos); 104 return TRUE; 105 } 106 107 function hr($match, $state, $pos) { 108 $this->__addCall('hr',array(),$pos); 109 return TRUE; 110 } 111 112 function __nestingTag($match, $state, $pos, $name) { 113 switch ( $state ) { 114 case DOKU_LEXER_ENTER: 115 $this->__addCall($name.'_open', array(), $pos); 116 break; 117 case DOKU_LEXER_EXIT: 118 $this->__addCall($name.'_close', array(), $pos); 119 break; 120 case DOKU_LEXER_UNMATCHED: 121 $this->__addCall('cdata',array($match), $pos); 122 break; 123 } 124 } 125 126 function strong($match, $state, $pos) { 127 $this->__nestingTag($match, $state, $pos, 'strong'); 128 return TRUE; 129 } 130 131 function emphasis($match, $state, $pos) { 132 $this->__nestingTag($match, $state, $pos, 'emphasis'); 133 return TRUE; 134 } 135 136 function underline($match, $state, $pos) { 137 $this->__nestingTag($match, $state, $pos, 'underline'); 138 return TRUE; 139 } 140 141 function monospace($match, $state, $pos) { 142 $this->__nestingTag($match, $state, $pos, 'monospace'); 143 return TRUE; 144 } 145 146 function subscript($match, $state, $pos) { 147 $this->__nestingTag($match, $state, $pos, 'subscript'); 148 return TRUE; 149 } 150 151 function superscript($match, $state, $pos) { 152 $this->__nestingTag($match, $state, $pos, 'superscript'); 153 return TRUE; 154 } 155 156 function deleted($match, $state, $pos) { 157 $this->__nestingTag($match, $state, $pos, 'deleted'); 158 return TRUE; 159 } 160 161 162 function footnote($match, $state, $pos) { 163 $this->__nestingTag($match, $state, $pos, 'footnote'); 164 return TRUE; 165 } 166 167 function listblock($match, $state, $pos) { 168 switch ( $state ) { 169 case DOKU_LEXER_ENTER: 170 $ReWriter = & new Doku_Handler_List($this->CallWriter); 171 $this->CallWriter = & $ReWriter; 172 $this->__addCall('list_open', array($match), $pos); 173 break; 174 case DOKU_LEXER_EXIT: 175 $this->__addCall('list_close', array(), $pos); 176 $this->CallWriter->process(); 177 $ReWriter = & $this->CallWriter; 178 $this->CallWriter = & $ReWriter->CallWriter; 179 break; 180 case DOKU_LEXER_MATCHED: 181 $this->__addCall('list_item', array($match), $pos); 182 break; 183 case DOKU_LEXER_UNMATCHED: 184 $this->__addCall('cdata', array($match), $pos); 185 break; 186 } 187 return TRUE; 188 } 189 190 function unformatted($match, $state, $pos) { 191 if ( $state == DOKU_LEXER_UNMATCHED ) { 192 $this->__addCall('unformatted',array($match), $pos); 193 } 194 return TRUE; 195 } 196 197 function php($match, $state, $pos) { 198 if ( $state == DOKU_LEXER_UNMATCHED ) { 199 $this->__addCall('php',array($match), $pos); 200 } 201 return TRUE; 202 } 203 204 function html($match, $state, $pos) { 205 if ( $state == DOKU_LEXER_UNMATCHED ) { 206 $this->__addCall('html',array($match), $pos); 207 } 208 return TRUE; 209 } 210 211 function preformatted($match, $state, $pos) { 212 switch ( $state ) { 213 case DOKU_LEXER_ENTER: 214 $ReWriter = & new Doku_Handler_Preformatted($this->CallWriter); 215 $this->CallWriter = & $ReWriter; 216 $this->__addCall('preformatted_start',array(), $pos); 217 break; 218 case DOKU_LEXER_EXIT: 219 $this->__addCall('preformatted_end',array(), $pos); 220 $this->CallWriter->process(); 221 $ReWriter = & $this->CallWriter; 222 $this->CallWriter = & $ReWriter->CallWriter; 223 break; 224 case DOKU_LEXER_MATCHED: 225 $this->__addCall('preformatted_newline',array(), $pos); 226 break; 227 case DOKU_LEXER_UNMATCHED: 228 $this->__addCall('preformatted_content',array($match), $pos); 229 break; 230 } 231 232 return TRUE; 233 } 234 235 function file($match, $state, $pos) { 236 if ( $state == DOKU_LEXER_UNMATCHED ) { 237 $this->__addCall('file',array($match), $pos); 238 } 239 return TRUE; 240 } 241 242 function quote($match, $state, $pos) { 243 244 switch ( $state ) { 245 246 case DOKU_LEXER_ENTER: 247 $ReWriter = & new Doku_Handler_Quote($this->CallWriter); 248 $this->CallWriter = & $ReWriter; 249 $this->__addCall('quote_start',array($match), $pos); 250 break; 251 252 case DOKU_LEXER_EXIT: 253 $this->__addCall('quote_end',array(), $pos); 254 $this->CallWriter->process(); 255 $ReWriter = & $this->CallWriter; 256 $this->CallWriter = & $ReWriter->CallWriter; 257 break; 258 259 case DOKU_LEXER_MATCHED: 260 $this->__addCall('quote_newline',array($match), $pos); 261 break; 262 263 case DOKU_LEXER_UNMATCHED: 264 $this->__addCall('cdata',array($match), $pos); 265 break; 266 267 } 268 269 return TRUE; 270 } 271 272 function code($match, $state, $pos) { 273 switch ( $state ) { 274 case DOKU_LEXER_UNMATCHED: 275 $matches = preg_split('/>/u',$match,2); 276 $matches[0] = trim($matches[0]); 277 if ( trim($matches[0]) == '' ) { 278 $matches[0] = NULL; 279 } 280 # $matches[0] contains name of programming language 281 # if available 282 $this->__addCall( 283 'code', 284 array($matches[1],$matches[0]), 285 $pos 286 ); 287 break; 288 } 289 return TRUE; 290 } 291 292 function acronym($match, $state, $pos) { 293 $this->__addCall('acronym',array($match), $pos); 294 return TRUE; 295 } 296 297 function smiley($match, $state, $pos) { 298 $this->__addCall('smiley',array($match), $pos); 299 return TRUE; 300 } 301 302 function wordblock($match, $state, $pos) { 303 $this->__addCall('wordblock',array($match), $pos); 304 return TRUE; 305 } 306 307 function entity($match, $state, $pos) { 308 $this->__addCall('entity',array($match), $pos); 309 return TRUE; 310 } 311 312 function multiplyentity($match, $state, $pos) { 313 preg_match_all('/\d+/',$match,$matches); 314 $this->__addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos); 315 return TRUE; 316 } 317 318 function singlequoteopening($match, $state, $pos) { 319 $this->__addCall('singlequoteopening',array(), $pos); 320 return TRUE; 321 } 322 323 function singlequoteclosing($match, $state, $pos) { 324 $this->__addCall('singlequoteclosing',array(), $pos); 325 return TRUE; 326 } 327 328 function doublequoteopening($match, $state, $pos) { 329 $this->__addCall('doublequoteopening',array(), $pos); 330 return TRUE; 331 } 332 333 function doublequoteclosing($match, $state, $pos) { 334 $this->__addCall('doublequoteclosing',array(), $pos); 335 return TRUE; 336 } 337 338 function camelcaselink($match, $state, $pos) { 339 $this->__addCall('camelcaselink',array($match), $pos); 340 return TRUE; 341 } 342 343 /* 344 */ 345 function internallink($match, $state, $pos) { 346 // Strip the opening and closing markup 347 $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match); 348 349 // Split title from URL 350 $link = preg_split('/\|/u',$link,2); 351 if ( !isset($link[1]) ) { 352 $link[1] = NULL; 353 354 // If the title is an image, convert it to an array containing the image details 355 } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) { 356 $media = Doku_Handler_Parse_Media($link[1]); 357 358 // Check it's really an image, not a link to a file 359 if ( !strpos($media['type'], 'link') ) { 360 $link[1] = $media; 361 } 362 } 363 364 //decide which kind of link it is 365 366 if ( preg_match('/^[a-zA-Z]+>{1}[\w()\/\\#~:.?+=&%@!\-;,]+$/u',$link[0]) ) { 367 // Interwiki 368 $interwiki = preg_split('/>/u',$link[0]); 369 $this->__addCall( 370 'interwikilink', 371 array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), 372 $pos 373 ); 374 }elseif ( preg_match('/\\\\\\\\[\w.:?\-;,]+?\\\\/u',$link[0]) ) { 375 // Windows Share 376 $this->__addCall( 377 'windowssharelink', 378 array($link[0],$link[1]), 379 $pos 380 ); 381 }elseif ( preg_match('#([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i',$link[0]) ) { 382 // E-Mail 383 $this->__addCall( 384 'emaillink', 385 array($link[0],$link[1]), 386 $pos 387 ); 388 }elseif ( preg_match('#^([a-z0-9]+?)://#i',$link[0]) ) { 389 // external link (accepts all protocols) 390 $this->__addCall( 391 'externallink', 392 array($link[0],$link[1]), 393 $pos 394 ); 395 }else{ 396 // internal link 397 $this->__addCall( 398 'internallink', 399 array($link[0],$link[1]), 400 $pos 401 ); 402 } 403 404 return TRUE; 405 } 406 407 function filelink($match, $state, $pos) { 408 $this->__addCall('filelink',array($match, NULL), $pos); 409 return TRUE; 410 } 411 412 function windowssharelink($match, $state, $pos) { 413 $this->__addCall('windowssharelink',array($match, NULL), $pos); 414 return TRUE; 415 } 416 417 function media($match, $state, $pos) { 418 $p = Doku_Handler_Parse_Media($match); 419 420 // If it's not an image, build a normal link 421 if ( strpos($p['type'], 'link') ) { 422 $this->__addCall($p['type'],array($p['src'], $p['title']), $pos); 423 } else { 424 $this->__addCall( 425 $p['type'], 426 array($p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache']), 427 $pos 428 ); 429 } 430 return TRUE; 431 } 432 433 function externallink($match, $state, $pos) { 434 // Prevent use of multibyte strings in URLs 435 // See: http://www.boingboing.net/2005/02/06/shmoo_group_exploit_.html 436 // Not worried about other charsets so long as page is output as UTF-8 437 /*if ( strlen($match) != utf8_strlen($match) ) { 438 $this->__addCall('cdata',array($match), $pos); 439 } else {*/ 440 441 $this->__addCall('externallink',array($match, NULL), $pos); 442 //} 443 return TRUE; 444 } 445 446 function email($match, $state, $pos) { 447 $email = preg_replace(array('/^</','/>$/'),'',$match); 448 $this->__addCall('email',array($email, NULL), $pos); 449 return TRUE; 450 } 451 452 function table($match, $state, $pos) { 453 switch ( $state ) { 454 455 case DOKU_LEXER_ENTER: 456 457 $ReWriter = & new Doku_Handler_Table($this->CallWriter); 458 $this->CallWriter = & $ReWriter; 459 460 $this->__addCall('table_start', array(), $pos); 461 //$this->__addCall('table_row', array(), $pos); 462 if ( trim($match) == '^' ) { 463 $this->__addCall('tableheader', array(), $pos); 464 } else { 465 $this->__addCall('tablecell', array(), $pos); 466 } 467 break; 468 469 case DOKU_LEXER_EXIT: 470 $this->__addCall('table_end', array(), $pos); 471 $this->CallWriter->process(); 472 $ReWriter = & $this->CallWriter; 473 $this->CallWriter = & $ReWriter->CallWriter; 474 break; 475 476 case DOKU_LEXER_UNMATCHED: 477 if ( trim($match) != '' ) { 478 $this->__addCall('cdata',array($match), $pos); 479 } 480 break; 481 482 case DOKU_LEXER_MATCHED: 483 if ( preg_match('/.{2}/',$match) ) { 484 $this->__addCall('table_align', array($match), $pos); 485 } else if ( $match == "\n|" ) { 486 $this->__addCall('table_row', array(), $pos); 487 $this->__addCall('tablecell', array(), $pos); 488 } else if ( $match == "\n^" ) { 489 $this->__addCall('table_row', array(), $pos); 490 $this->__addCall('tableheader', array(), $pos); 491 } else if ( $match == '|' ) { 492 $this->__addCall('tablecell', array(), $pos); 493 } else if ( $match == '^' ) { 494 $this->__addCall('tableheader', array(), $pos); 495 } 496 break; 497 } 498 return TRUE; 499 } 500} 501 502//------------------------------------------------------------------------ 503function Doku_Handler_Parse_Media($match) { 504 505 // Strip the opening and closing markup 506 $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); 507 508 // Split title from URL 509 $link = preg_split('/\|/u',$link,2); 510 511 512 // Check alignment 513 $ralign = (bool)preg_match('/^ /',$link[0]); 514 $lalign = (bool)preg_match('/ $/',$link[0]); 515 516 // Logic = what's that ;)... 517 if ( $lalign & $ralign ) { 518 $align = 'center'; 519 } else if ( $ralign ) { 520 $align = 'right'; 521 } else if ( $lalign ) { 522 $align = 'left'; 523 } else { 524 $align = NULL; 525 } 526 527 // The title... 528 if ( !isset($link[1]) ) { 529 $link[1] = NULL; 530 } 531 532 // img src url from params 533 // What if it's an external image where URL contains '?' char? 534 $src = preg_split('/\?/u',$link[0],2); 535 536 // Strip any alignment whitespace 537 $src[0] = trim($src[0]); 538 539 // Check for width, height and caching params 540 if ( isset($src[1]) ) { 541 542 if(preg_match('#(\d*)(x(\d*))?#i',$src[1],$matches)){ 543 544 if(isset($matches[1])) { 545 $width = $matches[1]; 546 } else { 547 $width = NULL; 548 } 549 550 if(isset($matches[3])) { 551 $height = $matches[3]; 552 } else { 553 $height = NULL; 554 } 555 556 $cache = !(bool)preg_match('/nocache/i',$src[1]); 557 } 558 559 } else { 560 $width = NULL; 561 $height = NULL; 562 $cache = TRUE; 563 } 564 565 // Check whether this is a local or remote image 566 if ( substr($src[0],0,4) == 'http' ) { 567 $call = 'external'; 568 } else { 569 $call = 'internal'; 570 } 571 572 // Check this is actually an image... 573 if ( !preg_match('/\.(gif|png|jpe?g)$/',$src[0] ) ) { 574 // Security implications?... 575 $call .= 'link'; 576 } else { 577 $call .= 'media'; 578 } 579 580 $params = array( 581 'type'=>$call, 582 'src'=>$src[0], 583 'title'=>$link[1], 584 'align'=>$align, 585 'width'=>$width, 586 'height'=>$height, 587 'cache'=>$cache, 588 ); 589 590 return $params; 591} 592 593//------------------------------------------------------------------------ 594class Doku_Handler_CallWriter { 595 596 var $Handler; 597 598 function Doku_Handler_CallWriter(& $Handler) { 599 $this->Handler = & $Handler; 600 } 601 602 function writeCall($call) { 603 $this->Handler->calls[] = $call; 604 } 605 606 function writeCalls($calls) { 607 $this->Handler->calls = array_merge($this->Handler->calls, $calls); 608 } 609} 610 611//------------------------------------------------------------------------ 612class Doku_Handler_List { 613 614 var $CallWriter; 615 616 var $calls = array(); 617 var $listCalls = array(); 618 var $listStack = array(); 619 620 function Doku_Handler_List(& $CallWriter) { 621 $this->CallWriter = & $CallWriter; 622 } 623 624 function writeCall($call) { 625 $this->calls[] = $call; 626 } 627 628 // Probably not needed but just in case... 629 function writeCalls($calls) { 630 $this->calls = array_merge($this->calls, $calls); 631 $this->CallWriter->writeCalls($this->calls); 632 } 633 634 //------------------------------------------------------------------------ 635 function process() { 636 foreach ( $this->calls as $call ) { 637 switch ($call[0]) { 638 case 'list_item': 639 $this->listOpen($call); 640 break; 641 case 'list_open': 642 $this->listStart($call); 643 break; 644 case 'list_close': 645 $this->listEnd($call); 646 break; 647 default: 648 $this->listContent($call); 649 break; 650 } 651 } 652 653 $this->CallWriter->writeCalls($this->listCalls); 654 } 655 656 //------------------------------------------------------------------------ 657 function listStart($call) { 658 $depth = $this->interpretSyntax($call[1][0], $listType); 659 660 $this->initialDepth = $depth; 661 $this->listStack[] = array($listType, $depth); 662 663 $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); 664 $this->listCalls[] = array('listitem_open',array(1),$call[2]); 665 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 666 } 667 668 //------------------------------------------------------------------------ 669 function listEnd($call) { 670 $closeContent = TRUE; 671 672 while ( $list = array_pop($this->listStack) ) { 673 if ( $closeContent ) { 674 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 675 $closeContent = FALSE; 676 } 677 $this->listCalls[] = array('listitem_close',array(),$call[2]); 678 $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); 679 } 680 } 681 682 //------------------------------------------------------------------------ 683 function listOpen($call) { 684 $depth = $this->interpretSyntax($call[1][0], $listType); 685 $end = end($this->listStack); 686 687 // Not allowed to be shallower than initialDepth 688 if ( $depth < $this->initialDepth ) { 689 $depth = $this->initialDepth; 690 } 691 692 //------------------------------------------------------------------------ 693 if ( $depth == $end[1] ) { 694 695 // Just another item in the list... 696 if ( $listType == $end[0] ) { 697 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 698 $this->listCalls[] = array('listitem_close',array(),$call[2]); 699 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 700 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 701 702 // Switched list type... 703 } else { 704 705 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 706 $this->listCalls[] = array('listitem_close',array(),$call[2]); 707 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 708 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 709 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 710 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 711 712 array_pop($this->listStack); 713 $this->listStack[] = array($listType, $depth); 714 } 715 716 //------------------------------------------------------------------------ 717 // Getting deeper... 718 } else if ( $depth > $end[1] ) { 719 720 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 721 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 722 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 723 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 724 725 $this->listStack[] = array($listType, $depth); 726 727 //------------------------------------------------------------------------ 728 // Getting shallower ( $depth < $end[1] ) 729 } else { 730 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 731 $this->listCalls[] = array('listitem_close',array(),$call[2]); 732 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 733 734 // Throw away the end - done 735 array_pop($this->listStack); 736 737 while (1) { 738 $end = end($this->listStack); 739 740 if ( $end[1] <= $depth ) { 741 742 // Normalize depths 743 $depth = $end[1]; 744 745 $this->listCalls[] = array('listitem_close',array(),$call[2]); 746 747 if ( $end[0] == $listType ) { 748 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 749 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 750 751 } else { 752 // Switching list type... 753 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 754 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 755 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 756 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 757 758 array_pop($this->listStack); 759 $this->listStack[] = array($listType, $depth); 760 } 761 762 break; 763 764 // Haven't dropped down far enough yet.... ( $end[1] > $depth ) 765 } else { 766 767 $this->listCalls[] = array('listitem_close',array(),$call[2]); 768 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 769 770 array_pop($this->listStack); 771 772 } 773 774 } 775 776 } 777 } 778 779 //------------------------------------------------------------------------ 780 function listContent($call) { 781 $this->listCalls[] = $call; 782 } 783 784 //------------------------------------------------------------------------ 785 function interpretSyntax($match, & $type) { 786 if ( substr($match,-1) == '*' ) { 787 $type = 'u'; 788 } else { 789 $type = 'o'; 790 } 791 return count(explode(' ',str_replace("\t",' ',$match))); 792 } 793} 794 795//------------------------------------------------------------------------ 796class Doku_Handler_Preformatted { 797 798 var $CallWriter; 799 800 var $calls = array(); 801 var $pos; 802 var $text =''; 803 804 805 806 function Doku_Handler_Preformatted(& $CallWriter) { 807 $this->CallWriter = & $CallWriter; 808 } 809 810 function writeCall($call) { 811 $this->calls[] = $call; 812 } 813 814 // Probably not needed but just in case... 815 function writeCalls($calls) { 816 $this->calls = array_merge($this->calls, $calls); 817 $this->CallWriter->writeCalls($this->calls); 818 } 819 820 function process() { 821 foreach ( $this->calls as $call ) { 822 switch ($call[0]) { 823 case 'preformatted_start': 824 $this->pos = $call[2]; 825 break; 826 case 'preformatted_newline': 827 $this->text .= "\n"; 828 break; 829 case 'preformatted_content': 830 $this->text .= $call[1][0]; 831 break; 832 case 'preformatted_end': 833 $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos)); 834 break; 835 } 836 } 837 } 838} 839 840//------------------------------------------------------------------------ 841class Doku_Handler_Quote { 842 843 var $CallWriter; 844 845 var $calls = array(); 846 847 var $quoteCalls = array(); 848 849 function Doku_Handler_Quote(& $CallWriter) { 850 $this->CallWriter = & $CallWriter; 851 } 852 853 function writeCall($call) { 854 $this->calls[] = $call; 855 } 856 857 // Probably not needed but just in case... 858 function writeCalls($calls) { 859 $this->calls = array_merge($this->calls, $calls); 860 $this->CallWriter->writeCalls($this->calls); 861 } 862 863 function process() { 864 865 $quoteDepth = 1; 866 867 foreach ( $this->calls as $call ) { 868 switch ($call[0]) { 869 870 case 'quote_start': 871 872 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 873 874 case 'quote_newline': 875 876 $quoteLength = $this->getDepth($call[1][0]); 877 878 if ( $quoteLength > $quoteDepth ) { 879 $quoteDiff = $quoteLength - $quoteDepth; 880 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 881 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 882 } 883 } else if ( $quoteLength < $quoteDepth ) { 884 $quoteDiff = $quoteDepth - $quoteLength; 885 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 886 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 887 } 888 } 889 890 $quoteDepth = $quoteLength; 891 892 break; 893 894 case 'quote_end': 895 896 if ( $quoteDepth > 1 ) { 897 $quoteDiff = $quoteDepth - 1; 898 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 899 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 900 } 901 } 902 903 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 904 905 $this->CallWriter->writeCalls($this->quoteCalls); 906 break; 907 908 default: 909 $this->quoteCalls[] = $call; 910 break; 911 } 912 } 913 } 914 915 function getDepth($marker) { 916 preg_match('/>{1,}/', $marker, $matches); 917 $quoteLength = strlen($matches[0]); 918 return $quoteLength; 919 } 920} 921 922//------------------------------------------------------------------------ 923class Doku_Handler_Table { 924 925 var $CallWriter; 926 927 var $calls = array(); 928 var $tableCalls = array(); 929 var $maxCols = 0; 930 var $maxRows = 1; 931 var $currentCols = 0; 932 var $firstCell = FALSE; 933 var $lastCellType = 'tablecell'; 934 935 function Doku_Handler_Table(& $CallWriter) { 936 $this->CallWriter = & $CallWriter; 937 } 938 939 function writeCall($call) { 940 $this->calls[] = $call; 941 } 942 943 // Probably not needed but just in case... 944 function writeCalls($calls) { 945 $this->calls = array_merge($this->calls, $calls); 946 $this->CallWriter->writeCalls($this->calls); 947 } 948 949 //------------------------------------------------------------------------ 950 function process() { 951 foreach ( $this->calls as $call ) { 952 switch ( $call[0] ) { 953 case 'table_start': 954 $this->tableStart($call); 955 break; 956 case 'table_row': 957 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 958 $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); 959 break; 960 case 'tableheader': 961 case 'tablecell': 962 $this->tableCell($call); 963 break; 964 case 'table_end': 965 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 966 $this->tableEnd($call); 967 break; 968 default: 969 $this->tableDefault($call); 970 break; 971 } 972 } 973 $this->CallWriter->writeCalls($this->tableCalls); 974 } 975 976 function tableStart($call) { 977 $this->tableCalls[] = array('table_open',array(),$call[2]); 978 $this->tableCalls[] = array('tablerow_open',array(),$call[2]); 979 $this->firstCell = TRUE; 980 } 981 982 function tableEnd($call) { 983 $this->tableCalls[] = array('table_close',array(),$call[2]); 984 $this->finalizeTable(); 985 } 986 987 function tableRowOpen($call) { 988 $this->tableCalls[] = $call; 989 $this->currentCols = 0; 990 $this->firstCell = TRUE; 991 $this->lastCellType = 'tablecell'; 992 $this->maxRows++; 993 } 994 995 function tableRowClose($call) { 996 // Strip off final cell opening and anything after it 997 while ( $discard = array_pop($this->tableCalls ) ) { 998 999 if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { 1000 1001 // Its a spanning element - put it back and close it 1002 if ( $discard[1][0] > 1 ) { 1003 1004 $this->tableCalls[] = $discard; 1005 if ( strstr($discard[0],'cell') ) { 1006 $name = 'tablecell'; 1007 } else { 1008 $name = 'tableheader'; 1009 } 1010 $this->tableCalls[] = array($name.'_close',array(),$call[2]); 1011 } 1012 1013 break; 1014 } 1015 } 1016 $this->tableCalls[] = $call; 1017 1018 if ( $this->currentCols > $this->maxCols ) { 1019 $this->maxCols = $this->currentCols; 1020 } 1021 } 1022 1023 function tableCell($call) { 1024 if ( !$this->firstCell ) { 1025 1026 // Increase the span 1027 $lastCall = end($this->tableCalls); 1028 1029 // A cell call which follows an open cell means an empty cell so span 1030 if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) { 1031 $this->tableCalls[] = array('colspan',array(),$call[2]); 1032 1033 } 1034 1035 $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); 1036 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1037 $this->lastCellType = $call[0]; 1038 1039 } else { 1040 1041 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1042 $this->lastCellType = $call[0]; 1043 $this->firstCell = FALSE; 1044 1045 } 1046 1047 $this->currentCols++; 1048 } 1049 1050 function tableDefault($call) { 1051 $this->tableCalls[] = $call; 1052 } 1053 1054 function finalizeTable() { 1055 1056 // Add the max cols and rows to the table opening 1057 if ( $this->tableCalls[0][0] == 'table_open' ) { 1058 // Adjust to num cols not num col delimeters 1059 $this->tableCalls[0][1][] = $this->maxCols - 1; 1060 $this->tableCalls[0][1][] = $this->maxRows; 1061 } else { 1062 trigger_error('First element in table call list is not table_open'); 1063 } 1064 1065 $lastRow = 0; 1066 $lastCell = 0; 1067 $toDelete = array(); 1068 1069 // Look for the colspan elements and increment the colspan on the 1070 // previous non-empty opening cell. Once done, delete all the cells 1071 // that contain colspans 1072 foreach ( $this->tableCalls as $key => $call ) { 1073 1074 if ( $call[0] == 'tablerow_open' ) { 1075 1076 $lastRow = $key; 1077 1078 } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) { 1079 1080 $lastCell = $key; 1081 1082 } else if ( $call[0] == 'table_align' ) { 1083 1084 // If the previous element was a cell open, align right 1085 if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) { 1086 $this->tableCalls[$key-1][1][1] = 'right'; 1087 1088 // If the next element if the close of an element, align either center or left 1089 } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) { 1090 if ( $this->tableCalls[$lastCell][1][1] == 'right' ) { 1091 $this->tableCalls[$lastCell][1][1] = 'center'; 1092 } else { 1093 $this->tableCalls[$lastCell][1][1] = 'left'; 1094 } 1095 1096 } 1097 1098 // Now convert the whitespace back to cdata 1099 $this->tableCalls[$key][0] = 'cdata'; 1100 1101 } else if ( $call[0] == 'colspan' ) { 1102 1103 $this->tableCalls[$key-1][1][0] = FALSE; 1104 1105 for($i = $key-2; $i > $lastRow; $i--) { 1106 1107 if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) { 1108 1109 if ( FALSE !== $this->tableCalls[$i][1][0] ) { 1110 $this->tableCalls[$i][1][0]++; 1111 break; 1112 } 1113 1114 1115 } 1116 } 1117 1118 $toDelete[] = $key-1; 1119 $toDelete[] = $key; 1120 $toDelete[] = $key+1; 1121 } 1122 } 1123 1124 foreach ( $toDelete as $delete ) { 1125 unset($this->tableCalls[$delete]); 1126 } 1127 1128 $this->tableCalls = array_values($this->tableCalls); 1129 } 1130} 1131 1132//------------------------------------------------------------------------ 1133class Doku_Handler_Section { 1134 1135 function process($calls) { 1136 1137 $sectionCalls = array(); 1138 $inSection = FALSE; 1139 1140 foreach ( $calls as $call ) { 1141 1142 if ( $call[0] == 'header' ) { 1143 1144 if ( $inSection ) { 1145 $sectionCalls[] = array('section_close',array(), $call[2]); 1146 } 1147 1148 $sectionCalls[] = $call; 1149 $sectionCalls[] = array('section_open',array($call[1][1]), $call[2]); 1150 $inSection = TRUE; 1151 1152 } else { 1153 $sectionCalls[] = $call; 1154 } 1155 } 1156 1157 if ( $inSection ) { 1158 $sectionCalls[] = array('section_close',array(), $call[2]); 1159 } 1160 1161 return $sectionCalls; 1162 } 1163 1164} 1165 1166//------------------------------------------------------------------------ 1167class Doku_Handler_Block { 1168 1169 var $calls = array(); 1170 1171 var $blockStack = array(); 1172 1173 var $inParagraph = FALSE; 1174 var $atStart = TRUE; 1175 1176 // Blocks don't contain linefeeds 1177 var $blockOpen = array( 1178 'header', 1179 'listu_open','listo_open','listitem_open', 1180 'table_open','tablerow_open','tablecell_open','tableheader_open', 1181 'quote_open', 1182 'section_open', // Needed to prevent p_open between header and section_open 1183 'code','file','php','html','hr','preformatted', 1184 ); 1185 1186 var $blockClose = array( 1187 'header', 1188 'listu_close','listo_close','listitem_close', 1189 'table_close','tablerow_close','tablecell_close','tableheader_close', 1190 'quote_close', 1191 'section_close', // Needed to prevent p_close after section_close 1192 'code','file','php','html','hr','preformatted', 1193 ); 1194 1195 // Stacks can contain linefeeds 1196 var $stackOpen = array( 1197 'footnote_open','section_open', 1198 ); 1199 1200 var $stackClose = array( 1201 'footnote_close','section_close', 1202 ); 1203 1204 function process($calls) { 1205 foreach ( $calls as $key => $call ) { 1206 1207 // Process blocks which are stack like... (contain linefeeds) 1208 if ( in_array($call[0],$this->stackOpen ) ) { 1209 /* 1210 if ( $this->atStart ) { 1211 $this->calls[] = array('p_open',array(), $call[2]); 1212 $this->atStart = FALSE; 1213 $this->inParagraph = TRUE; 1214 } 1215 */ 1216 $this->calls[] = $call; 1217 1218 // Hack - footnotes shouldn't immediately contain a p_open 1219 if ( $call[0] != 'footnote_open' ) { 1220 $this->addToStack(); 1221 } else { 1222 $this->addToStack(FALSE); 1223 } 1224 continue; 1225 } 1226 1227 if ( in_array($call[0],$this->stackClose ) ) { 1228 1229 if ( $this->inParagraph ) { 1230 $this->calls[] = array('p_close',array(), $call[2]); 1231 } 1232 $this->calls[] = $call; 1233 $this->removeFromStack(); 1234 continue; 1235 } 1236 1237 if ( !$this->atStart ) { 1238 1239 if ( $call[0] == 'eol' ) { 1240 1241 if ( $this->inParagraph ) { 1242 $this->calls[] = array('p_close',array(), $call[2]); 1243 } 1244 $this->calls[] = array('p_open',array(), $call[2]); 1245 $this->inParagraph = TRUE; 1246 1247 } else { 1248 1249 $storeCall = TRUE; 1250 1251 if ( $this->inParagraph && in_array($call[0], $this->blockOpen) ) { 1252 $this->calls[] = array('p_close',array(), $call[2]); 1253 $this->inParagraph = FALSE; 1254 $this->calls[] = $call; 1255 $storeCall = FALSE; 1256 } 1257 1258 if ( in_array($call[0], $this->blockClose) ) { 1259 if ( $this->inParagraph ) { 1260 $this->calls[] = array('p_close',array(), $call[2]); 1261 $this->inParagraph = FALSE; 1262 } 1263 if ( $storeCall ) { 1264 $this->calls[] = $call; 1265 $storeCall = FALSE; 1266 } 1267 1268 // This really sucks and suggests this whole class sucks but... 1269 if ( isset($calls[$key+1]) 1270 && 1271 !in_array($calls[$key+1][0], $this->blockOpen) 1272 && 1273 !in_array($calls[$key+1][0], $this->blockClose) 1274 ) { 1275 1276 $this->calls[] = array('p_open',array(), $call[2]); 1277 $this->inParagraph = TRUE; 1278 } 1279 } 1280 1281 if ( $storeCall ) { 1282 $this->calls[] = $call; 1283 } 1284 1285 } 1286 1287 1288 } else { 1289 1290 // Unless there's already a block at the start, start a paragraph 1291 if ( !in_array($call[0],$this->blockOpen) ) { 1292 $this->calls[] = array('p_open',array(), $call[2]); 1293 if ( $call[0] != 'eol' ) { 1294 $this->calls[] = $call; 1295 } 1296 $this->atStart = FALSE; 1297 $this->inParagraph = TRUE; 1298 } else { 1299 $this->calls[] = $call; 1300 $this->atStart = FALSE; 1301 } 1302 1303 } 1304 1305 } 1306 1307 if ( $this->inParagraph ) { 1308 if ( $call[0] == 'p_open' ) { 1309 // Ditch the last call 1310 array_pop($this->calls); 1311 } else if ( !in_array($call[0], $this->blockClose) ) { 1312 $this->calls[] = array('p_close',array(), $call[2]); 1313 } else { 1314 $last_call = array_pop($this->calls); 1315 $this->calls[] = array('p_close',array(), $call[2]); 1316 $this->calls[] = $last_call; 1317 } 1318 } 1319 1320 return $this->calls; 1321 } 1322 1323 function addToStack($newStart = TRUE) { 1324 $this->blockStack[] = array($this->atStart, $this->inParagraph); 1325 $this->atStart = $newStart; 1326 $this->inParagraph = FALSE; 1327 } 1328 1329 function removeFromStack() { 1330 $state = array_pop($this->blockStack); 1331 $this->atStart = $state[0]; 1332 $this->inParagraph = $state[1]; 1333 } 1334} 1335//------------------------------------------------------------------------ 1336define('DOKU_TOC_OPEN',1); 1337define('DOKU_TOCBRANCH_OPEN',2); 1338define('DOKU_TOCITEM_OPEN',3); 1339define('DOKU_TOC_ELEMENT',4); 1340define('DOKU_TOCITEM_CLOSE',5); 1341define('DOKU_TOCBRANCH_CLOSE',6); 1342define('DOKU_TOC_CLOSE',7); 1343 1344class Doku_Handler_Toc { 1345 1346 var $calls = array(); 1347 var $tocStack = array(); 1348 var $toc = array(); 1349 var $numHeaders = 0; 1350 1351 function process($calls) { 1352 1353 foreach ( $calls as $call ) { 1354 if ( $call[0] == 'header' && $call[1][1] < 4 ) { 1355 $this->numHeaders++; 1356 $this->addToToc($call); 1357 } 1358 $this->calls[] = $call; 1359 } 1360 1361 // Complete the table of contents then prepend to the calls 1362 $this->finalizeToc($call); 1363 return $this->calls; 1364 } 1365 1366 function addToToc($call) { 1367 1368 $depth = $call[1][1]; 1369 1370 // If it's the opening item... 1371 if ( count ( $this->toc) == 0 ) { 1372 1373 $this->addTocCall($call, DOKU_TOC_OPEN); 1374 1375 for ( $i = 1; $i <= $depth; $i++ ) { 1376 1377 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1378 1379 if ( $i != $depth ) { 1380 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1381 } else { 1382 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1383 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOC_ELEMENT); 1384 } 1385 1386 $this->tocStack[] = $i; 1387 1388 } 1389 return; 1390 } 1391 1392 $currentDepth = end($this->tocStack); 1393 $initialDepth = $currentDepth; 1394 1395 // Create new branches as needed 1396 if ( $depth > $currentDepth ) { 1397 1398 for ($i = $currentDepth+1; $i <= $depth; $i++ ) { 1399 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1400 // It's just a filler 1401 if ( $i != $depth ) { 1402 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1403 } else { 1404 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1405 } 1406 $this->tocStack[] = $i; 1407 } 1408 1409 $currentDepth = $i-1; 1410 1411 } 1412 1413 // Going down 1414 if ( $depth < $currentDepth ) { 1415 for ( $i = $currentDepth; $i >= $depth; $i-- ) { 1416 if ( $i != $depth ) { 1417 array_pop($this->tocStack); 1418 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1419 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_CLOSE); 1420 } else { 1421 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1422 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_OPEN); 1423 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1424 return; 1425 } 1426 } 1427 } 1428 1429 if ( $depth == $initialDepth ) { 1430 $this->addTocCall($call, DOKU_TOCITEM_CLOSE); 1431 $this->addTocCall($call, DOKU_TOCITEM_OPEN); 1432 } 1433 1434 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1435 1436 1437 } 1438 1439 function addTocCall($call, $type) { 1440 switch ( $type ) { 1441 case DOKU_TOC_OPEN: 1442 $this->toc[] = array('toc_open',array(),$call[2]); 1443 break; 1444 1445 case DOKU_TOCBRANCH_OPEN: 1446 $this->toc[] = array('tocbranch_open',array($call[1][1]),$call[2]); 1447 break; 1448 1449 case DOKU_TOCITEM_OPEN: 1450 if ( isset( $call[1][2] ) ) { 1451 $this->toc[] = array('tocitem_open',array($call[1][1], TRUE),$call[2]); 1452 } else { 1453 $this->toc[] = array('tocitem_open',array($call[1][1]),$call[2]); 1454 } 1455 break; 1456 1457 case DOKU_TOC_ELEMENT: 1458 $this->toc[] = array('tocelement',array($call[1][1],$call[1][0]),$call[2]); 1459 break; 1460 1461 case DOKU_TOCITEM_CLOSE: 1462 $this->toc[] = array('tocitem_close',array($call[1][1]),$call[2]); 1463 break; 1464 1465 case DOKU_TOCBRANCH_CLOSE: 1466 $this->toc[] = array('tocbranch_close',array($call[1][1]),$call[2]); 1467 break; 1468 1469 case DOKU_TOC_CLOSE: 1470 if ( count($this->toc) > 0 ) { 1471 $this->toc[] = array('toc_close',array(),$call[2]); 1472 } 1473 break; 1474 } 1475 } 1476 1477 function finalizeToc($call) { 1478 if ( $this->numHeaders < 3 ) { 1479 return; 1480 } 1481 if ( count ($this->tocStack) > 0 ) { 1482 while ( NULL !== ($toc = array_pop($this->tocStack)) ) { 1483 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCITEM_CLOSE); 1484 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCBRANCH_CLOSE); 1485 } 1486 } 1487 $this->addTocCall($call, DOKU_TOC_CLOSE); 1488 $this->calls = array_merge($this->toc, $this->calls); 1489 } 1490 1491} 1492 1493?> 1494