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