xref: /dokuwiki/inc/Parsing/Handler.php (revision 8a34b0d87864546b9e35ee6a4621d30bf4cd6475)
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        // core modes: dispatch through the mode object's handle() method
111        if (isset($this->modeObjects[$modeName])) {
112            return $this->modeObjects[$modeName]->handle($match, $state, $pos, $this);
113        }
114
115        // plugin modes: extract plugin name and call plugin()
116        if (str_starts_with($modeName, 'plugin_')) {
117            [, $plugin] = sexplode('_', $modeName, 2, '');
118            return $this->plugin($match, $state, $pos, $plugin);
119        }
120
121        // should not be reached — all modes should have registered objects
122        return false;
123    }
124
125    /**
126     * Add a new call by passing it to the current CallWriter
127     *
128     * @param string $handler handler method name (see mode handlers below)
129     * @param mixed $args arguments for this call
130     * @param int $pos byte position in the original source file
131     */
132    public function addCall($handler, $args, $pos)
133    {
134        $call = [$handler, $args, $pos];
135        $this->callWriter->writeCall($call);
136    }
137
138    /**
139     * Accessor for the current CallWriter
140     *
141     * @return CallWriterInterface
142     */
143    public function getCallWriter()
144    {
145        return $this->callWriter;
146    }
147
148    /**
149     * Set a new CallWriter
150     *
151     * @param CallWriterInterface $callWriter
152     */
153    public function setCallWriter($callWriter)
154    {
155        $this->callWriter = $callWriter;
156    }
157
158    /**
159     * Return the current internal status of the given name
160     *
161     * @param string $status
162     * @return mixed|null
163     */
164    public function getStatus($status)
165    {
166        if (!isset($this->status[$status])) return null;
167        return $this->status[$status];
168    }
169
170    /**
171     * Set a new internal status
172     *
173     * @param string $status
174     * @param mixed $value
175     */
176    public function setStatus($status, $value)
177    {
178        $this->status[$status] = $value;
179    }
180
181    /** @deprecated 2019-10-31 use addCall() instead */
182    // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- backward compatibility
183    public function _addCall($handler, $args, $pos)
184    {
185        dbg_deprecated('addCall');
186        $this->addCall($handler, $args, $pos);
187    }
188
189    /**
190     * Similar to addCall, but adds a plugin call
191     *
192     * @param string $plugin name of the plugin
193     * @param mixed $args arguments for this call
194     * @param int $state a LEXER_STATE_* constant
195     * @param int $pos byte position in the original source file
196     * @param string $match matched syntax
197     */
198    public function addPluginCall($plugin, $args, $state, $pos, $match)
199    {
200        $call = ['plugin', [$plugin, $args, $state, $match], $pos];
201        $this->callWriter->writeCall($call);
202    }
203
204    /**
205     * Finishes handling
206     *
207     * Called from the parser. Calls finalise() on the call writer, closes open
208     * sections, rewrites blocks and adds document_start and document_end calls.
209     *
210     * @triggers PARSER_HANDLER_DONE
211     */
212    public function finalize()
213    {
214        $this->callWriter->finalise();
215
216        if ($this->status['section']) {
217            $last_call = end($this->calls);
218            $this->calls[] = ['section_close', [], $last_call[2]];
219        }
220
221        $B = new Block();
222        $this->calls = $B->process($this->calls);
223
224        Event::createAndTrigger('PARSER_HANDLER_DONE', $this);
225
226        array_unshift($this->calls, ['document_start', [], 0]);
227        $last_call = end($this->calls);
228        $this->calls[] = ['document_end', [], $last_call[2]];
229    }
230
231    /**
232     * Special plugin handler
233     *
234     * This handler is called for all modes starting with 'plugin_'.
235     * An additional parameter with the plugin name is passed. The plugin's handle()
236     * method is called here
237     *
238     * @param string $match matched syntax
239     * @param int $state a LEXER_STATE_* constant
240     * @param int $pos byte position in the original source file
241     * @param string $pluginname name of the plugin
242     * @return bool mode handled?
243     * @author Andreas Gohr <andi@splitbrain.org>
244     */
245    public function plugin($match, $state, $pos, $pluginname)
246    {
247        $data = [$match];
248        /** @var SyntaxPlugin $plugin */
249        $plugin = plugin_load('syntax', $pluginname);
250        if ($plugin != null) {
251            $data = $plugin->handle($match, $state, $pos, $this);
252        }
253        if ($data !== false) {
254            $this->addPluginCall($pluginname, $data, $state, $pos, $match);
255        }
256        return true;
257    }
258
259    // region deprecated wrappers — called by plugins, delegate to mode objects
260
261    /**
262     * @deprecated 2026-04-16 use the Base mode object's handle() method
263     */
264    public function base($match, $state, $pos)
265    {
266        dbg_deprecated(Base::class . '::handle()');
267        return $this->modeObjects['base']->handle($match, $state, $pos, $this);
268    }
269
270    /**
271     * @deprecated 2026-04-16 use the Header mode object's handle() method
272     */
273    public function header($match, $state, $pos)
274    {
275        dbg_deprecated(Header::class . '::handle()');
276        return $this->modeObjects['header']->handle($match, $state, $pos, $this);
277    }
278
279    /**
280     * @deprecated 2026-04-16 use the Internallink mode object's handle() method
281     */
282    public function internallink($match, $state, $pos)
283    {
284        dbg_deprecated(Internallink::class . '::handle()');
285        return $this->modeObjects['internallink']->handle($match, $state, $pos, $this);
286    }
287
288    /**
289     * @deprecated 2026-04-16 use the Media mode object's handle() method
290     */
291    public function media($match, $state, $pos)
292    {
293        dbg_deprecated(Media::class . '::handle()');
294        return $this->modeObjects['media']->handle($match, $state, $pos, $this);
295    }
296
297    // endregion deprecated wrappers
298}
299