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