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 bool should blocks be rewritten? FIXME seems to always be true */ 32 protected $rewriteBlocks = true; 33 34 /** @var array<string, ModeInterface> mode name → mode object for dispatch */ 35 protected $modeObjects = []; 36 37 /** @var string the original (pre-remap) mode name for the current token */ 38 protected $currentModeName = ''; 39 40 /** 41 * Handler constructor. 42 */ 43 public function __construct() 44 { 45 $this->callWriter = new CallWriter($this); 46 } 47 48 /** 49 * Register a mode object for token dispatch. 50 * 51 * Called by the Parser when modes are added. 52 * 53 * @param string $name Mode name 54 * @param ModeInterface $obj The mode object 55 */ 56 public function registerModeObject($name, ModeInterface $obj) 57 { 58 $this->modeObjects[$name] = $obj; 59 } 60 61 /** 62 * Get the original mode name for the current token. 63 * 64 * This is the mode name as registered in the Lexer, before any 65 * mapHandler() remapping. Useful for modes that register multiple 66 * patterns under different names mapped to the same mode object. 67 * 68 * @return string 69 */ 70 public function getModeName() 71 { 72 return $this->currentModeName; 73 } 74 75 /** 76 * Dispatch a token to the appropriate handler. 77 * 78 * This is the single entry point called by the Lexer for every token. 79 * It dispatches to mode objects, plugins, or sub-mode handler methods. 80 * 81 * @param string $modeName The resolved mode name 82 * @param string $match The matched text 83 * @param int $state The lexer state (DOKU_LEXER_* constant) 84 * @param int $pos Byte position in the source 85 * @param string $originalModeName The original mode name before mapHandler remapping 86 * @return bool 87 */ 88 public function handleToken($modeName, $match, $state, $pos, $originalModeName = '') 89 { 90 $this->currentModeName = $originalModeName ?: $modeName; 91 92 // core modes: dispatch through the mode object's handle() method 93 if (isset($this->modeObjects[$modeName])) { 94 return $this->modeObjects[$modeName]->handle($match, $state, $pos, $this); 95 } 96 97 // plugin modes: extract plugin name and call plugin() 98 if (str_starts_with($modeName, 'plugin_')) { 99 [, $plugin] = sexplode('_', $modeName, 2, ''); 100 return $this->plugin($match, $state, $pos, $plugin); 101 } 102 103 // should not be reached — all modes should have registered objects 104 return false; 105 } 106 107 /** 108 * Add a new call by passing it to the current CallWriter 109 * 110 * @param string $handler handler method name (see mode handlers below) 111 * @param mixed $args arguments for this call 112 * @param int $pos byte position in the original source file 113 */ 114 public function addCall($handler, $args, $pos) 115 { 116 $call = [$handler, $args, $pos]; 117 $this->callWriter->writeCall($call); 118 } 119 120 /** 121 * Accessor for the current CallWriter 122 * 123 * @return CallWriterInterface 124 */ 125 public function getCallWriter() 126 { 127 return $this->callWriter; 128 } 129 130 /** 131 * Set a new CallWriter 132 * 133 * @param CallWriterInterface $callWriter 134 */ 135 public function setCallWriter($callWriter) 136 { 137 $this->callWriter = $callWriter; 138 } 139 140 /** 141 * Return the current internal status of the given name 142 * 143 * @param string $status 144 * @return mixed|null 145 */ 146 public function getStatus($status) 147 { 148 if (!isset($this->status[$status])) return null; 149 return $this->status[$status]; 150 } 151 152 /** 153 * Set a new internal status 154 * 155 * @param string $status 156 * @param mixed $value 157 */ 158 public function setStatus($status, $value) 159 { 160 $this->status[$status] = $value; 161 } 162 163 /** @deprecated 2019-10-31 use addCall() instead */ 164 // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- backward compatibility 165 public function _addCall($handler, $args, $pos) 166 { 167 dbg_deprecated('addCall'); 168 $this->addCall($handler, $args, $pos); 169 } 170 171 /** 172 * Similar to addCall, but adds a plugin call 173 * 174 * @param string $plugin name of the plugin 175 * @param mixed $args arguments for this call 176 * @param int $state a LEXER_STATE_* constant 177 * @param int $pos byte position in the original source file 178 * @param string $match matched syntax 179 */ 180 public function addPluginCall($plugin, $args, $state, $pos, $match) 181 { 182 $call = ['plugin', [$plugin, $args, $state, $match], $pos]; 183 $this->callWriter->writeCall($call); 184 } 185 186 /** 187 * Finishes handling 188 * 189 * Called from the parser. Calls finalise() on the call writer, closes open 190 * sections, rewrites blocks and adds document_start and document_end calls. 191 * 192 * @triggers PARSER_HANDLER_DONE 193 */ 194 public function finalize() 195 { 196 $this->callWriter->finalise(); 197 198 if ($this->status['section']) { 199 $last_call = end($this->calls); 200 $this->calls[] = ['section_close', [], $last_call[2]]; 201 } 202 203 if ($this->rewriteBlocks) { 204 $B = new Block(); 205 $this->calls = $B->process($this->calls); 206 } 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(ParserMode\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(ParserMode\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(ParserMode\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(ParserMode\Media::class . '::handle()'); 278 return $this->modeObjects['media']->handle($match, $state, $pos, $this); 279 } 280 281 // endregion deprecated wrappers 282} 283