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