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