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