[ 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 preformatted($match, $state, $pos) {
 549          switch ( $state ) {
 550              case DOKU_LEXER_ENTER:
 551                  $this->callWriter = new Preformatted($this->callWriter);
 552                  $this->addCall('preformatted_start', array(), $pos);
 553              break;
 554              case DOKU_LEXER_EXIT:
 555                  $this->addCall('preformatted_end', array(), $pos);
 556                  /** @var Preformatted $reWriter */
 557                  $reWriter = $this->callWriter;
 558                  $this->callWriter = $reWriter->process();
 559              break;
 560              case DOKU_LEXER_MATCHED:
 561                  $this->addCall('preformatted_newline', array(), $pos);
 562              break;
 563              case DOKU_LEXER_UNMATCHED:
 564                  $this->addCall('preformatted_content', array($match), $pos);
 565              break;
 566          }
 567  
 568          return true;
 569      }
 570  
 571      /**
 572       * @param string $match matched syntax
 573       * @param int $state a LEXER_STATE_* constant
 574       * @param int $pos byte position in the original source file
 575       * @return bool mode handled?
 576       */
 577      public function quote($match, $state, $pos) {
 578  
 579          switch ( $state ) {
 580  
 581              case DOKU_LEXER_ENTER:
 582                  $this->callWriter = new Quote($this->callWriter);
 583                  $this->addCall('quote_start', array($match), $pos);
 584              break;
 585  
 586              case DOKU_LEXER_EXIT:
 587                  $this->addCall('quote_end', array(), $pos);
 588                  /** @var Lists $reWriter */
 589                  $reWriter = $this->callWriter;
 590                  $this->callWriter = $reWriter->process();
 591              break;
 592  
 593              case DOKU_LEXER_MATCHED:
 594                  $this->addCall('quote_newline', array($match), $pos);
 595              break;
 596  
 597              case DOKU_LEXER_UNMATCHED:
 598                  $this->addCall('cdata', array($match), $pos);
 599              break;
 600  
 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 file($match, $state, $pos) {
 613          return $this->code($match, $state, $pos, 'file');
 614      }
 615  
 616      /**
 617       * @param string $match matched syntax
 618       * @param int $state a LEXER_STATE_* constant
 619       * @param int $pos byte position in the original source file
 620       * @param string $type either 'code' or 'file'
 621       * @return bool mode handled?
 622       */
 623      public function code($match, $state, $pos, $type='code') {
 624          if ( $state == DOKU_LEXER_UNMATCHED ) {
 625              $matches = sexplode('>',$match,2,'');
 626              // Cut out variable options enclosed in []
 627              preg_match('/\[.*\]/', $matches[0], $options);
 628              if (!empty($options[0])) {
 629                  $matches[0] = str_replace($options[0], '', $matches[0]);
 630              }
 631              $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
 632              while(count($param) < 2) array_push($param, null);
 633              // We shortcut html here.
 634              if ($param[0] == 'html') $param[0] = 'html4strict';
 635              if ($param[0] == '-') $param[0] = null;
 636              array_unshift($param, $matches[1]);
 637              if (!empty($options[0])) {
 638                  $param [] = $this->parse_highlight_options ($options[0]);
 639              }
 640              $this->addCall($type, $param, $pos);
 641          }
 642          return true;
 643      }
 644  
 645      /**
 646       * @param string $match matched syntax
 647       * @param int $state a LEXER_STATE_* constant
 648       * @param int $pos byte position in the original source file
 649       * @return bool mode handled?
 650       */
 651      public function acronym($match, $state, $pos) {
 652          $this->addCall('acronym', array($match), $pos);
 653          return true;
 654      }
 655  
 656      /**
 657       * @param string $match matched syntax
 658       * @param int $state a LEXER_STATE_* constant
 659       * @param int $pos byte position in the original source file
 660       * @return bool mode handled?
 661       */
 662      public function smiley($match, $state, $pos) {
 663          $this->addCall('smiley', array($match), $pos);
 664          return true;
 665      }
 666  
 667      /**
 668       * @param string $match matched syntax
 669       * @param int $state a LEXER_STATE_* constant
 670       * @param int $pos byte position in the original source file
 671       * @return bool mode handled?
 672       */
 673      public function wordblock($match, $state, $pos) {
 674          $this->addCall('wordblock', array($match), $pos);
 675          return true;
 676      }
 677  
 678      /**
 679       * @param string $match matched syntax
 680       * @param int $state a LEXER_STATE_* constant
 681       * @param int $pos byte position in the original source file
 682       * @return bool mode handled?
 683       */
 684      public function entity($match, $state, $pos) {
 685          $this->addCall('entity', array($match), $pos);
 686          return true;
 687      }
 688  
 689      /**
 690       * @param string $match matched syntax
 691       * @param int $state a LEXER_STATE_* constant
 692       * @param int $pos byte position in the original source file
 693       * @return bool mode handled?
 694       */
 695      public function multiplyentity($match, $state, $pos) {
 696          preg_match_all('/\d+/',$match,$matches);
 697          $this->addCall('multiplyentity', array($matches[0][0], $matches[0][1]), $pos);
 698          return true;
 699      }
 700  
 701      /**
 702       * @param string $match matched syntax
 703       * @param int $state a LEXER_STATE_* constant
 704       * @param int $pos byte position in the original source file
 705       * @return bool mode handled?
 706       */
 707      public function singlequoteopening($match, $state, $pos) {
 708          $this->addCall('singlequoteopening', array(), $pos);
 709          return true;
 710      }
 711  
 712      /**
 713       * @param string $match matched syntax
 714       * @param int $state a LEXER_STATE_* constant
 715       * @param int $pos byte position in the original source file
 716       * @return bool mode handled?
 717       */
 718      public function singlequoteclosing($match, $state, $pos) {
 719          $this->addCall('singlequoteclosing', array(), $pos);
 720          return true;
 721      }
 722  
 723      /**
 724       * @param string $match matched syntax
 725       * @param int $state a LEXER_STATE_* constant
 726       * @param int $pos byte position in the original source file
 727       * @return bool mode handled?
 728       */
 729      public function apostrophe($match, $state, $pos) {
 730          $this->addCall('apostrophe', array(), $pos);
 731          return true;
 732      }
 733  
 734      /**
 735       * @param string $match matched syntax
 736       * @param int $state a LEXER_STATE_* constant
 737       * @param int $pos byte position in the original source file
 738       * @return bool mode handled?
 739       */
 740      public function doublequoteopening($match, $state, $pos) {
 741          $this->addCall('doublequoteopening', array(), $pos);
 742          $this->status['doublequote']++;
 743          return true;
 744      }
 745  
 746      /**
 747       * @param string $match matched syntax
 748       * @param int $state a LEXER_STATE_* constant
 749       * @param int $pos byte position in the original source file
 750       * @return bool mode handled?
 751       */
 752      public function doublequoteclosing($match, $state, $pos) {
 753          if ($this->status['doublequote'] <= 0) {
 754              $this->doublequoteopening($match, $state, $pos);
 755          } else {
 756              $this->addCall('doublequoteclosing', array(), $pos);
 757              $this->status['doublequote'] = max(0, --$this->status['doublequote']);
 758          }
 759          return true;
 760      }
 761  
 762      /**
 763       * @param string $match matched syntax
 764       * @param int $state a LEXER_STATE_* constant
 765       * @param int $pos byte position in the original source file
 766       * @return bool mode handled?
 767       */
 768      public function camelcaselink($match, $state, $pos) {
 769          $this->addCall('camelcaselink', array($match), $pos);
 770          return true;
 771      }
 772  
 773      /**
 774       * @param string $match matched syntax
 775       * @param int $state a LEXER_STATE_* constant
 776       * @param int $pos byte position in the original source file
 777       * @return bool mode handled?
 778       */
 779      public function internallink($match, $state, $pos) {
 780          // Strip the opening and closing markup
 781          $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match);
 782  
 783          // Split title from URL
 784          $link = sexplode('|',$link,2);
 785          if ( $link[1] === null ) {
 786              $link[1] = null;
 787          } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) {
 788              // If the title is an image, convert it to an array containing the image details
 789              $link[1] = Doku_Handler_Parse_Media($link[1]);
 790          }
 791          $link[0] = trim($link[0]);
 792  
 793          //decide which kind of link it is
 794  
 795          if ( link_isinterwiki($link[0]) ) {
 796              // Interwiki
 797              $interwiki = sexplode('>',$link[0],2,'');
 798              $this->addCall(
 799                  'interwikilink',
 800                  array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]),
 801                  $pos
 802                  );
 803          }elseif ( preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u',$link[0]) ) {
 804              // Windows Share
 805              $this->addCall(
 806                  'windowssharelink',
 807                  array($link[0],$link[1]),
 808                  $pos
 809                  );
 810          }elseif ( preg_match('#^([a-z0-9\-\.+]+?)://#i',$link[0]) ) {
 811              // external link (accepts all protocols)
 812              $this->addCall(
 813                      'externallink',
 814                      array($link[0],$link[1]),
 815                      $pos
 816                      );
 817          }elseif ( preg_match('<'.PREG_PATTERN_VALID_EMAIL.'>',$link[0]) ) {
 818              // E-Mail (pattern above is defined in inc/mail.php)
 819              $this->addCall(
 820                  'emaillink',
 821                  array($link[0],$link[1]),
 822                  $pos
 823                  );
 824          }elseif ( preg_match('!^#.+!',$link[0]) ){
 825              // local link
 826              $this->addCall(
 827                  'locallink',
 828                  array(substr($link[0],1),$link[1]),
 829                  $pos
 830                  );
 831          }else{
 832              // internal link
 833              $this->addCall(
 834                  'internallink',
 835                  array($link[0],$link[1]),
 836                  $pos
 837                  );
 838          }
 839  
 840          return true;
 841      }
 842  
 843      /**
 844       * @param string $match matched syntax
 845       * @param int $state a LEXER_STATE_* constant
 846       * @param int $pos byte position in the original source file
 847       * @return bool mode handled?
 848       */
 849      public function filelink($match, $state, $pos) {
 850          $this->addCall('filelink', array($match, null), $pos);
 851          return true;
 852      }
 853  
 854      /**
 855       * @param string $match matched syntax
 856       * @param int $state a LEXER_STATE_* constant
 857       * @param int $pos byte position in the original source file
 858       * @return bool mode handled?
 859       */
 860      public function windowssharelink($match, $state, $pos) {
 861          $this->addCall('windowssharelink', array($match, null), $pos);
 862          return true;
 863      }
 864  
 865      /**
 866       * @param string $match matched syntax
 867       * @param int $state a LEXER_STATE_* constant
 868       * @param int $pos byte position in the original source file
 869       * @return bool mode handled?
 870       */
 871      public function media($match, $state, $pos) {
 872          $p = Doku_Handler_Parse_Media($match);
 873  
 874          $this->addCall(
 875                $p['type'],
 876                array($p['src'], $p['title'], $p['align'], $p['width'],
 877                       $p['height'], $p['cache'], $p['linking']),
 878                $pos
 879               );
 880          return true;
 881      }
 882  
 883      /**
 884       * @param string $match matched syntax
 885       * @param int $state a LEXER_STATE_* constant
 886       * @param int $pos byte position in the original source file
 887       * @return bool mode handled?
 888       */
 889      public function rss($match, $state, $pos) {
 890          $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match);
 891  
 892          // get params
 893          list($link, $params) = sexplode(' ', $link, 2, '');
 894  
 895          $p = array();
 896          if(preg_match('/\b(\d+)\b/',$params,$match)){
 897              $p['max'] = $match[1];
 898          }else{
 899              $p['max'] = 8;
 900          }
 901          $p['reverse'] = (preg_match('/rev/',$params));
 902          $p['author']  = (preg_match('/\b(by|author)/',$params));
 903          $p['date']    = (preg_match('/\b(date)/',$params));
 904          $p['details'] = (preg_match('/\b(desc|detail)/',$params));
 905          $p['nosort']  = (preg_match('/\b(nosort)\b/',$params));
 906  
 907          if (preg_match('/\b(\d+)([dhm])\b/',$params,$match)) {
 908              $period = array('d' => 86400, 'h' => 3600, 'm' => 60);
 909              $p['refresh'] = max(600,$match[1]*$period[$match[2]]);  // n * period in seconds, minimum 10 minutes
 910          } else {
 911              $p['refresh'] = 14400;   // default to 4 hours
 912          }
 913  
 914          $this->addCall('rss', array($link, $p), $pos);
 915          return true;
 916      }
 917  
 918      /**
 919       * @param string $match matched syntax
 920       * @param int $state a LEXER_STATE_* constant
 921       * @param int $pos byte position in the original source file
 922       * @return bool mode handled?
 923       */
 924      public function externallink($match, $state, $pos) {
 925          $url   = $match;
 926          $title = null;
 927  
 928          // add protocol on simple short URLs
 929          if(substr($url,0,3) == 'ftp' && (substr($url,0,6) != 'ftp://')){
 930              $title = $url;
 931              $url   = 'ftp://'.$url;
 932          }
 933          if(substr($url,0,3) == 'www' && (substr($url,0,7) != 'http://')){
 934              $title = $url;
 935              $url = 'http://'.$url;
 936          }
 937  
 938          $this->addCall('externallink', array($url, $title), $pos);
 939          return true;
 940      }
 941  
 942      /**
 943       * @param string $match matched syntax
 944       * @param int $state a LEXER_STATE_* constant
 945       * @param int $pos byte position in the original source file
 946       * @return bool mode handled?
 947       */
 948      public function emaillink($match, $state, $pos) {
 949          $email = preg_replace(array('/^</','/>$/'),'',$match);
 950          $this->addCall('emaillink', array($email, null), $pos);
 951          return true;
 952      }
 953  
 954      /**
 955       * @param string $match matched syntax
 956       * @param int $state a LEXER_STATE_* constant
 957       * @param int $pos byte position in the original source file
 958       * @return bool mode handled?
 959       */
 960      public function table($match, $state, $pos) {
 961          switch ( $state ) {
 962  
 963              case DOKU_LEXER_ENTER:
 964  
 965                  $this->callWriter = new Table($this->callWriter);
 966  
 967                  $this->addCall('table_start', array($pos + 1), $pos);
 968                  if ( trim($match) == '^' ) {
 969                      $this->addCall('tableheader', array(), $pos);
 970                  } else {
 971                      $this->addCall('tablecell', array(), $pos);
 972                  }
 973              break;
 974  
 975              case DOKU_LEXER_EXIT:
 976                  $this->addCall('table_end', array($pos), $pos);
 977                  /** @var Table $reWriter */
 978                  $reWriter = $this->callWriter;
 979                  $this->callWriter = $reWriter->process();
 980              break;
 981  
 982              case DOKU_LEXER_UNMATCHED:
 983                  if ( trim($match) != '' ) {
 984                      $this->addCall('cdata', array($match), $pos);
 985                  }
 986              break;
 987  
 988              case DOKU_LEXER_MATCHED:
 989                  if ( $match == ' ' ){
 990                      $this->addCall('cdata', array($match), $pos);
 991                  } else if ( preg_match('/:::/',$match) ) {
 992                      $this->addCall('rowspan', array($match), $pos);
 993                  } else if ( preg_match('/\t+/',$match) ) {
 994                      $this->addCall('table_align', array($match), $pos);
 995                  } else if ( preg_match('/ {2,}/',$match) ) {
 996                      $this->addCall('table_align', array($match), $pos);
 997                  } else if ( $match == "\n|" ) {
 998                      $this->addCall('table_row', array(), $pos);
 999                      $this->addCall('tablecell', array(), $pos);
1000                  } else if ( $match == "\n^" ) {
1001                      $this->addCall('table_row', array(), $pos);
1002                      $this->addCall('tableheader', array(), $pos);
1003                  } else if ( $match == '|' ) {
1004                      $this->addCall('tablecell', array(), $pos);
1005                  } else if ( $match == '^' ) {
1006                      $this->addCall('tableheader', array(), $pos);
1007                  }
1008              break;
1009          }
1010          return true;
1011      }
1012  
1013      // endregion modes
1014  }
1015  
1016  //------------------------------------------------------------------------
1017  function Doku_Handler_Parse_Media($match) {
1018  
1019      // Strip the opening and closing markup
1020      $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match);
1021  
1022      // Split title from URL
1023      $link = sexplode('|', $link, 2);
1024  
1025      // Check alignment
1026      $ralign = (bool)preg_match('/^ /',$link[0]);
1027      $lalign = (bool)preg_match('/ $/',$link[0]);
1028  
1029      // Logic = what's that ;)...
1030      if ( $lalign & $ralign ) {
1031          $align = 'center';
1032      } else if ( $ralign ) {
1033          $align = 'right';
1034      } else if ( $lalign ) {
1035          $align = 'left';
1036      } else {
1037          $align = null;
1038      }
1039  
1040      // The title...
1041      if ( !isset($link[1]) ) {
1042          $link[1] = null;
1043      }
1044  
1045      //remove aligning spaces
1046      $link[0] = trim($link[0]);
1047  
1048      //split into src and parameters (using the very last questionmark)
1049      $pos = strrpos($link[0], '?');
1050      if($pos !== false){
1051          $src   = substr($link[0],0,$pos);
1052          $param = substr($link[0],$pos+1);
1053      }else{
1054          $src   = $link[0];
1055          $param = '';
1056      }
1057  
1058      //parse width and height
1059      if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){
1060          !empty($size[1]) ? $w = $size[1] : $w = null;
1061          !empty($size[3]) ? $h = $size[3] : $h = null;
1062      } else {
1063          $w = null;
1064          $h = null;
1065      }
1066  
1067      //get linking command
1068      if(preg_match('/nolink/i',$param)){
1069          $linking = 'nolink';
1070      }else if(preg_match('/direct/i',$param)){
1071          $linking = 'direct';
1072      }else if(preg_match('/linkonly/i',$param)){
1073          $linking = 'linkonly';
1074      }else{
1075          $linking = 'details';
1076      }
1077  
1078      //get caching command
1079      if (preg_match('/(nocache|recache)/i',$param,$cachemode)){
1080          $cache = $cachemode[1];
1081      }else{
1082          $cache = 'cache';
1083      }
1084  
1085      // Check whether this is a local or remote image or interwiki
1086      if (media_isexternal($src) || link_isinterwiki($src)){
1087          $call = 'externalmedia';
1088      } else {
1089          $call = 'internalmedia';
1090      }
1091  
1092      $params = array(
1093          'type'=>$call,
1094          'src'=>$src,
1095          'title'=>$link[1],
1096          'align'=>$align,
1097          'width'=>$w,
1098          'height'=>$h,
1099          'cache'=>$cache,
1100          'linking'=>$linking,
1101      );
1102  
1103      return $params;
1104  }
1105