@nent/core
Version:
289 lines (288 loc) • 8.87 kB
JavaScript
/*!
* NENT 2022
*/
/* istanbul ignore file */
/**
* TS adaption of https://github.com/pillarjs/path-to-regexp/blob/master/index.js
*/
/**
* Default configs.
*/
const DEFAULT_DELIMITER = '/';
const DEFAULT_DELIMITERS = './';
/**
* The main path matching regexp utility.
*/
const PATH_REGEXP = new RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?',
].join('|'), 'g');
/**
* Parse a string for the raw tokens.
*/
export const parse = (string, options) => {
const tokens = [];
let key = 0;
let index = 0;
let path = '';
const defaultDelimiter = (options === null || options === void 0 ? void 0 : options.delimiter) || DEFAULT_DELIMITER;
const delimiters = (options === null || options === void 0 ? void 0 : options.delimiters) || DEFAULT_DELIMITERS;
let pathEscaped = false;
let res;
while ((res = PATH_REGEXP.exec(string)) !== null) {
const m = res[0];
const escaped = res[1];
const offset = res.index;
path += string.slice(index, offset);
index = offset + m.length;
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1];
pathEscaped = true;
continue;
}
let previous = '';
const next = string[index];
const name = res[2];
const capture = res[3];
const group = res[4];
const modifier = res[5];
if (!pathEscaped && path.length > 0) {
const k = path.length - 1;
if (delimiters.includes(path[k])) {
previous = path[k];
path = path.slice(0, k);
}
}
// Push the current path onto the tokens.
if (path) {
tokens.push(path);
path = '';
pathEscaped = false;
}
const partial = previous !== '' && next !== undefined && next !== previous;
const repeat = modifier === '+' || modifier === '*';
const optional = modifier === '?' || modifier === '*';
const delimiter = previous || defaultDelimiter;
const pattern = capture || group;
tokens.push({
name: name || key++,
prefix: previous,
delimiter,
optional,
repeat,
partial,
pattern: pattern
? escapeGroup(pattern)
: `[^${escapeString(delimiter)}]+?`,
});
}
// Push any remaining characters.
if (path || index < string.length) {
tokens.push(path + string.slice(index));
}
return tokens;
};
/**
* Compile a string to a template function for the path.
*/
export const compile = (string, options) => tokensToFunction(parse(string, options));
/**
* Expose a method for transforming tokens into the path function.
*/
export const tokensToFunction = (tokens) => {
// Compile all the tokens into regexps.
const matches = new Array(tokens.length);
// Compile all the patterns before compilation.
for (const [i, token] of tokens.entries()) {
if (typeof token === 'object') {
matches[i] = new RegExp(`^(?:${token.pattern})$`);
}
}
return (data, options) => {
let path = '';
const encode = (options === null || options === void 0 ? void 0 : options.encode) || encodeURIComponent;
for (const [i, token] of tokens.entries()) {
if (typeof token === 'string') {
path += token;
continue;
}
const value = data ? data[token.name] : undefined;
let segment;
if (Array.isArray(value)) {
if (!token.repeat) {
throw new TypeError(`Expected "${token.name}" to not repeat, but got array`);
}
if (value.length === 0) {
if (token.optional) {
continue;
}
throw new TypeError(`Expected "${token.name}" to not be empty`);
}
for (const [j, element] of value.entries()) {
segment = encode(element);
if (!matches[i].test(segment)) {
throw new TypeError(`Expected all "${token.name}" to match "${token.pattern}"`);
}
path += `${j === 0 ? token.prefix : token.delimiter}${segment}`;
}
continue;
}
if (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean') {
segment = encode(String(value));
if (!matches[i].test(segment)) {
throw new TypeError(`Expected "${token.name}" to match "${token.pattern}", but got "${segment}"`);
}
path += `${token.prefix}${segment}`;
continue;
}
if (token.optional) {
// Prepend partial segment prefixes.
if (token.partial) {
path += token.prefix;
}
continue;
}
throw new TypeError(`Expected "${token.name}" to be ${token.repeat ? 'an array' : 'a string'}`);
}
return path;
};
};
/**
* Escape a regular expression string.
*/
export const escapeString = (string) => string.replace(/([.+*?=^!:${}()\[\]|/\\])/g, '\\$1');
/**
* Escape the capturing group by escaping special characters and meaning.
*/
const escapeGroup = (group) => group.replace(/([=!:$/()])/g, '\\$1');
/**
* Get the flags for a regexp from the options.
*/
const flags = (options) => (options === null || options === void 0 ? void 0 : options.sensitive) ? '' : 'i';
/**
* Pull out keys from a regexp.
*/
const regexpToRegexp = (path, keys) => {
if (!keys) {
return path;
}
// Use a negative lookahead to match only capturing groups.
const groups = path.source.match(/\((?!\?)/g);
if (groups) {
for (let i = 0; i < groups.length; i++) {
keys.push({
name: i,
prefix: null,
delimiter: null,
optional: false,
repeat: false,
partial: false,
pattern: null,
});
}
}
return path;
};
/**
* Transform an array into a regexp.
*/
const arrayToRegexp = (path, keys, options) => {
const parts = [];
for (const element of path) {
parts.push(pathToRegexp(element, keys, options).source);
}
return new RegExp(`(?:${parts.join('|')})`, flags(options));
};
/**
* Create a path regexp from string input.
*/
const stringToRegexp = (path, keys, options) => tokensToRegExp(parse(path, options), keys, options);
/**
* Expose a function for taking tokens and returning a RegExp.
*/
export const tokensToRegExp = (tokens, keys, options) => {
var _a;
options = options || {};
const { strict } = options;
const end = options.end !== false;
const delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER);
const delimiters = options.delimiters || DEFAULT_DELIMITERS;
const endsWith = (((_a = options.endsWith) === null || _a === void 0 ? void 0 : _a.length)
? [...options.endsWith]
: options.endsWith
? [options.endsWith]
: [])
.map(i => escapeString(i))
.concat('$')
.join('|');
let route = '';
let isEndDelimited = false;
// Iterate over the tokens and create our regexp string.
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (typeof token === 'string') {
route += escapeString(token);
isEndDelimited =
i === tokens.length - 1 &&
delimiters.includes(token[token.length - 1]);
}
else {
const prefix = escapeString(token.prefix || '');
const capture = token.repeat
? `(?:${token.pattern})(?:${prefix}(?:${token.pattern}))*`
: token.pattern;
if (keys) {
keys.push(token);
}
if (token.optional) {
route += token.partial
? `${prefix}(${capture})?`
: `(?:${prefix}(${capture}))?`;
}
else {
route += `${prefix}(${capture})`;
}
}
}
if (end) {
if (!strict) {
route += `(?:${delimiter})?`;
}
route += endsWith === '$' ? '$' : `(?=${endsWith})`;
}
else {
if (!strict) {
route += `(?:${delimiter}(?=${endsWith}))?`;
}
if (!isEndDelimited) {
route += `(?=${delimiter}|${endsWith})`;
}
}
return new RegExp(`^${route}`, flags(options));
};
/**
* Normalize the given path string, returning a regular expression.
*
* An empty array can be passed in for the keys, which will hold the
* placeholder key descriptions. For example, using `/user/:id`, `keys` will
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
*/
export const pathToRegexp = (path, keys, options) => {
if (path instanceof RegExp) {
return regexpToRegexp(path, keys);
}
if (Array.isArray(path)) {
return arrayToRegexp(path, keys, options);
}
return stringToRegexp(path, keys, options);
};