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