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