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