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