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