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