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