[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/parser/ -> handler.php (source)

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