@uirouter/core
Version:
UI-Router Core: Framework agnostic, State-based routing for JavaScript Single Page Apps
533 lines • 25.2 kB
JavaScript
"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