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