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