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