UNPKG

@ima/core

Version:

IMA.js framework for isomorphic javascript application

365 lines (363 loc) 15.9 kB
import { AbstractRoute, LOOSE_SLASHES_REGEXP } from './AbstractRoute'; /** * Regular expression matching all control characters used in regular * expressions. The regular expression is used to match these characters in * path expressions and replace them appropriately so the path expression can * be compiled to a regular expression. */ const CONTROL_CHARACTERS_REGEXP = /[\\.+*?^$[\](){}/'#]/g; /** * Regular expression used to match the parameter names from a path expression. */ const PARAMS_REGEXP_UNIVERSAL = /:\??([\w-]+)/g; /** * Regular expression used to match the required parameter names from a path expression. */ const PARAMS_REGEXP_REQUIRED = /(?:^|\\\/):([a-z0-9]+)(?=\\\/|$)/gi; /** * Regular expression used to separate a camelCase parameter name */ const PARAMS_REGEXP_CORE_NAME = /[a-z0-9]+/i; /** * Regular expression used to match start of parameter names from a path expression. */ const PARAMS_START_PATTERN = '(^|/|[_-])'; /** * Regular expression used to match end of parameter names from a path expression. */ const PARAMS_END_PATTERN = '[/?_-]|$'; /** * Regular expression used to never match the parameter names from a path expression. * It's used for wrong parameters order (optional vs. required ones) */ const PARAMS_NEVER_MATCH_REGEXP = /$a/; /** * Regular expression used to match all main parameter names from a path expression. */ const PARAMS_MAIN_REGEXP = /(?:\\\/|^):\\\?([a-z0-9]+)(?=\\\/|$)|(?:^|\\\/):([a-z0-9]+)(?=\\\/|$)/gi; /** * Regular expression used to match the required subparameter names from a path expression. * (e.g. for path '/:paramA-:paramB/:nextParam' are subparameters 'paramA' and 'paramB') */ const SUBPARAMS_REQUIRED_REGEXP = { LAST: /([_-]{1})((\w-)?:[a-z0-9]+)(?=\\\/|$)/gi, OTHERS: /(:[a-z0-9]+)(?=[_-]{1})/gi }; /** * Regular expression used to match the optional parameter names from a path expression. */ const SUBPARAMS_OPT_REGEXP = { LAST: /([_-]{1}(\w-)?:\\\?[a-z0-9]+)(?=\\\/|$)/gi, OTHERS: /(:\\\?[a-z0-9]+)(?=[_-]{1}(\w-)?)/gi }; /** * Regular expression used to match the parameter names from a path expression. */ const PARAMS_REGEXP_OPT = /(?:^:\\\?([a-z0-9]+)(?=\\\/|$))|(?:(\\\/):\\\?([a-z0-9]+)(?=\\\/|$))/gi; // last part: |(?::\\\?([a-z0-9]+)(?=\\\/|$)) /** * Utility for representing and manipulating a single static route in the * router's configuration using string representation of the path expression * with special param fields identified by `:paramName` prefix. */ export class StaticRoute extends AbstractRoute { _trimmedPathExpression; _parameterNames; _hasParameters; _matcher; /** * @inheritDoc * @param pathExpression A path expression specifying the URL path * part matching this route (must not contain a query string), * optionally containing named parameter placeholders specified as * `:parameterName`. */ constructor(name, pathExpression, controller, view, options){ super(name, pathExpression, controller, view, options); /** * The path expression with the trailing slashes trimmed. */ this._trimmedPathExpression = this.getTrimmedPath(pathExpression); /** * The names of the parameters in this route. */ this._parameterNames = this._getParameterNames(pathExpression); /** * Set to `true` if this route contains parameters in its path. */ this._hasParameters = !!this._parameterNames.length; /** * A regexp used to match URL path against this route and extract the * parameter values from the matched URL paths. */ this._matcher = this._compileToRegExp(this._trimmedPathExpression); } /** * @inheritDoc */ toPath(params = {}) { let path = this._pathExpression; const queryParams = {}; for (const paramName of Object.keys(params)){ if (this._isRequiredParamInPath(path, paramName)) { path = this._substituteRequiredParamInPath(path, paramName, params[paramName]); } else if (this._isOptionalParamInPath(path, paramName)) { path = this._substituteOptionalParamInPath(path, paramName, params[paramName]); } else { queryParams[paramName] = params[paramName]; } } path = this._cleanUnusedOptionalParams(path); if (Object.keys(queryParams).length) { path += `?${new URLSearchParams(queryParams).toString()}`; } return this.getTrimmedPath(path); } /** * @inheritDoc */ matches(path) { const trimmedPath = this.getTrimmedPath(path); return this._matcher.test(trimmedPath); } /** * @inheritDoc */ extractParameters(path, baseUrl) { const trimmedPath = this.getTrimmedPath(path); const parameters = this._getParameters(trimmedPath); return { ...parameters, ...Object.fromEntries(new URL(`${baseUrl}${path}`).searchParams) }; } /** * Replace required parameter placeholder in path with parameter value. */ _substituteRequiredParamInPath(path, paramName, paramValue) { return path.replace(new RegExp(`${PARAMS_START_PATTERN}:${paramName}(${PARAMS_END_PATTERN})`), paramValue ? '$1' + encodeURIComponent(paramValue) + '$2' : ''); } /** * Replace optional param placeholder in path with parameter value. */ _substituteOptionalParamInPath(path, paramName, paramValue) { const paramRegexp = `${PARAMS_START_PATTERN}:\\?${paramName}(${PARAMS_END_PATTERN})`; return path.replace(new RegExp(paramRegexp), paramValue ? '$1' + encodeURIComponent(paramValue) + '$2' : '/'); } /** * Remove unused optional param placeholders in path. */ _cleanUnusedOptionalParams(path) { let replacedPath = path; // remove last subparameters replacedPath = replacedPath.replace(/([_-])(:\?([a-z0-9]+))(?=\/)/gi, '$1'); // remove parameters replacedPath = replacedPath.replace(/(\/:\?([a-z0-9]+))|(:\?([a-z0-9]+)\/?)/gi, ''); return replacedPath; } /** * Returns true, if paramName is placed in path. */ _isOptionalParamInPath(path, paramName) { const paramRegexp = `${PARAMS_START_PATTERN}:\\?${paramName}(?:${PARAMS_END_PATTERN})`; const regexp = new RegExp(paramRegexp); return regexp.test(path); } /** * Returns true, if paramName is placed in path and it's required. */ _isRequiredParamInPath(path, paramName) { const regexp = new RegExp(`:${paramName}`); return regexp.test(path); } /** * Extract clear parameter name, e.q. '?name' or 'name' */ _getClearParamName(rawParam) { const regExpr = /\??[a-z0-9]+/i; const paramMatches = rawParam.match(regExpr); const param = paramMatches ? paramMatches[0] : ''; return param; } /** * Get pattern for subparameter. */ _getSubparamPattern(delimiter) { const pattern = `([^${delimiter}?/]+)`; return pattern; } /** * Check if all optional params are below required ones */ _checkOptionalParamsOrder(allMainParams) { let optionalLastId = -1; const count = allMainParams.length; for(let idx = 0; idx < count; idx++){ const item = allMainParams[idx]; if (item.substr(0, 1) === '?') { optionalLastId = idx; } else { if (optionalLastId > -1 && idx > optionalLastId) { return false; } } } return true; } /** * Check if main parameters have correct order. * It means that required param cannot follow optional one. * * @param clearedPathExpr The cleared URL path (removed first and last slash, ...). * @return Returns TRUE if order is correct. */ _checkParametersOrder(clearedPathExpr) { const mainParamsMatches = clearedPathExpr.match(PARAMS_MAIN_REGEXP) || []; const allMainParamsCleared = mainParamsMatches.map((paramExpr)=>this._getClearParamName(paramExpr)); const isCorrectParamOrder = this._checkOptionalParamsOrder(allMainParamsCleared); return isCorrectParamOrder; } /** * Convert main optional parameters to capture sequences * * @param path The URL path. * @param optionalParams List of main optimal parameter expressions * @return RegExp pattern. */ _replaceOptionalParametersInPath(path, optionalParams) { const pattern = optionalParams.reduce((path, paramExpr)=>{ const separator = paramExpr.startsWith('\\/') ? '/' : ''; const regExpr = '(?:' + separator + `([^/?]+)(?=/|$|\\?))?`; return path.replace(paramExpr, regExpr); }, path); return pattern; } /** * Convert required subparameters to capture sequences * * @param path The URL path (route definition). * @param clearedPathExpr The original cleared URL path. * @return RegExp pattern. */ _replaceRequiredSubParametersInPath(path, clearedPathExpr) { const requiredSubparamsOthers = clearedPathExpr.match(SUBPARAMS_REQUIRED_REGEXP.OTHERS) || []; const requiredSubparamsLast = clearedPathExpr.match(SUBPARAMS_REQUIRED_REGEXP.LAST) || []; path = requiredSubparamsOthers.reduce((pattern, paramExpr)=>{ const paramIdx = pattern.indexOf(paramExpr) + paramExpr.length; const delimiter = pattern.substr(paramIdx, 1); const regExpr = this._getSubparamPattern(delimiter); return pattern.replace(paramExpr, regExpr); }, path); path = requiredSubparamsLast.reduce((pattern, rawParamExpr)=>{ const paramExpr = rawParamExpr.substr(1); const regExpr = '([^/?]+)'; return pattern.replace(paramExpr, regExpr); }, path); return path; } /** * Convert optional subparameters to capture sequences * * @param path The URL path (route definition). * @param optionalSubparamsOthers List of all subparam. expressions but last ones * @param optionalSubparamsLast List of last subparam. expressions * @return RegExp pattern. */ _replaceOptionalSubParametersInPath(path, optionalSubparamsOthers, optionalSubparamsLast) { path = optionalSubparamsOthers.reduce((pattern, paramExpr)=>{ const paramIdx = pattern.indexOf(paramExpr) + paramExpr.length; const delimiter = pattern.substr(paramIdx, 1); const paramPattern = this._getSubparamPattern(delimiter); const regExpr = paramPattern + '?'; return pattern.replace(paramExpr, regExpr); }, path); path = optionalSubparamsLast.reduce((pattern, rawParamExpr)=>{ const paramExpr = rawParamExpr.substr(1); const regExpr = '([^/?]+)?'; return pattern.replace(paramExpr, regExpr); }, path); return path; } /** * Compiles the path expression to a regular expression that can be used * for easier matching of URL paths against this route, and extracting the * path parameter values from the URL path. * * @param pathExpression The path expression to compile. * @return The compiled regular expression. */ _compileToRegExp(pathExpression) { const clearedPathExpr = pathExpression.replace(LOOSE_SLASHES_REGEXP, '').replace(CONTROL_CHARACTERS_REGEXP, '\\$&'); const requiredMatches = clearedPathExpr.match(PARAMS_REGEXP_REQUIRED) || []; const optionalMatches = clearedPathExpr.match(PARAMS_REGEXP_OPT) || []; const optionalSubparamsLast = clearedPathExpr.match(SUBPARAMS_OPT_REGEXP.LAST) || []; const optionalSubparamsOthers = clearedPathExpr.match(SUBPARAMS_OPT_REGEXP.OTHERS) || []; const optionalSubparams = [ ...optionalSubparamsOthers, ...optionalSubparamsLast ]; const optionalSubparamsCleanNames = optionalSubparams.map((paramExpr)=>{ return this._getClearParamName(paramExpr); }); const optionalParams = optionalMatches.filter((paramExpr)=>{ const param = this._getClearParamName(paramExpr); return !optionalSubparamsCleanNames.includes(param); }); if (!!requiredMatches.length && !!optionalParams.length) { const isCorrectParamOrder = this._checkParametersOrder(clearedPathExpr); if (!isCorrectParamOrder) { return PARAMS_NEVER_MATCH_REGEXP; } } // convert required parameters to capture sequences let pattern = requiredMatches.reduce((pattern, rawParamExpr)=>{ const paramExpr = ':' + this._getClearParamName(rawParamExpr); const regExpr = '([^/?#]+)'; return pattern.replace(paramExpr, regExpr); }, clearedPathExpr); pattern = this._replaceOptionalParametersInPath(pattern, optionalParams); pattern = this._replaceRequiredSubParametersInPath(pattern, clearedPathExpr); pattern = this._replaceOptionalSubParametersInPath(pattern, optionalSubparamsOthers, optionalSubparamsLast); // add path root pattern = '^\\/' + pattern; // add query parameters matcher const pairPattern = '[^=&;]*(?:=[^&;]*)?'; pattern += `(?:[\\?\\#](?:${pairPattern})(?:[&;]${pairPattern})*)?$`; return new RegExp(pattern); } /** * Parses the provided path and extract the in-path parameters. The method * decodes the parameters and returns them in a hash object. */ _getParameters(path) { if (!this._hasParameters) { return {}; } const parameterValues = path.match(this._matcher); if (!parameterValues) { return {}; } parameterValues.shift(); // remove the match on whole path, and other parts return this._extractParameters(parameterValues); } /** * Extract parameters from given path. */ _extractParameters(parameterValues) { const parameters = {}; const parametersCount = this._parameterNames.length; // Cycle for names and values from last to 0 for(let i = parametersCount - 1; i >= 0; i--){ const [rawName, rawValue] = [ this._parameterNames[i], parameterValues[i] ]; const cleanParamName = this._cleanOptParamName(rawName); const matchesName = cleanParamName.match(PARAMS_REGEXP_CORE_NAME); const currentCoreName = matchesName ? matchesName[0] : ''; if (currentCoreName) { let value; try { value = decodeURIComponent(rawValue); } catch { value = ''; } parameters[currentCoreName] = rawValue ? value : rawValue; } } return parameters; } /** * Returns optional param name without "?" * * @param paramName Full param name with "?" * @return Strict param name without "?" */ _cleanOptParamName(paramName) { return paramName.replace('?', ''); } /** * Checks if parameter is optional or not. * * @param paramName * @return return true if is optional, otherwise false */ _isParamOptional(paramName) { return /\?.+/.test(paramName); } /** * Extracts the parameter names from the provided path expression. * * @param pathExpression The path expression. * @return The names of the parameters defined in the provided * path expression. */ _getParameterNames(pathExpression) { const rawNames = pathExpression.match(PARAMS_REGEXP_UNIVERSAL) || []; return rawNames.map((rawParameterName)=>{ return rawParameterName.substring(1).replace('?', ''); }); } } //# sourceMappingURL=StaticRoute.js.map