xref: /dokuwiki/inc/Parsing/Lexer/Lexer.php (revision fe58309edafb067d90f3f40ef3d416100d558a04)
1<?php
2
3/**
4 * Lexer adapted from Simple Test: http://sourceforge.net/projects/simpletest/
5 * For an intro to the Lexer see:
6 * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes
7 *
8 * @author Marcus Baker http://www.lastcraft.com
9 */
10
11namespace dokuwiki\Parsing\Lexer;
12
13use dokuwiki\Parsing\Handler;
14
15/**
16 * Accepts text and breaks it into tokens.
17 *
18 * Some optimisation to make the sure the content is only scanned by the PHP regex
19 * parser once. Lexer modes must not start with leading underscores.
20 */
21class Lexer
22{
23    /** @var string Signal for leaving a mode */
24    public const MODE_EXIT = '__exit';
25    /** @var string Prefix marking special (enter-and-exit) patterns */
26    public const MODE_SPECIAL_PREFIX = '_';
27    /**
28     * Pattern matching a paragraph break: a blank line — two newlines
29     * possibly separated by horizontal whitespace. The usual boundary
30     * for addCloserPattern().
31     *
32     * @var string
33     */
34    public const PARA_BREAK = '\n[ \t]*\n';
35
36    /** @var ParallelRegex[] */
37    protected $regexes = [];
38    /** @var Handler */
39    protected $handler;
40    /** @var StateStack */
41    protected $modeStack;
42    /** @var array mode "rewrites" */
43    protected $mode_handlers = [];
44    /** @var CloserPattern[] closer-existence checks, keyed by the mode they guard */
45    protected $closerPatterns = [];
46    /** @var bool case sensitive? */
47    protected $case;
48
49    /**
50     * Sets up the lexer in case insensitive matching by default.
51     *
52     * @param Handler $handler  Handling strategy by reference.
53     * @param string $start            Starting handler.
54     * @param boolean $case            True for case sensitive.
55     */
56    public function __construct($handler, $start = "accept", $case = false)
57    {
58        $this->case = $case;
59        $this->handler = $handler;
60        $this->modeStack = new StateStack($start);
61    }
62
63    /**
64     * Adds a token search pattern for a particular parsing mode.
65     *
66     * The pattern does not change the current mode.
67     *
68     * @param string $pattern      Perl style regex, but ( and )
69     *                             lose the usual meaning.
70     * @param string $mode         Should only apply this
71     *                             pattern when dealing with
72     *                             this type of input.
73     */
74    public function addPattern($pattern, $mode = "accept")
75    {
76        if (! isset($this->regexes[$mode])) {
77            $this->regexes[$mode] = new ParallelRegex($this->case);
78        }
79        $this->regexes[$mode]->addPattern($pattern);
80    }
81
82    /**
83     * Adds a pattern that will enter a new parsing mode.
84     *
85     * Useful for entering parenthesis, strings, tags, etc.
86     *
87     * @param string $pattern      Perl style regex, but ( and ) lose the usual meaning.
88     * @param string $mode         Should only apply this pattern when dealing with this type of input.
89     * @param string $new_mode     Change parsing to this new nested mode.
90     */
91    public function addEntryPattern($pattern, $mode, $new_mode)
92    {
93        if (! isset($this->regexes[$mode])) {
94            $this->regexes[$mode] = new ParallelRegex($this->case);
95        }
96        $this->regexes[$mode]->addPattern($pattern, $new_mode);
97    }
98
99    /**
100     * Requires a closer to exist ahead before any entry pattern may enter
101     * the given mode.
102     *
103     * Whenever an entry pattern for $mode matches, the subject is scanned
104     * from the end of that match for $pattern — before the next $boundary
105     * match if a boundary is given, anywhere ahead otherwise. If no closer
106     * is found the entry is rejected and its delimiter stays literal text;
107     * see reduce() for how the rejected match is discarded.
108     *
109     * Keeping the check out of the entry pattern is what makes it
110     * affordable. A closer lookahead written into the entry pattern is
111     * re-evaluated for every candidate the regex engine tries — quadratic
112     * on input dense with delimiters that never close. Here the scan runs
113     * once per position and its verdict is memoized, so together with the
114     * lexer consuming each entered span the whole parse stays linear; see
115     * CloserPattern for the scan and its memo.
116     *
117     * The pattern must match where the closing delimiter starts, with
118     * flanking requirements expressed as lookarounds (the convention exit
119     * patterns follow, e.g. (?<=[^\s])\*\* for strong). A pattern that
120     * consumed flanking context instead would skew the closer positions
121     * canEnter() compares across modes, and could not see a closer whose
122     * delimiter directly follows the opener, since the scan starts only
123     * after the entry match.
124     *
125     * The closer must not be able to match where the boundary matches,
126     * since the scan stops unconditionally at the boundary. It also does
127     * not look inside content the lexer consumes atomically; see
128     * opaqueSpans().
129     *
130     * A mode has exactly one closer check, shared by all its entry
131     * patterns; registering another replaces it. The memo is reset at the
132     * start of every parse() run.
133     *
134     * @param string $pattern regex fragment matching the closing delimiter,
135     *                        flanking context expressed as lookarounds
136     * @param string $mode    the mode entered by the guarded entry patterns
137     * @param string|null $boundary regex fragment the closer must occur
138     *                              before (usually self::PARA_BREAK); null
139     *                              to scan to the end of the subject
140     * @return void
141     */
142    public function addCloserPattern($pattern, $mode, $boundary = null)
143    {
144        $this->closerPatterns[$mode] = new CloserPattern($pattern, $boundary);
145    }
146
147    /**
148     * Adds a pattern that will exit the current mode and re-enter the previous one.
149     *
150     * @param string $pattern      Perl style regex, but ( and ) lose the usual meaning.
151     * @param string $mode         Mode to leave.
152     */
153    public function addExitPattern($pattern, $mode)
154    {
155        if (! isset($this->regexes[$mode])) {
156            $this->regexes[$mode] = new ParallelRegex($this->case);
157        }
158        $this->regexes[$mode]->addPattern($pattern, self::MODE_EXIT);
159    }
160
161    /**
162     * Adds a pattern that has a special mode.
163     *
164     * Acts as an entry and exit pattern in one go, effectively calling a special
165     * parser handler for this token only.
166     *
167     * @param string $pattern      Perl style regex, but ( and ) lose the usual meaning.
168     * @param string $mode         Should only apply this pattern when dealing with this type of input.
169     * @param string $special      Use this mode for this one token.
170     */
171    public function addSpecialPattern($pattern, $mode, $special)
172    {
173        if (! isset($this->regexes[$mode])) {
174            $this->regexes[$mode] = new ParallelRegex($this->case);
175        }
176        $this->regexes[$mode]->addPattern($pattern, self::MODE_SPECIAL_PREFIX . $special);
177    }
178
179    /**
180     * Adds a mapping from a mode to another handler.
181     *
182     * @param string $mode        Mode to be remapped.
183     * @param string $handler     New target handler.
184     */
185    public function mapHandler($mode, $handler)
186    {
187        $this->mode_handlers[$mode] = $handler;
188    }
189
190    /**
191     * Splits the page text into tokens.
192     *
193     * Will fail if the handlers report an error or if no content is consumed. If successful then each
194     * unparsed and parsed token invokes a call to the held listener.
195     *
196     * @param string $raw        Raw HTML text.
197     * @return boolean           True on success, else false.
198     */
199    public function parse($raw)
200    {
201        if (! isset($this->handler)) {
202            return false;
203        }
204        $this->resetCloserMemos();
205        $offset = 0;
206        while (is_array($parsed = $this->reduce($raw, $offset))) {
207            [$unmatched, $matched, $mode] = $parsed;
208            $matchPos = $offset + strlen($unmatched);
209            if (! $this->dispatchTokens($unmatched, $matched, $mode, $offset, $matchPos)) {
210                return false;
211            }
212            $newOffset = $matchPos + strlen($matched);
213            if ($newOffset === $offset && $mode !== self::MODE_EXIT) {
214                // No byte was consumed. For an ordinary match this means the
215                // pattern set cannot advance and we must stop to avoid an
216                // infinite loop. A zero-width EXIT (a lookahead-only exit
217                // pattern such as Preformatted's (?=\n[^ \t\n])) is the
218                // exception: it makes progress by popping the mode stack,
219                // leaving the boundary byte for the parent mode to consume on
220                // the next iteration. The stack strictly shrinks on each such
221                // exit, so this cannot loop forever.
222                return false;
223            }
224            $offset = $newOffset;
225        }
226        if (!$parsed) {
227            return false;
228        }
229        return $this->invokeHandler(substr($raw, $offset), DOKU_LEXER_UNMATCHED, $offset);
230    }
231
232    /**
233     * Gives plugins access to the mode stack
234     *
235     * @return StateStack
236     */
237    public function getModeStack()
238    {
239        return $this->modeStack;
240    }
241
242    /**
243     * Sends the matched token and any leading unmatched
244     * text to the parser changing the lexer to a new
245     * mode if one is listed.
246     *
247     * @param string $unmatched Unmatched leading portion.
248     * @param string $matched Actual token match.
249     * @param bool|string $mode Mode after match. A boolean false mode causes no change.
250     * @param int $initialPos
251     * @param int $matchPos Current byte index location in raw doc thats being parsed
252     * @return boolean             False if there was any error from the parser.
253     */
254    protected function dispatchTokens($unmatched, $matched, $mode, $initialPos, $matchPos)
255    {
256        if (! $this->invokeHandler($unmatched, DOKU_LEXER_UNMATCHED, $initialPos)) {
257            return false;
258        }
259        if ($this->isModeEnd($mode)) {
260            if (! $this->invokeHandler($matched, DOKU_LEXER_EXIT, $matchPos)) {
261                return false;
262            }
263            return $this->modeStack->leave();
264        }
265        if ($this->isSpecialMode($mode)) {
266            $this->modeStack->enter($this->decodeSpecial($mode));
267            if (! $this->invokeHandler($matched, DOKU_LEXER_SPECIAL, $matchPos)) {
268                return false;
269            }
270            return $this->modeStack->leave();
271        }
272        if (is_string($mode)) {
273            $this->modeStack->enter($mode);
274            return $this->invokeHandler($matched, DOKU_LEXER_ENTER, $matchPos);
275        }
276        return $this->invokeHandler($matched, DOKU_LEXER_MATCHED, $matchPos);
277    }
278
279    /**
280     * Tests to see if the new mode is actually to leave the current mode and pop an item from the matching
281     * mode stack.
282     *
283     * @param string $mode    Mode to test.
284     * @return boolean        True if this is the exit mode.
285     */
286    protected function isModeEnd($mode)
287    {
288        return ($mode === self::MODE_EXIT);
289    }
290
291    /**
292     * Test to see if the mode is one where this mode is entered for this token only and automatically
293     * leaves immediately afterwoods.
294     *
295     * @param string $mode    Mode to test.
296     * @return boolean        True if this is the exit mode.
297     */
298    protected function isSpecialMode($mode)
299    {
300        return str_starts_with($mode, self::MODE_SPECIAL_PREFIX);
301    }
302
303    /**
304     * Strips the magic underscore marking single token modes.
305     *
306     * @param string $mode    Mode to decode.
307     * @return string         Underlying mode name.
308     */
309    protected function decodeSpecial($mode)
310    {
311        return substr($mode, strlen(self::MODE_SPECIAL_PREFIX));
312    }
313
314    /**
315     * Dispatches a token to the handler.
316     *
317     * Resolves mode name aliases (e.g. unformattedalt → unformatted) and
318     * delegates all dispatch logic to Handler::handleToken().
319     *
320     * @param string $content Text parsed.
321     * @param int $state One of the DOKU_LEXER_* constants identifying the
322     *                   lexer event (ENTER / MATCHED / UNMATCHED / EXIT /
323     *                   SPECIAL).
324     * @param int $pos Current byte index location in raw doc
325     *                             thats being parsed
326     * @return bool
327     */
328    protected function invokeHandler($content, $state, $pos)
329    {
330        if ($content === false) {
331            return true;
332        }
333        // Empty content is a no-op for every state EXCEPT EXIT: a zero-width
334        // exit pattern (lookahead-only) must still fire the mode's exit
335        // handler so cleanup like restoring a buffered call writer happens.
336        // Skipping it would pop the mode stack but leave the handler-side
337        // state stale.
338        if ($content === '' && $state !== DOKU_LEXER_EXIT) {
339            return true;
340        }
341        $originalName = $this->modeStack->getCurrent();
342        $modeName = $this->mode_handlers[$originalName] ?? $originalName;
343
344        return $this->handler->handleToken($modeName, $content, $state, $pos, $originalName);
345    }
346
347    /**
348     * Tries to match the next token starting at `$offset` in `$raw`.
349     *
350     * The full subject is passed to the regex engine (rather than a
351     * truncated tail) so that lookbehind assertions in the registered
352     * patterns can see characters before the current offset. Empty
353     * subjects (offset past end) will not be matched.
354     *
355     * A matched entry pattern for a guarded mode (one with a closer
356     * pattern) is discarded when canEnter() rejects it: its delimiter
357     * stays unparsed text and matching resumes one byte on, so a shorter
358     * delimiter overlapping the rejected one still gets its turn.
359     * Resuming past the delimiter also skips any rival pattern anchored
360     * at that exact byte, which is safe: guarded modes are registered
361     * only by the core, and two core delimiters sharing a byte share an
362     * equivalent closer, so one rejection implies the other. Plugin
363     * patterns are never guarded and so never take this path.
364     *
365     * @param string $raw     The full subject to parse.
366     * @param int    $offset  Byte offset at which to resume matching.
367     * @return array|bool     Three item list of unparsed content followed by the
368     *                        recognised token and finally the action the parser is to take.
369     *                        True if no match, false if there is a parsing error.
370     */
371    protected function reduce($raw, $offset)
372    {
373        if (! isset($this->regexes[$this->modeStack->getCurrent()])) {
374            return false;
375        }
376        if ($offset >= strlen($raw)) {
377            return true;
378        }
379        $initialOffset = $offset;
380        while ($action = $this->regexes[$this->modeStack->getCurrent()]->split($raw, $split, $offset)) {
381            [$unparsed, $match] = $split;
382            $matchPos = $offset + strlen($unparsed);
383
384            if (
385                is_string($action)
386                && isset($this->closerPatterns[$action])
387                && !$this->canEnter($action, $raw, $matchPos + strlen($match))
388            ) {
389                $offset = $matchPos + 1;
390                if ($offset >= strlen($raw)) {
391                    return true;
392                }
393                continue;
394            }
395
396            return [substr($raw, $initialOffset, $matchPos - $initialOffset), $match, $action];
397        }
398        return true;
399    }
400
401    /**
402     * May an entry pattern for the given mode enter at this position?
403     *
404     * Two conditions must hold. First, a valid closer for the mode must
405     * exist ahead, before its boundary — otherwise a delimiter that can
406     * never close would stay open forever. Second, when the entry sits
407     * inside a guarded mode, that mode's closer must not come first: an
408     * inner delimiter whose closer lies beyond the enclosing closer can
409     * never close within its parent, so it stays literal rather than span
410     * across the parent boundary (e.g. a stray '*' in ''glob/*.conf''
411     * pairing with the '*' of a following ''…'' span).
412     *
413     * The enclosing mode is the nearest guarded ancestor on the stack, not
414     * necessarily the immediate parent; see nearestGuardedAncestor().
415     *
416     * @param string $mode the mode entered by the matched entry pattern
417     * @param string $subject the full subject being lexed
418     * @param int $from byte position just after the entry pattern match
419     * @return bool
420     */
421    protected function canEnter(string $mode, string $subject, int $from): bool
422    {
423        $closerPos = $this->closerPosition($mode, $subject, $from);
424        if ($closerPos === null) {
425            return false;
426        }
427
428        $enclosing = $this->nearestGuardedAncestor($mode);
429        if ($enclosing !== null) {
430            $enclosingCloserPos = $this->closerPosition($enclosing, $subject, $from);
431            if ($enclosingCloserPos !== null && $enclosingCloserPos < $closerPos) {
432                return false;
433            }
434        }
435
436        return true;
437    }
438
439    /**
440     * The nearest mode on the stack that has its own closer and could thus
441     * constrain where a delimiter entering $mode may close, or null if none
442     * does.
443     *
444     * The search walks from the immediate parent outward, stepping over
445     * unguarded modes — plugins, list items, anything with no closer. Such
446     * a mode offers no closer to compare against, yet a guarded ancestor
447     * beyond it still constrains the inner delimiter (e.g. ''strong'' around
448     * a plugin span holding an ''emphasis'' whose only closer lies further
449     * on), so skipping it reaches that ancestor.
450     *
451     * Only the nearest guarded ancestor matters: when it opened it was
452     * validated against its own nearest guarded ancestor, so the
453     * "closes before its parent" relation chains up the stack and one level
454     * is enough. The walk also stops at $mode itself — a same-mode ancestor
455     * shares this candidate's closer pattern, so its closer cannot fall
456     * before the candidate's and can never be the rejecting constraint.
457     *
458     * @param string $mode the mode about to be entered
459     * @return string|null the nearest guarded ancestor, or null if none
460     */
461    protected function nearestGuardedAncestor(string $mode): ?string
462    {
463        $stack = $this->modeStack->getStack();
464        for ($i = count($stack) - 1; $i >= 0; $i--) {
465            $enclosing = $stack[$i];
466            if ($enclosing === $mode) {
467                return null;
468            }
469            if (isset($this->closerPatterns[$enclosing])) {
470                return $enclosing;
471            }
472        }
473        return null;
474    }
475
476    /**
477     * Byte position where the first valid closer for the given mode at or
478     * after $from starts, or null if none exists; delegates to the mode's
479     * CloserPattern (see position()).
480     *
481     * The scan regex is compiled on first use, not in addCloserPattern(),
482     * because the opaque-span derivation needs the patterns of all connected
483     * modes, and other modes' connectTo() calls may run after this mode's
484     * postConnect() has registered the closer.
485     *
486     * @param string $mode the mode whose closer pattern applies
487     * @param string $subject the full subject being lexed
488     * @param int $from byte position just after the entry pattern match
489     * @return int|null
490     */
491    protected function closerPosition(string $mode, string $subject, int $from): ?int
492    {
493        $closerPattern = $this->closerPatterns[$mode];
494        if (!$closerPattern->isCompiled()) {
495            $closerPattern->compile($this->opaqueSpans($mode), $this->case);
496        }
497        return $closerPattern->position($subject, $from);
498    }
499
500    /**
501     * Derives the spans within the given mode whose content a closer scan
502     * must not look into.
503     *
504     * Content the lexer consumes without exposing it to the mode's exit
505     * pattern can never hold a real closer — a %%..%% span may contain the
506     * characters that would close an enclosing bold span, yet the bold exit
507     * cannot fire inside it. Such content is identified from the already
508     * registered patterns:
509     *
510     * - A plain or special pattern is consumed in one step, so the pattern
511     *   itself describes the span. (Zero-width matches would stall parse(),
512     *   so each consumes at least one byte.)
513     * - An entry pattern leads into a nested mode. If that mode is verbatim
514     *   (see verbatimExit()), the lexer consumes up to its first exit, so
515     *   the span is the entry pattern, a lazy body, and the exit. Other
516     *   nested modes have no statically known extent — their content is
517     *   scanned as plain text, leaving the closer check an approximation
518     *   there.
519     *
520     * @param string $mode the mode whose closer scan needs the spans
521     * @return string[] regex fragments, each matching one whole span
522     */
523    protected function opaqueSpans(string $mode): array
524    {
525        if (!isset($this->regexes[$mode])) {
526            return [];
527        }
528
529        $spans = [];
530        foreach ($this->regexes[$mode]->getPatterns() as $registered) {
531            $label = $registered['label'];
532            if ($label === self::MODE_EXIT) {
533                continue;
534            }
535            if ($label === true || $this->isSpecialMode($label)) {
536                $spans[] = '(?:' . ParallelRegex::escapePattern($registered['pattern']) . ')';
537                continue;
538            }
539            $exit = $this->verbatimExit($label);
540            if ($exit !== null) {
541                $spans[] = '(?:' . ParallelRegex::escapePattern($registered['pattern']) . '.*?' . $exit . ')';
542            }
543        }
544        return $spans;
545    }
546
547    /**
548     * The exit pattern of the given mode if the mode is verbatim, null
549     * otherwise.
550     *
551     * A mode is verbatim when its pattern set consists solely of exit
552     * patterns (e.g. nowiki): nothing can match inside it, so it consumes
553     * everything up to its first exit match. Several exits are combined
554     * into an alternation.
555     *
556     * @param string $mode the mode entered by an entry pattern
557     * @return string|null regex fragment matching the mode's exit
558     */
559    protected function verbatimExit(string $mode): ?string
560    {
561        if (!isset($this->regexes[$mode])) {
562            return null;
563        }
564
565        $exits = [];
566        foreach ($this->regexes[$mode]->getPatterns() as $registered) {
567            if ($registered['label'] !== self::MODE_EXIT) {
568                return null;
569            }
570            $exits[] = ParallelRegex::escapePattern($registered['pattern']);
571        }
572        if ($exits === []) {
573            return null;
574        }
575        return '(?:' . implode('|', $exits) . ')';
576    }
577
578    /**
579     * Forgets all closer scan verdicts. Called at the start of every
580     * parse() run, since the memos only hold for the subject they were
581     * computed on.
582     *
583     * @return void
584     */
585    protected function resetCloserMemos(): void
586    {
587        foreach ($this->closerPatterns as $closerPattern) {
588            $closerPattern->reset();
589        }
590    }
591
592    /**
593     * Escapes regex characters other than (, ) and /
594     *
595     * @param string $str
596     * @return string
597     */
598    public static function escape($str)
599    {
600        $chars = [
601            '/\\\\/',
602            '/\./',
603            '/\+/',
604            '/\*/',
605            '/\?/',
606            '/\[/',
607            '/\^/',
608            '/\]/',
609            '/\$/',
610            '/\{/',
611            '/\}/',
612            '/\=/',
613            '/\!/',
614            '/\</',
615            '/\>/',
616            '/\|/',
617            '/\:/'
618        ];
619
620        $escaped = [
621            '\\\\\\\\',
622            '\.',
623            '\+',
624            '\*',
625            '\?',
626            '\[',
627            '\^',
628            '\]',
629            '\$',
630            '\{',
631            '\}',
632            '\=',
633            '\!',
634            '\<',
635            '\>',
636            '\|',
637            '\:'
638        ];
639
640        return preg_replace($chars, $escaped, $str);
641    }
642}
643