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