cm-tarnation
Version:
An alternative parser for CodeMirror 6
134 lines • 5.68 kB
JavaScript
/* 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