1<?php
2
3use dokuwiki\Extension\Event;
4use dokuwiki\Extension\SyntaxPlugin;
5use dokuwiki\Parsing\Handler\Block;
6use dokuwiki\Parsing\Handler\CallWriter;
7use dokuwiki\Parsing\Handler\CallWriterInterface;
8use dokuwiki\Parsing\Handler\Lists;
9use dokuwiki\Parsing\Handler\Nest;
10use dokuwiki\Parsing\Handler\Preformatted;
11use dokuwiki\Parsing\Handler\Quote;
12use dokuwiki\Parsing\Handler\Table;
13
14/**
15 * Class Doku_Handler
16 */
17class Doku_Handler {
18    /** @var CallWriterInterface */
19    protected $callWriter = null;
20
21    /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
22    public $calls = array();
23
24    /** @var array internal status holders for some modes */
25    protected $status = array(
26        'section' => false,
27        'doublequote' => 0,
28    );
29
30    /** @var bool should blocks be rewritten? FIXME seems to always be true */
31    protected $rewriteBlocks = true;
32
33    /**
34     * Doku_Handler constructor.
35     */
36    public function __construct() {
37        $this->callWriter = new CallWriter($this);
38    }
39
40    /**
41     * Add a new call by passing it to the current CallWriter
42     *
43     * @param string $handler handler method name (see mode handlers below)
44     * @param mixed $args arguments for this call
45     * @param int $pos  byte position in the original source file
46     */
47    public function addCall($handler, $args, $pos) {
48        $call = array($handler,$args, $pos);
49        $this->callWriter->writeCall($call);
50    }
51
52    /**
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//------------------------------------------------------------------------
1017function 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
1106