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