1<?php 2 3use dokuwiki\Extension\Event; 4use dokuwiki\Extension\SyntaxPlugin; 5use dokuwiki\Parsing\Handler\Block; 6use dokuwiki\Parsing\Handler\CallWriter; 7use dokuwiki\Parsing\Handler\CallWriterInterface; 8use dokuwiki\Parsing\Handler\Lists; 9use dokuwiki\Parsing\Handler\Nest; 10use dokuwiki\Parsing\Handler\Preformatted; 11use dokuwiki\Parsing\Handler\Quote; 12use dokuwiki\Parsing\Handler\Table; 13 14/** 15 * Class Doku_Handler 16 */ 17class Doku_Handler { 18 /** @var CallWriterInterface */ 19 protected $callWriter = null; 20 21 /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */ 22 public $calls = array(); 23 24 /** @var array internal status holders for some modes */ 25 protected $status = array( 26 'section' => false, 27 'doublequote' => 0, 28 ); 29 30 /** @var bool should blocks be rewritten? FIXME seems to always be true */ 31 protected $rewriteBlocks = true; 32 33 /** 34 * Doku_Handler constructor. 35 */ 36 public function __construct() { 37 $this->callWriter = new CallWriter($this); 38 } 39 40 /** 41 * Add a new call by passing it to the current CallWriter 42 * 43 * @param string $handler handler method name (see mode handlers below) 44 * @param mixed $args arguments for this call 45 * @param int $pos byte position in the original source file 46 */ 47 protected function addCall($handler, $args, $pos) { 48 $call = array($handler,$args, $pos); 49 $this->callWriter->writeCall($call); 50 } 51 52 /** 53 * Similar to addCall, but adds a plugin call 54 * 55 * @param string $plugin name of the plugin 56 * @param mixed $args arguments for this call 57 * @param int $state a LEXER_STATE_* constant 58 * @param int $pos byte position in the original source file 59 * @param string $match matched syntax 60 */ 61 protected function addPluginCall($plugin, $args, $state, $pos, $match) { 62 $call = array('plugin',array($plugin, $args, $state, $match), $pos); 63 $this->callWriter->writeCall($call); 64 } 65 66 /** 67 * Finishes handling 68 * 69 * Called from the parser. Calls finalise() on the call writer, closes open 70 * sections, rewrites blocks and adds document_start and document_end calls. 71 * 72 * @triggers PARSER_HANDLER_DONE 73 */ 74 public function finalize(){ 75 $this->callWriter->finalise(); 76 77 if ( $this->status['section'] ) { 78 $last_call = end($this->calls); 79 array_push($this->calls,array('section_close',array(), $last_call[2])); 80 } 81 82 if ( $this->rewriteBlocks ) { 83 $B = new Block(); 84 $this->calls = $B->process($this->calls); 85 } 86 87 Event::createAndTrigger('PARSER_HANDLER_DONE',$this); 88 89 array_unshift($this->calls,array('document_start',array(),0)); 90 $last_call = end($this->calls); 91 array_push($this->calls,array('document_end',array(),$last_call[2])); 92 } 93 94 /** 95 * fetch the current call and advance the pointer to the next one 96 * 97 * @fixme seems to be unused? 98 * @return bool|mixed 99 */ 100 public function fetch() { 101 $call = current($this->calls); 102 if($call !== false) { 103 next($this->calls); //advance the pointer 104 return $call; 105 } 106 return false; 107 } 108 109 110 /** 111 * Internal function for parsing highlight options. 112 * $options is parsed for key value pairs separated by commas. 113 * A value might also be missing in which case the value will simple 114 * be set to true. Commas in strings are ignored, e.g. option="4,56" 115 * will work as expected and will only create one entry. 116 * 117 * @param string $options space separated list of key-value pairs, 118 * e.g. option1=123, option2="456" 119 * @return array|null Array of key-value pairs $array['key'] = 'value'; 120 * or null if no entries found 121 */ 122 protected function parse_highlight_options($options) { 123 $result = array(); 124 preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER); 125 foreach ($matches as $match) { 126 $equal_sign = strpos($match [0], '='); 127 if ($equal_sign === false) { 128 $key = trim($match[0]); 129 $result [$key] = 1; 130 } else { 131 $key = substr($match[0], 0, $equal_sign); 132 $value = substr($match[0], $equal_sign+1); 133 $value = trim($value, '"'); 134 if (strlen($value) > 0) { 135 $result [$key] = $value; 136 } else { 137 $result [$key] = 1; 138 } 139 } 140 } 141 142 // Check for supported options 143 $result = array_intersect_key( 144 $result, 145 array_flip(array( 146 'enable_line_numbers', 147 'start_line_numbers_at', 148 'highlight_lines_extra', 149 'enable_keyword_links') 150 ) 151 ); 152 153 // Sanitize values 154 if(isset($result['enable_line_numbers'])) { 155 if($result['enable_line_numbers'] === 'false') { 156 $result['enable_line_numbers'] = false; 157 } 158 $result['enable_line_numbers'] = (bool) $result['enable_line_numbers']; 159 } 160 if(isset($result['highlight_lines_extra'])) { 161 $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra'])); 162 $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']); 163 $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']); 164 } 165 if(isset($result['start_line_numbers_at'])) { 166 $result['start_line_numbers_at'] = (int) $result['start_line_numbers_at']; 167 } 168 if(isset($result['enable_keyword_links'])) { 169 if($result['enable_keyword_links'] === 'false') { 170 $result['enable_keyword_links'] = false; 171 } 172 $result['enable_keyword_links'] = (bool) $result['enable_keyword_links']; 173 } 174 if (count($result) == 0) { 175 return null; 176 } 177 178 return $result; 179 } 180 181 /** 182 * Simplifies handling for the formatting tags which all behave the same 183 * 184 * @param string $match matched syntax 185 * @param int $state a LEXER_STATE_* constant 186 * @param int $pos byte position in the original source file 187 * @param string $name actual mode name 188 */ 189 protected function nestingTag($match, $state, $pos, $name) { 190 switch ( $state ) { 191 case DOKU_LEXER_ENTER: 192 $this->addCall($name.'_open', array(), $pos); 193 break; 194 case DOKU_LEXER_EXIT: 195 $this->addCall($name.'_close', array(), $pos); 196 break; 197 case DOKU_LEXER_UNMATCHED: 198 $this->addCall('cdata', array($match), $pos); 199 break; 200 } 201 } 202 203 204 /** 205 * The following methods define the handlers for the different Syntax modes 206 * 207 * The handlers are called from dokuwiki\Parsing\Lexer\Lexer\invokeParser() 208 * 209 * @todo it might make sense to move these into their own class or merge them with the 210 * ParserMode classes some time. 211 */ 212 // region mode handlers 213 214 /** 215 * Special plugin handler 216 * 217 * This handler is called for all modes starting with 'plugin_'. 218 * An additional parameter with the plugin name is passed. The plugin's handle() 219 * method is called here 220 * 221 * @author Andreas Gohr <andi@splitbrain.org> 222 * 223 * @param string $match matched syntax 224 * @param int $state a LEXER_STATE_* constant 225 * @param int $pos byte position in the original source file 226 * @param string $pluginname name of the plugin 227 * @return bool mode handled? 228 */ 229 public function plugin($match, $state, $pos, $pluginname){ 230 $data = array($match); 231 /** @var SyntaxPlugin $plugin */ 232 $plugin = plugin_load('syntax',$pluginname); 233 if($plugin != null){ 234 $data = $plugin->handle($match, $state, $pos, $this); 235 } 236 if ($data !== false) { 237 $this->addPluginCall($pluginname,$data,$state,$pos,$match); 238 } 239 return true; 240 } 241 242 /** 243 * @param string $match matched syntax 244 * @param int $state a LEXER_STATE_* constant 245 * @param int $pos byte position in the original source file 246 * @return bool mode handled? 247 */ 248 public function base($match, $state, $pos) { 249 switch ( $state ) { 250 case DOKU_LEXER_UNMATCHED: 251 $this->addCall('cdata', array($match), $pos); 252 return true; 253 break; 254 } 255 return false; 256 } 257 258 /** 259 * @param string $match matched syntax 260 * @param int $state a LEXER_STATE_* constant 261 * @param int $pos byte position in the original source file 262 * @return bool mode handled? 263 */ 264 public function header($match, $state, $pos) { 265 // get level and title 266 $title = trim($match); 267 $level = 7 - strspn($title,'='); 268 if($level < 1) $level = 1; 269 $title = trim($title,'='); 270 $title = trim($title); 271 272 if ($this->status['section']) $this->addCall('section_close', array(), $pos); 273 274 $this->addCall('header', array($title, $level, $pos), $pos); 275 276 $this->addCall('section_open', array($level), $pos); 277 $this->status['section'] = true; 278 return true; 279 } 280 281 /** 282 * @param string $match matched syntax 283 * @param int $state a LEXER_STATE_* constant 284 * @param int $pos byte position in the original source file 285 * @return bool mode handled? 286 */ 287 public function notoc($match, $state, $pos) { 288 $this->addCall('notoc', array(), $pos); 289 return true; 290 } 291 292 /** 293 * @param string $match matched syntax 294 * @param int $state a LEXER_STATE_* constant 295 * @param int $pos byte position in the original source file 296 * @return bool mode handled? 297 */ 298 public function nocache($match, $state, $pos) { 299 $this->addCall('nocache', array(), $pos); 300 return true; 301 } 302 303 /** 304 * @param string $match matched syntax 305 * @param int $state a LEXER_STATE_* constant 306 * @param int $pos byte position in the original source file 307 * @return bool mode handled? 308 */ 309 public function linebreak($match, $state, $pos) { 310 $this->addCall('linebreak', array(), $pos); 311 return true; 312 } 313 314 /** 315 * @param string $match matched syntax 316 * @param int $state a LEXER_STATE_* constant 317 * @param int $pos byte position in the original source file 318 * @return bool mode handled? 319 */ 320 public function eol($match, $state, $pos) { 321 $this->addCall('eol', array(), $pos); 322 return true; 323 } 324 325 /** 326 * @param string $match matched syntax 327 * @param int $state a LEXER_STATE_* constant 328 * @param int $pos byte position in the original source file 329 * @return bool mode handled? 330 */ 331 public function hr($match, $state, $pos) { 332 $this->addCall('hr', array(), $pos); 333 return true; 334 } 335 336 /** 337 * @param string $match matched syntax 338 * @param int $state a LEXER_STATE_* constant 339 * @param int $pos byte position in the original source file 340 * @return bool mode handled? 341 */ 342 public function strong($match, $state, $pos) { 343 $this->nestingTag($match, $state, $pos, 'strong'); 344 return true; 345 } 346 347 /** 348 * @param string $match matched syntax 349 * @param int $state a LEXER_STATE_* constant 350 * @param int $pos byte position in the original source file 351 * @return bool mode handled? 352 */ 353 public function emphasis($match, $state, $pos) { 354 $this->nestingTag($match, $state, $pos, 'emphasis'); 355 return true; 356 } 357 358 /** 359 * @param string $match matched syntax 360 * @param int $state a LEXER_STATE_* constant 361 * @param int $pos byte position in the original source file 362 * @return bool mode handled? 363 */ 364 public function underline($match, $state, $pos) { 365 $this->nestingTag($match, $state, $pos, 'underline'); 366 return true; 367 } 368 369 /** 370 * @param string $match matched syntax 371 * @param int $state a LEXER_STATE_* constant 372 * @param int $pos byte position in the original source file 373 * @return bool mode handled? 374 */ 375 public function monospace($match, $state, $pos) { 376 $this->nestingTag($match, $state, $pos, 'monospace'); 377 return true; 378 } 379 380 /** 381 * @param string $match matched syntax 382 * @param int $state a LEXER_STATE_* constant 383 * @param int $pos byte position in the original source file 384 * @return bool mode handled? 385 */ 386 public function subscript($match, $state, $pos) { 387 $this->nestingTag($match, $state, $pos, 'subscript'); 388 return true; 389 } 390 391 /** 392 * @param string $match matched syntax 393 * @param int $state a LEXER_STATE_* constant 394 * @param int $pos byte position in the original source file 395 * @return bool mode handled? 396 */ 397 public function superscript($match, $state, $pos) { 398 $this->nestingTag($match, $state, $pos, 'superscript'); 399 return true; 400 } 401 402 /** 403 * @param string $match matched syntax 404 * @param int $state a LEXER_STATE_* constant 405 * @param int $pos byte position in the original source file 406 * @return bool mode handled? 407 */ 408 public function deleted($match, $state, $pos) { 409 $this->nestingTag($match, $state, $pos, 'deleted'); 410 return true; 411 } 412 413 /** 414 * @param string $match matched syntax 415 * @param int $state a LEXER_STATE_* constant 416 * @param int $pos byte position in the original source file 417 * @return bool mode handled? 418 */ 419 public function footnote($match, $state, $pos) { 420 if (!isset($this->_footnote)) $this->_footnote = false; 421 422 switch ( $state ) { 423 case DOKU_LEXER_ENTER: 424 // footnotes can not be nested - however due to limitations in lexer it can't be prevented 425 // we will still enter a new footnote mode, we just do nothing 426 if ($this->_footnote) { 427 $this->addCall('cdata', array($match), $pos); 428 break; 429 } 430 $this->_footnote = true; 431 432 $this->callWriter = new Nest($this->callWriter, 'footnote_close'); 433 $this->addCall('footnote_open', array(), $pos); 434 break; 435 case DOKU_LEXER_EXIT: 436 // check whether we have already exitted the footnote mode, can happen if the modes were nested 437 if (!$this->_footnote) { 438 $this->addCall('cdata', array($match), $pos); 439 break; 440 } 441 442 $this->_footnote = false; 443 $this->addCall('footnote_close', array(), $pos); 444 445 /** @var Nest $reWriter */ 446 $reWriter = $this->callWriter; 447 $this->callWriter = $reWriter->process(); 448 break; 449 case DOKU_LEXER_UNMATCHED: 450 $this->addCall('cdata', array($match), $pos); 451 break; 452 } 453 return true; 454 } 455 456 /** 457 * @param string $match matched syntax 458 * @param int $state a LEXER_STATE_* constant 459 * @param int $pos byte position in the original source file 460 * @return bool mode handled? 461 */ 462 public function listblock($match, $state, $pos) { 463 switch ( $state ) { 464 case DOKU_LEXER_ENTER: 465 $this->callWriter = new Lists($this->callWriter); 466 $this->addCall('list_open', array($match), $pos); 467 break; 468 case DOKU_LEXER_EXIT: 469 $this->addCall('list_close', array(), $pos); 470 /** @var Lists $reWriter */ 471 $reWriter = $this->callWriter; 472 $this->callWriter = $reWriter->process(); 473 break; 474 case DOKU_LEXER_MATCHED: 475 $this->addCall('list_item', array($match), $pos); 476 break; 477 case DOKU_LEXER_UNMATCHED: 478 $this->addCall('cdata', array($match), $pos); 479 break; 480 } 481 return true; 482 } 483 484 /** 485 * @param string $match matched syntax 486 * @param int $state a LEXER_STATE_* constant 487 * @param int $pos byte position in the original source file 488 * @return bool mode handled? 489 */ 490 public function unformatted($match, $state, $pos) { 491 if ( $state == DOKU_LEXER_UNMATCHED ) { 492 $this->addCall('unformatted', array($match), $pos); 493 } 494 return true; 495 } 496 497 /** 498 * @param string $match matched syntax 499 * @param int $state a LEXER_STATE_* constant 500 * @param int $pos byte position in the original source file 501 * @return bool mode handled? 502 */ 503 public function php($match, $state, $pos) { 504 if ( $state == DOKU_LEXER_UNMATCHED ) { 505 $this->addCall('php', array($match), $pos); 506 } 507 return true; 508 } 509 510 /** 511 * @param string $match matched syntax 512 * @param int $state a LEXER_STATE_* constant 513 * @param int $pos byte position in the original source file 514 * @return bool mode handled? 515 */ 516 public function phpblock($match, $state, $pos) { 517 if ( $state == DOKU_LEXER_UNMATCHED ) { 518 $this->addCall('phpblock', array($match), $pos); 519 } 520 return true; 521 } 522 523 /** 524 * @param string $match matched syntax 525 * @param int $state a LEXER_STATE_* constant 526 * @param int $pos byte position in the original source file 527 * @return bool mode handled? 528 */ 529 public function html($match, $state, $pos) { 530 if ( $state == DOKU_LEXER_UNMATCHED ) { 531 $this->addCall('html', array($match), $pos); 532 } 533 return true; 534 } 535 536 /** 537 * @param string $match matched syntax 538 * @param int $state a LEXER_STATE_* constant 539 * @param int $pos byte position in the original source file 540 * @return bool mode handled? 541 */ 542 public function htmlblock($match, $state, $pos) { 543 if ( $state == DOKU_LEXER_UNMATCHED ) { 544 $this->addCall('htmlblock', array($match), $pos); 545 } 546 return true; 547 } 548 549 /** 550 * @param string $match matched syntax 551 * @param int $state a LEXER_STATE_* constant 552 * @param int $pos byte position in the original source file 553 * @return bool mode handled? 554 */ 555 public function preformatted($match, $state, $pos) { 556 switch ( $state ) { 557 case DOKU_LEXER_ENTER: 558 $this->callWriter = new Preformatted($this->callWriter); 559 $this->addCall('preformatted_start', array(), $pos); 560 break; 561 case DOKU_LEXER_EXIT: 562 $this->addCall('preformatted_end', array(), $pos); 563 /** @var Preformatted $reWriter */ 564 $reWriter = $this->callWriter; 565 $this->callWriter = $reWriter->process(); 566 break; 567 case DOKU_LEXER_MATCHED: 568 $this->addCall('preformatted_newline', array(), $pos); 569 break; 570 case DOKU_LEXER_UNMATCHED: 571 $this->addCall('preformatted_content', array($match), $pos); 572 break; 573 } 574 575 return true; 576 } 577 578 /** 579 * @param string $match matched syntax 580 * @param int $state a LEXER_STATE_* constant 581 * @param int $pos byte position in the original source file 582 * @return bool mode handled? 583 */ 584 public function quote($match, $state, $pos) { 585 586 switch ( $state ) { 587 588 case DOKU_LEXER_ENTER: 589 $this->callWriter = new Quote($this->callWriter); 590 $this->addCall('quote_start', array($match), $pos); 591 break; 592 593 case DOKU_LEXER_EXIT: 594 $this->addCall('quote_end', array(), $pos); 595 /** @var Lists $reWriter */ 596 $reWriter = $this->callWriter; 597 $this->callWriter = $reWriter->process(); 598 break; 599 600 case DOKU_LEXER_MATCHED: 601 $this->addCall('quote_newline', array($match), $pos); 602 break; 603 604 case DOKU_LEXER_UNMATCHED: 605 $this->addCall('cdata', array($match), $pos); 606 break; 607 608 } 609 610 return true; 611 } 612 613 /** 614 * @param string $match matched syntax 615 * @param int $state a LEXER_STATE_* constant 616 * @param int $pos byte position in the original source file 617 * @return bool mode handled? 618 */ 619 public function file($match, $state, $pos) { 620 return $this->code($match, $state, $pos, 'file'); 621 } 622 623 /** 624 * @param string $match matched syntax 625 * @param int $state a LEXER_STATE_* constant 626 * @param int $pos byte position in the original source file 627 * @param string $type either 'code' or 'file' 628 * @return bool mode handled? 629 */ 630 public function code($match, $state, $pos, $type='code') { 631 if ( $state == DOKU_LEXER_UNMATCHED ) { 632 $matches = explode('>',$match,2); 633 // Cut out variable options enclosed in [] 634 preg_match('/\[.*\]/', $matches[0], $options); 635 if (!empty($options[0])) { 636 $matches[0] = str_replace($options[0], '', $matches[0]); 637 } 638 $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY); 639 while(count($param) < 2) array_push($param, null); 640 // We shortcut html here. 641 if ($param[0] == 'html') $param[0] = 'html4strict'; 642 if ($param[0] == '-') $param[0] = null; 643 array_unshift($param, $matches[1]); 644 if (!empty($options[0])) { 645 $param [] = $this->parse_highlight_options ($options[0]); 646 } 647 $this->addCall($type, $param, $pos); 648 } 649 return true; 650 } 651 652 /** 653 * @param string $match matched syntax 654 * @param int $state a LEXER_STATE_* constant 655 * @param int $pos byte position in the original source file 656 * @return bool mode handled? 657 */ 658 public function acronym($match, $state, $pos) { 659 $this->addCall('acronym', array($match), $pos); 660 return true; 661 } 662 663 /** 664 * @param string $match matched syntax 665 * @param int $state a LEXER_STATE_* constant 666 * @param int $pos byte position in the original source file 667 * @return bool mode handled? 668 */ 669 public function smiley($match, $state, $pos) { 670 $this->addCall('smiley', array($match), $pos); 671 return true; 672 } 673 674 /** 675 * @param string $match matched syntax 676 * @param int $state a LEXER_STATE_* constant 677 * @param int $pos byte position in the original source file 678 * @return bool mode handled? 679 */ 680 public function wordblock($match, $state, $pos) { 681 $this->addCall('wordblock', array($match), $pos); 682 return true; 683 } 684 685 /** 686 * @param string $match matched syntax 687 * @param int $state a LEXER_STATE_* constant 688 * @param int $pos byte position in the original source file 689 * @return bool mode handled? 690 */ 691 public function entity($match, $state, $pos) { 692 $this->addCall('entity', array($match), $pos); 693 return true; 694 } 695 696 /** 697 * @param string $match matched syntax 698 * @param int $state a LEXER_STATE_* constant 699 * @param int $pos byte position in the original source file 700 * @return bool mode handled? 701 */ 702 public function multiplyentity($match, $state, $pos) { 703 preg_match_all('/\d+/',$match,$matches); 704 $this->addCall('multiplyentity', array($matches[0][0], $matches[0][1]), $pos); 705 return true; 706 } 707 708 /** 709 * @param string $match matched syntax 710 * @param int $state a LEXER_STATE_* constant 711 * @param int $pos byte position in the original source file 712 * @return bool mode handled? 713 */ 714 public function singlequoteopening($match, $state, $pos) { 715 $this->addCall('singlequoteopening', array(), $pos); 716 return true; 717 } 718 719 /** 720 * @param string $match matched syntax 721 * @param int $state a LEXER_STATE_* constant 722 * @param int $pos byte position in the original source file 723 * @return bool mode handled? 724 */ 725 public function singlequoteclosing($match, $state, $pos) { 726 $this->addCall('singlequoteclosing', array(), $pos); 727 return true; 728 } 729 730 /** 731 * @param string $match matched syntax 732 * @param int $state a LEXER_STATE_* constant 733 * @param int $pos byte position in the original source file 734 * @return bool mode handled? 735 */ 736 public function apostrophe($match, $state, $pos) { 737 $this->addCall('apostrophe', array(), $pos); 738 return true; 739 } 740 741 /** 742 * @param string $match matched syntax 743 * @param int $state a LEXER_STATE_* constant 744 * @param int $pos byte position in the original source file 745 * @return bool mode handled? 746 */ 747 public function doublequoteopening($match, $state, $pos) { 748 $this->addCall('doublequoteopening', array(), $pos); 749 $this->status['doublequote']++; 750 return true; 751 } 752 753 /** 754 * @param string $match matched syntax 755 * @param int $state a LEXER_STATE_* constant 756 * @param int $pos byte position in the original source file 757 * @return bool mode handled? 758 */ 759 public function doublequoteclosing($match, $state, $pos) { 760 if ($this->status['doublequote'] <= 0) { 761 $this->doublequoteopening($match, $state, $pos); 762 } else { 763 $this->addCall('doublequoteclosing', array(), $pos); 764 $this->status['doublequote'] = max(0, --$this->status['doublequote']); 765 } 766 return true; 767 } 768 769 /** 770 * @param string $match matched syntax 771 * @param int $state a LEXER_STATE_* constant 772 * @param int $pos byte position in the original source file 773 * @return bool mode handled? 774 */ 775 public function camelcaselink($match, $state, $pos) { 776 $this->addCall('camelcaselink', array($match), $pos); 777 return true; 778 } 779 780 /** 781 * @param string $match matched syntax 782 * @param int $state a LEXER_STATE_* constant 783 * @param int $pos byte position in the original source file 784 * @return bool mode handled? 785 */ 786 public function internallink($match, $state, $pos) { 787 // Strip the opening and closing markup 788 $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match); 789 790 // Split title from URL 791 $link = explode('|',$link,2); 792 if ( !isset($link[1]) ) { 793 $link[1] = null; 794 } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) { 795 // If the title is an image, convert it to an array containing the image details 796 $link[1] = Doku_Handler_Parse_Media($link[1]); 797 } 798 $link[0] = trim($link[0]); 799 800 //decide which kind of link it is 801 802 if ( link_isinterwiki($link[0]) ) { 803 // Interwiki 804 $interwiki = explode('>',$link[0],2); 805 $this->addCall( 806 'interwikilink', 807 array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), 808 $pos 809 ); 810 }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) { 811 // Windows Share 812 $this->addCall( 813 'windowssharelink', 814 array($link[0],$link[1]), 815 $pos 816 ); 817 }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) { 818 // external link (accepts all protocols) 819 $this->addCall( 820 'externallink', 821 array($link[0],$link[1]), 822 $pos 823 ); 824 }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) { 825 // E-Mail (pattern above is defined in inc/mail.php) 826 $this->addCall( 827 'emaillink', 828 array($link[0],$link[1]), 829 $pos 830 ); 831 }elseif ( preg_match('!^#.+!',$link[0]) ){ 832 // local link 833 $this->addCall( 834 'locallink', 835 array(substr($link[0],1),$link[1]), 836 $pos 837 ); 838 }else{ 839 // internal link 840 $this->addCall( 841 'internallink', 842 array($link[0],$link[1]), 843 $pos 844 ); 845 } 846 847 return true; 848 } 849 850 /** 851 * @param string $match matched syntax 852 * @param int $state a LEXER_STATE_* constant 853 * @param int $pos byte position in the original source file 854 * @return bool mode handled? 855 */ 856 public function filelink($match, $state, $pos) { 857 $this->addCall('filelink', array($match, null), $pos); 858 return true; 859 } 860 861 /** 862 * @param string $match matched syntax 863 * @param int $state a LEXER_STATE_* constant 864 * @param int $pos byte position in the original source file 865 * @return bool mode handled? 866 */ 867 public function windowssharelink($match, $state, $pos) { 868 $this->addCall('windowssharelink', array($match, null), $pos); 869 return true; 870 } 871 872 /** 873 * @param string $match matched syntax 874 * @param int $state a LEXER_STATE_* constant 875 * @param int $pos byte position in the original source file 876 * @return bool mode handled? 877 */ 878 public function media($match, $state, $pos) { 879 $p = Doku_Handler_Parse_Media($match); 880 881 $this->addCall( 882 $p['type'], 883 array($p['src'], $p['title'], $p['align'], $p['width'], 884 $p['height'], $p['cache'], $p['linking']), 885 $pos 886 ); 887 return true; 888 } 889 890 /** 891 * @param string $match matched syntax 892 * @param int $state a LEXER_STATE_* constant 893 * @param int $pos byte position in the original source file 894 * @return bool mode handled? 895 */ 896 public function rss($match, $state, $pos) { 897 $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match); 898 899 // get params 900 list($link,$params) = explode(' ',$link,2); 901 902 $p = array(); 903 if(preg_match('/\b(\d+)\b/',$params,$match)){ 904 $p['max'] = $match[1]; 905 }else{ 906 $p['max'] = 8; 907 } 908 $p['reverse'] = (preg_match('/rev/',$params)); 909 $p['author'] = (preg_match('/\b(by|author)/',$params)); 910 $p['date'] = (preg_match('/\b(date)/',$params)); 911 $p['details'] = (preg_match('/\b(desc|detail)/',$params)); 912 $p['nosort'] = (preg_match('/\b(nosort)\b/',$params)); 913 914 if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) { 915 $period = array('d' => 86400, 'h' => 3600, 'm' => 60); 916 $p['refresh'] = max(600,$match[1]*$period[$match[2]]); // n * period in seconds, minimum 10 minutes 917 } else { 918 $p['refresh'] = 14400; // default to 4 hours 919 } 920 921 $this->addCall('rss', array($link, $p), $pos); 922 return true; 923 } 924 925 /** 926 * @param string $match matched syntax 927 * @param int $state a LEXER_STATE_* constant 928 * @param int $pos byte position in the original source file 929 * @return bool mode handled? 930 */ 931 public function externallink($match, $state, $pos) { 932 $url = $match; 933 $title = null; 934 935 // add protocol on simple short URLs 936 if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){ 937 $title = $url; 938 $url = 'ftp://'.$url; 939 } 940 if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){ 941 $title = $url; 942 $url = 'http://'.$url; 943 } 944 945 $this->addCall('externallink', array($url, $title), $pos); 946 return true; 947 } 948 949 /** 950 * @param string $match matched syntax 951 * @param int $state a LEXER_STATE_* constant 952 * @param int $pos byte position in the original source file 953 * @return bool mode handled? 954 */ 955 public function emaillink($match, $state, $pos) { 956 $email = preg_replace(array('/^</','/>$/'),'',$match); 957 $this->addCall('emaillink', array($email, null), $pos); 958 return true; 959 } 960 961 /** 962 * @param string $match matched syntax 963 * @param int $state a LEXER_STATE_* constant 964 * @param int $pos byte position in the original source file 965 * @return bool mode handled? 966 */ 967 public function table($match, $state, $pos) { 968 switch ( $state ) { 969 970 case DOKU_LEXER_ENTER: 971 972 $this->callWriter = new Table($this->callWriter); 973 974 $this->addCall('table_start', array($pos + 1), $pos); 975 if ( trim($match) == '^' ) { 976 $this->addCall('tableheader', array(), $pos); 977 } else { 978 $this->addCall('tablecell', array(), $pos); 979 } 980 break; 981 982 case DOKU_LEXER_EXIT: 983 $this->addCall('table_end', array($pos), $pos); 984 /** @var Table $reWriter */ 985 $reWriter = $this->callWriter; 986 $this->callWriter = $reWriter->process(); 987 break; 988 989 case DOKU_LEXER_UNMATCHED: 990 if ( trim($match) != '' ) { 991 $this->addCall('cdata', array($match), $pos); 992 } 993 break; 994 995 case DOKU_LEXER_MATCHED: 996 if ( $match == ' ' ){ 997 $this->addCall('cdata', array($match), $pos); 998 } else if ( preg_match('/:::/',$match) ) { 999 $this->addCall('rowspan', array($match), $pos); 1000 } else if ( preg_match('/\t+/',$match) ) { 1001 $this->addCall('table_align', array($match), $pos); 1002 } else if ( preg_match('/ {2,}/',$match) ) { 1003 $this->addCall('table_align', array($match), $pos); 1004 } else if ( $match == "\n|" ) { 1005 $this->addCall('table_row', array(), $pos); 1006 $this->addCall('tablecell', array(), $pos); 1007 } else if ( $match == "\n^" ) { 1008 $this->addCall('table_row', array(), $pos); 1009 $this->addCall('tableheader', array(), $pos); 1010 } else if ( $match == '|' ) { 1011 $this->addCall('tablecell', array(), $pos); 1012 } else if ( $match == '^' ) { 1013 $this->addCall('tableheader', array(), $pos); 1014 } 1015 break; 1016 } 1017 return true; 1018 } 1019 1020 // endregion modes 1021} 1022 1023//------------------------------------------------------------------------ 1024function Doku_Handler_Parse_Media($match) { 1025 1026 // Strip the opening and closing markup 1027 $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); 1028 1029 // Split title from URL 1030 $link = explode('|',$link,2); 1031 1032 // Check alignment 1033 $ralign = (bool)preg_match('/^ /',$link[0]); 1034 $lalign = (bool)preg_match('/ $/',$link[0]); 1035 1036 // Logic = what's that ;)... 1037 if ( $lalign & $ralign ) { 1038 $align = 'center'; 1039 } else if ( $ralign ) { 1040 $align = 'right'; 1041 } else if ( $lalign ) { 1042 $align = 'left'; 1043 } else { 1044 $align = null; 1045 } 1046 1047 // The title... 1048 if ( !isset($link[1]) ) { 1049 $link[1] = null; 1050 } 1051 1052 //remove aligning spaces 1053 $link[0] = trim($link[0]); 1054 1055 //split into src and parameters (using the very last questionmark) 1056 $pos = strrpos($link[0], '?'); 1057 if($pos !== false){ 1058 $src = substr($link[0],0,$pos); 1059 $param = substr($link[0],$pos+1); 1060 }else{ 1061 $src = $link[0]; 1062 $param = ''; 1063 } 1064 1065 //parse width and height 1066 if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){ 1067 !empty($size[1]) ? $w = $size[1] : $w = null; 1068 !empty($size[3]) ? $h = $size[3] : $h = null; 1069 } else { 1070 $w = null; 1071 $h = null; 1072 } 1073 1074 //get linking command 1075 if(preg_match('/nolink/i',$param)){ 1076 $linking = 'nolink'; 1077 }else if(preg_match('/direct/i',$param)){ 1078 $linking = 'direct'; 1079 }else if(preg_match('/linkonly/i',$param)){ 1080 $linking = 'linkonly'; 1081 }else{ 1082 $linking = 'details'; 1083 } 1084 1085 //get caching command 1086 if (preg_match('/(nocache|recache)/i',$param,$cachemode)){ 1087 $cache = $cachemode[1]; 1088 }else{ 1089 $cache = 'cache'; 1090 } 1091 1092 // Check whether this is a local or remote image or interwiki 1093 if (media_isexternal($src) || link_isinterwiki($src)){ 1094 $call = 'externalmedia'; 1095 } else { 1096 $call = 'internalmedia'; 1097 } 1098 1099 $params = array( 1100 'type'=>$call, 1101 'src'=>$src, 1102 'title'=>$link[1], 1103 'align'=>$align, 1104 'width'=>$w, 1105 'height'=>$h, 1106 'cache'=>$cache, 1107 'linking'=>$linking, 1108 ); 1109 1110 return $params; 1111} 1112 1113