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