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 // core modes: dispatch through the mode object's handle() method 111 if (isset($this->modeObjects[$modeName])) { 112 return $this->modeObjects[$modeName]->handle($match, $state, $pos, $this); 113 } 114 115 // plugin modes: extract plugin name and call plugin() 116 if (str_starts_with($modeName, 'plugin_')) { 117 [, $plugin] = sexplode('_', $modeName, 2, ''); 118 return $this->plugin($match, $state, $pos, $plugin); 119 } 120 121 // should not be reached — all modes should have registered objects 122 return false; 123 } 124 125 /** 126 * Add a new call by passing it to the current CallWriter 127 * 128 * @param string $handler handler method name (see mode handlers below) 129 * @param mixed $args arguments for this call 130 * @param int $pos byte position in the original source file 131 */ 132 public function addCall($handler, $args, $pos) 133 { 134 $call = [$handler, $args, $pos]; 135 $this->callWriter->writeCall($call); 136 } 137 138 /** 139 * Accessor for the current CallWriter 140 * 141 * @return CallWriterInterface 142 */ 143 public function getCallWriter() 144 { 145 return $this->callWriter; 146 } 147 148 /** 149 * Set a new CallWriter 150 * 151 * @param CallWriterInterface $callWriter 152 */ 153 public function setCallWriter($callWriter) 154 { 155 $this->callWriter = $callWriter; 156 } 157 158 /** 159 * Return the current internal status of the given name 160 * 161 * @param string $status 162 * @return mixed|null 163 */ 164 public function getStatus($status) 165 { 166 if (!isset($this->status[$status])) return null; 167 return $this->status[$status]; 168 } 169 170 /** 171 * Set a new internal status 172 * 173 * @param string $status 174 * @param mixed $value 175 */ 176 public function setStatus($status, $value) 177 { 178 $this->status[$status] = $value; 179 } 180 181 /** @deprecated 2019-10-31 use addCall() instead */ 182 // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- backward compatibility 183 public function _addCall($handler, $args, $pos) 184 { 185 dbg_deprecated('addCall'); 186 $this->addCall($handler, $args, $pos); 187 } 188 189 /** 190 * Similar to addCall, but adds a plugin call 191 * 192 * @param string $plugin name of the plugin 193 * @param mixed $args arguments for this call 194 * @param int $state a LEXER_STATE_* constant 195 * @param int $pos byte position in the original source file 196 * @param string $match matched syntax 197 */ 198 public function addPluginCall($plugin, $args, $state, $pos, $match) 199 { 200 $call = ['plugin', [$plugin, $args, $state, $match], $pos]; 201 $this->callWriter->writeCall($call); 202 } 203 204 /** 205 * Finishes handling 206 * 207 * Called from the parser. Calls finalise() on the call writer, closes open 208 * sections, rewrites blocks and adds document_start and document_end calls. 209 * 210 * @triggers PARSER_HANDLER_DONE 211 */ 212 public function finalize() 213 { 214 $this->callWriter->finalise(); 215 216 if ($this->status['section']) { 217 $last_call = end($this->calls); 218 $this->calls[] = ['section_close', [], $last_call[2]]; 219 } 220 221 $B = new Block(); 222 $this->calls = $B->process($this->calls); 223 224 Event::createAndTrigger('PARSER_HANDLER_DONE', $this); 225 226 array_unshift($this->calls, ['document_start', [], 0]); 227 $last_call = end($this->calls); 228 $this->calls[] = ['document_end', [], $last_call[2]]; 229 } 230 231 /** 232 * Special plugin handler 233 * 234 * This handler is called for all modes starting with 'plugin_'. 235 * An additional parameter with the plugin name is passed. The plugin's handle() 236 * method is called here 237 * 238 * @param string $match matched syntax 239 * @param int $state a LEXER_STATE_* constant 240 * @param int $pos byte position in the original source file 241 * @param string $pluginname name of the plugin 242 * @return bool mode handled? 243 * @author Andreas Gohr <andi@splitbrain.org> 244 */ 245 public function plugin($match, $state, $pos, $pluginname) 246 { 247 $data = [$match]; 248 /** @var SyntaxPlugin $plugin */ 249 $plugin = plugin_load('syntax', $pluginname); 250 if ($plugin != null) { 251 $data = $plugin->handle($match, $state, $pos, $this); 252 } 253 if ($data !== false) { 254 $this->addPluginCall($pluginname, $data, $state, $pos, $match); 255 } 256 return true; 257 } 258 259 // region deprecated wrappers — called by plugins, delegate to mode objects 260 261 /** 262 * @deprecated 2026-04-16 use the Base mode object's handle() method 263 */ 264 public function base($match, $state, $pos) 265 { 266 dbg_deprecated(Base::class . '::handle()'); 267 return $this->modeObjects['base']->handle($match, $state, $pos, $this); 268 } 269 270 /** 271 * @deprecated 2026-04-16 use the Header mode object's handle() method 272 */ 273 public function header($match, $state, $pos) 274 { 275 dbg_deprecated(Header::class . '::handle()'); 276 return $this->modeObjects['header']->handle($match, $state, $pos, $this); 277 } 278 279 /** 280 * @deprecated 2026-04-16 use the Internallink mode object's handle() method 281 */ 282 public function internallink($match, $state, $pos) 283 { 284 dbg_deprecated(Internallink::class . '::handle()'); 285 return $this->modeObjects['internallink']->handle($match, $state, $pos, $this); 286 } 287 288 /** 289 * @deprecated 2026-04-16 use the Media mode object's handle() method 290 */ 291 public function media($match, $state, $pos) 292 { 293 dbg_deprecated(Media::class . '::handle()'); 294 return $this->modeObjects['media']->handle($match, $state, $pos, $this); 295 } 296 297 // endregion deprecated wrappers 298} 299