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