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