simplify URL linking in handler
[dokuwiki.git] / inc / parser / handler.php
blob8910c28fe36c746384e4b39556af5c6562f7bd0a
1 <?php
3 use dokuwiki\Extension\Event;
4 use dokuwiki\Extension\SyntaxPlugin;
5 use dokuwiki\Parsing\Handler\Block;
6 use dokuwiki\Parsing\Handler\CallWriter;
7 use dokuwiki\Parsing\Handler\CallWriterInterface;
8 use dokuwiki\Parsing\Handler\Lists;
9 use dokuwiki\Parsing\Handler\Nest;
10 use dokuwiki\Parsing\Handler\Preformatted;
11 use dokuwiki\Parsing\Handler\Quote;
12 use dokuwiki\Parsing\Handler\Table;
14 /**
15 * Class Doku_Handler
17 class Doku_Handler
19 /** @var CallWriterInterface */
20 protected $callWriter;
22 /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
23 public $calls = [];
25 /** @var array internal status holders for some modes */
26 protected $status = [
27 'section' => false,
28 'doublequote' => 0
31 /** @var bool should blocks be rewritten? FIXME seems to always be true */
32 protected $rewriteBlocks = true;
34 /**
35 * @var bool are we in a footnote already?
37 protected $footnote;
39 /**
40 * Doku_Handler constructor.
42 public function __construct()
44 $this->callWriter = new CallWriter($this);
47 /**
48 * Add a new call by passing it to the current CallWriter
50 * @param string $handler handler method name (see mode handlers below)
51 * @param mixed $args arguments for this call
52 * @param int $pos byte position in the original source file
54 public function addCall($handler, $args, $pos)
56 $call = [$handler, $args, $pos];
57 $this->callWriter->writeCall($call);
60 /**
61 * Accessor for the current CallWriter
63 * @return CallWriterInterface
65 public function getCallWriter()
67 return $this->callWriter;
70 /**
71 * Set a new CallWriter
73 * @param CallWriterInterface $callWriter
75 public function setCallWriter($callWriter)
77 $this->callWriter = $callWriter;
80 /**
81 * Return the current internal status of the given name
83 * @param string $status
84 * @return mixed|null
86 public function getStatus($status)
88 if (!isset($this->status[$status])) return null;
89 return $this->status[$status];
92 /**
93 * Set a new internal status
95 * @param string $status
96 * @param mixed $value
98 public function setStatus($status, $value)
100 $this->status[$status] = $value;
103 /** @deprecated 2019-10-31 use addCall() instead */
104 public function _addCall($handler, $args, $pos)
106 dbg_deprecated('addCall');
107 $this->addCall($handler, $args, $pos);
111 * Similar to addCall, but adds a plugin call
113 * @param string $plugin name of the plugin
114 * @param mixed $args arguments for this call
115 * @param int $state a LEXER_STATE_* constant
116 * @param int $pos byte position in the original source file
117 * @param string $match matched syntax
119 public function addPluginCall($plugin, $args, $state, $pos, $match)
121 $call = ['plugin', [$plugin, $args, $state, $match], $pos];
122 $this->callWriter->writeCall($call);
126 * Finishes handling
128 * Called from the parser. Calls finalise() on the call writer, closes open
129 * sections, rewrites blocks and adds document_start and document_end calls.
131 * @triggers PARSER_HANDLER_DONE
133 public function finalize()
135 $this->callWriter->finalise();
137 if ($this->status['section']) {
138 $last_call = end($this->calls);
139 $this->calls[] = ['section_close', [], $last_call[2]];
142 if ($this->rewriteBlocks) {
143 $B = new Block();
144 $this->calls = $B->process($this->calls);
147 Event::createAndTrigger('PARSER_HANDLER_DONE', $this);
149 array_unshift($this->calls, ['document_start', [], 0]);
150 $last_call = end($this->calls);
151 $this->calls[] = ['document_end', [], $last_call[2]];
155 * fetch the current call and advance the pointer to the next one
157 * @fixme seems to be unused?
158 * @return bool|mixed
160 public function fetch()
162 $call = current($this->calls);
163 if ($call !== false) {
164 next($this->calls); //advance the pointer
165 return $call;
167 return false;
172 * Internal function for parsing highlight options.
173 * $options is parsed for key value pairs separated by commas.
174 * A value might also be missing in which case the value will simple
175 * be set to true. Commas in strings are ignored, e.g. option="4,56"
176 * will work as expected and will only create one entry.
178 * @param string $options space separated list of key-value pairs,
179 * e.g. option1=123, option2="456"
180 * @return array|null Array of key-value pairs $array['key'] = 'value';
181 * or null if no entries found
183 protected function parse_highlight_options($options)
185 $result = [];
186 preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
187 foreach ($matches as $match) {
188 $equal_sign = strpos($match [0], '=');
189 if ($equal_sign === false) {
190 $key = trim($match[0]);
191 $result [$key] = 1;
192 } else {
193 $key = substr($match[0], 0, $equal_sign);
194 $value = substr($match[0], $equal_sign + 1);
195 $value = trim($value, '"');
196 if (strlen($value) > 0) {
197 $result [$key] = $value;
198 } else {
199 $result [$key] = 1;
204 // Check for supported options
205 $result = array_intersect_key(
206 $result,
207 array_flip([
208 'enable_line_numbers',
209 'start_line_numbers_at',
210 'highlight_lines_extra',
211 'enable_keyword_links'
215 // Sanitize values
216 if (isset($result['enable_line_numbers'])) {
217 if ($result['enable_line_numbers'] === 'false') {
218 $result['enable_line_numbers'] = false;
220 $result['enable_line_numbers'] = (bool)$result['enable_line_numbers'];
222 if (isset($result['highlight_lines_extra'])) {
223 $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
224 $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
225 $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
227 if (isset($result['start_line_numbers_at'])) {
228 $result['start_line_numbers_at'] = (int)$result['start_line_numbers_at'];
230 if (isset($result['enable_keyword_links'])) {
231 if ($result['enable_keyword_links'] === 'false') {
232 $result['enable_keyword_links'] = false;
234 $result['enable_keyword_links'] = (bool)$result['enable_keyword_links'];
236 if (count($result) == 0) {
237 return null;
240 return $result;
244 * Simplifies handling for the formatting tags which all behave the same
246 * @param string $match matched syntax
247 * @param int $state a LEXER_STATE_* constant
248 * @param int $pos byte position in the original source file
249 * @param string $name actual mode name
251 protected function nestingTag($match, $state, $pos, $name)
253 switch ($state) {
254 case DOKU_LEXER_ENTER:
255 $this->addCall($name . '_open', [], $pos);
256 break;
257 case DOKU_LEXER_EXIT:
258 $this->addCall($name . '_close', [], $pos);
259 break;
260 case DOKU_LEXER_UNMATCHED:
261 $this->addCall('cdata', [$match], $pos);
262 break;
268 * The following methods define the handlers for the different Syntax modes
270 * The handlers are called from dokuwiki\Parsing\Lexer\Lexer\invokeParser()
272 * @todo it might make sense to move these into their own class or merge them with the
273 * ParserMode classes some time.
275 // region mode handlers
278 * Special plugin handler
280 * This handler is called for all modes starting with 'plugin_'.
281 * An additional parameter with the plugin name is passed. The plugin's handle()
282 * method is called here
284 * @param string $match matched syntax
285 * @param int $state a LEXER_STATE_* constant
286 * @param int $pos byte position in the original source file
287 * @param string $pluginname name of the plugin
288 * @return bool mode handled?
289 * @author Andreas Gohr <andi@splitbrain.org>
292 public function plugin($match, $state, $pos, $pluginname)
294 $data = [$match];
295 /** @var SyntaxPlugin $plugin */
296 $plugin = plugin_load('syntax', $pluginname);
297 if ($plugin != null) {
298 $data = $plugin->handle($match, $state, $pos, $this);
300 if ($data !== false) {
301 $this->addPluginCall($pluginname, $data, $state, $pos, $match);
303 return true;
307 * @param string $match matched syntax
308 * @param int $state a LEXER_STATE_* constant
309 * @param int $pos byte position in the original source file
310 * @return bool mode handled?
312 public function base($match, $state, $pos)
314 if ($state === DOKU_LEXER_UNMATCHED) {
315 $this->addCall('cdata', [$match], $pos);
316 return true;
318 return false;
322 * @param string $match matched syntax
323 * @param int $state a LEXER_STATE_* constant
324 * @param int $pos byte position in the original source file
325 * @return bool mode handled?
327 public function header($match, $state, $pos)
329 // get level and title
330 $title = trim($match);
331 $level = 7 - strspn($title, '=');
332 if ($level < 1) $level = 1;
333 $title = trim($title, '=');
334 $title = trim($title);
336 if ($this->status['section']) $this->addCall('section_close', [], $pos);
338 $this->addCall('header', [$title, $level, $pos], $pos);
340 $this->addCall('section_open', [$level], $pos);
341 $this->status['section'] = true;
342 return true;
346 * @param string $match matched syntax
347 * @param int $state a LEXER_STATE_* constant
348 * @param int $pos byte position in the original source file
349 * @return bool mode handled?
351 public function notoc($match, $state, $pos)
353 $this->addCall('notoc', [], $pos);
354 return true;
358 * @param string $match matched syntax
359 * @param int $state a LEXER_STATE_* constant
360 * @param int $pos byte position in the original source file
361 * @return bool mode handled?
363 public function nocache($match, $state, $pos)
365 $this->addCall('nocache', [], $pos);
366 return true;
370 * @param string $match matched syntax
371 * @param int $state a LEXER_STATE_* constant
372 * @param int $pos byte position in the original source file
373 * @return bool mode handled?
375 public function linebreak($match, $state, $pos)
377 $this->addCall('linebreak', [], $pos);
378 return true;
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?
387 public function eol($match, $state, $pos)
389 $this->addCall('eol', [], $pos);
390 return true;
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?
399 public function hr($match, $state, $pos)
401 $this->addCall('hr', [], $pos);
402 return true;
406 * @param string $match matched syntax
407 * @param int $state a LEXER_STATE_* constant
408 * @param int $pos byte position in the original source file
409 * @return bool mode handled?
411 public function strong($match, $state, $pos)
413 $this->nestingTag($match, $state, $pos, 'strong');
414 return true;
418 * @param string $match matched syntax
419 * @param int $state a LEXER_STATE_* constant
420 * @param int $pos byte position in the original source file
421 * @return bool mode handled?
423 public function emphasis($match, $state, $pos)
425 $this->nestingTag($match, $state, $pos, 'emphasis');
426 return true;
430 * @param string $match matched syntax
431 * @param int $state a LEXER_STATE_* constant
432 * @param int $pos byte position in the original source file
433 * @return bool mode handled?
435 public function underline($match, $state, $pos)
437 $this->nestingTag($match, $state, $pos, 'underline');
438 return true;
442 * @param string $match matched syntax
443 * @param int $state a LEXER_STATE_* constant
444 * @param int $pos byte position in the original source file
445 * @return bool mode handled?
447 public function monospace($match, $state, $pos)
449 $this->nestingTag($match, $state, $pos, 'monospace');
450 return true;
454 * @param string $match matched syntax
455 * @param int $state a LEXER_STATE_* constant
456 * @param int $pos byte position in the original source file
457 * @return bool mode handled?
459 public function subscript($match, $state, $pos)
461 $this->nestingTag($match, $state, $pos, 'subscript');
462 return true;
466 * @param string $match matched syntax
467 * @param int $state a LEXER_STATE_* constant
468 * @param int $pos byte position in the original source file
469 * @return bool mode handled?
471 public function superscript($match, $state, $pos)
473 $this->nestingTag($match, $state, $pos, 'superscript');
474 return true;
478 * @param string $match matched syntax
479 * @param int $state a LEXER_STATE_* constant
480 * @param int $pos byte position in the original source file
481 * @return bool mode handled?
483 public function deleted($match, $state, $pos)
485 $this->nestingTag($match, $state, $pos, 'deleted');
486 return true;
490 * @param string $match matched syntax
491 * @param int $state a LEXER_STATE_* constant
492 * @param int $pos byte position in the original source file
493 * @return bool mode handled?
495 public function footnote($match, $state, $pos)
497 if (!isset($this->footnote)) $this->footnote = false;
499 switch ($state) {
500 case DOKU_LEXER_ENTER:
501 // footnotes can not be nested - however due to limitations in lexer it can't be prevented
502 // we will still enter a new footnote mode, we just do nothing
503 if ($this->footnote) {
504 $this->addCall('cdata', [$match], $pos);
505 break;
507 $this->footnote = true;
509 $this->callWriter = new Nest($this->callWriter, 'footnote_close');
510 $this->addCall('footnote_open', [], $pos);
511 break;
512 case DOKU_LEXER_EXIT:
513 // check whether we have already exitted the footnote mode, can happen if the modes were nested
514 if (!$this->footnote) {
515 $this->addCall('cdata', [$match], $pos);
516 break;
519 $this->footnote = false;
520 $this->addCall('footnote_close', [], $pos);
522 /** @var Nest $reWriter */
523 $reWriter = $this->callWriter;
524 $this->callWriter = $reWriter->process();
525 break;
526 case DOKU_LEXER_UNMATCHED:
527 $this->addCall('cdata', [$match], $pos);
528 break;
530 return true;
534 * @param string $match matched syntax
535 * @param int $state a LEXER_STATE_* constant
536 * @param int $pos byte position in the original source file
537 * @return bool mode handled?
539 public function listblock($match, $state, $pos)
541 switch ($state) {
542 case DOKU_LEXER_ENTER:
543 $this->callWriter = new Lists($this->callWriter);
544 $this->addCall('list_open', [$match], $pos);
545 break;
546 case DOKU_LEXER_EXIT:
547 $this->addCall('list_close', [], $pos);
548 /** @var Lists $reWriter */
549 $reWriter = $this->callWriter;
550 $this->callWriter = $reWriter->process();
551 break;
552 case DOKU_LEXER_MATCHED:
553 $this->addCall('list_item', [$match], $pos);
554 break;
555 case DOKU_LEXER_UNMATCHED:
556 $this->addCall('cdata', [$match], $pos);
557 break;
559 return true;
563 * @param string $match matched syntax
564 * @param int $state a LEXER_STATE_* constant
565 * @param int $pos byte position in the original source file
566 * @return bool mode handled?
568 public function unformatted($match, $state, $pos)
570 if ($state == DOKU_LEXER_UNMATCHED) {
571 $this->addCall('unformatted', [$match], $pos);
573 return true;
577 * @param string $match matched syntax
578 * @param int $state a LEXER_STATE_* constant
579 * @param int $pos byte position in the original source file
580 * @return bool mode handled?
582 public function preformatted($match, $state, $pos)
584 switch ($state) {
585 case DOKU_LEXER_ENTER:
586 $this->callWriter = new Preformatted($this->callWriter);
587 $this->addCall('preformatted_start', [], $pos);
588 break;
589 case DOKU_LEXER_EXIT:
590 $this->addCall('preformatted_end', [], $pos);
591 /** @var Preformatted $reWriter */
592 $reWriter = $this->callWriter;
593 $this->callWriter = $reWriter->process();
594 break;
595 case DOKU_LEXER_MATCHED:
596 $this->addCall('preformatted_newline', [], $pos);
597 break;
598 case DOKU_LEXER_UNMATCHED:
599 $this->addCall('preformatted_content', [$match], $pos);
600 break;
603 return true;
607 * @param string $match matched syntax
608 * @param int $state a LEXER_STATE_* constant
609 * @param int $pos byte position in the original source file
610 * @return bool mode handled?
612 public function quote($match, $state, $pos)
615 switch ($state) {
617 case DOKU_LEXER_ENTER:
618 $this->callWriter = new Quote($this->callWriter);
619 $this->addCall('quote_start', [$match], $pos);
620 break;
622 case DOKU_LEXER_EXIT:
623 $this->addCall('quote_end', [], $pos);
624 /** @var Lists $reWriter */
625 $reWriter = $this->callWriter;
626 $this->callWriter = $reWriter->process();
627 break;
629 case DOKU_LEXER_MATCHED:
630 $this->addCall('quote_newline', [$match], $pos);
631 break;
633 case DOKU_LEXER_UNMATCHED:
634 $this->addCall('cdata', [$match], $pos);
635 break;
639 return true;
643 * @param string $match matched syntax
644 * @param int $state a LEXER_STATE_* constant
645 * @param int $pos byte position in the original source file
646 * @return bool mode handled?
648 public function file($match, $state, $pos)
650 return $this->code($match, $state, $pos, 'file');
654 * @param string $match matched syntax
655 * @param int $state a LEXER_STATE_* constant
656 * @param int $pos byte position in the original source file
657 * @param string $type either 'code' or 'file'
658 * @return bool mode handled?
660 public function code($match, $state, $pos, $type = 'code')
662 if ($state == DOKU_LEXER_UNMATCHED) {
663 $matches = sexplode('>', $match, 2, '');
664 // Cut out variable options enclosed in []
665 preg_match('/\[.*\]/', $matches[0], $options);
666 if (!empty($options[0])) {
667 $matches[0] = str_replace($options[0], '', $matches[0]);
669 $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
670 while (count($param) < 2) $param[] = null;
671 // We shortcut html here.
672 if ($param[0] == 'html') $param[0] = 'html4strict';
673 if ($param[0] == '-') $param[0] = null;
674 array_unshift($param, $matches[1]);
675 if (!empty($options[0])) {
676 $param [] = $this->parse_highlight_options($options[0]);
678 $this->addCall($type, $param, $pos);
680 return true;
684 * @param string $match matched syntax
685 * @param int $state a LEXER_STATE_* constant
686 * @param int $pos byte position in the original source file
687 * @return bool mode handled?
689 public function acronym($match, $state, $pos)
691 $this->addCall('acronym', [$match], $pos);
692 return true;
696 * @param string $match matched syntax
697 * @param int $state a LEXER_STATE_* constant
698 * @param int $pos byte position in the original source file
699 * @return bool mode handled?
701 public function smiley($match, $state, $pos)
703 $this->addCall('smiley', [$match], $pos);
704 return true;
708 * @param string $match matched syntax
709 * @param int $state a LEXER_STATE_* constant
710 * @param int $pos byte position in the original source file
711 * @return bool mode handled?
713 public function wordblock($match, $state, $pos)
715 $this->addCall('wordblock', [$match], $pos);
716 return true;
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?
725 public function entity($match, $state, $pos)
727 $this->addCall('entity', [$match], $pos);
728 return true;
732 * @param string $match matched syntax
733 * @param int $state a LEXER_STATE_* constant
734 * @param int $pos byte position in the original source file
735 * @return bool mode handled?
737 public function multiplyentity($match, $state, $pos)
739 preg_match_all('/\d+/', $match, $matches);
740 $this->addCall('multiplyentity', [$matches[0][0], $matches[0][1]], $pos);
741 return true;
745 * @param string $match matched syntax
746 * @param int $state a LEXER_STATE_* constant
747 * @param int $pos byte position in the original source file
748 * @return bool mode handled?
750 public function singlequoteopening($match, $state, $pos)
752 $this->addCall('singlequoteopening', [], $pos);
753 return true;
757 * @param string $match matched syntax
758 * @param int $state a LEXER_STATE_* constant
759 * @param int $pos byte position in the original source file
760 * @return bool mode handled?
762 public function singlequoteclosing($match, $state, $pos)
764 $this->addCall('singlequoteclosing', [], $pos);
765 return true;
769 * @param string $match matched syntax
770 * @param int $state a LEXER_STATE_* constant
771 * @param int $pos byte position in the original source file
772 * @return bool mode handled?
774 public function apostrophe($match, $state, $pos)
776 $this->addCall('apostrophe', [], $pos);
777 return true;
781 * @param string $match matched syntax
782 * @param int $state a LEXER_STATE_* constant
783 * @param int $pos byte position in the original source file
784 * @return bool mode handled?
786 public function doublequoteopening($match, $state, $pos)
788 $this->addCall('doublequoteopening', [], $pos);
789 $this->status['doublequote']++;
790 return true;
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?
799 public function doublequoteclosing($match, $state, $pos)
801 if ($this->status['doublequote'] <= 0) {
802 $this->doublequoteopening($match, $state, $pos);
803 } else {
804 $this->addCall('doublequoteclosing', [], $pos);
805 $this->status['doublequote'] = max(0, --$this->status['doublequote']);
807 return true;
811 * @param string $match matched syntax
812 * @param int $state a LEXER_STATE_* constant
813 * @param int $pos byte position in the original source file
814 * @return bool mode handled?
816 public function camelcaselink($match, $state, $pos)
818 $this->addCall('camelcaselink', [$match], $pos);
819 return true;
823 * @param string $match matched syntax
824 * @param int $state a LEXER_STATE_* constant
825 * @param int $pos byte position in the original source file
826 * @return bool mode handled?
828 public function internallink($match, $state, $pos)
830 // Strip the opening and closing markup
831 $link = preg_replace(['/^\[\[/', '/\]\]$/u'], '', $match);
833 // Split title from URL
834 $link = sexplode('|', $link, 2);
835 if ($link[1] === null) {
836 $link[1] = null;
837 } elseif (preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
838 // If the title is an image, convert it to an array containing the image details
839 $link[1] = Doku_Handler_Parse_Media($link[1]);
841 $link[0] = trim($link[0]);
843 //decide which kind of link it is
845 if (link_isinterwiki($link[0])) {
846 // Interwiki
847 $interwiki = sexplode('>', $link[0], 2, '');
848 $this->addCall(
849 'interwikilink',
850 [$link[0], $link[1], strtolower($interwiki[0]), $interwiki[1]],
851 $pos
853 } elseif (preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) {
854 // Windows Share
855 $this->addCall(
856 'windowssharelink',
857 [$link[0], $link[1]],
858 $pos
860 } elseif (preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) {
861 // external link (accepts all protocols)
862 $this->addCall(
863 'externallink',
864 [$link[0], $link[1]],
865 $pos
867 } elseif (preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) {
868 // E-Mail (pattern above is defined in inc/mail.php)
869 $this->addCall(
870 'emaillink',
871 [$link[0], $link[1]],
872 $pos
874 } elseif (preg_match('!^#.+!', $link[0])) {
875 // local link
876 $this->addCall(
877 'locallink',
878 [substr($link[0], 1), $link[1]],
879 $pos
881 } else {
882 // internal link
883 $this->addCall(
884 'internallink',
885 [$link[0], $link[1]],
886 $pos
890 return true;
894 * @param string $match matched syntax
895 * @param int $state a LEXER_STATE_* constant
896 * @param int $pos byte position in the original source file
897 * @return bool mode handled?
899 public function filelink($match, $state, $pos)
901 $this->addCall('filelink', [$match, null], $pos);
902 return true;
906 * @param string $match matched syntax
907 * @param int $state a LEXER_STATE_* constant
908 * @param int $pos byte position in the original source file
909 * @return bool mode handled?
911 public function windowssharelink($match, $state, $pos)
913 $this->addCall('windowssharelink', [$match, null], $pos);
914 return true;
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?
923 public function media($match, $state, $pos)
925 $p = Doku_Handler_Parse_Media($match);
927 $this->addCall(
928 $p['type'],
929 [$p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache'], $p['linking']],
930 $pos
932 return true;
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?
941 public function rss($match, $state, $pos)
943 $link = preg_replace(['/^\{\{rss>/', '/\}\}$/'], '', $match);
945 // get params
946 [$link, $params] = sexplode(' ', $link, 2, '');
948 $p = [];
949 if (preg_match('/\b(\d+)\b/', $params, $match)) {
950 $p['max'] = $match[1];
951 } else {
952 $p['max'] = 8;
954 $p['reverse'] = (preg_match('/rev/', $params));
955 $p['author'] = (preg_match('/\b(by|author)/', $params));
956 $p['date'] = (preg_match('/\b(date)/', $params));
957 $p['details'] = (preg_match('/\b(desc|detail)/', $params));
958 $p['nosort'] = (preg_match('/\b(nosort)\b/', $params));
960 if (preg_match('/\b(\d+)([dhm])\b/', $params, $match)) {
961 $period = ['d' => 86400, 'h' => 3600, 'm' => 60];
962 $p['refresh'] = max(600, $match[1] * $period[$match[2]]); // n * period in seconds, minimum 10 minutes
963 } else {
964 $p['refresh'] = 14400; // default to 4 hours
967 $this->addCall('rss', [$link, $p], $pos);
968 return true;
972 * @param string $match matched syntax
973 * @param int $state a LEXER_STATE_* constant
974 * @param int $pos byte position in the original source file
975 * @return bool mode handled?
977 public function externallink($match, $state, $pos)
979 $url = $match;
980 $title = null;
982 // add protocol on simple short URLs
983 if (str_starts_with($url, 'ftp') && !str_starts_with($url, 'ftp://')) {
984 $title = $url;
985 $url = 'ftp://' . $url;
987 if (str_starts_with($url, 'www')) {
988 $title = $url;
989 $url = 'http://' . $url;
992 $this->addCall('externallink', [$url, $title], $pos);
993 return true;
997 * @param string $match matched syntax
998 * @param int $state a LEXER_STATE_* constant
999 * @param int $pos byte position in the original source file
1000 * @return bool mode handled?
1002 public function emaillink($match, $state, $pos)
1004 $email = preg_replace(['/^</', '/>$/'], '', $match);
1005 $this->addCall('emaillink', [$email, null], $pos);
1006 return true;
1010 * @param string $match matched syntax
1011 * @param int $state a LEXER_STATE_* constant
1012 * @param int $pos byte position in the original source file
1013 * @return bool mode handled?
1015 public function table($match, $state, $pos)
1017 switch ($state) {
1019 case DOKU_LEXER_ENTER:
1021 $this->callWriter = new Table($this->callWriter);
1023 $this->addCall('table_start', [$pos + 1], $pos);
1024 if (trim($match) == '^') {
1025 $this->addCall('tableheader', [], $pos);
1026 } else {
1027 $this->addCall('tablecell', [], $pos);
1029 break;
1031 case DOKU_LEXER_EXIT:
1032 $this->addCall('table_end', [$pos], $pos);
1033 /** @var Table $reWriter */
1034 $reWriter = $this->callWriter;
1035 $this->callWriter = $reWriter->process();
1036 break;
1038 case DOKU_LEXER_UNMATCHED:
1039 if (trim($match) != '') {
1040 $this->addCall('cdata', [$match], $pos);
1042 break;
1044 case DOKU_LEXER_MATCHED:
1045 if ($match == ' ') {
1046 $this->addCall('cdata', [$match], $pos);
1047 } elseif (preg_match('/:::/', $match)) {
1048 $this->addCall('rowspan', [$match], $pos);
1049 } elseif (preg_match('/\t+/', $match)) {
1050 $this->addCall('table_align', [$match], $pos);
1051 } elseif (preg_match('/ {2,}/', $match)) {
1052 $this->addCall('table_align', [$match], $pos);
1053 } elseif ($match == "\n|") {
1054 $this->addCall('table_row', [], $pos);
1055 $this->addCall('tablecell', [], $pos);
1056 } elseif ($match == "\n^") {
1057 $this->addCall('table_row', [], $pos);
1058 $this->addCall('tableheader', [], $pos);
1059 } elseif ($match == '|') {
1060 $this->addCall('tablecell', [], $pos);
1061 } elseif ($match == '^') {
1062 $this->addCall('tableheader', [], $pos);
1064 break;
1066 return true;
1069 // endregion modes
1072 //------------------------------------------------------------------------
1073 function Doku_Handler_Parse_Media($match)
1076 // Strip the opening and closing markup
1077 $link = preg_replace(['/^\{\{/', '/\}\}$/u'], '', $match);
1079 // Split title from URL
1080 $link = sexplode('|', $link, 2);
1082 // Check alignment
1083 $ralign = (bool)preg_match('/^ /', $link[0]);
1084 $lalign = (bool)preg_match('/ $/', $link[0]);
1086 // Logic = what's that ;)...
1087 if ($lalign & $ralign) {
1088 $align = 'center';
1089 } elseif ($ralign) {
1090 $align = 'right';
1091 } elseif ($lalign) {
1092 $align = 'left';
1093 } else {
1094 $align = null;
1097 // The title...
1098 if (!isset($link[1])) {
1099 $link[1] = null;
1102 //remove aligning spaces
1103 $link[0] = trim($link[0]);
1105 //split into src and parameters (using the very last questionmark)
1106 $pos = strrpos($link[0], '?');
1107 if ($pos !== false) {
1108 $src = substr($link[0], 0, $pos);
1109 $param = substr($link[0], $pos + 1);
1110 } else {
1111 $src = $link[0];
1112 $param = '';
1115 //parse width and height
1116 if (preg_match('#(\d+)(x(\d+))?#i', $param, $size)) {
1117 $w = empty($size[1]) ? null : $size[1];
1118 $h = empty($size[3]) ? null : $size[3];
1119 } else {
1120 $w = null;
1121 $h = null;
1124 //get linking command
1125 if (preg_match('/nolink/i', $param)) {
1126 $linking = 'nolink';
1127 } elseif (preg_match('/direct/i', $param)) {
1128 $linking = 'direct';
1129 } elseif (preg_match('/linkonly/i', $param)) {
1130 $linking = 'linkonly';
1131 } else {
1132 $linking = 'details';
1135 //get caching command
1136 if (preg_match('/(nocache|recache)/i', $param, $cachemode)) {
1137 $cache = $cachemode[1];
1138 } else {
1139 $cache = 'cache';
1142 // Check whether this is a local or remote image or interwiki
1143 if (media_isexternal($src) || link_isinterwiki($src)) {
1144 $call = 'externalmedia';
1145 } else {
1146 $call = 'internalmedia';
1149 $params = [
1150 'type' => $call,
1151 'src' => $src,
1152 'title' => $link[1],
1153 'align' => $align,
1154 'width' => $w,
1155 'height' => $h,
1156 'cache' => $cache,
1157 'linking' => $linking
1160 return $params;