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