xref: /dokuwiki/inc/Parsing/Lexer/CloserPattern.php (revision 1c00c02121477c81b95fe750b94acdf109cba20a)
1*1c00c021SAndreas Gohr<?php
2*1c00c021SAndreas Gohr
3*1c00c021SAndreas Gohrnamespace dokuwiki\Parsing\Lexer;
4*1c00c021SAndreas Gohr
5*1c00c021SAndreas Gohr/**
6*1c00c021SAndreas Gohr * A closer-existence check guarding a lexer mode.
7*1c00c021SAndreas Gohr *
8*1c00c021SAndreas Gohr * Holds one mode's closer and boundary patterns, the scan regex compiled
9*1c00c021SAndreas Gohr * from them, and the memoized verdicts of earlier scans. The Lexer calls
10*1c00c021SAndreas Gohr * position() to ask whether a valid closer exists ahead of an entry match;
11*1c00c021SAndreas Gohr * memoizing the verdict keeps that check linear over a whole parse.
12*1c00c021SAndreas Gohr *
13*1c00c021SAndreas Gohr * See Lexer::addCloserPattern() for the contract the patterns follow.
14*1c00c021SAndreas Gohr */
15*1c00c021SAndreas Gohrclass CloserPattern
16*1c00c021SAndreas Gohr{
17*1c00c021SAndreas Gohr    /** @var string pattern matching a valid closer, in plain PCRE syntax */
18*1c00c021SAndreas Gohr    protected string $closer;
19*1c00c021SAndreas Gohr    /** @var string|null pattern the closer must occur before, in plain PCRE syntax */
20*1c00c021SAndreas Gohr    protected ?string $boundary;
21*1c00c021SAndreas Gohr    /** @var string|null scan regex built by compile(), null until then */
22*1c00c021SAndreas Gohr    protected ?string $scan = null;
23*1c00c021SAndreas Gohr    /** @var int|null start of the range proven to see the memoized closer */
24*1c00c021SAndreas Gohr    protected ?int $successFrom = null;
25*1c00c021SAndreas Gohr    /** @var int|null memoized closer position, valid from successFrom on */
26*1c00c021SAndreas Gohr    protected ?int $closerPos = null;
27*1c00c021SAndreas Gohr    /** @var int|null start of the range proven closer-free */
28*1c00c021SAndreas Gohr    protected ?int $failFrom = null;
29*1c00c021SAndreas Gohr    /** @var int|null exclusive end of the range proven closer-free */
30*1c00c021SAndreas Gohr    protected ?int $failUntil = null;
31*1c00c021SAndreas Gohr
32*1c00c021SAndreas Gohr    /**
33*1c00c021SAndreas Gohr     * Translates both patterns from the lexer convention into plain PCRE
34*1c00c021SAndreas Gohr     * syntax. Building the scan regex is deferred to compile().
35*1c00c021SAndreas Gohr     *
36*1c00c021SAndreas Gohr     * @param string $pattern regex fragment matching the closing delimiter,
37*1c00c021SAndreas Gohr     *                        flanking context expressed as lookarounds
38*1c00c021SAndreas Gohr     * @param string|null $boundary regex fragment the closer must occur
39*1c00c021SAndreas Gohr     *                              before; null to scan to the end of the
40*1c00c021SAndreas Gohr     *                              subject
41*1c00c021SAndreas Gohr     */
42*1c00c021SAndreas Gohr    public function __construct(string $pattern, ?string $boundary = null)
43*1c00c021SAndreas Gohr    {
44*1c00c021SAndreas Gohr        $this->closer = ParallelRegex::escapePattern($pattern);
45*1c00c021SAndreas Gohr        $this->boundary = $boundary === null ? null : ParallelRegex::escapePattern($boundary);
46*1c00c021SAndreas Gohr    }
47*1c00c021SAndreas Gohr
48*1c00c021SAndreas Gohr    /**
49*1c00c021SAndreas Gohr     * Whether compile() has built the scan regex yet.
50*1c00c021SAndreas Gohr     *
51*1c00c021SAndreas Gohr     * @return bool
52*1c00c021SAndreas Gohr     */
53*1c00c021SAndreas Gohr    public function isCompiled(): bool
54*1c00c021SAndreas Gohr    {
55*1c00c021SAndreas Gohr        return $this->scan !== null;
56*1c00c021SAndreas Gohr    }
57*1c00c021SAndreas Gohr
58*1c00c021SAndreas Gohr    /**
59*1c00c021SAndreas Gohr     * Builds the scan regex from the mode's patterns and the opaque spans
60*1c00c021SAndreas Gohr     * supplied by the Lexer.
61*1c00c021SAndreas Gohr     *
62*1c00c021SAndreas Gohr     * It matches the earliest of three things ahead: the boundary (named
63*1c00c021SAndreas Gohr     * group "bound"), the closer (named group "closer"), or an opaque span
64*1c00c021SAndreas Gohr     * — a stretch the lexer consumes atomically, so a closer lookalike
65*1c00c021SAndreas Gohr     * inside it can never really close the mode (see Lexer::opaqueSpans()).
66*1c00c021SAndreas Gohr     * Alternative order breaks same-position ties: the boundary wins over a
67*1c00c021SAndreas Gohr     * span starting at the same byte, and the closer is never swallowed by
68*1c00c021SAndreas Gohr     * one. position() interprets the matches.
69*1c00c021SAndreas Gohr     *
70*1c00c021SAndreas Gohr     * @param string[] $opaqueSpans regex fragments in plain PCRE syntax,
71*1c00c021SAndreas Gohr     *                              each matching one whole span the scan
72*1c00c021SAndreas Gohr     *                              must not look into
73*1c00c021SAndreas Gohr     * @param bool $case true for case sensitive matching
74*1c00c021SAndreas Gohr     * @return void
75*1c00c021SAndreas Gohr     */
76*1c00c021SAndreas Gohr    public function compile(array $opaqueSpans, bool $case): void
77*1c00c021SAndreas Gohr    {
78*1c00c021SAndreas Gohr        $alternatives = [];
79*1c00c021SAndreas Gohr        if ($this->boundary !== null) {
80*1c00c021SAndreas Gohr            $alternatives[] = '(?<bound>' . $this->boundary . ')';
81*1c00c021SAndreas Gohr        }
82*1c00c021SAndreas Gohr        $alternatives[] = '(?<closer>' . $this->closer . ')';
83*1c00c021SAndreas Gohr        $alternatives = array_merge($alternatives, $opaqueSpans);
84*1c00c021SAndreas Gohr
85*1c00c021SAndreas Gohr        $flags = $case ? 'ms' : 'msi';
86*1c00c021SAndreas Gohr        $this->scan = '/' . implode('|', $alternatives) . '/' . $flags;
87*1c00c021SAndreas Gohr    }
88*1c00c021SAndreas Gohr
89*1c00c021SAndreas Gohr    /**
90*1c00c021SAndreas Gohr     * Byte position where the first valid closer at or after $from starts,
91*1c00c021SAndreas Gohr     * or null if none occurs before the boundary (or the end of subject).
92*1c00c021SAndreas Gohr     * Requires compile() to have run.
93*1c00c021SAndreas Gohr     *
94*1c00c021SAndreas Gohr     * Answers from the memo when possible, otherwise runs the scan (see
95*1c00c021SAndreas Gohr     * compile()), hopping over opaque spans, and memoizes the verdict: on
96*1c00c021SAndreas Gohr     * success the range from $from up to the closer, on failure the
97*1c00c021SAndreas Gohr     * closer-free range up to the boundary (or end of subject). A verdict
98*1c00c021SAndreas Gohr     * holds for any position in its range because the leftmost scan proves
99*1c00c021SAndreas Gohr     * no closer lies earlier; positions inside opaque spans are never
100*1c00c021SAndreas Gohr     * queried, since the lexer consumes those without matching entry
101*1c00c021SAndreas Gohr     * patterns in them.
102*1c00c021SAndreas Gohr     *
103*1c00c021SAndreas Gohr     * @param string $subject the full subject being lexed
104*1c00c021SAndreas Gohr     * @param int $from byte position just after the entry pattern match
105*1c00c021SAndreas Gohr     * @return int|null
106*1c00c021SAndreas Gohr     */
107*1c00c021SAndreas Gohr    public function position(string $subject, int $from): ?int
108*1c00c021SAndreas Gohr    {
109*1c00c021SAndreas Gohr        if ($this->successFrom !== null && $from >= $this->successFrom && $from <= $this->closerPos) {
110*1c00c021SAndreas Gohr            return $this->closerPos;
111*1c00c021SAndreas Gohr        }
112*1c00c021SAndreas Gohr        if ($this->failFrom !== null && $from >= $this->failFrom && $from < $this->failUntil) {
113*1c00c021SAndreas Gohr            return null;
114*1c00c021SAndreas Gohr        }
115*1c00c021SAndreas Gohr
116*1c00c021SAndreas Gohr        $pos = $from;
117*1c00c021SAndreas Gohr        while (preg_match($this->scan, $subject, $match, PREG_OFFSET_CAPTURE, $pos) === 1) {
118*1c00c021SAndreas Gohr            if (isset($match['bound']) && $match['bound'][1] !== -1) {
119*1c00c021SAndreas Gohr                $this->failFrom = $from;
120*1c00c021SAndreas Gohr                $this->failUntil = $match['bound'][1] + 1;
121*1c00c021SAndreas Gohr                return null;
122*1c00c021SAndreas Gohr            }
123*1c00c021SAndreas Gohr            if (isset($match['closer']) && $match['closer'][1] !== -1) {
124*1c00c021SAndreas Gohr                $this->successFrom = $from;
125*1c00c021SAndreas Gohr                $this->closerPos = $match['closer'][1];
126*1c00c021SAndreas Gohr                return $this->closerPos;
127*1c00c021SAndreas Gohr            }
128*1c00c021SAndreas Gohr            // an opaque span: resume behind it
129*1c00c021SAndreas Gohr            $pos = max($match[0][1] + strlen($match[0][0]), $pos + 1);
130*1c00c021SAndreas Gohr        }
131*1c00c021SAndreas Gohr
132*1c00c021SAndreas Gohr        $this->failFrom = $from;
133*1c00c021SAndreas Gohr        $this->failUntil = strlen($subject) + 1;
134*1c00c021SAndreas Gohr        return null;
135*1c00c021SAndreas Gohr    }
136*1c00c021SAndreas Gohr
137*1c00c021SAndreas Gohr    /**
138*1c00c021SAndreas Gohr     * Forgets all memoized scan verdicts, which only hold for the subject
139*1c00c021SAndreas Gohr     * they were computed on. The compiled scan regex is kept — it depends
140*1c00c021SAndreas Gohr     * only on the connected patterns, not on the subject.
141*1c00c021SAndreas Gohr     *
142*1c00c021SAndreas Gohr     * @return void
143*1c00c021SAndreas Gohr     */
144*1c00c021SAndreas Gohr    public function reset(): void
145*1c00c021SAndreas Gohr    {
146*1c00c021SAndreas Gohr        $this->successFrom = null;
147*1c00c021SAndreas Gohr        $this->closerPos = null;
148*1c00c021SAndreas Gohr        $this->failFrom = null;
149*1c00c021SAndreas Gohr        $this->failUntil = null;
150*1c00c021SAndreas Gohr    }
151*1c00c021SAndreas Gohr}
152