UNPKG

@routejs/router

Version:

Fast and lightweight http routing engine for nodejs

304 lines (278 loc) 7.71 kB
export default function hostRegex(str, options = { caseSensitive: false }) { const caseSensitive = options?.caseSensitive ?? false; const tokens = []; let i = 0; let key = 0; while (i < str.length) { let char = str[i]; if (char === "\\") { tokens.push({ type: "ESCAPED_CHAR", value: str[i + 1] ?? "\\" }); i = i + 2; continue; } if (char === "*" || char === "+" || char === "?") { if (char === "*") { tokens.push({ type: "MODIFIER", value: str[i++], name: key++, regex: "(.*)", }); } else { tokens.push({ type: "MODIFIER", value: str[i++] }); } continue; } if (char === ".") { tokens.push({ type: "DELIMITER", value: str[i++] }); continue; } if (char === ":") { let name = ""; let j = i + 1; while (j < str.length) { const code = str.charCodeAt(j); if ( // `0-9` (code >= 48 && code <= 57) || // `A-Z` (code >= 65 && code <= 90) || // `a-z` (code >= 97 && code <= 122) || // `_` code === 95 ) { name += str[j++]; continue; } break; } if (!name) throw new TypeError(`Missing parameter name at ${i}`); tokens.push({ type: "PARAM", value: name, name, regex: "([^\\.]+?)", }); i = j; continue; } if (char === "(") { let count = 1; let pattern = ""; let j = i + 1; if (str[j] === "?") { throw new TypeError(`Pattern cannot start with "?" at ${j}`); } while (j < str.length) { if (str[j] === "\\") { pattern += str[j++] + str[j++]; continue; } if (str[j] === ")") { count--; if (count === 0) { j++; break; } } else if (str[j] === "(") { count++; if (str[j + 1] !== "?") { throw new TypeError(`Capturing groups are not allowed at ${j}`); } } pattern += str[j++]; } if (count) throw new TypeError(`Unbalanced pattern at ${i}`); if (!pattern) throw new TypeError(`Missing pattern at ${i}`); if (tokens.length > 0 && tokens[tokens.length - 1].type === "PARAM") { tokens[tokens.length - 1] = { type: "PARAM_REGEX", value: pattern, name: tokens[tokens.length - 1].name, regex: "(" + pattern + ")", }; } else { tokens.push({ type: "REGEX", value: pattern, name: key++, regex: "(" + pattern + ")", }); } i = j; continue; } let path = ""; let j = i; while (j < str.length) { if ( str[j] === "\\" || str[j] === "*" || str[j] === "+" || str[j] === "?" || str[j] === ":" || str[j] === "(" || str[j] === "." ) { break; } // Escape special characters path += str[j++]; continue; } tokens.push({ type: "PATH", value: path }); i = j; } const params = []; let pathRegex = ""; let tokenIndex = 0; while (tokenIndex < tokens.length) { if (tokens[tokenIndex].type === "DELIMITER") { if ( tokens[tokenIndex + 1] && (tokens[tokenIndex + 1].type === "PARAM" || tokens[tokenIndex + 1].type === "PARAM_REGEX" || tokens[tokenIndex + 1].type === "REGEX") ) { if ( tokens[tokenIndex + 2] && tokens[tokenIndex + 2].type === "MODIFIER" && tokens[tokenIndex + 2].value === "?" && (typeof tokens[tokenIndex + 3] === "undefined" || tokens[tokenIndex + 3].type === "DELIMITER") ) { tokenIndex++; continue; } } pathRegex += tokens[tokenIndex++].value; continue; } if ( tokens[tokenIndex].type === "PARAM" || tokens[tokenIndex].type === "PARAM_REGEX" || tokens[tokenIndex].type === "REGEX" ) { let param = { name: tokens[tokenIndex].name, regex: tokens[tokenIndex].regex, optional: tokens[tokenIndex + 1] ? tokens[tokenIndex + 1].type === "MODIFIER" && tokens[tokenIndex + 1].value === "?" : false, }; if (param.optional === true) { if ( tokens[tokenIndex - 1] && tokens[tokenIndex - 1].type === "DELIMITER" && (typeof tokens[tokenIndex + 2] === "undefined" || tokens[tokenIndex + 2].type === "DELIMITER") ) { param.regex = "(?:\\." + param.regex + ")?"; tokenIndex = tokenIndex + 2; } else if ( typeof tokens[tokenIndex - 1] === "undefined" && typeof tokens[tokenIndex + 2] && tokens[tokenIndex + 2].type === "DELIMITER" ) { param.regex = "(?:" + param.regex + "\\.)?"; tokenIndex = tokenIndex + 3; } else { param.regex = param.regex; tokenIndex++; } } else { param.regex = param.regex; tokenIndex++; } params.push(param); pathRegex += param.regex; continue; } if ( tokens[tokenIndex].type === "MODIFIER" && tokens[tokenIndex].value === "*" ) { let param = { name: tokens[tokenIndex].name, regex: tokens[tokenIndex].regex, optional: false, }; params.push(param); pathRegex += param.regex; tokenIndex++; continue; } if (tokens[tokenIndex].type === "ESCAPED_CHAR") { pathRegex += "\\" + tokens[tokenIndex++].value; continue; } if (tokens[tokenIndex].type === "PATH") { pathRegex += tokens[tokenIndex++].value.replace( /([.+*?=^!:${}()[\]|/\\])/g, "\\$1" ); continue; } pathRegex += tokens[tokenIndex++].value; } pathRegex = pathRegex.replace(/\/$/gm, ""); if (pathRegex === "") { pathRegex = ""; } else { pathRegex = `^${pathRegex}(?:\\:\\d+)?$`; } let regex = caseSensitive === false ? new RegExp(pathRegex, "i") : new RegExp(pathRegex); return { host: str, params, regex: regex, compile(params = {}, options = { validate: false }) { const validate = options?.validate ?? false; let compiledPath = ""; let i = 0; while (i < tokens.length) { if (tokens[i].type === "DELIMITER") { compiledPath += tokens[i++].value; continue; } if (tokens[i].type === "PATH") { compiledPath += tokens[i++].value; continue; } if ( tokens[i].type === "PARAM" || tokens[i].type === "PARAM_REGEX" || tokens[i].type === "REGEX" || (tokens[i].type === "MODIFIER" && tokens[i].value === "*") ) { if (params.hasOwnProperty(tokens[i].name)) { compiledPath += params[tokens[i++].name]; continue; } else { if ( tokens[i + 1] && tokens[i + 1].type === "MODIFIER" && tokens[i + 1].value === "?" ) { if (tokens[i + 2] && tokens[i + 2].type === "DELIMITER") { i = i + 3; continue; } } else { throw new TypeError( "invalid route parameters, please provide all route parameters" ); } } } i++; } return compiledPath; }, }; }