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