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