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