UNPKG

ima

Version:

IMA.js framework for isomorphic javascript application

822 lines (693 loc) 24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * 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 * @type {RegExp} */ const CONTROL_CHARACTERS_REGEXP = /[\\.+*?^$[\](){}/'#]/g; /** * Regular expression used to match and remove the starting and trailing * forward slashes from a path expression or a URL path. * * @const * @type {RegExp} */ const LOOSE_SLASHES_REGEXP = /^\/|\/$/g; /** * Regular expression used to match the parameter names from a path expression. * * @const * @type {RegExp} */ const PARAMS_REGEXP_UNIVERSAL = /:\??([\w-]+)/g; /** * Regular expression used to match the required parameter names from a path expression. * * @const * @type {RegExp} */ const PARAMS_REGEXP_REQUIRED = /(?:^|\\\/):([a-z0-9]+)(?=\\\/|$)/gi; /** * Regular expression used to separate a camelCase parameter name * * @const * @type {RegExp} */ const PARAMS_REGEXP_CORE_NAME = /[a-z0-9]+/i; /** * Regular expression used to match start of parameter names from a path expression. * * @const * @type {String} */ const PARAMS_START_PATTERN = '(^|/|[_-])'; /** * Regular expression used to match end of parameter names from a path expression. * * @const * @type {String} */ 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 * @type {RegExp} */ const PARAMS_NEVER_MATCH_REGEXP = /$a/; /** * Regular expression used to match all main parameter names from a path expression. * * @const * @type {RegExp} */ 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 subparametres 'paramA' and 'paramB') * * @const * @type {Object<String, RegExp>} */ 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 * @type {Object<String, RegExp>} */ 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 * @type {RegExp} */ const PARAMS_REGEXP_OPT = /(?:^:\\\?([a-z0-9]+)(?=\\\/|$))|(?:(\\\/):\\\?([a-z0-9]+)(?=\\\/|$))/gi; // last part: |(?::\\\?([a-z0-9]+)(?=\\\/|$)) /** * Utility for representing and manipulating a single route in the router's * configuration. */ class Route { /** * Initializes the route. * * @param {string} name The unique name of this route, identifying it among * the rest of the routes in the application. * @param {string} 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 * {@code :parameterName}. * @param {string} controller The full name of Object Container alias * identifying the controller associated with this route. * @param {string} view The full name or Object Container alias identifying * the view class associated with this route. * @param {{ * onlyUpdate: ( * boolean| * function( * (string|function(new: Controller, ...*)), * (string|function( * new: React.Component, * Object<string, *>, * ?Object<string, *> * )) * ): boolean * )=, * autoScroll: boolean=, * allowSPA: boolean=, * documentView: ?AbstractDocumentView=, * managedRootView: ?function(new: React.Component)=, * viewAdapter: ?function(new: React.Component)= * }} options The route additional options. */ constructor(name, pathExpression, controller, view, options) { /** * The unique name of this route, identifying it among the rest of the * routes in the application. * * @type {string} */ this._name = name; /** * The original URL path expression from which this route was created. * * @type {string} */ this._pathExpression = pathExpression; /** * The full name of Object Container alias identifying the controller * associated with this route. * * @type {string} */ this._controller = controller; /** * The full name or Object Container alias identifying the view class * associated with this route. * * @type {React.Component} */ this._view = view; /** * The route additional options. * * @type {{ * onlyUpdate: ( * boolean| * function( * (string|function(new: Controller, ...*)), * (string|function( * new: React.Component, * Object<string, *>, * ?Object<string, *> * )) * ): boolean * ), * autoScroll: boolean, * allowSPA: boolean, * documentView: ?function(new: AbstractDocumentView), * managedRootView: ?function(new: React.Component), * viewAdapter: ?function(new: React.Component) * }} */ this._options = Object.assign({ onlyUpdate: false, autoScroll: true, allowSPA: true, documentView: null, managedRootView: null, viewAdapter: null }, options); /** * The path expression with the trailing slashes trimmed. * * @type {string} */ this._trimmedPathExpression = this._getTrimmedPath(pathExpression); /** * The names of the parameters in this route. * * @type {string[]} */ this._parameterNames = this._getParameterNames(pathExpression); /** * Set to {@code true} if this route contains parameters in its path. * * @type {boolean} */ 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. * * @type {RegExp} */ this._matcher = this._compileToRegExp(this._trimmedPathExpression); } /** * Creates the URL and query parts of a URL by substituting the route's * parameter placeholders by the provided parameter value. * * The extraneous parameters that do not match any of the route's * placeholders will be appended as the query string. * * @param {Object<string, (number|string)>=} [params={}] The route * parameter values. * @return {string} Path and, if necessary, query parts of the URL * representing this route with its parameters replaced by the * provided parameter values. */ toPath(params = {}) { let path = this._pathExpression; let query = []; for (let 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 { const pair = [paramName, params[paramName]]; query.push(pair.map(encodeURIComponent).join('=')); } } path = this._cleanUnusedOptionalParams(path); path = query.length ? path + '?' + query.join('&') : path; path = this._getTrimmedPath(path); return path; } /** * Returns the unique identifying name of this route. * * @return {string} The name of the route, identifying it. */ getName() { return this._name; } /** * Returns the full name of the controller to use when this route is * matched by the current URL, or an Object Container-registered alias of * the controller. * * @return {string} The name of alias of the controller. */ getController() { return this._controller; } /** * Returns the full name of the view class or an Object * Container-registered alias for the view class, representing the view to * use when this route is matched by the current URL. * * @return {string} The name or alias of the view class. */ getView() { return this._view; } /** * Return route additional options. * * @return {{ * onlyUpdate: ( * boolean| * function( * (string|function(new: Controller, ...*)), * (string|function( * new: React.Component, * Object<string, *>, * ?Object<string, *> * )) * ): boolean * ), * autoScroll: boolean, * allowSPA: boolean, * documentView: ?AbstractDocumentView, * managedRootView: ?function(new: React.Component), * viewAdapter: ?function(new: React.Component) * }} */ getOptions() { return this._options; } /** * Returns the path expression, which is the parametrized pattern matching * the URL paths matched by this route. * * @return {string} The path expression. */ getPathExpression() { return this._pathExpression; } /** * Tests whether the provided URL path matches this route. The provided * path may contain the query. * * @param {string} path The URL path. * @return {boolean} {@code true} if the provided path matches this route. */ matches(path) { let trimmedPath = this._getTrimmedPath(path); return this._matcher.test(trimmedPath); } /** * Extracts the parameter values from the provided path. The method * extracts both the in-path parameters and parses the query, allowing the * query parameters to override the in-path parameters. * * The method returns an empty hash object if the path does not match this * route. * * @param {string} path * @return {Object<string, ?string>} Map of parameter names to parameter * values. */ extractParameters(path) { let trimmedPath = this._getTrimmedPath(path); let parameters = this._getParameters(trimmedPath); let query = this._getQuery(trimmedPath); return Object.assign({}, parameters, query); } /** * Replace required parameter placeholder in path with parameter value. * * @param {string} path * @param {string} paramName * @param {string} paramValue * @return {string} New path. */ _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. * * @param {string} path * @param {string} paramName * @param {string} paramValue * @return {string} New path. */ _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. * * @param {string} path * @return {string} New 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. * * @param {string} path * @param {string} paramName * @return {boolean} */ _isOptionalParamInPath(path, paramName) { const paramRegexp = `${PARAMS_START_PATTERN}:\\?${paramName}(?:${PARAMS_END_PATTERN})`; let regexp = new RegExp(paramRegexp); return regexp.test(path); } /** * Returns true, if paramName is placed in path and it's required. * * @param {string} path * @param {string} paramName * @return {boolean} */ _isRequiredParamInPath(path, paramName) { let regexp = new RegExp(`:${paramName}`); return regexp.test(path); } /** * Extract clear parameter name, e.q. '?name' or 'name' * * @param {string} rawParam * @return {string} */ _getClearParamName(rawParam) { const regExpr = /\??[a-z0-9]+/i; const paramMatches = rawParam.match(regExpr); const param = paramMatches ? paramMatches[0] : ''; return param; } /** * Get pattern for subparameter. * * @param {string} delimeter Parameters delimeter * @return {string} */ _getSubparamPattern(delimeter) { const pattern = `([^${delimeter}?/]+)`; return pattern; } /** * Check if all optional params are below required ones * * @param {array<string>} allMainParams * @return {boolean} */ _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 parametres have correct order. * It means that required param cannot follow optional one. * * @param {string} clearedPathExpr The cleared URL path (removed first and last slash, ...). * @return {Bool} 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 {string} path The URL path. * @param {array<string>} optionalParams List of main optimal parameter expressions * @return {string} RegExp pattern. */ _replaceOptionalParametersInPath(path, optionalParams) { const pattern = optionalParams.reduce((path, paramExpr, idx, matches) => { const lastIdx = matches.length - 1; const hasSlash = paramExpr.substr(0, 2) === '\\/'; let separator = ''; if (idx === 0) { separator = '(?:' + (hasSlash ? '/' : ''); } else { separator = hasSlash ? '/?' : ''; } let regExpr = separator + `([^/?]+)?(?=/|$)?`; if (idx === lastIdx) { regExpr += ')?'; } return path.replace(paramExpr, regExpr); }, path); return pattern; } /** * Convert required subparameters to capture sequences * * @param {string} path The URL path (route definition). * @param {string} clearedPathExpr The original cleared URL path. * @return {string} 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 delimeter = pattern.substr(paramIdx, 1); const regExpr = this._getSubparamPattern(delimeter); 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 {string} path The URL path (route definition). * @param {array<string>} optionalSubparamsOthers List of all subparam. expressions but last ones * @param {array<string>} optionalSubparamsLast List of last subparam. expressions * @return {string} RegExp pattern. */ _replaceOptionalSubParametersInPath(path, optionalSubparamsOthers, optionalSubparamsLast) { path = optionalSubparamsOthers.reduce((pattern, paramExpr) => { const paramIdx = pattern.indexOf(paramExpr) + paramExpr.length; const delimeter = pattern.substr(paramIdx, 1); const paramPattern = this._getSubparamPattern(delimeter); 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 {string} pathExpression The path expression to compile. * @return {RegExp} 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 let 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. * * @param {string} path The URL path. * @return {Object<string, string>} The parsed path parameters. */ _getParameters(path) { if (!this._hasParameters) { return {}; } let 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. * * @param {string[]} parameterValues * @return {Object<string, ?string>} Params object. */ _extractParameters(parameterValues) { let parameters = {}; const parametersCount = this._parameterNames.length; // Cycle for names and values from last to 0 for (let i = parametersCount - 1; i >= 0; i--) { let [rawName, rawValue] = [this._parameterNames[i], parameterValues[i]]; let cleanParamName = this._cleanOptParamName(rawName); const matchesName = cleanParamName.match(PARAMS_REGEXP_CORE_NAME); const currentCoreName = matchesName ? matchesName[0] : ''; if (currentCoreName) { const value = this._decodeURIParameter(rawValue); parameters[currentCoreName] = value; } } return parameters; } /** * Decoding parameters. * * @param {string} parameterValue * @return {string} decodedValue */ _decodeURIParameter(parameterValue) { let decodedValue; if (parameterValue) { decodedValue = decodeURIComponent(parameterValue); } return decodedValue; } /** * Returns optional param name without "?" * * @param {string} paramName Full param name with "?" * @return {string} Strict param name without "?" */ _cleanOptParamName(paramName) { return paramName.replace('?', ''); } /** * Checks if parameter is optional or not. * * @param {string} paramName * @return {boolean} return true if is optional, otherwise false */ _isParamOptional(paramName) { return /\?.+/.test(paramName); } /** * Extracts and decodes the query parameters from the provided URL path and * query. * * @param {string} path The URL path, including the optional query string * (if any). * @return {Object<string, ?string>} Parsed query parameters. */ _getQuery(path) { let query = {}; let queryStart = path.indexOf('?'); let hasQuery = queryStart > -1 && queryStart !== path.length - 1; if (hasQuery) { let pairs = path.substring(queryStart + 1).split(/[&;]/); for (let parameterPair of pairs) { let pair = parameterPair.split('='); query[decodeURIComponent(pair[0])] = pair.length > 1 ? decodeURIComponent(pair[1]) : true; } } return query; } /** * Trims the trailing forward slash from the provided URL path. * * @param {string} path The path to trim. * @return {string} Trimmed path. */ _getTrimmedPath(path) { return `/${path.replace(LOOSE_SLASHES_REGEXP, '')}`; } /** * Extracts the parameter names from the provided path expression. * * @param {string} pathExpression The path expression. * @return {string[]} The names of the parameters defined in the provided * path expression. */ _getParameterNames(pathExpression) { let rawNames = pathExpression.match(PARAMS_REGEXP_UNIVERSAL) || []; return rawNames.map(rawParameterName => { return rawParameterName.substring(1).replace('?', ''); }); } } exports.default = Route; typeof $IMA !== 'undefined' && $IMA !== null && $IMA.Loader && $IMA.Loader.register('ima/router/Route', [], function (_export, _context) { 'use strict'; return { setters: [], execute: function () { _export('default', exports.default); } }; });