1be906b56SAndreas Gohr<?php 2d4f83172SAndreas Gohr 3be906b56SAndreas Gohr/** 4be906b56SAndreas Gohr * Lexer adapted from Simple Test: http://sourceforge.net/projects/simpletest/ 5be906b56SAndreas Gohr * For an intro to the Lexer see: 6be906b56SAndreas Gohr * https://web.archive.org/web/20120125041816/http://www.phppatterns.com/docs/develop/simple_test_lexer_notes 7be906b56SAndreas Gohr * 8be906b56SAndreas Gohr * @author Marcus Baker http://www.lastcraft.com 9be906b56SAndreas Gohr */ 10be906b56SAndreas Gohr 11be906b56SAndreas Gohrnamespace dokuwiki\Parsing\Lexer; 12be906b56SAndreas Gohr 13be906b56SAndreas Gohr/** 14be906b56SAndreas Gohr * Compounded regular expression. 15be906b56SAndreas Gohr * 16be906b56SAndreas Gohr * Any of the contained patterns could match and when one does it's label is returned. 17be906b56SAndreas Gohr */ 18be906b56SAndreas Gohrclass ParallelRegex 19be906b56SAndreas Gohr{ 20be906b56SAndreas Gohr /** @var string[] patterns to match */ 21bcaec9f4SAndreas Gohr protected $patterns = []; 22be906b56SAndreas Gohr /** @var string[] labels for above patterns */ 23bcaec9f4SAndreas Gohr protected $labels = []; 24be906b56SAndreas Gohr /** @var string the compound regex matching all patterns */ 25be906b56SAndreas Gohr protected $regex; 26be906b56SAndreas Gohr /** @var bool case sensitive matching? */ 27be906b56SAndreas Gohr protected $case; 28be906b56SAndreas Gohr 29be906b56SAndreas Gohr /** 30be906b56SAndreas Gohr * Constructor. Starts with no patterns. 31be906b56SAndreas Gohr * 32be906b56SAndreas Gohr * @param boolean $case True for case sensitive, false 33be906b56SAndreas Gohr * for insensitive. 34be906b56SAndreas Gohr */ 35be906b56SAndreas Gohr public function __construct($case) 36be906b56SAndreas Gohr { 37be906b56SAndreas Gohr $this->case = $case; 38be906b56SAndreas Gohr } 39be906b56SAndreas Gohr 40be906b56SAndreas Gohr /** 41be906b56SAndreas Gohr * Adds a pattern with an optional label. 42be906b56SAndreas Gohr * 43be906b56SAndreas Gohr * @param mixed $pattern Perl style regex. Must be UTF-8 44be906b56SAndreas Gohr * encoded. If its a string, the (, ) 45be906b56SAndreas Gohr * lose their meaning unless they 46be906b56SAndreas Gohr * form part of a lookahead or 47be906b56SAndreas Gohr * lookbehind assertation. 48be906b56SAndreas Gohr * @param bool|string $label Label of regex to be returned 49be906b56SAndreas Gohr * on a match. Label must be ASCII 50be906b56SAndreas Gohr */ 51be906b56SAndreas Gohr public function addPattern($pattern, $label = true) 52be906b56SAndreas Gohr { 53be906b56SAndreas Gohr $count = count($this->patterns); 54be906b56SAndreas Gohr $this->patterns[$count] = $pattern; 55be906b56SAndreas Gohr $this->labels[$count] = $label; 56be906b56SAndreas Gohr $this->regex = null; 57be906b56SAndreas Gohr } 58be906b56SAndreas Gohr 59be906b56SAndreas Gohr /** 60*1c00c021SAndreas Gohr * Lists the registered patterns together with their labels, in 61*1c00c021SAndreas Gohr * registration order. 62*1c00c021SAndreas Gohr * 63*1c00c021SAndreas Gohr * The label tells how the lexer treats a match: true for a plain 64*1c00c021SAndreas Gohr * pattern consumed in place, Lexer::MODE_EXIT for an exit pattern, 65*1c00c021SAndreas Gohr * a mode name prefixed with Lexer::MODE_SPECIAL_PREFIX for a special 66*1c00c021SAndreas Gohr * pattern, and a bare mode name for an entry pattern into that mode. 67*1c00c021SAndreas Gohr * 68*1c00c021SAndreas Gohr * @return array[] list of ['pattern' => string, 'label' => bool|string] 69*1c00c021SAndreas Gohr */ 70*1c00c021SAndreas Gohr public function getPatterns() 71*1c00c021SAndreas Gohr { 72*1c00c021SAndreas Gohr return array_map( 73*1c00c021SAndreas Gohr static fn($pattern, $label) => ['pattern' => $pattern, 'label' => $label], 74*1c00c021SAndreas Gohr $this->patterns, 75*1c00c021SAndreas Gohr $this->labels 76*1c00c021SAndreas Gohr ); 77*1c00c021SAndreas Gohr } 78*1c00c021SAndreas Gohr 79*1c00c021SAndreas Gohr /** 80864d6c6dSAndreas Gohr * Attempts to split the string against all patterns at once. 81864d6c6dSAndreas Gohr * 82864d6c6dSAndreas Gohr * When `$offset` is non-zero, the match begins at that byte position in 83864d6c6dSAndreas Gohr * `$subject`, but the full subject is still passed to PCRE so any 84864d6c6dSAndreas Gohr * lookbehinds in the patterns can see characters before the offset. 85864d6c6dSAndreas Gohr * This is essential for inline-formatting closers like 86864d6c6dSAndreas Gohr * `(?<=[^\s])\*\*`, whose preceding non-whitespace character may have 87864d6c6dSAndreas Gohr * been consumed as part of a previous token (e.g. a `[[link]]`). 88be906b56SAndreas Gohr * 89be906b56SAndreas Gohr * @param string $subject String to match against. 90be906b56SAndreas Gohr * @param array $split The split result: array containing, pre-match, match & post-match strings 91864d6c6dSAndreas Gohr * @param int $offset Byte offset into `$subject` at which to start matching. 92be906b56SAndreas Gohr * @return boolean True on success. 93be906b56SAndreas Gohr * 94be906b56SAndreas Gohr * @author Christopher Smith <chris@jalakai.co.uk> 95be906b56SAndreas Gohr */ 96864d6c6dSAndreas Gohr public function split($subject, &$split, $offset = 0) 97be906b56SAndreas Gohr { 98be906b56SAndreas Gohr if (count($this->patterns) == 0) { 99be906b56SAndreas Gohr return false; 100be906b56SAndreas Gohr } 101be906b56SAndreas Gohr 102864d6c6dSAndreas Gohr if (! preg_match($this->getCompoundedRegex(), $subject, $matches, PREG_OFFSET_CAPTURE, $offset)) { 103be906b56SAndreas Gohr if (function_exists('preg_last_error')) { 104be906b56SAndreas Gohr $err = preg_last_error(); 105be906b56SAndreas Gohr switch ($err) { 106be906b56SAndreas Gohr case PREG_BACKTRACK_LIMIT_ERROR: 107be906b56SAndreas Gohr msg('A PCRE backtrack error occured. Try to increase the pcre.backtrack_limit in php.ini', -1); 108be906b56SAndreas Gohr break; 109*1c00c021SAndreas Gohr case PREG_JIT_STACKLIMIT_ERROR: 110*1c00c021SAndreas Gohr msg('A PCRE JIT stacklimit error occured. Try to disable pcre.jit in php.ini', -1); 111*1c00c021SAndreas Gohr break; 112be906b56SAndreas Gohr case PREG_RECURSION_LIMIT_ERROR: 113be906b56SAndreas Gohr msg('A PCRE recursion error occured. Try to increase the pcre.recursion_limit in php.ini', -1); 114be906b56SAndreas Gohr break; 115be906b56SAndreas Gohr case PREG_BAD_UTF8_ERROR: 116be906b56SAndreas Gohr msg('A PCRE UTF-8 error occured. This might be caused by a faulty plugin', -1); 117be906b56SAndreas Gohr break; 118be906b56SAndreas Gohr case PREG_INTERNAL_ERROR: 119be906b56SAndreas Gohr msg('A PCRE internal error occured. This might be caused by a faulty plugin', -1); 120be906b56SAndreas Gohr break; 121be906b56SAndreas Gohr } 122be906b56SAndreas Gohr } 123be906b56SAndreas Gohr 124864d6c6dSAndreas Gohr $split = [substr($subject, $offset), "", ""]; 125be906b56SAndreas Gohr return false; 126be906b56SAndreas Gohr } 127be906b56SAndreas Gohr 128be906b56SAndreas Gohr $idx = count($matches) - 2; 129864d6c6dSAndreas Gohr $matchText = (string) $matches[0][0]; 130864d6c6dSAndreas Gohr // Byte offset from PREG_OFFSET_CAPTURE; cast makes the int type 131864d6c6dSAndreas Gohr // obvious to static analysers that don't model the flag. 132864d6c6dSAndreas Gohr $matchStart = (int) $matches[0][1]; 133864d6c6dSAndreas Gohr $pre = substr($subject, $offset, $matchStart - $offset); 134864d6c6dSAndreas Gohr $post = substr($subject, $matchStart + strlen($matchText)); 135864d6c6dSAndreas Gohr $split = [$pre, $matchText, $post]; 136be906b56SAndreas Gohr 137bcaec9f4SAndreas Gohr return $this->labels[$idx] ?? true; 138be906b56SAndreas Gohr } 139be906b56SAndreas Gohr 140be906b56SAndreas Gohr /** 141*1c00c021SAndreas Gohr * Translates a pattern from the lexer convention into plain PCRE 142*1c00c021SAndreas Gohr * syntax: bare ( and ) match literally — only the (?...) group forms 143*1c00c021SAndreas Gohr * keep their regex meaning — and / needs no escaping despite the 144*1c00c021SAndreas Gohr * /-delimited compound. Any fragment embedded into a /-delimited 145*1c00c021SAndreas Gohr * regex alongside the registered patterns must go through this 146*1c00c021SAndreas Gohr * translation to compose correctly. 147be906b56SAndreas Gohr * 148*1c00c021SAndreas Gohr * @param string $pattern pattern in the lexer convention 149*1c00c021SAndreas Gohr * @return string plain PCRE pattern fragment 150be906b56SAndreas Gohr */ 151*1c00c021SAndreas Gohr public static function escapePattern($pattern) 152be906b56SAndreas Gohr { 153be906b56SAndreas Gohr /* 154be906b56SAndreas Gohr * decompose the input pattern into "(", "(?", ")", 155be906b56SAndreas Gohr * "[...]", "[]..]", "[^]..]", "[...[:...:]..]", "\x"... 156be906b56SAndreas Gohr * elements. 157be906b56SAndreas Gohr */ 158be906b56SAndreas Gohr preg_match_all('/\\\\.|' . 159be906b56SAndreas Gohr '\(\?|' . 160be906b56SAndreas Gohr '[()]|' . 161be906b56SAndreas Gohr '\[\^?\]?(?:\\\\.|\[:[^]]*:\]|[^]\\\\])*\]|' . 162*1c00c021SAndreas Gohr '[^[()\\\\]+/', $pattern, $elts); 163be906b56SAndreas Gohr 164*1c00c021SAndreas Gohr $escaped = ""; 165be906b56SAndreas Gohr $level = 0; 166be906b56SAndreas Gohr 167be906b56SAndreas Gohr foreach ($elts[0] as $elt) { 168be906b56SAndreas Gohr /* 169be906b56SAndreas Gohr * for "(", ")" remember the nesting level, add "\" 170be906b56SAndreas Gohr * only to the non-"(?" ones. 171be906b56SAndreas Gohr */ 172be906b56SAndreas Gohr 173be906b56SAndreas Gohr switch ($elt) { 174be906b56SAndreas Gohr case '(': 175*1c00c021SAndreas Gohr $escaped .= '\('; 176be906b56SAndreas Gohr break; 177be906b56SAndreas Gohr case ')': 178be906b56SAndreas Gohr if ($level > 0) 179be906b56SAndreas Gohr $level--; /* closing (? */ 180*1c00c021SAndreas Gohr else $escaped .= '\\'; 181*1c00c021SAndreas Gohr $escaped .= ')'; 182be906b56SAndreas Gohr break; 183be906b56SAndreas Gohr case '(?': 184be906b56SAndreas Gohr $level++; 185*1c00c021SAndreas Gohr $escaped .= '(?'; 186be906b56SAndreas Gohr break; 187be906b56SAndreas Gohr default: 1886c16a3a9Sfiwswe if (str_starts_with($elt, '\\')) 189*1c00c021SAndreas Gohr $escaped .= $elt; 190*1c00c021SAndreas Gohr else $escaped .= str_replace('/', '\/', $elt); 191be906b56SAndreas Gohr } 192be906b56SAndreas Gohr } 193*1c00c021SAndreas Gohr return $escaped; 194be906b56SAndreas Gohr } 195*1c00c021SAndreas Gohr 196*1c00c021SAndreas Gohr /** 197*1c00c021SAndreas Gohr * Compounds the patterns into a single 198*1c00c021SAndreas Gohr * regular expression separated with the 199*1c00c021SAndreas Gohr * "or" operator. Caches the regex. 200*1c00c021SAndreas Gohr * Will automatically escape (, ) and / tokens. 201*1c00c021SAndreas Gohr * 202*1c00c021SAndreas Gohr * @return null|string 203*1c00c021SAndreas Gohr */ 204*1c00c021SAndreas Gohr protected function getCompoundedRegex() 205*1c00c021SAndreas Gohr { 206*1c00c021SAndreas Gohr if ($this->regex == null) { 207*1c00c021SAndreas Gohr $groups = array_map( 208*1c00c021SAndreas Gohr static fn($pattern) => '(' . self::escapePattern($pattern) . ')', 209*1c00c021SAndreas Gohr $this->patterns 210*1c00c021SAndreas Gohr ); 211*1c00c021SAndreas Gohr $this->regex = "/" . implode("|", $groups) . "/" . $this->getPerlMatchingFlags(); 212be906b56SAndreas Gohr } 213be906b56SAndreas Gohr return $this->regex; 214be906b56SAndreas Gohr } 215be906b56SAndreas Gohr 216be906b56SAndreas Gohr /** 217be906b56SAndreas Gohr * Accessor for perl regex mode flags to use. 218be906b56SAndreas Gohr * @return string Perl regex flags. 219be906b56SAndreas Gohr */ 220be906b56SAndreas Gohr protected function getPerlMatchingFlags() 221be906b56SAndreas Gohr { 222be906b56SAndreas Gohr return ($this->case ? "msS" : "msSi"); 223be906b56SAndreas Gohr } 224be906b56SAndreas Gohr} 225