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