xref: /dokuwiki/inc/Parsing/ParserMode/AbstractMode.php (revision 1c00c02121477c81b95fe750b94acdf109cba20a)
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