UNPKG

cm-tarnation

Version:

An alternative parser for CodeMirror 6

134 lines 5.68 kB
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { escapeRegExp, hasCapturingGroups, isRegExpString, re } from "../../util"; /** * `RegExp` wrapper class. Designed to improve performance by implementing * certain operations (like matching) with edge case handling and short * circuits. In order to assist with this goal, the internal `RegExp` is * always set to be `sticky`, so this class won't work if you want to use a * non-sticky `RegExp`. */ export class RegExpMatcher { /** * @param src - The source `RegExp` to wrap. * @param ignoreCase - If `true`, the ignore case flag is set automatically. * @param variables - A variable table to use when expanding variables. */ constructor(src, ignoreCase = false, variables) { const flags = ignoreCase ? "iymu" : "ymu"; if (variables) src = expandVariables(src, variables); const regexp = re(src, flags); if (!regexp) throw new Error(`Invalid RegExp: ${src}`); this.regexp = regexp; this.hasCapturingGroups = this.regexp ? hasCapturingGroups(this.regexp) : false; } /** * Performs the standard `RegExp.test` operation on a string. * * @param str - The string to match. * @param pos - The position to start matching at. */ test(str, pos) { this.regexp.lastIndex = pos; return this.regexp.test(str); } /** * Performs the standard `RegExp.exec` operation on a string. This is an * internal method because consumers should use the `match` method instead. * * @param str - The string to match. * @param pos - The position to start matching at. */ exec(str, pos) { this.regexp.lastIndex = pos; return this.regexp.exec(str); } /** * Returns the results of attempting to match a string against this * regular expression. * * @param str - The string to match. * @param pos - The position to start matching at. */ match(str, pos) { // if we don't have any capturing groups, we can cheat a little bit, // and only use the `test` method, which is much faster if (!this.hasCapturingGroups) { if (this.test(str, pos)) { // regexp remembers where it ended up, so we can use that to figure // out where the match ended const length = this.regexp.lastIndex - pos; const total = str.slice(pos, pos + length); return { total, captures: null, length }; } return null; } // this dumb looking code is actually faster than just calling `exec` by itself // I don't really know why - I guess because `test` is much faster than `exec` // and most of the time a regexp doesn't match anything, so it's not worth // trying a full match first if (this.test(str, pos)) { const match = this.exec(str, pos); if (match) { const total = match[0]; const captures = match.length > 1 ? match.slice(1) : null; const length = total.length; // capturing groups can be undefined if an entire group was marked as optional // so we'll replace those with an empty string to keep the interface consistent if (captures) { let len = 0; for (let i = 0; i < captures.length; i++) { if (captures[i] === undefined) captures[i] = ""; len += captures[i].length; } // tarnation can only accept regexps that have their capturing groups // consist of contigous ranges covering the entirety of the match if (len !== length) throw new Error("Invalid capturing group lengths"); } return { total, captures, length }; } } return null; } } /** * Expands the variables in a regex source using a variable table. * * @param src - The source to expand. * @param variables - The variable table to use. */ function expandVariables(src, variables) { let depth = 0; while (/@\w/.test(src) && depth < 5) { depth++; src = src.replace(/@(\w+)/g, (_, ident) => { const value = variables[ident]; if (value === undefined) { throw new Error(`Variable ${ident} not found`); } else if (typeof value === "object" && "test" in value) { throw new Error(`Variable ${ident} is a Matcher`); } if (typeof value === "string" && isRegExpString(value)) { const regexp = re(value); if (!regexp) throw new Error(`Variable ${ident} is an invalid regex`); return `(?:${regexp.source})`; } // not regex string const variable = typeof value === "string" ? escapeRegExp(value) : value instanceof RegExp ? value.source : value.map(str => escapeRegExp(str)).join("|"); return `(?:${variable})`; }); } return src; } //# sourceMappingURL=regexp.js.map