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