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