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 * 56 * Paragraph boundaries are blank lines — two newlines possibly separated 57 * by horizontal whitespace. The lexer compiles all patterns with the `s` 58 * (DOTALL) flag, so a plain `.*` inside an entry-pattern lookahead would 59 * match across blank lines and let an unclosed delimiter greedily consume 60 * following paragraphs. Place this assertion before a character class to 61 * stop the match at a paragraph boundary. 62 */ 63 protected const NOT_AT_PARA_BREAK = '(?!\n[ \t]*\n)'; 64 65 /** 66 * Quantified group matching any character that does not start a paragraph 67 * break. Convenience for the common case of "consume until paragraph end". 68 * 69 * Example: 70 * return '\*\*(?=' . self::CONTENT_UNTIL_PARA . '\*\*)'; 71 */ 72 protected const CONTENT_UNTIL_PARA = '(?:' . self::NOT_AT_PARA_BREAK . '.)*'; 73 74 /** 75 * Character class: a single "non-word" character — ASCII whitespace or 76 * any ASCII punctuation character except the underscore. 77 * 78 * The `_` is excluded because it is itself a delimiter for emphasis in 79 * GFM/CommonMark; treating it as non-word would let `__foo` incorrectly 80 * open emphasis at the second `_`. 81 * 82 * Multibyte rationale: the lexer compiles patterns without the `u` flag, 83 * so UTF-8 is treated as individual bytes. Multibyte characters begin 84 * with bytes >= 0x80, which fall outside every ASCII character class. 85 * Checking that the surrounding context matches NON_WORD_CHAR positively 86 * therefore correctly treats multibyte letters as word-like — preventing 87 * intraword matches in non-Latin text (e.g. `für_etwas`, `日本_語`) 88 * without requiring `u` flag support across the whole lexer. 89 */ 90 protected const NON_WORD_CHAR = '[\s!"#$%&\'()*+,\-./:;<=>?@\[\\\\\]^`{|}~]'; 91 92 /** 93 * Zero-width assertion: current position is preceded by a non-word 94 * character, or is at the start of input/line. See {@see self::NON_WORD_CHAR} 95 * for the multibyte reasoning. 96 */ 97 protected const NO_WORD_BEFORE = '(?:^|(?<=' . self::NON_WORD_CHAR . '))'; 98 99 /** 100 * Zero-width assertion: current position is followed by a non-word 101 * character, or is at the end of input. Complement to 102 * {@see self::NO_WORD_BEFORE}. 103 */ 104 protected const NO_WORD_AFTER = '(?:\z|(?=' . self::NON_WORD_CHAR . '))'; 105 106 //endregion 107 108 //region Lexer connection 109 110 /** 111 * Returns a number used to determine in which order modes are added. 112 * 113 * @return int 114 */ 115 abstract public function getSort(); 116 117 /** 118 * Handle a matched token from the lexer. 119 * 120 * @param string $match The matched text 121 * @param int $state The lexer state (DOKU_LEXER_ENTER, _EXIT, _MATCHED, etc.) 122 * @param int $pos Byte position in the source 123 * @param Handler $handler The handler (for addCall, status, etc.) 124 * @return bool 125 */ 126 abstract public function handle($match, $state, $pos, Handler $handler); 127 128 /** 129 * Called before any calls to connectTo. 130 * 131 * @return void 132 */ 133 public function preConnect() 134 { 135 } 136 137 /** 138 * Connects the mode. 139 * 140 * @param string $mode 141 * @return void 142 */ 143 public function connectTo($mode) 144 { 145 } 146 147 /** 148 * Called after all calls to connectTo. 149 * 150 * @return void 151 */ 152 public function postConnect() 153 { 154 } 155 156 //endregion 157 158 //region Dependency injection 159 160 /** 161 * Attach the registry of the parse this mode is taking part in and resolve 162 * the set of modes this mode accepts as nested content. 163 * 164 * Called by Parser::addMode() / addBaseMode() as the mode joins the parser. 165 * This is the earliest point the per-parse registry is available, so the 166 * accepted-mode list is resolved here, once: allowedCategories() mapped to 167 * concrete mode names via the registry taxonomy (complete by now, plugin 168 * modes included), merged with any names the subclass assigned to 169 * $allowedModes directly, then passed through filterAllowedModes(). A subclass 170 * that does not use categories has its directly-assigned $allowedModes used 171 * as-is. 172 * 173 * @param ModeRegistry $registry 174 * @return void 175 */ 176 public function setModeRegistry(ModeRegistry $registry): void 177 { 178 $this->registry = $registry; 179 180 $modes = (array) $this->allowedModes; 181 $categories = $this->allowedCategories(); 182 if ($categories) { 183 $modes = array_merge($modes, $registry->getModesForCategories($categories)); 184 } 185 $this->allowedModes = $this->filterAllowedModes(array_values(array_unique($modes))); 186 } 187 188 /** 189 * Attach the lexer this mode registers its patterns with. 190 * 191 * Called by Parser::addMode() / addBaseMode() as the mode joins the parser, 192 * before any connect callback runs. 193 * 194 * @param Lexer $lexer 195 * @return void 196 */ 197 public function setLexer(Lexer $lexer): void 198 { 199 $this->Lexer = $lexer; 200 } 201 202 /** 203 * The lexer this mode registers its patterns with. 204 * 205 * @return Lexer 206 */ 207 public function getLexer(): Lexer 208 { 209 return $this->Lexer; 210 } 211 212 //endregion 213 214 //region Nested mode resolution 215 216 /** 217 * CATEGORY_* constants whose modes may nest inside this mode. 218 * 219 * Override to declare the categories this mode accepts; accepts() resolves 220 * them to concrete mode names lazily (once the registry is attached) via 221 * the parse's taxonomy. Returning [] means "use $this->allowedModes as-is" 222 * (the default, empty unless a subclass sets it). 223 * 224 * @return string[] 225 */ 226 protected function allowedCategories(): array 227 { 228 return []; 229 } 230 231 /** 232 * Post-process the resolved allowedModes list. 233 * 234 * Override to remove entries (e.g. a mode excluding itself to prevent 235 * self-nesting). Applied once, after allowedCategories() is resolved. 236 * 237 * @param string[] $modes 238 * @return string[] 239 */ 240 protected function filterAllowedModes(array $modes): array 241 { 242 return $modes; 243 } 244 245 /** 246 * Check if the given mode is accepted inside this mode. 247 * 248 * The accepted-mode list is resolved once in setModeRegistry(); see there. 249 * 250 * @param string $mode 251 * @return bool 252 */ 253 public function accepts($mode) 254 { 255 return in_array($mode, $this->allowedModes, true); 256 } 257 258 //endregion 259} 260