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