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