1<?php 2 3namespace dokuwiki\Parsing\ParserMode; 4 5use dokuwiki\Parsing\Handler; 6use dokuwiki\Parsing\Lexer\Lexer; 7use dokuwiki\Parsing\ModeRegistry; 8 9/** 10 * Base class for every parser mode (syntax component) in the Parser. 11 * 12 * Besides reducing the effort required to register modes with the Lexer, this 13 * class defines the mode contract the engine relies on: getSort() and handle() 14 * are abstract and must be implemented by every mode; preConnect(), connectTo(), 15 * postConnect() and accepts() carry default implementations subclasses override 16 * as needed. Parser, Handler and ModeRegistry type-hint this class directly. 17 * 18 * @author Harry Fuecks <hfuecks@gmail.com> 19 */ 20abstract class AbstractMode 21{ 22 /** 23 * @var Lexer the lexer this mode registers its patterns with. 24 * 25 * Injected via setLexer() by Parser::addMode() / addBaseMode() before any 26 * connect callback runs, so every core mode and plugin reads it as 27 * $this->Lexer from connectTo(). External code reads it via getLexer(). 28 */ 29 protected Lexer $Lexer; 30 31 /** 32 * @var ModeRegistry the registry of the parse this mode belongs to. 33 * Injected by Parser::addMode() before any connect/handle callback runs, 34 * so subclasses may read $this->registry unconditionally from preConnect(), 35 * connectTo(), postConnect(), handle() and accepts(). 36 */ 37 protected ModeRegistry $registry; 38 39 /** 40 * @var string[] mode names accepted as nested content inside this mode. 41 * 42 * Resolved once in setModeRegistry(): allowedCategories() mapped to concrete 43 * mode names via the registry and merged with any names a subclass assigned 44 * to this list directly, then passed through filterAllowedModes(). A subclass 45 * may therefore combine categories with individual mode names (e.g. a sibling 46 * component), or, when it declares no categories, assign the list directly and 47 * have it used as-is. 48 */ 49 protected $allowedModes = []; 50 51 //region Pattern building blocks 52 53 /** 54 * Zero-width assertion: not at the start of a paragraph break 55 * (Lexer::PARA_BREAK). 56 * 57 * The lexer compiles all patterns with the `s` (DOTALL) flag, so a 58 * plain `.*` inside an entry-pattern lookahead would match across blank 59 * lines and let an unclosed delimiter greedily consume following 60 * paragraphs. Place this assertion before a character class to stop the 61 * match at a paragraph boundary. 62 * 63 * @var string 64 */ 65 protected const NOT_AT_PARA_BREAK = '(?!' . Lexer::PARA_BREAK . ')'; 66 67 /** 68 * Character class: a single "non-word" character — ASCII whitespace or 69 * any ASCII punctuation character except the underscore. 70 * 71 * The `_` is excluded because it is itself a delimiter for emphasis in 72 * GFM/CommonMark; treating it as non-word would let `__foo` incorrectly 73 * open emphasis at the second `_`. 74 * 75 * Multibyte rationale: the lexer compiles patterns without the `u` flag, 76 * so UTF-8 is treated as individual bytes. Multibyte characters begin 77 * with bytes >= 0x80, which fall outside every ASCII character class. 78 * Checking that the surrounding context matches NON_WORD_CHAR positively 79 * therefore correctly treats multibyte letters as word-like — preventing 80 * intraword matches in non-Latin text (e.g. `für_etwas`, `日本_語`) 81 * without requiring `u` flag support across the whole lexer. 82 * 83 * @var string 84 */ 85 protected const NON_WORD_CHAR = '[\s!"#$%&\'()*+,\-./:;<=>?@\[\\\\\]^`{|}~]'; 86 87 /** 88 * Zero-width assertion: current position is preceded by a non-word 89 * character, or is at the start of input/line. See {@see self::NON_WORD_CHAR} 90 * for the multibyte reasoning. 91 * 92 * @var string 93 */ 94 protected const NO_WORD_BEFORE = '(?:^|(?<=' . self::NON_WORD_CHAR . '))'; 95 96 /** 97 * Zero-width assertion: current position is followed by a non-word 98 * character, or is at the end of input. Complement to 99 * {@see self::NO_WORD_BEFORE}. 100 * 101 * @var string 102 */ 103 protected const NO_WORD_AFTER = '(?:\z|(?=' . self::NON_WORD_CHAR . '))'; 104 105 //endregion 106 107 //region Lexer connection 108 109 /** 110 * Returns a number used to determine in which order modes are added. 111 * 112 * @return int 113 */ 114 abstract public function getSort(); 115 116 /** 117 * Handle a matched token from the lexer. 118 * 119 * @param string $match The matched text 120 * @param int $state The lexer state (DOKU_LEXER_ENTER, _EXIT, _MATCHED, etc.) 121 * @param int $pos Byte position in the source 122 * @param Handler $handler The handler (for addCall, status, etc.) 123 * @return bool 124 */ 125 abstract public function handle($match, $state, $pos, Handler $handler); 126 127 /** 128 * Called before any calls to connectTo. 129 * 130 * @return void 131 */ 132 public function preConnect() 133 { 134 } 135 136 /** 137 * Connects the mode. 138 * 139 * @param string $mode 140 * @return void 141 */ 142 public function connectTo($mode) 143 { 144 } 145 146 /** 147 * Called after all calls to connectTo. 148 * 149 * @return void 150 */ 151 public function postConnect() 152 { 153 } 154 155 //endregion 156 157 //region Dependency injection 158 159 /** 160 * Attach the registry of the parse this mode is taking part in and resolve 161 * the set of modes this mode accepts as nested content. 162 * 163 * Called by Parser::addMode() / addBaseMode() as the mode joins the parser. 164 * This is the earliest point the per-parse registry is available, so the 165 * accepted-mode list is resolved here, once: allowedCategories() mapped to 166 * concrete mode names via the registry taxonomy (complete by now, plugin 167 * modes included), merged with any names the subclass assigned to 168 * $allowedModes directly, then passed through filterAllowedModes(). A subclass 169 * that does not use categories has its directly-assigned $allowedModes used 170 * as-is. 171 * 172 * @param ModeRegistry $registry 173 * @return void 174 */ 175 public function setModeRegistry(ModeRegistry $registry): void 176 { 177 $this->registry = $registry; 178 179 $modes = (array) $this->allowedModes; 180 $categories = $this->allowedCategories(); 181 if ($categories) { 182 $modes = array_merge($modes, $registry->getModesForCategories($categories)); 183 } 184 $this->allowedModes = $this->filterAllowedModes(array_values(array_unique($modes))); 185 } 186 187 /** 188 * Attach the lexer this mode registers its patterns with. 189 * 190 * Called by Parser::addMode() / addBaseMode() as the mode joins the parser, 191 * before any connect callback runs. 192 * 193 * @param Lexer $lexer 194 * @return void 195 */ 196 public function setLexer(Lexer $lexer): void 197 { 198 $this->Lexer = $lexer; 199 } 200 201 /** 202 * The lexer this mode registers its patterns with. 203 * 204 * @return Lexer 205 */ 206 public function getLexer(): Lexer 207 { 208 return $this->Lexer; 209 } 210 211 //endregion 212 213 //region Nested mode resolution 214 215 /** 216 * CATEGORY_* constants whose modes may nest inside this mode. 217 * 218 * Override to declare the categories this mode accepts; accepts() resolves 219 * them to concrete mode names lazily (once the registry is attached) via 220 * the parse's taxonomy. Returning [] means "use $this->allowedModes as-is" 221 * (the default, empty unless a subclass sets it). 222 * 223 * @return string[] 224 */ 225 protected function allowedCategories(): array 226 { 227 return []; 228 } 229 230 /** 231 * Post-process the resolved allowedModes list. 232 * 233 * Override to remove entries (e.g. a mode excluding itself to prevent 234 * self-nesting). Applied once, after allowedCategories() is resolved. 235 * 236 * @param string[] $modes 237 * @return string[] 238 */ 239 protected function filterAllowedModes(array $modes): array 240 { 241 return $modes; 242 } 243 244 /** 245 * Check if the given mode is accepted inside this mode. 246 * 247 * The accepted-mode list is resolved once in setModeRegistry(); see there. 248 * 249 * @param string $mode 250 * @return bool 251 */ 252 public function accepts($mode) 253 { 254 return in_array($mode, $this->allowedModes, true); 255 } 256 257 //endregion 258} 259