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 * @TODO What about email? 345 */ 346 function internallink($match, $state, $pos) { 347 // Strip the opening and closing markup 348 $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match); 349 350 // Split title from URL 351 $link = preg_split('/\|/u',$link,2); 352 if ( !isset($link[1]) ) { 353 $link[1] = NULL; 354 355 // If the title is an image, convert it to an array containing the image details 356 } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) { 357 $media = Doku_Handler_Parse_Media($link[1]); 358 359 // Check it's really an image, not a link to a file 360 if ( !strpos($media['type'], 'link') ) { 361 $link[1] = $media; 362 } 363 } 364 365 // Interwiki links 366 if ( preg_match('/^[a-zA-Z]+>{1}[\w()\/\\#~:.?+=&%@!\-;,]+$/u',$link[0]) ) { 367 $interwiki = preg_split('/>/u',$link[0]); 368 $this->__addCall( 369 'interwikilink', 370 // UTF8 problem... 371 array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), 372 $pos 373 ); 374 375 // Link to internal wiki page 376 } else if ( preg_match('/^[\w:#]+$/u',$link[0]) ) { 377 $this->__addCall( 378 'internallink', 379 array($link[0],$link[1]), 380 $pos 381 ); 382 383 // Otherwise it's some kind of link to elsewhere 384 } else { 385 386 // file:// 387 if ( substr($link[0],0,4) == 'file' ) { 388 $this->__addCall( 389 'filelink', 390 array($link[0],$link[1]), 391 $pos 392 ); 393 394 // Check for Windows shares - potential security issues here? 395 // i.e. mode not loaded but still matched here... 396 } else if ( preg_match('/\\\\\\\\[\w.:?\-;,]+?\\\\/u',$link[0]) ) { 397 $this->__addCall( 398 'windowssharelink', 399 array($link[0],$link[1]), 400 $pos 401 ); 402 403 // Otherwise it's one of the other valid internal links 404 } else { 405 406 // Add the scheme as needed... 407 $leader = substr($link[0],0,3); 408 if ( $leader == 'www' ) { 409 $link[0] = 'http://'.$link[0]; 410 } else if ( $leader == 'ftp' ) { 411 $link[0] = 'ftp://'.$link[0]; 412 } 413 414 $this->__addCall( 415 'externallink', 416 array($link[0],$link[1]), 417 $pos 418 ); 419 } 420 } 421 return TRUE; 422 } 423 424 function filelink($match, $state, $pos) { 425 $this->__addCall('filelink',array($match, NULL), $pos); 426 return TRUE; 427 } 428 429 function windowssharelink($match, $state, $pos) { 430 $this->__addCall('windowssharelink',array($match, NULL), $pos); 431 return TRUE; 432 } 433 434 function media($match, $state, $pos) { 435 $p = Doku_Handler_Parse_Media($match); 436 437 // If it's not an image, build a normal link 438 if ( strpos($p['type'], 'link') ) { 439 $this->__addCall($p['type'],array($p['src'], $p['title']), $pos); 440 } else { 441 $this->__addCall( 442 $p['type'], 443 array($p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache']), 444 $pos 445 ); 446 } 447 return TRUE; 448 } 449 450 function externallink($match, $state, $pos) { 451 // Prevent use of multibyte strings in URLs 452 // See: http://www.boingboing.net/2005/02/06/shmoo_group_exploit_.html 453 // Not worried about other charsets so long as page is output as UTF-8 454 /*if ( strlen($match) != utf8_strlen($match) ) { 455 $this->__addCall('cdata',array($match), $pos); 456 } else {*/ 457 458 $this->__addCall('externallink',array($match, NULL), $pos); 459 //} 460 return TRUE; 461 } 462 463 function email($match, $state, $pos) { 464 $email = preg_replace(array('/^</','/>$/'),'',$match); 465 $this->__addCall('email',array($email, NULL), $pos); 466 return TRUE; 467 } 468 469 function table($match, $state, $pos) { 470 switch ( $state ) { 471 472 case DOKU_LEXER_ENTER: 473 474 $ReWriter = & new Doku_Handler_Table($this->CallWriter); 475 $this->CallWriter = & $ReWriter; 476 477 $this->__addCall('table_start', array(), $pos); 478 //$this->__addCall('table_row', array(), $pos); 479 if ( trim($match) == '^' ) { 480 $this->__addCall('tableheader', array(), $pos); 481 } else { 482 $this->__addCall('tablecell', array(), $pos); 483 } 484 break; 485 486 case DOKU_LEXER_EXIT: 487 $this->__addCall('table_end', array(), $pos); 488 $this->CallWriter->process(); 489 $ReWriter = & $this->CallWriter; 490 $this->CallWriter = & $ReWriter->CallWriter; 491 break; 492 493 case DOKU_LEXER_UNMATCHED: 494 if ( trim($match) != '' ) { 495 $this->__addCall('cdata',array($match), $pos); 496 } 497 break; 498 499 case DOKU_LEXER_MATCHED: 500 if ( preg_match('/.{2}/',$match) ) { 501 $this->__addCall('table_align', array($match), $pos); 502 } else if ( $match == "\n|" ) { 503 $this->__addCall('table_row', array(), $pos); 504 $this->__addCall('tablecell', array(), $pos); 505 } else if ( $match == "\n^" ) { 506 $this->__addCall('table_row', array(), $pos); 507 $this->__addCall('tableheader', array(), $pos); 508 } else if ( $match == '|' ) { 509 $this->__addCall('tablecell', array(), $pos); 510 } else if ( $match == '^' ) { 511 $this->__addCall('tableheader', array(), $pos); 512 } 513 break; 514 } 515 return TRUE; 516 } 517} 518 519//------------------------------------------------------------------------ 520function Doku_Handler_Parse_Media($match) { 521 522 // Strip the opening and closing markup 523 $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); 524 525 // Split title from URL 526 $link = preg_split('/\|/u',$link,2); 527 528 529 // Check alignment 530 $ralign = (bool)preg_match('/^ /',$link[0]); 531 $lalign = (bool)preg_match('/ $/',$link[0]); 532 533 // Logic = what's that ;)... 534 if ( $lalign & $ralign ) { 535 $align = 'center'; 536 } else if ( $ralign ) { 537 $align = 'right'; 538 } else if ( $lalign ) { 539 $align = 'left'; 540 } else { 541 $align = NULL; 542 } 543 544 // The title... 545 if ( !isset($link[1]) ) { 546 $link[1] = NULL; 547 } 548 549 // img src url from params 550 // What if it's an external image where URL contains '?' char? 551 $src = preg_split('/\?/u',$link[0],2); 552 553 // Strip any alignment whitespace 554 $src[0] = trim($src[0]); 555 556 // Check for width, height and caching params 557 if ( isset($src[1]) ) { 558 559 if(preg_match('#(\d*)(x(\d*))?#i',$src[1],$matches)){ 560 561 if(isset($matches[1])) { 562 $width = $matches[1]; 563 } else { 564 $width = NULL; 565 } 566 567 if(isset($matches[3])) { 568 $height = $matches[3]; 569 } else { 570 $height = NULL; 571 } 572 573 $cache = !(bool)preg_match('/nocache/i',$src[1]); 574 } 575 576 } else { 577 $width = NULL; 578 $height = NULL; 579 $cache = TRUE; 580 } 581 582 // Check whether this is a local or remote image 583 if ( substr($src[0],0,4) == 'http' ) { 584 $call = 'external'; 585 } else { 586 $call = 'internal'; 587 } 588 589 // Check this is actually an image... 590 if ( !preg_match('/\.(gif|png|jpe?g)$/',$src[0] ) ) { 591 // Security implications?... 592 $call .= 'link'; 593 } else { 594 $call .= 'media'; 595 } 596 597 $params = array( 598 'type'=>$call, 599 'src'=>$src[0], 600 'title'=>$link[1], 601 'align'=>$align, 602 'width'=>$width, 603 'height'=>$height, 604 'cache'=>$cache, 605 ); 606 607 return $params; 608} 609 610//------------------------------------------------------------------------ 611class Doku_Handler_CallWriter { 612 613 var $Handler; 614 615 function Doku_Handler_CallWriter(& $Handler) { 616 $this->Handler = & $Handler; 617 } 618 619 function writeCall($call) { 620 $this->Handler->calls[] = $call; 621 } 622 623 function writeCalls($calls) { 624 $this->Handler->calls = array_merge($this->Handler->calls, $calls); 625 } 626} 627 628//------------------------------------------------------------------------ 629class Doku_Handler_List { 630 631 var $CallWriter; 632 633 var $calls = array(); 634 var $listCalls = array(); 635 var $listStack = array(); 636 637 function Doku_Handler_List(& $CallWriter) { 638 $this->CallWriter = & $CallWriter; 639 } 640 641 function writeCall($call) { 642 $this->calls[] = $call; 643 } 644 645 // Probably not needed but just in case... 646 function writeCalls($calls) { 647 $this->calls = array_merge($this->calls, $calls); 648 $this->CallWriter->writeCalls($this->calls); 649 } 650 651 //------------------------------------------------------------------------ 652 function process() { 653 foreach ( $this->calls as $call ) { 654 switch ($call[0]) { 655 case 'list_item': 656 $this->listOpen($call); 657 break; 658 case 'list_open': 659 $this->listStart($call); 660 break; 661 case 'list_close': 662 $this->listEnd($call); 663 break; 664 default: 665 $this->listContent($call); 666 break; 667 } 668 } 669 670 $this->CallWriter->writeCalls($this->listCalls); 671 } 672 673 //------------------------------------------------------------------------ 674 function listStart($call) { 675 $depth = $this->interpretSyntax($call[1][0], $listType); 676 677 $this->initialDepth = $depth; 678 $this->listStack[] = array($listType, $depth); 679 680 $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); 681 $this->listCalls[] = array('listitem_open',array(1),$call[2]); 682 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 683 } 684 685 //------------------------------------------------------------------------ 686 function listEnd($call) { 687 $closeContent = TRUE; 688 689 while ( $list = array_pop($this->listStack) ) { 690 if ( $closeContent ) { 691 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 692 $closeContent = FALSE; 693 } 694 $this->listCalls[] = array('listitem_close',array(),$call[2]); 695 $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); 696 } 697 } 698 699 //------------------------------------------------------------------------ 700 function listOpen($call) { 701 $depth = $this->interpretSyntax($call[1][0], $listType); 702 $end = end($this->listStack); 703 704 // Not allowed to be shallower than initialDepth 705 if ( $depth < $this->initialDepth ) { 706 $depth = $this->initialDepth; 707 } 708 709 //------------------------------------------------------------------------ 710 if ( $depth == $end[1] ) { 711 712 // Just another item in the list... 713 if ( $listType == $end[0] ) { 714 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 715 $this->listCalls[] = array('listitem_close',array(),$call[2]); 716 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 717 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 718 719 // Switched list type... 720 } else { 721 722 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 723 $this->listCalls[] = array('listitem_close',array(),$call[2]); 724 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 725 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 726 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 727 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 728 729 array_pop($this->listStack); 730 $this->listStack[] = array($listType, $depth); 731 } 732 733 //------------------------------------------------------------------------ 734 // Getting deeper... 735 } else if ( $depth > $end[1] ) { 736 737 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 738 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 739 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 740 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 741 742 $this->listStack[] = array($listType, $depth); 743 744 //------------------------------------------------------------------------ 745 // Getting shallower ( $depth < $end[1] ) 746 } else { 747 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 748 $this->listCalls[] = array('listitem_close',array(),$call[2]); 749 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 750 751 // Throw away the end - done 752 array_pop($this->listStack); 753 754 while (1) { 755 $end = end($this->listStack); 756 757 if ( $end[1] <= $depth ) { 758 759 // Normalize depths 760 $depth = $end[1]; 761 762 $this->listCalls[] = array('listitem_close',array(),$call[2]); 763 764 if ( $end[0] == $listType ) { 765 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 766 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 767 768 } else { 769 // Switching list type... 770 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 771 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 772 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 773 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 774 775 array_pop($this->listStack); 776 $this->listStack[] = array($listType, $depth); 777 } 778 779 break; 780 781 // Haven't dropped down far enough yet.... ( $end[1] > $depth ) 782 } else { 783 784 $this->listCalls[] = array('listitem_close',array(),$call[2]); 785 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 786 787 array_pop($this->listStack); 788 789 } 790 791 } 792 793 } 794 } 795 796 //------------------------------------------------------------------------ 797 function listContent($call) { 798 $this->listCalls[] = $call; 799 } 800 801 //------------------------------------------------------------------------ 802 function interpretSyntax($match, & $type) { 803 if ( substr($match,-1) == '*' ) { 804 $type = 'u'; 805 } else { 806 $type = 'o'; 807 } 808 return count(explode(' ',str_replace("\t",' ',$match))); 809 } 810} 811 812//------------------------------------------------------------------------ 813class Doku_Handler_Preformatted { 814 815 var $CallWriter; 816 817 var $calls = array(); 818 var $pos; 819 var $text =''; 820 821 822 823 function Doku_Handler_Preformatted(& $CallWriter) { 824 $this->CallWriter = & $CallWriter; 825 } 826 827 function writeCall($call) { 828 $this->calls[] = $call; 829 } 830 831 // Probably not needed but just in case... 832 function writeCalls($calls) { 833 $this->calls = array_merge($this->calls, $calls); 834 $this->CallWriter->writeCalls($this->calls); 835 } 836 837 function process() { 838 foreach ( $this->calls as $call ) { 839 switch ($call[0]) { 840 case 'preformatted_start': 841 $this->pos = $call[2]; 842 break; 843 case 'preformatted_newline': 844 $this->text .= "\n"; 845 break; 846 case 'preformatted_content': 847 $this->text .= $call[1][0]; 848 break; 849 case 'preformatted_end': 850 $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos)); 851 break; 852 } 853 } 854 } 855} 856 857//------------------------------------------------------------------------ 858class Doku_Handler_Quote { 859 860 var $CallWriter; 861 862 var $calls = array(); 863 864 var $quoteCalls = array(); 865 866 function Doku_Handler_Quote(& $CallWriter) { 867 $this->CallWriter = & $CallWriter; 868 } 869 870 function writeCall($call) { 871 $this->calls[] = $call; 872 } 873 874 // Probably not needed but just in case... 875 function writeCalls($calls) { 876 $this->calls = array_merge($this->calls, $calls); 877 $this->CallWriter->writeCalls($this->calls); 878 } 879 880 function process() { 881 882 $quoteDepth = 1; 883 884 foreach ( $this->calls as $call ) { 885 switch ($call[0]) { 886 887 case 'quote_start': 888 889 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 890 891 case 'quote_newline': 892 893 $quoteLength = $this->getDepth($call[1][0]); 894 895 if ( $quoteLength > $quoteDepth ) { 896 $quoteDiff = $quoteLength - $quoteDepth; 897 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 898 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 899 } 900 } else if ( $quoteLength < $quoteDepth ) { 901 $quoteDiff = $quoteDepth - $quoteLength; 902 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 903 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 904 } 905 } 906 907 $quoteDepth = $quoteLength; 908 909 break; 910 911 case 'quote_end': 912 913 if ( $quoteDepth > 1 ) { 914 $quoteDiff = $quoteDepth - 1; 915 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 916 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 917 } 918 } 919 920 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 921 922 $this->CallWriter->writeCalls($this->quoteCalls); 923 break; 924 925 default: 926 $this->quoteCalls[] = $call; 927 break; 928 } 929 } 930 } 931 932 function getDepth($marker) { 933 preg_match('/>{1,}/', $marker, $matches); 934 $quoteLength = strlen($matches[0]); 935 return $quoteLength; 936 } 937} 938 939//------------------------------------------------------------------------ 940class Doku_Handler_Table { 941 942 var $CallWriter; 943 944 var $calls = array(); 945 var $tableCalls = array(); 946 var $maxCols = 0; 947 var $maxRows = 1; 948 var $currentCols = 0; 949 var $firstCell = FALSE; 950 var $lastCellType = 'tablecell'; 951 952 function Doku_Handler_Table(& $CallWriter) { 953 $this->CallWriter = & $CallWriter; 954 } 955 956 function writeCall($call) { 957 $this->calls[] = $call; 958 } 959 960 // Probably not needed but just in case... 961 function writeCalls($calls) { 962 $this->calls = array_merge($this->calls, $calls); 963 $this->CallWriter->writeCalls($this->calls); 964 } 965 966 //------------------------------------------------------------------------ 967 function process() { 968 foreach ( $this->calls as $call ) { 969 switch ( $call[0] ) { 970 case 'table_start': 971 $this->tableStart($call); 972 break; 973 case 'table_row': 974 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 975 $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); 976 break; 977 case 'tableheader': 978 case 'tablecell': 979 $this->tableCell($call); 980 break; 981 case 'table_end': 982 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 983 $this->tableEnd($call); 984 break; 985 default: 986 $this->tableDefault($call); 987 break; 988 } 989 } 990 $this->CallWriter->writeCalls($this->tableCalls); 991 } 992 993 function tableStart($call) { 994 $this->tableCalls[] = array('table_open',array(),$call[2]); 995 $this->tableCalls[] = array('tablerow_open',array(),$call[2]); 996 $this->firstCell = TRUE; 997 } 998 999 function tableEnd($call) { 1000 $this->tableCalls[] = array('table_close',array(),$call[2]); 1001 $this->finalizeTable(); 1002 } 1003 1004 function tableRowOpen($call) { 1005 $this->tableCalls[] = $call; 1006 $this->currentCols = 0; 1007 $this->firstCell = TRUE; 1008 $this->lastCellType = 'tablecell'; 1009 $this->maxRows++; 1010 } 1011 1012 function tableRowClose($call) { 1013 // Strip off final cell opening and anything after it 1014 while ( $discard = array_pop($this->tableCalls ) ) { 1015 1016 if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { 1017 1018 // Its a spanning element - put it back and close it 1019 if ( $discard[1][0] > 1 ) { 1020 1021 $this->tableCalls[] = $discard; 1022 if ( strstr($discard[0],'cell') ) { 1023 $name = 'tablecell'; 1024 } else { 1025 $name = 'tableheader'; 1026 } 1027 $this->tableCalls[] = array($name.'_close',array(),$call[2]); 1028 } 1029 1030 break; 1031 } 1032 } 1033 $this->tableCalls[] = $call; 1034 1035 if ( $this->currentCols > $this->maxCols ) { 1036 $this->maxCols = $this->currentCols; 1037 } 1038 } 1039 1040 function tableCell($call) { 1041 if ( !$this->firstCell ) { 1042 1043 // Increase the span 1044 $lastCall = end($this->tableCalls); 1045 1046 // A cell call which follows an open cell means an empty cell so span 1047 if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) { 1048 $this->tableCalls[] = array('colspan',array(),$call[2]); 1049 1050 } 1051 1052 $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); 1053 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1054 $this->lastCellType = $call[0]; 1055 1056 } else { 1057 1058 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1059 $this->lastCellType = $call[0]; 1060 $this->firstCell = FALSE; 1061 1062 } 1063 1064 $this->currentCols++; 1065 } 1066 1067 function tableDefault($call) { 1068 $this->tableCalls[] = $call; 1069 } 1070 1071 function finalizeTable() { 1072 1073 // Add the max cols and rows to the table opening 1074 if ( $this->tableCalls[0][0] == 'table_open' ) { 1075 // Adjust to num cols not num col delimeters 1076 $this->tableCalls[0][1][] = $this->maxCols - 1; 1077 $this->tableCalls[0][1][] = $this->maxRows; 1078 } else { 1079 trigger_error('First element in table call list is not table_open'); 1080 } 1081 1082 $lastRow = 0; 1083 $lastCell = 0; 1084 $toDelete = array(); 1085 1086 // Look for the colspan elements and increment the colspan on the 1087 // previous non-empty opening cell. Once done, delete all the cells 1088 // that contain colspans 1089 foreach ( $this->tableCalls as $key => $call ) { 1090 1091 if ( $call[0] == 'tablerow_open' ) { 1092 1093 $lastRow = $key; 1094 1095 } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) { 1096 1097 $lastCell = $key; 1098 1099 } else if ( $call[0] == 'table_align' ) { 1100 1101 // If the previous element was a cell open, align right 1102 if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) { 1103 $this->tableCalls[$key-1][1][1] = 'right'; 1104 1105 // If the next element if the close of an element, align either center or left 1106 } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) { 1107 if ( $this->tableCalls[$lastCell][1][1] == 'right' ) { 1108 $this->tableCalls[$lastCell][1][1] = 'center'; 1109 } else { 1110 $this->tableCalls[$lastCell][1][1] = 'left'; 1111 } 1112 1113 } 1114 1115 // Now convert the whitespace back to cdata 1116 $this->tableCalls[$key][0] = 'cdata'; 1117 1118 } else if ( $call[0] == 'colspan' ) { 1119 1120 $this->tableCalls[$key-1][1][0] = FALSE; 1121 1122 for($i = $key-2; $i > $lastRow; $i--) { 1123 1124 if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) { 1125 1126 if ( FALSE !== $this->tableCalls[$i][1][0] ) { 1127 $this->tableCalls[$i][1][0]++; 1128 break; 1129 } 1130 1131 1132 } 1133 } 1134 1135 $toDelete[] = $key-1; 1136 $toDelete[] = $key; 1137 $toDelete[] = $key+1; 1138 } 1139 } 1140 1141 foreach ( $toDelete as $delete ) { 1142 unset($this->tableCalls[$delete]); 1143 } 1144 1145 $this->tableCalls = array_values($this->tableCalls); 1146 } 1147} 1148 1149//------------------------------------------------------------------------ 1150class Doku_Handler_Section { 1151 1152 function process($calls) { 1153 1154 $sectionCalls = array(); 1155 $inSection = FALSE; 1156 1157 foreach ( $calls as $call ) { 1158 1159 if ( $call[0] == 'header' ) { 1160 1161 if ( $inSection ) { 1162 $sectionCalls[] = array('section_close',array(), $call[2]); 1163 } 1164 1165 $sectionCalls[] = $call; 1166 $sectionCalls[] = array('section_open',array($call[1][1]), $call[2]); 1167 $inSection = TRUE; 1168 1169 } else { 1170 $sectionCalls[] = $call; 1171 } 1172 } 1173 1174 if ( $inSection ) { 1175 $sectionCalls[] = array('section_close',array(), $call[2]); 1176 } 1177 1178 return $sectionCalls; 1179 } 1180 1181} 1182 1183//------------------------------------------------------------------------ 1184class Doku_Handler_Block { 1185 1186 var $calls = array(); 1187 1188 var $blockStack = array(); 1189 1190 var $inParagraph = FALSE; 1191 var $atStart = TRUE; 1192 1193 // Blocks don't contain linefeeds 1194 var $blockOpen = array( 1195 'header', 1196 'listu_open','listo_open','listitem_open', 1197 'table_open','tablerow_open','tablecell_open','tableheader_open', 1198 'quote_open', 1199 'section_open', // Needed to prevent p_open between header and section_open 1200 'code','file','php','html','hr','preformatted', 1201 ); 1202 1203 var $blockClose = array( 1204 'header', 1205 'listu_close','listo_close','listitem_close', 1206 'table_close','tablerow_close','tablecell_close','tableheader_close', 1207 'quote_close', 1208 'section_close', // Needed to prevent p_close after section_close 1209 'code','file','php','html','hr','preformatted', 1210 ); 1211 1212 // Stacks can contain linefeeds 1213 var $stackOpen = array( 1214 'footnote_open','section_open', 1215 ); 1216 1217 var $stackClose = array( 1218 'footnote_close','section_close', 1219 ); 1220 1221 function process($calls) { 1222 foreach ( $calls as $key => $call ) { 1223 1224 // Process blocks which are stack like... (contain linefeeds) 1225 if ( in_array($call[0],$this->stackOpen ) ) { 1226 /* 1227 if ( $this->atStart ) { 1228 $this->calls[] = array('p_open',array(), $call[2]); 1229 $this->atStart = FALSE; 1230 $this->inParagraph = TRUE; 1231 } 1232 */ 1233 $this->calls[] = $call; 1234 1235 // Hack - footnotes shouldn't immediately contain a p_open 1236 if ( $call[0] != 'footnote_open' ) { 1237 $this->addToStack(); 1238 } else { 1239 $this->addToStack(FALSE); 1240 } 1241 continue; 1242 } 1243 1244 if ( in_array($call[0],$this->stackClose ) ) { 1245 1246 if ( $this->inParagraph ) { 1247 $this->calls[] = array('p_close',array(), $call[2]); 1248 } 1249 $this->calls[] = $call; 1250 $this->removeFromStack(); 1251 continue; 1252 } 1253 1254 if ( !$this->atStart ) { 1255 1256 if ( $call[0] == 'eol' ) { 1257 1258 if ( $this->inParagraph ) { 1259 $this->calls[] = array('p_close',array(), $call[2]); 1260 } 1261 $this->calls[] = array('p_open',array(), $call[2]); 1262 $this->inParagraph = TRUE; 1263 1264 } else { 1265 1266 $storeCall = TRUE; 1267 1268 if ( $this->inParagraph && in_array($call[0], $this->blockOpen) ) { 1269 $this->calls[] = array('p_close',array(), $call[2]); 1270 $this->inParagraph = FALSE; 1271 $this->calls[] = $call; 1272 $storeCall = FALSE; 1273 } 1274 1275 if ( in_array($call[0], $this->blockClose) ) { 1276 if ( $this->inParagraph ) { 1277 $this->calls[] = array('p_close',array(), $call[2]); 1278 $this->inParagraph = FALSE; 1279 } 1280 if ( $storeCall ) { 1281 $this->calls[] = $call; 1282 $storeCall = FALSE; 1283 } 1284 1285 // This really sucks and suggests this whole class sucks but... 1286 if ( isset($calls[$key+1]) 1287 && 1288 !in_array($calls[$key+1][0], $this->blockOpen) 1289 && 1290 !in_array($calls[$key+1][0], $this->blockClose) 1291 ) { 1292 1293 $this->calls[] = array('p_open',array(), $call[2]); 1294 $this->inParagraph = TRUE; 1295 } 1296 } 1297 1298 if ( $storeCall ) { 1299 $this->calls[] = $call; 1300 } 1301 1302 } 1303 1304 1305 } else { 1306 1307 // Unless there's already a block at the start, start a paragraph 1308 if ( !in_array($call[0],$this->blockOpen) ) { 1309 $this->calls[] = array('p_open',array(), $call[2]); 1310 if ( $call[0] != 'eol' ) { 1311 $this->calls[] = $call; 1312 } 1313 $this->atStart = FALSE; 1314 $this->inParagraph = TRUE; 1315 } else { 1316 $this->calls[] = $call; 1317 $this->atStart = FALSE; 1318 } 1319 1320 } 1321 1322 } 1323 1324 if ( $this->inParagraph ) { 1325 if ( $call[0] == 'p_open' ) { 1326 // Ditch the last call 1327 array_pop($this->calls); 1328 } else if ( !in_array($call[0], $this->blockClose) ) { 1329 $this->calls[] = array('p_close',array(), $call[2]); 1330 } else { 1331 $last_call = array_pop($this->calls); 1332 $this->calls[] = array('p_close',array(), $call[2]); 1333 $this->calls[] = $last_call; 1334 } 1335 } 1336 1337 return $this->calls; 1338 } 1339 1340 function addToStack($newStart = TRUE) { 1341 $this->blockStack[] = array($this->atStart, $this->inParagraph); 1342 $this->atStart = $newStart; 1343 $this->inParagraph = FALSE; 1344 } 1345 1346 function removeFromStack() { 1347 $state = array_pop($this->blockStack); 1348 $this->atStart = $state[0]; 1349 $this->inParagraph = $state[1]; 1350 } 1351} 1352//------------------------------------------------------------------------ 1353define('DOKU_TOC_OPEN',1); 1354define('DOKU_TOCBRANCH_OPEN',2); 1355define('DOKU_TOCITEM_OPEN',3); 1356define('DOKU_TOC_ELEMENT',4); 1357define('DOKU_TOCITEM_CLOSE',5); 1358define('DOKU_TOCBRANCH_CLOSE',6); 1359define('DOKU_TOC_CLOSE',7); 1360 1361class Doku_Handler_Toc { 1362 1363 var $calls = array(); 1364 var $tocStack = array(); 1365 var $toc = array(); 1366 var $numHeaders = 0; 1367 1368 function process($calls) { 1369 1370 foreach ( $calls as $call ) { 1371 if ( $call[0] == 'header' && $call[1][1] < 4 ) { 1372 $this->numHeaders++; 1373 $this->addToToc($call); 1374 } 1375 $this->calls[] = $call; 1376 } 1377 1378 // Complete the table of contents then prepend to the calls 1379 $this->finalizeToc($call); 1380 return $this->calls; 1381 } 1382 1383 function addToToc($call) { 1384 1385 $depth = $call[1][1]; 1386 1387 // If it's the opening item... 1388 if ( count ( $this->toc) == 0 ) { 1389 1390 $this->addTocCall($call, DOKU_TOC_OPEN); 1391 1392 for ( $i = 1; $i <= $depth; $i++ ) { 1393 1394 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1395 1396 if ( $i != $depth ) { 1397 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1398 } else { 1399 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1400 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOC_ELEMENT); 1401 } 1402 1403 $this->tocStack[] = $i; 1404 1405 } 1406 return; 1407 } 1408 1409 $currentDepth = end($this->tocStack); 1410 $initialDepth = $currentDepth; 1411 1412 // Create new branches as needed 1413 if ( $depth > $currentDepth ) { 1414 1415 for ($i = $currentDepth+1; $i <= $depth; $i++ ) { 1416 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1417 // It's just a filler 1418 if ( $i != $depth ) { 1419 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1420 } else { 1421 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1422 } 1423 $this->tocStack[] = $i; 1424 } 1425 1426 $currentDepth = $i-1; 1427 1428 } 1429 1430 // Going down 1431 if ( $depth < $currentDepth ) { 1432 for ( $i = $currentDepth; $i >= $depth; $i-- ) { 1433 if ( $i != $depth ) { 1434 array_pop($this->tocStack); 1435 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1436 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_CLOSE); 1437 } else { 1438 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1439 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_OPEN); 1440 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1441 return; 1442 } 1443 } 1444 } 1445 1446 if ( $depth == $initialDepth ) { 1447 $this->addTocCall($call, DOKU_TOCITEM_CLOSE); 1448 $this->addTocCall($call, DOKU_TOCITEM_OPEN); 1449 } 1450 1451 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1452 1453 1454 } 1455 1456 function addTocCall($call, $type) { 1457 switch ( $type ) { 1458 case DOKU_TOC_OPEN: 1459 $this->toc[] = array('toc_open',array(),$call[2]); 1460 break; 1461 1462 case DOKU_TOCBRANCH_OPEN: 1463 $this->toc[] = array('tocbranch_open',array($call[1][1]),$call[2]); 1464 break; 1465 1466 case DOKU_TOCITEM_OPEN: 1467 if ( isset( $call[1][2] ) ) { 1468 $this->toc[] = array('tocitem_open',array($call[1][1], TRUE),$call[2]); 1469 } else { 1470 $this->toc[] = array('tocitem_open',array($call[1][1]),$call[2]); 1471 } 1472 break; 1473 1474 case DOKU_TOC_ELEMENT: 1475 $this->toc[] = array('tocelement',array($call[1][1],$call[1][0]),$call[2]); 1476 break; 1477 1478 case DOKU_TOCITEM_CLOSE: 1479 $this->toc[] = array('tocitem_close',array($call[1][1]),$call[2]); 1480 break; 1481 1482 case DOKU_TOCBRANCH_CLOSE: 1483 $this->toc[] = array('tocbranch_close',array($call[1][1]),$call[2]); 1484 break; 1485 1486 case DOKU_TOC_CLOSE: 1487 if ( count($this->toc) > 0 ) { 1488 $this->toc[] = array('toc_close',array(),$call[2]); 1489 } 1490 break; 1491 } 1492 } 1493 1494 function finalizeToc($call) { 1495 if ( $this->numHeaders < 3 ) { 1496 return; 1497 } 1498 if ( count ($this->tocStack) > 0 ) { 1499 while ( NULL !== ($toc = array_pop($this->tocStack)) ) { 1500 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCITEM_CLOSE); 1501 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCBRANCH_CLOSE); 1502 } 1503 } 1504 $this->addTocCall($call, DOKU_TOC_CLOSE); 1505 $this->calls = array_merge($this->toc, $this->calls); 1506 } 1507 1508} 1509 1510?> 1511