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