UNPKG

@uirouter/core

Version:

UI-Router Core: Framework agnostic, State-based routing for JavaScript Single Page Apps

533 lines 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UrlMatcher = void 0; var common_1 = require("../common/common"); var hof_1 = require("../common/hof"); var predicates_1 = require("../common/predicates"); var param_1 = require("../params/param"); var strings_1 = require("../common/strings"); var common_2 = require("../common"); function quoteRegExp(str, param) { var surroundPattern = ['', ''], result = str.replace(/[\\\[\]\^$*+?.()|{}]/g, '\\$&'); if (!param) return result; switch (param.squash) { case false: surroundPattern = ['(', ')' + (param.isOptional ? '?' : '')]; break; case true: result = result.replace(/\/$/, ''); surroundPattern = ['(?:/(', ')|/)?']; break; default: surroundPattern = ["(" + param.squash + "|", ')?']; break; } return result + surroundPattern[0] + param.type.pattern.source + surroundPattern[1]; } var memoizeTo = function (obj, _prop, fn) { return (obj[_prop] = obj[_prop] || fn()); }; var splitOnSlash = strings_1.splitOnDelim('/'); var defaultConfig = { state: { params: {} }, strict: true, caseInsensitive: true, decodeParams: true, }; /** * Matches URLs against patterns. * * Matches URLs against patterns and extracts named parameters from the path or the search * part of the URL. * * A URL pattern consists of a path pattern, optionally followed by '?' and a list of search (query) * parameters. Multiple search parameter names are separated by '&'. Search parameters * do not influence whether or not a URL is matched, but their values are passed through into * the matched parameters returned by [[UrlMatcher.exec]]. * * - *Path parameters* are defined using curly brace placeholders (`/somepath/{param}`) * or colon placeholders (`/somePath/:param`). * * - *A parameter RegExp* may be defined for a param after a colon * (`/somePath/{param:[a-zA-Z0-9]+}`) in a curly brace placeholder. * The regexp must match for the url to be matched. * Should the regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. * * Note: a RegExp parameter will encode its value using either [[ParamTypes.path]] or [[ParamTypes.query]]. * * - *Custom parameter types* may also be specified after a colon (`/somePath/{param:int}`) in curly brace parameters. * See [[UrlMatcherFactory.type]] for more information. * * - *Catch-all parameters* are defined using an asterisk placeholder (`/somepath/*catchallparam`). * A catch-all * parameter value will contain the remainder of the URL. * * --- * * Parameter names may contain only word characters (latin letters, digits, and underscore) and * must be unique within the pattern (across both path and search parameters). * A path parameter matches any number of characters other than '/'. For catch-all * placeholders the path parameter matches any number of characters. * * Examples: * * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for * trailing slashes, and patterns have to match the entire path, not just a prefix. * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or * '/user/bob/details'. The second path segment will be captured as the parameter 'id'. * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax. * * `'/user/{id:[^/]*}'` - Same as the previous example. * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id * parameter consists of 1 to 8 hex digits. * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the * path into the parameter 'path'. * * `'/files/*path'` - ditto. * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined * in the built-in `date` ParamType matches `2014-11-12`) and provides a Date object in $stateParams.start * */ var UrlMatcher = /** @class */ (function () { /** * @param pattern The pattern to compile into a matcher. * @param paramTypes The [[ParamTypes]] registry * @param paramFactory A [[ParamFactory]] object * @param config A [[UrlMatcherCompileConfig]] configuration object */ function UrlMatcher(pattern, paramTypes, paramFactory, config) { var _this = this; /** @internal */ this._cache = { path: [this] }; /** @internal */ this._children = []; /** @internal */ this._params = []; /** @internal */ this._segments = []; /** @internal */ this._compiled = []; this.config = config = common_2.defaults(config, defaultConfig); this.pattern = pattern; // Find all placeholders and create a compiled pattern, using either classic or curly syntax: // '*' name // ':' name // '{' name '}' // '{' name ':' regexp '}' // The regular expression is somewhat complicated due to the need to allow curly braces // inside the regular expression. The placeholder regexp breaks down as follows: // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case) // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either // [^{}\\]+ - anything other than curly braces or backslash // \\. - a backslash escape // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g; var searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g; var patterns = []; var last = 0; var matchArray; var checkParamErrors = function (id) { if (!UrlMatcher.nameValidator.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); if (common_1.find(_this._params, hof_1.propEq('id', id))) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); }; // Split into static segments separated by path parameter placeholders. // The number of segments is always 1 more than the number of parameters. var matchDetails = function (m, isSearch) { // IE[78] returns '' for unmatched groups instead of null var id = m[2] || m[3]; var regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '[\\s\\S]*' : null); var makeRegexpType = function (str) { return common_1.inherit(paramTypes.type(isSearch ? 'query' : 'path'), { pattern: new RegExp(str, _this.config.caseInsensitive ? 'i' : undefined), }); }; return { id: id, regexp: regexp, segment: pattern.substring(last, m.index), type: !regexp ? null : paramTypes.type(regexp) || makeRegexpType(regexp), }; }; var details; var segment; // tslint:disable-next-line:no-conditional-assignment while ((matchArray = placeholder.exec(pattern))) { details = matchDetails(matchArray, false); if (details.segment.indexOf('?') >= 0) break; // we're into the search part checkParamErrors(details.id); this._params.push(paramFactory.fromPath(details.id, details.type, config.state)); this._segments.push(details.segment); patterns.push([details.segment, common_1.tail(this._params)]); last = placeholder.lastIndex; } segment = pattern.substring(last); // Find any search parameter names and remove them from the last segment var i = segment.indexOf('?'); if (i >= 0) { var search = segment.substring(i); segment = segment.substring(0, i); if (search.length > 0) { last = 0; // tslint:disable-next-line:no-conditional-assignment while ((matchArray = searchPlaceholder.exec(search))) { details = matchDetails(matchArray, true); checkParamErrors(details.id); this._params.push(paramFactory.fromSearch(details.id, details.type, config.state)); last = placeholder.lastIndex; // check if ?& } } } this._segments.push(segment); this._compiled = patterns.map(function (_pattern) { return quoteRegExp.apply(null, _pattern); }).concat(quoteRegExp(segment)); } /** @internal */ UrlMatcher.encodeDashes = function (str) { // Replace dashes with encoded "\-" return encodeURIComponent(str).replace(/-/g, function (c) { return "%5C%" + c.charCodeAt(0).toString(16).toUpperCase(); }); }; /** @internal Given a matcher, return an array with the matcher's path segments and path params, in order */ UrlMatcher.pathSegmentsAndParams = function (matcher) { var staticSegments = matcher._segments; var pathParams = matcher._params.filter(function (p) { return p.location === param_1.DefType.PATH; }); return common_1.arrayTuples(staticSegments, pathParams.concat(undefined)) .reduce(common_1.unnestR, []) .filter(function (x) { return x !== '' && predicates_1.isDefined(x); }); }; /** @internal Given a matcher, return an array with the matcher's query params */ UrlMatcher.queryParams = function (matcher) { return matcher._params.filter(function (p) { return p.location === param_1.DefType.SEARCH; }); }; /** * Compare two UrlMatchers * * This comparison function converts a UrlMatcher into static and dynamic path segments. * Each static path segment is a static string between a path separator (slash character). * Each dynamic segment is a path parameter. * * The comparison function sorts static segments before dynamic ones. */ UrlMatcher.compare = function (a, b) { /** * Turn a UrlMatcher and all its parent matchers into an array * of slash literals '/', string literals, and Param objects * * This example matcher matches strings like "/foo/:param/tail": * var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail")); * var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ] * * Caches the result as `matcher._cache.segments` */ var segments = function (matcher) { return (matcher._cache.segments = matcher._cache.segments || matcher._cache.path .map(UrlMatcher.pathSegmentsAndParams) .reduce(common_1.unnestR, []) .reduce(strings_1.joinNeighborsR, []) .map(function (x) { return (predicates_1.isString(x) ? splitOnSlash(x) : x); }) .reduce(common_1.unnestR, [])); }; /** * Gets the sort weight for each segment of a UrlMatcher * * Caches the result as `matcher._cache.weights` */ var weights = function (matcher) { return (matcher._cache.weights = matcher._cache.weights || segments(matcher).map(function (segment) { // Sort slashes first, then static strings, the Params if (segment === '/') return 1; if (predicates_1.isString(segment)) return 2; if (segment instanceof param_1.Param) return 3; })); }; /** * Pads shorter array in-place (mutates) */ var padArrays = function (l, r, padVal) { var len = Math.max(l.length, r.length); while (l.length < len) l.push(padVal); while (r.length < len) r.push(padVal); }; var weightsA = weights(a), weightsB = weights(b); padArrays(weightsA, weightsB, 0); var _pairs = common_1.arrayTuples(weightsA, weightsB); var cmp, i; for (i = 0; i < _pairs.length; i++) { cmp = _pairs[i][0] - _pairs[i][1]; if (cmp !== 0) return cmp; } return 0; }; /** * Creates a new concatenated UrlMatcher * * Builds a new UrlMatcher by appending another UrlMatcher to this one. * * @param url A `UrlMatcher` instance to append as a child of the current `UrlMatcher`. */ UrlMatcher.prototype.append = function (url) { this._children.push(url); url._cache = { path: this._cache.path.concat(url), parent: this, pattern: null, }; return url; }; /** @internal */ UrlMatcher.prototype.isRoot = function () { return this._cache.path[0] === this; }; /** Returns the input pattern string */ UrlMatcher.prototype.toString = function () { return this.pattern; }; UrlMatcher.prototype._getDecodedParamValue = function (value, param) { if (predicates_1.isDefined(value)) { if (this.config.decodeParams && !param.type.raw) { if (predicates_1.isArray(value)) { value = value.map(function (paramValue) { return decodeURIComponent(paramValue); }); } else { value = decodeURIComponent(value); } } value = param.type.decode(value); } return param.value(value); }; /** * Tests the specified url/path against this matcher. * * Tests if the given url matches this matcher's pattern, and returns an object containing the captured * parameter values. Returns null if the path does not match. * * The returned object contains the values * of any search parameters that are mentioned in the pattern, but their value may be null if * they are not present in `search`. This means that search parameters are always treated * as optional. * * #### Example: * ```js * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', { * x: '1', q: 'hello' * }); * // returns { id: 'bob', q: 'hello', r: null } * ``` * * @param path The URL path to match, e.g. `$location.path()`. * @param search URL search parameters, e.g. `$location.search()`. * @param hash URL hash e.g. `$location.hash()`. * @param options * * @returns The captured parameter values. */ UrlMatcher.prototype.exec = function (path, search, hash, options) { var _this = this; if (search === void 0) { search = {}; } if (options === void 0) { options = {}; } var match = memoizeTo(this._cache, 'pattern', function () { return new RegExp([ '^', common_1.unnest(_this._cache.path.map(hof_1.prop('_compiled'))).join(''), _this.config.strict === false ? '/?' : '', '$', ].join(''), _this.config.caseInsensitive ? 'i' : undefined); }).exec(path); if (!match) return null; // options = defaults(options, { isolate: false }); var allParams = this.parameters(), pathParams = allParams.filter(function (param) { return !param.isSearch(); }), searchParams = allParams.filter(function (param) { return param.isSearch(); }), nPathSegments = this._cache.path.map(function (urlm) { return urlm._segments.length - 1; }).reduce(function (a, x) { return a + x; }), values = {}; if (nPathSegments !== match.length - 1) throw new Error("Unbalanced capture group in route '" + this.pattern + "'"); function decodePathArray(paramVal) { var reverseString = function (str) { return str.split('').reverse().join(''); }; var unquoteDashes = function (str) { return str.replace(/\\-/g, '-'); }; var split = reverseString(paramVal).split(/-(?!\\)/); var allReversed = common_1.map(split, reverseString); return common_1.map(allReversed, unquoteDashes).reverse(); } for (var i = 0; i < nPathSegments; i++) { var param = pathParams[i]; var value = match[i + 1]; // if the param value matches a pre-replace pair, replace the value before decoding. for (var j = 0; j < param.replace.length; j++) { if (param.replace[j].from === value) value = param.replace[j].to; } if (value && param.array === true) value = decodePathArray(value); values[param.id] = this._getDecodedParamValue(value, param); } searchParams.forEach(function (param) { var value = search[param.id]; for (var j = 0; j < param.replace.length; j++) { if (param.replace[j].from === value) value = param.replace[j].to; } values[param.id] = _this._getDecodedParamValue(value, param); }); if (hash) values['#'] = hash; return values; }; /** * @internal * Returns all the [[Param]] objects of all path and search parameters of this pattern in order of appearance. * * @returns {Array.<Param>} An array of [[Param]] objects. Must be treated as read-only. If the * pattern has no parameters, an empty array is returned. */ UrlMatcher.prototype.parameters = function (opts) { if (opts === void 0) { opts = {}; } if (opts.inherit === false) return this._params; return common_1.unnest(this._cache.path.map(function (matcher) { return matcher._params; })); }; /** * @internal * Returns a single parameter from this UrlMatcher by id * * @param id * @param opts * @returns {T|Param|any|boolean|UrlMatcher|null} */ UrlMatcher.prototype.parameter = function (id, opts) { var _this = this; if (opts === void 0) { opts = {}; } var findParam = function () { for (var _i = 0, _a = _this._params; _i < _a.length; _i++) { var param = _a[_i]; if (param.id === id) return param; } }; var parent = this._cache.parent; return findParam() || (opts.inherit !== false && parent && parent.parameter(id, opts)) || null; }; /** * Validates the input parameter values against this UrlMatcher * * Checks an object hash of parameters to validate their correctness according to the parameter * types of this `UrlMatcher`. * * @param params The object hash of parameters to validate. * @returns Returns `true` if `params` validates, otherwise `false`. */ UrlMatcher.prototype.validates = function (params) { var validParamVal = function (param, val) { return !param || param.validates(val); }; params = params || {}; // I'm not sure why this checks only the param keys passed in, and not all the params known to the matcher var paramSchema = this.parameters().filter(function (paramDef) { return params.hasOwnProperty(paramDef.id); }); return paramSchema.map(function (paramDef) { return validParamVal(paramDef, params[paramDef.id]); }).reduce(common_1.allTrueR, true); }; /** * Given a set of parameter values, creates a URL from this UrlMatcher. * * Creates a URL that matches this pattern by substituting the specified values * for the path and search parameters. * * #### Example: * ```js * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' }); * // returns '/user/bob?q=yes' * ``` * * @param values the values to substitute for the parameters in this pattern. * @returns the formatted URL (path and optionally search part). */ UrlMatcher.prototype.format = function (values) { if (values === void 0) { values = {}; } // Build the full path of UrlMatchers (including all parent UrlMatchers) var urlMatchers = this._cache.path; // Extract all the static segments and Params (processed as ParamDetails) // into an ordered array var pathSegmentsAndParams = urlMatchers .map(UrlMatcher.pathSegmentsAndParams) .reduce(common_1.unnestR, []) .map(function (x) { return (predicates_1.isString(x) ? x : getDetails(x)); }); // Extract the query params into a separate array var queryParams = urlMatchers .map(UrlMatcher.queryParams) .reduce(common_1.unnestR, []) .map(getDetails); var isInvalid = function (param) { return param.isValid === false; }; if (pathSegmentsAndParams.concat(queryParams).filter(isInvalid).length) { return null; } /** * Given a Param, applies the parameter value, then returns detailed information about it */ function getDetails(param) { // Normalize to typed value var value = param.value(values[param.id]); var isValid = param.validates(value); var isDefaultValue = param.isDefaultValue(value); // Check if we're in squash mode for the parameter var squash = isDefaultValue ? param.squash : false; // Allow the Parameter's Type to encode the value var encoded = param.type.encode(value); return { param: param, value: value, isValid: isValid, isDefaultValue: isDefaultValue, squash: squash, encoded: encoded }; } // Build up the path-portion from the list of static segments and parameters var pathString = pathSegmentsAndParams.reduce(function (acc, x) { // The element is a static segment (a raw string); just append it if (predicates_1.isString(x)) return acc + x; // Otherwise, it's a ParamDetails. var squash = x.squash, encoded = x.encoded, param = x.param; // If squash is === true, try to remove a slash from the path if (squash === true) return acc.match(/\/$/) ? acc.slice(0, -1) : acc; // If squash is a string, use the string for the param value if (predicates_1.isString(squash)) return acc + squash; if (squash !== false) return acc; // ? if (encoded == null) return acc; // If this parameter value is an array, encode the value using encodeDashes if (predicates_1.isArray(encoded)) return acc + common_1.map(encoded, UrlMatcher.encodeDashes).join('-'); // If the parameter type is "raw", then do not encodeURIComponent if (param.raw) return acc + encoded; // Encode the value return acc + encodeURIComponent(encoded); }, ''); // Build the query string by applying parameter values (array or regular) // then mapping to key=value, then flattening and joining using "&" var queryString = queryParams .map(function (paramDetails) { var param = paramDetails.param, squash = paramDetails.squash, encoded = paramDetails.encoded, isDefaultValue = paramDetails.isDefaultValue; if (encoded == null || (isDefaultValue && squash !== false)) return; if (!predicates_1.isArray(encoded)) encoded = [encoded]; if (encoded.length === 0) return; if (!param.raw) encoded = common_1.map(encoded, encodeURIComponent); return encoded.map(function (val) { return param.id + "=" + val; }); }) .filter(common_1.identity) .reduce(common_1.unnestR, []) .join('&'); // Concat the pathstring with the queryString (if exists) and the hashString (if exists) return pathString + (queryString ? "?" + queryString : '') + (values['#'] ? '#' + values['#'] : ''); }; /** @internal */ UrlMatcher.nameValidator = /^\w+([-.]+\w+)*(?:\[\])?$/; return UrlMatcher; }()); exports.UrlMatcher = UrlMatcher; //# sourceMappingURL=urlMatcher.js.map