xref: /dokuwiki/inc/Parsing/Handler.php (revision 8788dbbd585b42284320d64cc932f3c875eab6b2)
171096e46SAndreas Gohr<?php
271096e46SAndreas Gohr
371096e46SAndreas Gohrnamespace dokuwiki\Parsing;
471096e46SAndreas Gohr
5*8788dbbdSsplitbrainuse dokuwiki\Parsing\ParserMode\Base;
6*8788dbbdSsplitbrainuse dokuwiki\Parsing\ParserMode\Header;
7*8788dbbdSsplitbrainuse dokuwiki\Parsing\ParserMode\Internallink;
8*8788dbbdSsplitbrainuse dokuwiki\Parsing\ParserMode\Media;
971096e46SAndreas Gohruse dokuwiki\Extension\Event;
1071096e46SAndreas Gohruse dokuwiki\Extension\SyntaxPlugin;
1171096e46SAndreas Gohruse dokuwiki\Parsing\Handler\Block;
1271096e46SAndreas Gohruse dokuwiki\Parsing\Handler\CallWriter;
1371096e46SAndreas Gohruse dokuwiki\Parsing\Handler\CallWriterInterface;
1471096e46SAndreas Gohruse dokuwiki\Parsing\ParserMode\ModeInterface;
1571096e46SAndreas Gohr
1671096e46SAndreas Gohr/**
1771096e46SAndreas Gohr * The Handler receives token events from the Lexer and turns them into
1871096e46SAndreas Gohr * instruction calls for the Renderer.
1971096e46SAndreas Gohr */
2071096e46SAndreas Gohrclass Handler
2171096e46SAndreas Gohr{
2271096e46SAndreas Gohr    /** @var CallWriterInterface */
2371096e46SAndreas Gohr    protected $callWriter;
2471096e46SAndreas Gohr
2571096e46SAndreas Gohr    /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
2671096e46SAndreas Gohr    public $calls = [];
2771096e46SAndreas Gohr
2871096e46SAndreas Gohr    /** @var array internal status holders for some modes */
2971096e46SAndreas Gohr    protected $status = [
3071096e46SAndreas Gohr        'section' => false,
3171096e46SAndreas Gohr        'doublequote' => 0,
3271096e46SAndreas Gohr        'footnote' => false,
3371096e46SAndreas Gohr    ];
3471096e46SAndreas Gohr
3571096e46SAndreas Gohr    /** @var array<string, ModeInterface> mode name → mode object for dispatch */
3671096e46SAndreas Gohr    protected $modeObjects = [];
3771096e46SAndreas Gohr
3871096e46SAndreas Gohr    /** @var string the original (pre-remap) mode name for the current token */
3971096e46SAndreas Gohr    protected $currentModeName = '';
4071096e46SAndreas Gohr
4171096e46SAndreas Gohr    /**
4271096e46SAndreas Gohr     * Handler constructor.
4371096e46SAndreas Gohr     */
4471096e46SAndreas Gohr    public function __construct()
4571096e46SAndreas Gohr    {
4671096e46SAndreas Gohr        $this->callWriter = new CallWriter($this);
4771096e46SAndreas Gohr    }
4871096e46SAndreas Gohr
4971096e46SAndreas Gohr    /**
5071096e46SAndreas Gohr     * Register a mode object for token dispatch.
5171096e46SAndreas Gohr     *
5271096e46SAndreas Gohr     * Called by the Parser when modes are added.
5371096e46SAndreas Gohr     *
5471096e46SAndreas Gohr     * @param string $name Mode name
5571096e46SAndreas Gohr     * @param ModeInterface $obj The mode object
5671096e46SAndreas Gohr     */
5771096e46SAndreas Gohr    public function registerModeObject($name, ModeInterface $obj)
5871096e46SAndreas Gohr    {
5971096e46SAndreas Gohr        $this->modeObjects[$name] = $obj;
6071096e46SAndreas Gohr    }
6171096e46SAndreas Gohr
6271096e46SAndreas Gohr    /**
6371096e46SAndreas Gohr     * Get the original mode name for the current token.
6471096e46SAndreas Gohr     *
6571096e46SAndreas Gohr     * This is the mode name as registered in the Lexer, before any
6671096e46SAndreas Gohr     * mapHandler() remapping. Useful for modes that register multiple
6771096e46SAndreas Gohr     * patterns under different names mapped to the same mode object.
6871096e46SAndreas Gohr     *
6971096e46SAndreas Gohr     * @return string
7071096e46SAndreas Gohr     */
7171096e46SAndreas Gohr    public function getModeName()
7271096e46SAndreas Gohr    {
7371096e46SAndreas Gohr        return $this->currentModeName;
7471096e46SAndreas Gohr    }
7571096e46SAndreas Gohr
7671096e46SAndreas Gohr    /**
7771096e46SAndreas Gohr     * Dispatch a token to the appropriate handler.
7871096e46SAndreas Gohr     *
7971096e46SAndreas Gohr     * This is the single entry point called by the Lexer for every token.
8071096e46SAndreas Gohr     * It dispatches to mode objects, plugins, or sub-mode handler methods.
8171096e46SAndreas Gohr     *
8271096e46SAndreas Gohr     * @param string $modeName The resolved mode name
8371096e46SAndreas Gohr     * @param string $match The matched text
8471096e46SAndreas Gohr     * @param int $state The lexer state (DOKU_LEXER_* constant)
8571096e46SAndreas Gohr     * @param int $pos Byte position in the source
8671096e46SAndreas Gohr     * @param string $originalModeName The original mode name before mapHandler remapping
8771096e46SAndreas Gohr     * @return bool
8871096e46SAndreas Gohr     */
8971096e46SAndreas Gohr    public function handleToken($modeName, $match, $state, $pos, $originalModeName = '')
9071096e46SAndreas Gohr    {
9171096e46SAndreas Gohr        $this->currentModeName = $originalModeName ?: $modeName;
9271096e46SAndreas Gohr
9371096e46SAndreas Gohr        // core modes: dispatch through the mode object's handle() method
9471096e46SAndreas Gohr        if (isset($this->modeObjects[$modeName])) {
9571096e46SAndreas Gohr            return $this->modeObjects[$modeName]->handle($match, $state, $pos, $this);
9671096e46SAndreas Gohr        }
9771096e46SAndreas Gohr
9871096e46SAndreas Gohr        // plugin modes: extract plugin name and call plugin()
9971096e46SAndreas Gohr        if (str_starts_with($modeName, 'plugin_')) {
10071096e46SAndreas Gohr            [, $plugin] = sexplode('_', $modeName, 2, '');
10171096e46SAndreas Gohr            return $this->plugin($match, $state, $pos, $plugin);
10271096e46SAndreas Gohr        }
10371096e46SAndreas Gohr
10471096e46SAndreas Gohr        // should not be reached — all modes should have registered objects
10571096e46SAndreas Gohr        return false;
10671096e46SAndreas Gohr    }
10771096e46SAndreas Gohr
10871096e46SAndreas Gohr    /**
10971096e46SAndreas Gohr     * Add a new call by passing it to the current CallWriter
11071096e46SAndreas Gohr     *
11171096e46SAndreas Gohr     * @param string $handler handler method name (see mode handlers below)
11271096e46SAndreas Gohr     * @param mixed $args arguments for this call
11371096e46SAndreas Gohr     * @param int $pos byte position in the original source file
11471096e46SAndreas Gohr     */
11571096e46SAndreas Gohr    public function addCall($handler, $args, $pos)
11671096e46SAndreas Gohr    {
11771096e46SAndreas Gohr        $call = [$handler, $args, $pos];
11871096e46SAndreas Gohr        $this->callWriter->writeCall($call);
11971096e46SAndreas Gohr    }
12071096e46SAndreas Gohr
12171096e46SAndreas Gohr    /**
12271096e46SAndreas Gohr     * Accessor for the current CallWriter
12371096e46SAndreas Gohr     *
12471096e46SAndreas Gohr     * @return CallWriterInterface
12571096e46SAndreas Gohr     */
12671096e46SAndreas Gohr    public function getCallWriter()
12771096e46SAndreas Gohr    {
12871096e46SAndreas Gohr        return $this->callWriter;
12971096e46SAndreas Gohr    }
13071096e46SAndreas Gohr
13171096e46SAndreas Gohr    /**
13271096e46SAndreas Gohr     * Set a new CallWriter
13371096e46SAndreas Gohr     *
13471096e46SAndreas Gohr     * @param CallWriterInterface $callWriter
13571096e46SAndreas Gohr     */
13671096e46SAndreas Gohr    public function setCallWriter($callWriter)
13771096e46SAndreas Gohr    {
13871096e46SAndreas Gohr        $this->callWriter = $callWriter;
13971096e46SAndreas Gohr    }
14071096e46SAndreas Gohr
14171096e46SAndreas Gohr    /**
14271096e46SAndreas Gohr     * Return the current internal status of the given name
14371096e46SAndreas Gohr     *
14471096e46SAndreas Gohr     * @param string $status
14571096e46SAndreas Gohr     * @return mixed|null
14671096e46SAndreas Gohr     */
14771096e46SAndreas Gohr    public function getStatus($status)
14871096e46SAndreas Gohr    {
14971096e46SAndreas Gohr        if (!isset($this->status[$status])) return null;
15071096e46SAndreas Gohr        return $this->status[$status];
15171096e46SAndreas Gohr    }
15271096e46SAndreas Gohr
15371096e46SAndreas Gohr    /**
15471096e46SAndreas Gohr     * Set a new internal status
15571096e46SAndreas Gohr     *
15671096e46SAndreas Gohr     * @param string $status
15771096e46SAndreas Gohr     * @param mixed $value
15871096e46SAndreas Gohr     */
15971096e46SAndreas Gohr    public function setStatus($status, $value)
16071096e46SAndreas Gohr    {
16171096e46SAndreas Gohr        $this->status[$status] = $value;
16271096e46SAndreas Gohr    }
16371096e46SAndreas Gohr
16471096e46SAndreas Gohr    /** @deprecated 2019-10-31 use addCall() instead */
1653ea3771bSAndreas Gohr    // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- backward compatibility
16671096e46SAndreas Gohr    public function _addCall($handler, $args, $pos)
16771096e46SAndreas Gohr    {
16871096e46SAndreas Gohr        dbg_deprecated('addCall');
16971096e46SAndreas Gohr        $this->addCall($handler, $args, $pos);
17071096e46SAndreas Gohr    }
17171096e46SAndreas Gohr
17271096e46SAndreas Gohr    /**
17371096e46SAndreas Gohr     * Similar to addCall, but adds a plugin call
17471096e46SAndreas Gohr     *
17571096e46SAndreas Gohr     * @param string $plugin name of the plugin
17671096e46SAndreas Gohr     * @param mixed $args arguments for this call
17771096e46SAndreas Gohr     * @param int $state a LEXER_STATE_* constant
17871096e46SAndreas Gohr     * @param int $pos byte position in the original source file
17971096e46SAndreas Gohr     * @param string $match matched syntax
18071096e46SAndreas Gohr     */
18171096e46SAndreas Gohr    public function addPluginCall($plugin, $args, $state, $pos, $match)
18271096e46SAndreas Gohr    {
18371096e46SAndreas Gohr        $call = ['plugin', [$plugin, $args, $state, $match], $pos];
18471096e46SAndreas Gohr        $this->callWriter->writeCall($call);
18571096e46SAndreas Gohr    }
18671096e46SAndreas Gohr
18771096e46SAndreas Gohr    /**
18871096e46SAndreas Gohr     * Finishes handling
18971096e46SAndreas Gohr     *
19071096e46SAndreas Gohr     * Called from the parser. Calls finalise() on the call writer, closes open
19171096e46SAndreas Gohr     * sections, rewrites blocks and adds document_start and document_end calls.
19271096e46SAndreas Gohr     *
19371096e46SAndreas Gohr     * @triggers PARSER_HANDLER_DONE
19471096e46SAndreas Gohr     */
19571096e46SAndreas Gohr    public function finalize()
19671096e46SAndreas Gohr    {
19771096e46SAndreas Gohr        $this->callWriter->finalise();
19871096e46SAndreas Gohr
19971096e46SAndreas Gohr        if ($this->status['section']) {
20071096e46SAndreas Gohr            $last_call = end($this->calls);
20171096e46SAndreas Gohr            $this->calls[] = ['section_close', [], $last_call[2]];
20271096e46SAndreas Gohr        }
20371096e46SAndreas Gohr
20471096e46SAndreas Gohr        $B = new Block();
20571096e46SAndreas Gohr        $this->calls = $B->process($this->calls);
20671096e46SAndreas Gohr
20771096e46SAndreas Gohr        Event::createAndTrigger('PARSER_HANDLER_DONE', $this);
20871096e46SAndreas Gohr
20971096e46SAndreas Gohr        array_unshift($this->calls, ['document_start', [], 0]);
21071096e46SAndreas Gohr        $last_call = end($this->calls);
21171096e46SAndreas Gohr        $this->calls[] = ['document_end', [], $last_call[2]];
21271096e46SAndreas Gohr    }
21371096e46SAndreas Gohr
21471096e46SAndreas Gohr    /**
21571096e46SAndreas Gohr     * Special plugin handler
21671096e46SAndreas Gohr     *
21771096e46SAndreas Gohr     * This handler is called for all modes starting with 'plugin_'.
21871096e46SAndreas Gohr     * An additional parameter with the plugin name is passed. The plugin's handle()
21971096e46SAndreas Gohr     * method is called here
22071096e46SAndreas Gohr     *
22171096e46SAndreas Gohr     * @param string $match matched syntax
22271096e46SAndreas Gohr     * @param int $state a LEXER_STATE_* constant
22371096e46SAndreas Gohr     * @param int $pos byte position in the original source file
22471096e46SAndreas Gohr     * @param string $pluginname name of the plugin
22571096e46SAndreas Gohr     * @return bool mode handled?
22671096e46SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
22771096e46SAndreas Gohr     */
22871096e46SAndreas Gohr    public function plugin($match, $state, $pos, $pluginname)
22971096e46SAndreas Gohr    {
23071096e46SAndreas Gohr        $data = [$match];
23171096e46SAndreas Gohr        /** @var SyntaxPlugin $plugin */
23271096e46SAndreas Gohr        $plugin = plugin_load('syntax', $pluginname);
23371096e46SAndreas Gohr        if ($plugin != null) {
23471096e46SAndreas Gohr            $data = $plugin->handle($match, $state, $pos, $this);
23571096e46SAndreas Gohr        }
23671096e46SAndreas Gohr        if ($data !== false) {
23771096e46SAndreas Gohr            $this->addPluginCall($pluginname, $data, $state, $pos, $match);
23871096e46SAndreas Gohr        }
23971096e46SAndreas Gohr        return true;
24071096e46SAndreas Gohr    }
24171096e46SAndreas Gohr
24271096e46SAndreas Gohr    // region deprecated wrappers — called by plugins, delegate to mode objects
24371096e46SAndreas Gohr
24471096e46SAndreas Gohr    /**
24571096e46SAndreas Gohr     * @deprecated 2026-04-16 use the Base mode object's handle() method
24671096e46SAndreas Gohr     */
24771096e46SAndreas Gohr    public function base($match, $state, $pos)
24871096e46SAndreas Gohr    {
249*8788dbbdSsplitbrain        dbg_deprecated(Base::class . '::handle()');
25071096e46SAndreas Gohr        return $this->modeObjects['base']->handle($match, $state, $pos, $this);
25171096e46SAndreas Gohr    }
25271096e46SAndreas Gohr
25371096e46SAndreas Gohr    /**
25471096e46SAndreas Gohr     * @deprecated 2026-04-16 use the Header mode object's handle() method
25571096e46SAndreas Gohr     */
25671096e46SAndreas Gohr    public function header($match, $state, $pos)
25771096e46SAndreas Gohr    {
258*8788dbbdSsplitbrain        dbg_deprecated(Header::class . '::handle()');
25971096e46SAndreas Gohr        return $this->modeObjects['header']->handle($match, $state, $pos, $this);
26071096e46SAndreas Gohr    }
26171096e46SAndreas Gohr
26271096e46SAndreas Gohr    /**
26371096e46SAndreas Gohr     * @deprecated 2026-04-16 use the Internallink mode object's handle() method
26471096e46SAndreas Gohr     */
26571096e46SAndreas Gohr    public function internallink($match, $state, $pos)
26671096e46SAndreas Gohr    {
267*8788dbbdSsplitbrain        dbg_deprecated(Internallink::class . '::handle()');
26871096e46SAndreas Gohr        return $this->modeObjects['internallink']->handle($match, $state, $pos, $this);
26971096e46SAndreas Gohr    }
27071096e46SAndreas Gohr
27171096e46SAndreas Gohr    /**
27271096e46SAndreas Gohr     * @deprecated 2026-04-16 use the Media mode object's handle() method
27371096e46SAndreas Gohr     */
27471096e46SAndreas Gohr    public function media($match, $state, $pos)
27571096e46SAndreas Gohr    {
276*8788dbbdSsplitbrain        dbg_deprecated(Media::class . '::handle()');
27771096e46SAndreas Gohr        return $this->modeObjects['media']->handle($match, $state, $pos, $this);
27871096e46SAndreas Gohr    }
27971096e46SAndreas Gohr
28071096e46SAndreas Gohr    // endregion deprecated wrappers
28171096e46SAndreas Gohr}
282