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