xref: /dokuwiki/inc/Parsing/Handler.php (revision 71096e46fcbfaeaa808667aba794e77fe2780169)
1<?php
2
3namespace dokuwiki\Parsing;
4
5use dokuwiki\Extension\Event;
6use dokuwiki\Extension\SyntaxPlugin;
7use dokuwiki\Parsing\Handler\Block;
8use dokuwiki\Parsing\Handler\CallWriter;
9use dokuwiki\Parsing\Handler\CallWriterInterface;
10use dokuwiki\Parsing\ParserMode\ModeInterface;
11
12/**
13 * The Handler receives token events from the Lexer and turns them into
14 * instruction calls for the Renderer.
15 */
16class Handler
17{
18    /** @var CallWriterInterface */
19    protected $callWriter;
20
21    /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
22    public $calls = [];
23
24    /** @var array internal status holders for some modes */
25    protected $status = [
26        'section' => false,
27        'doublequote' => 0,
28        'footnote' => false,
29    ];
30
31    /** @var bool should blocks be rewritten? FIXME seems to always be true */
32    protected $rewriteBlocks = true;
33
34    /** @var array<string, ModeInterface> mode name → mode object for dispatch */
35    protected $modeObjects = [];
36
37    /** @var string the original (pre-remap) mode name for the current token */
38    protected $currentModeName = '';
39
40    /**
41     * Handler constructor.
42     */
43    public function __construct()
44    {
45        $this->callWriter = new CallWriter($this);
46    }
47
48    /**
49     * Register a mode object for token dispatch.
50     *
51     * Called by the Parser when modes are added.
52     *
53     * @param string $name Mode name
54     * @param ModeInterface $obj The mode object
55     */
56    public function registerModeObject($name, ModeInterface $obj)
57    {
58        $this->modeObjects[$name] = $obj;
59    }
60
61    /**
62     * Get the original mode name for the current token.
63     *
64     * This is the mode name as registered in the Lexer, before any
65     * mapHandler() remapping. Useful for modes that register multiple
66     * patterns under different names mapped to the same mode object.
67     *
68     * @return string
69     */
70    public function getModeName()
71    {
72        return $this->currentModeName;
73    }
74
75    /**
76     * Dispatch a token to the appropriate handler.
77     *
78     * This is the single entry point called by the Lexer for every token.
79     * It dispatches to mode objects, plugins, or sub-mode handler methods.
80     *
81     * @param string $modeName The resolved mode name
82     * @param string $match The matched text
83     * @param int $state The lexer state (DOKU_LEXER_* constant)
84     * @param int $pos Byte position in the source
85     * @param string $originalModeName The original mode name before mapHandler remapping
86     * @return bool
87     */
88    public function handleToken($modeName, $match, $state, $pos, $originalModeName = '')
89    {
90        $this->currentModeName = $originalModeName ?: $modeName;
91
92        // core modes: dispatch through the mode object's handle() method
93        if (isset($this->modeObjects[$modeName])) {
94            return $this->modeObjects[$modeName]->handle($match, $state, $pos, $this);
95        }
96
97        // plugin modes: extract plugin name and call plugin()
98        if (str_starts_with($modeName, 'plugin_')) {
99            [, $plugin] = sexplode('_', $modeName, 2, '');
100            return $this->plugin($match, $state, $pos, $plugin);
101        }
102
103        // should not be reached — all modes should have registered objects
104        return false;
105    }
106
107    /**
108     * Add a new call by passing it to the current CallWriter
109     *
110     * @param string $handler handler method name (see mode handlers below)
111     * @param mixed $args arguments for this call
112     * @param int $pos byte position in the original source file
113     */
114    public function addCall($handler, $args, $pos)
115    {
116        $call = [$handler, $args, $pos];
117        $this->callWriter->writeCall($call);
118    }
119
120    /**
121     * Accessor for the current CallWriter
122     *
123     * @return CallWriterInterface
124     */
125    public function getCallWriter()
126    {
127        return $this->callWriter;
128    }
129
130    /**
131     * Set a new CallWriter
132     *
133     * @param CallWriterInterface $callWriter
134     */
135    public function setCallWriter($callWriter)
136    {
137        $this->callWriter = $callWriter;
138    }
139
140    /**
141     * Return the current internal status of the given name
142     *
143     * @param string $status
144     * @return mixed|null
145     */
146    public function getStatus($status)
147    {
148        if (!isset($this->status[$status])) return null;
149        return $this->status[$status];
150    }
151
152    /**
153     * Set a new internal status
154     *
155     * @param string $status
156     * @param mixed $value
157     */
158    public function setStatus($status, $value)
159    {
160        $this->status[$status] = $value;
161    }
162
163    /** @deprecated 2019-10-31 use addCall() instead */
164    public function _addCall($handler, $args, $pos)
165    {
166        dbg_deprecated('addCall');
167        $this->addCall($handler, $args, $pos);
168    }
169
170    /**
171     * Similar to addCall, but adds a plugin call
172     *
173     * @param string $plugin name of the plugin
174     * @param mixed $args arguments for this call
175     * @param int $state a LEXER_STATE_* constant
176     * @param int $pos byte position in the original source file
177     * @param string $match matched syntax
178     */
179    public function addPluginCall($plugin, $args, $state, $pos, $match)
180    {
181        $call = ['plugin', [$plugin, $args, $state, $match], $pos];
182        $this->callWriter->writeCall($call);
183    }
184
185    /**
186     * Finishes handling
187     *
188     * Called from the parser. Calls finalise() on the call writer, closes open
189     * sections, rewrites blocks and adds document_start and document_end calls.
190     *
191     * @triggers PARSER_HANDLER_DONE
192     */
193    public function finalize()
194    {
195        $this->callWriter->finalise();
196
197        if ($this->status['section']) {
198            $last_call = end($this->calls);
199            $this->calls[] = ['section_close', [], $last_call[2]];
200        }
201
202        if ($this->rewriteBlocks) {
203            $B = new Block();
204            $this->calls = $B->process($this->calls);
205        }
206
207        Event::createAndTrigger('PARSER_HANDLER_DONE', $this);
208
209        array_unshift($this->calls, ['document_start', [], 0]);
210        $last_call = end($this->calls);
211        $this->calls[] = ['document_end', [], $last_call[2]];
212    }
213
214    /**
215     * Special plugin handler
216     *
217     * This handler is called for all modes starting with 'plugin_'.
218     * An additional parameter with the plugin name is passed. The plugin's handle()
219     * method is called here
220     *
221     * @param string $match matched syntax
222     * @param int $state a LEXER_STATE_* constant
223     * @param int $pos byte position in the original source file
224     * @param string $pluginname name of the plugin
225     * @return bool mode handled?
226     * @author Andreas Gohr <andi@splitbrain.org>
227     */
228    public function plugin($match, $state, $pos, $pluginname)
229    {
230        $data = [$match];
231        /** @var SyntaxPlugin $plugin */
232        $plugin = plugin_load('syntax', $pluginname);
233        if ($plugin != null) {
234            $data = $plugin->handle($match, $state, $pos, $this);
235        }
236        if ($data !== false) {
237            $this->addPluginCall($pluginname, $data, $state, $pos, $match);
238        }
239        return true;
240    }
241
242    // region deprecated wrappers — called by plugins, delegate to mode objects
243
244    /**
245     * @deprecated 2026-04-16 use the Base mode object's handle() method
246     */
247    public function base($match, $state, $pos)
248    {
249        dbg_deprecated(ParserMode\Base::class . '::handle()');
250        return $this->modeObjects['base']->handle($match, $state, $pos, $this);
251    }
252
253    /**
254     * @deprecated 2026-04-16 use the Header mode object's handle() method
255     */
256    public function header($match, $state, $pos)
257    {
258        dbg_deprecated(ParserMode\Header::class . '::handle()');
259        return $this->modeObjects['header']->handle($match, $state, $pos, $this);
260    }
261
262    /**
263     * @deprecated 2026-04-16 use the Internallink mode object's handle() method
264     */
265    public function internallink($match, $state, $pos)
266    {
267        dbg_deprecated(ParserMode\Internallink::class . '::handle()');
268        return $this->modeObjects['internallink']->handle($match, $state, $pos, $this);
269    }
270
271    /**
272     * @deprecated 2026-04-16 use the Media mode object's handle() method
273     */
274    public function media($match, $state, $pos)
275    {
276        dbg_deprecated(ParserMode\Media::class . '::handle()');
277        return $this->modeObjects['media']->handle($match, $state, $pos, $this);
278    }
279
280    // endregion deprecated wrappers
281}
282