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