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