magic-regexp
Version:
A compiled-away, type-safe, readable RegExp alternative
288 lines (283 loc) • 8.13 kB
JavaScript
//#region src/core/wrap.ts
const NO_WRAP_RE = /^(?:\(.*\)|\\?.)$/;
function wrap(s) {
const v = s.toString();
return NO_WRAP_RE.test(v) ? v : `(?:${v})`;
}
//#endregion
//#region src/core/internal.ts
const GROUPED_AS_REPLACE_RE = /^(?:\(\?:(.+)\)|(.+))$/;
const GROUPED_REPLACE_RE = /^(?:\(\?:(.+)\)([?+*]|\{[\d,]+\})?|(.+))$/;
function createInput(s) {
const groupedAsFn = (key) => createInput(`(?<${key}>${`${s}`.replace(GROUPED_AS_REPLACE_RE, "$1$2")})`);
return {
toString: () => s.toString(),
and: Object.assign((...inputs) => createInput(`${s}${exactly(...inputs)}`), { referenceTo: (groupName) => createInput(`${s}\\k<${groupName}>`) }),
or: (...inputs) => createInput(`(?:${s}|${inputs.map((v) => exactly(v)).join("|")})`),
after: (...input) => createInput(`(?<=${exactly(...input)})${s}`),
before: (...input) => createInput(`${s}(?=${exactly(...input)})`),
notAfter: (...input) => createInput(`(?<!${exactly(...input)})${s}`),
notBefore: (...input) => createInput(`${s}(?!${exactly(...input)})`),
times: Object.assign((number) => createInput(`${wrap(s)}{${number}}`), {
any: () => createInput(`${wrap(s)}*`),
atLeast: (min) => createInput(`${wrap(s)}{${min},}`),
atMost: (max) => createInput(`${wrap(s)}{0,${max}}`),
between: (min, max) => createInput(`${wrap(s)}{${min},${max}}`)
}),
optionally: () => createInput(`${wrap(s)}?`),
as: groupedAsFn,
groupedAs: groupedAsFn,
grouped: () => createInput(`${s}`.replace(GROUPED_REPLACE_RE, "($1$3)$2")),
at: {
lineStart: () => createInput(`^${s}`),
lineEnd: () => createInput(`${s}$`)
}
};
}
//#endregion
//#region src/core/inputs.ts
const ESCAPE_REPLACE_RE = /[.*+?^${}()|[\]\\/]/g;
function createCharInput(raw) {
const input = createInput(`[${raw}]`);
const from = (charFrom, charTo) => createCharInput(`${raw}${escapeCharInput(charFrom)}-${escapeCharInput(charTo)}`);
const orChar = Object.assign((chars) => createCharInput(`${raw}${escapeCharInput(chars)}`), { from });
return Object.assign(input, {
orChar,
from
});
}
function escapeCharInput(raw) {
return raw.replace(/[-\\^\]]/g, "\\$&");
}
/** This matches any character in the string provided */
const charIn = Object.assign((chars) => {
return createCharInput(escapeCharInput(chars));
}, createCharInput(""));
/** This matches any character that is not in the string provided */
const charNotIn = Object.assign((chars) => {
return createCharInput(`^${escapeCharInput(chars)}`);
}, createCharInput("^"));
/**
* This takes a variable number of inputs and matches any of them
* @example
* anyOf('foo', maybe('bar'), 'baz') // => /(?:foo|(?:bar)?|baz)/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
function anyOf(...inputs) {
return createInput(`(?:${inputs.map((a) => exactly(a)).join("|")})`);
}
const char = createInput(".");
const word = createInput("\\b\\w+\\b");
const wordChar = createInput("\\w");
const wordBoundary = createInput("\\b");
const digit = createInput("\\d");
const whitespace = createInput("\\s");
const letter = Object.assign(createInput("[a-zA-Z]"), {
lowercase: createInput("[a-z]"),
uppercase: createInput("[A-Z]")
});
const tab = createInput("\\t");
const linefeed = createInput("\\n");
const carriageReturn = createInput("\\r");
const not = {
word: createInput("\\W+"),
wordChar: createInput("\\W"),
wordBoundary: createInput("\\B"),
digit: createInput("\\D"),
whitespace: createInput("\\S"),
letter: Object.assign(createInput("[^a-zA-Z]"), {
lowercase: createInput("[^a-z]"),
uppercase: createInput("[^A-Z]")
}),
tab: createInput("[^\\t]"),
linefeed: createInput("[^\\n]"),
carriageReturn: createInput("[^\\r]")
};
/**
* Equivalent to `?` - takes a variable number of inputs and marks them as optional
* @example
* maybe('foo', exactly('ba?r')) // => /(?:fooba\?r)?/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
function maybe(...inputs) {
return createInput(`${wrap(exactly(...inputs))}?`);
}
/**
* This takes a variable number of inputs and concatenate their patterns, and escapes string inputs to match it exactly
* @example
* exactly('fo?o', maybe('bar')) // => /fo\?o(?:bar)?/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
function exactly(...inputs) {
return createInput(inputs.map((input) => typeof input === "string" ? input.replace(ESCAPE_REPLACE_RE, "\\$&") : input).join(""));
}
/**
* Equivalent to `+` - this takes a variable number of inputs and marks them as repeatable, any number of times but at least once
* @example
* oneOrMore('foo', maybe('bar')) // => /(?:foo(?:bar)?)+/
* @argument inputs - arbitrary number of `string` or `Input`, where `string` will be escaped
*/
function oneOrMore(...inputs) {
return createInput(`${wrap(exactly(...inputs))}+`);
}
//#endregion
//#region src/core/flags.ts
/** Generate indices for substring matches */
const withIndices = "d";
/** Case-insensitive search */
const caseInsensitive = "i";
/** Global search */
const global = "g";
/** Multi-line search */
const multiline = "m";
/** Allows `.` to match newline characters */
const dotAll = "s";
/** Treat a pattern as a sequence of unicode code points */
const unicode = "u";
/** Perform a "sticky" search that matches starting at the current position in the target string */
const sticky = "y";
//#endregion
Object.defineProperty(exports, 'anyOf', {
enumerable: true,
get: function () {
return anyOf;
}
});
Object.defineProperty(exports, 'carriageReturn', {
enumerable: true,
get: function () {
return carriageReturn;
}
});
Object.defineProperty(exports, 'caseInsensitive', {
enumerable: true,
get: function () {
return caseInsensitive;
}
});
Object.defineProperty(exports, 'char', {
enumerable: true,
get: function () {
return char;
}
});
Object.defineProperty(exports, 'charIn', {
enumerable: true,
get: function () {
return charIn;
}
});
Object.defineProperty(exports, 'charNotIn', {
enumerable: true,
get: function () {
return charNotIn;
}
});
Object.defineProperty(exports, 'digit', {
enumerable: true,
get: function () {
return digit;
}
});
Object.defineProperty(exports, 'dotAll', {
enumerable: true,
get: function () {
return dotAll;
}
});
Object.defineProperty(exports, 'exactly', {
enumerable: true,
get: function () {
return exactly;
}
});
Object.defineProperty(exports, 'global', {
enumerable: true,
get: function () {
return global;
}
});
Object.defineProperty(exports, 'letter', {
enumerable: true,
get: function () {
return letter;
}
});
Object.defineProperty(exports, 'linefeed', {
enumerable: true,
get: function () {
return linefeed;
}
});
Object.defineProperty(exports, 'maybe', {
enumerable: true,
get: function () {
return maybe;
}
});
Object.defineProperty(exports, 'multiline', {
enumerable: true,
get: function () {
return multiline;
}
});
Object.defineProperty(exports, 'not', {
enumerable: true,
get: function () {
return not;
}
});
Object.defineProperty(exports, 'oneOrMore', {
enumerable: true,
get: function () {
return oneOrMore;
}
});
Object.defineProperty(exports, 'sticky', {
enumerable: true,
get: function () {
return sticky;
}
});
Object.defineProperty(exports, 'tab', {
enumerable: true,
get: function () {
return tab;
}
});
Object.defineProperty(exports, 'unicode', {
enumerable: true,
get: function () {
return unicode;
}
});
Object.defineProperty(exports, 'whitespace', {
enumerable: true,
get: function () {
return whitespace;
}
});
Object.defineProperty(exports, 'withIndices', {
enumerable: true,
get: function () {
return withIndices;
}
});
Object.defineProperty(exports, 'word', {
enumerable: true,
get: function () {
return word;
}
});
Object.defineProperty(exports, 'wordBoundary', {
enumerable: true,
get: function () {
return wordBoundary;
}
});
Object.defineProperty(exports, 'wordChar', {
enumerable: true,
get: function () {
return wordChar;
}
});