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