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