UNPKG

25.2 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.UrlMatcher = void 0;
4var common_1 = require("../common/common");
5var hof_1 = require("../common/hof");
6var predicates_1 = require("../common/predicates");
7var param_1 = require("../params/param");
8var strings_1 = require("../common/strings");
9var common_2 = require("../common");
10function quoteRegExp(str, param) {
11 var surroundPattern = ['', ''], result = str.replace(/[\\\[\]\^$*+?.()|{}]/g, '\\$&');
12 if (!param)
13 return result;
14 switch (param.squash) {
15 case false:
16 surroundPattern = ['(', ')' + (param.isOptional ? '?' : '')];
17 break;
18 case true:
19 result = result.replace(/\/$/, '');
20 surroundPattern = ['(?:/(', ')|/)?'];
21 break;
22 default:
23 surroundPattern = ["(" + param.squash + "|", ')?'];
24 break;
25 }
26 return result + surroundPattern[0] + param.type.pattern.source + surroundPattern[1];
27}
28var memoizeTo = function (obj, _prop, fn) { return (obj[_prop] = obj[_prop] || fn()); };
29var splitOnSlash = strings_1.splitOnDelim('/');
30var defaultConfig = {
31 state: { params: {} },
32 strict: true,
33 caseInsensitive: true,
34 decodeParams: true,
35};
36/**
37 * Matches URLs against patterns.
38 *
39 * Matches URLs against patterns and extracts named parameters from the path or the search
40 * part of the URL.
41 *
42 * A URL pattern consists of a path pattern, optionally followed by '?' and a list of search (query)
43 * parameters. Multiple search parameter names are separated by '&'. Search parameters
44 * do not influence whether or not a URL is matched, but their values are passed through into
45 * the matched parameters returned by [[UrlMatcher.exec]].
46 *
47 * - *Path parameters* are defined using curly brace placeholders (`/somepath/{param}`)
48 * or colon placeholders (`/somePath/:param`).
49 *
50 * - *A parameter RegExp* may be defined for a param after a colon
51 * (`/somePath/{param:[a-zA-Z0-9]+}`) in a curly brace placeholder.
52 * The regexp must match for the url to be matched.
53 * Should the regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash.
54 *
55 * Note: a RegExp parameter will encode its value using either [[ParamTypes.path]] or [[ParamTypes.query]].
56 *
57 * - *Custom parameter types* may also be specified after a colon (`/somePath/{param:int}`) in curly brace parameters.
58 * See [[UrlMatcherFactory.type]] for more information.
59 *
60 * - *Catch-all parameters* are defined using an asterisk placeholder (`/somepath/*catchallparam`).
61 * A catch-all * parameter value will contain the remainder of the URL.
62 *
63 * ---
64 *
65 * Parameter names may contain only word characters (latin letters, digits, and underscore) and
66 * must be unique within the pattern (across both path and search parameters).
67 * A path parameter matches any number of characters other than '/'. For catch-all
68 * placeholders the path parameter matches any number of characters.
69 *
70 * Examples:
71 *
72 * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for
73 * trailing slashes, and patterns have to match the entire path, not just a prefix.
74 * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or
75 * '/user/bob/details'. The second path segment will be captured as the parameter 'id'.
76 * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax.
77 * * `'/user/{id:[^/]*}'` - Same as the previous example.
78 * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id
79 * parameter consists of 1 to 8 hex digits.
80 * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the
81 * path into the parameter 'path'.
82 * * `'/files/*path'` - ditto.
83 * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined
84 * in the built-in `date` ParamType matches `2014-11-12`) and provides a Date object in $stateParams.start
85 *
86 */
87var UrlMatcher = /** @class */ (function () {
88 /**
89 * @param pattern The pattern to compile into a matcher.
90 * @param paramTypes The [[ParamTypes]] registry
91 * @param paramFactory A [[ParamFactory]] object
92 * @param config A [[UrlMatcherCompileConfig]] configuration object
93 */
94 function UrlMatcher(pattern, paramTypes, paramFactory, config) {
95 var _this = this;
96 /** @internal */
97 this._cache = { path: [this] };
98 /** @internal */
99 this._children = [];
100 /** @internal */
101 this._params = [];
102 /** @internal */
103 this._segments = [];
104 /** @internal */
105 this._compiled = [];
106 this.config = config = common_2.defaults(config, defaultConfig);
107 this.pattern = pattern;
108 // Find all placeholders and create a compiled pattern, using either classic or curly syntax:
109 // '*' name
110 // ':' name
111 // '{' name '}'
112 // '{' name ':' regexp '}'
113 // The regular expression is somewhat complicated due to the need to allow curly braces
114 // inside the regular expression. The placeholder regexp breaks down as follows:
115 // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case)
116 // \{([\w\[\]]+)(?:\:\s*( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
117 // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either
118 // [^{}\\]+ - anything other than curly braces or backslash
119 // \\. - a backslash escape
120 // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
121 var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g;
122 var searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g;
123 var patterns = [];
124 var last = 0;
125 var matchArray;
126 var checkParamErrors = function (id) {
127 if (!UrlMatcher.nameValidator.test(id))
128 throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
129 if (common_1.find(_this._params, hof_1.propEq('id', id)))
130 throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
131 };
132 // Split into static segments separated by path parameter placeholders.
133 // The number of segments is always 1 more than the number of parameters.
134 var matchDetails = function (m, isSearch) {
135 // IE[78] returns '' for unmatched groups instead of null
136 var id = m[2] || m[3];
137 var regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '[\\s\\S]*' : null);
138 var makeRegexpType = function (str) {
139 return common_1.inherit(paramTypes.type(isSearch ? 'query' : 'path'), {
140 pattern: new RegExp(str, _this.config.caseInsensitive ? 'i' : undefined),
141 });
142 };
143 return {
144 id: id,
145 regexp: regexp,
146 segment: pattern.substring(last, m.index),
147 type: !regexp ? null : paramTypes.type(regexp) || makeRegexpType(regexp),
148 };
149 };
150 var details;
151 var segment;
152 // tslint:disable-next-line:no-conditional-assignment
153 while ((matchArray = placeholder.exec(pattern))) {
154 details = matchDetails(matchArray, false);
155 if (details.segment.indexOf('?') >= 0)
156 break; // we're into the search part
157 checkParamErrors(details.id);
158 this._params.push(paramFactory.fromPath(details.id, details.type, config.state));
159 this._segments.push(details.segment);
160 patterns.push([details.segment, common_1.tail(this._params)]);
161 last = placeholder.lastIndex;
162 }
163 segment = pattern.substring(last);
164 // Find any search parameter names and remove them from the last segment
165 var i = segment.indexOf('?');
166 if (i >= 0) {
167 var search = segment.substring(i);
168 segment = segment.substring(0, i);
169 if (search.length > 0) {
170 last = 0;
171 // tslint:disable-next-line:no-conditional-assignment
172 while ((matchArray = searchPlaceholder.exec(search))) {
173 details = matchDetails(matchArray, true);
174 checkParamErrors(details.id);
175 this._params.push(paramFactory.fromSearch(details.id, details.type, config.state));
176 last = placeholder.lastIndex;
177 // check if ?&
178 }
179 }
180 }
181 this._segments.push(segment);
182 this._compiled = patterns.map(function (_pattern) { return quoteRegExp.apply(null, _pattern); }).concat(quoteRegExp(segment));
183 }
184 /** @internal */
185 UrlMatcher.encodeDashes = function (str) {
186 // Replace dashes with encoded "\-"
187 return encodeURIComponent(str).replace(/-/g, function (c) { return "%5C%" + c.charCodeAt(0).toString(16).toUpperCase(); });
188 };
189 /** @internal Given a matcher, return an array with the matcher's path segments and path params, in order */
190 UrlMatcher.pathSegmentsAndParams = function (matcher) {
191 var staticSegments = matcher._segments;
192 var pathParams = matcher._params.filter(function (p) { return p.location === param_1.DefType.PATH; });
193 return common_1.arrayTuples(staticSegments, pathParams.concat(undefined))
194 .reduce(common_1.unnestR, [])
195 .filter(function (x) { return x !== '' && predicates_1.isDefined(x); });
196 };
197 /** @internal Given a matcher, return an array with the matcher's query params */
198 UrlMatcher.queryParams = function (matcher) {
199 return matcher._params.filter(function (p) { return p.location === param_1.DefType.SEARCH; });
200 };
201 /**
202 * Compare two UrlMatchers
203 *
204 * This comparison function converts a UrlMatcher into static and dynamic path segments.
205 * Each static path segment is a static string between a path separator (slash character).
206 * Each dynamic segment is a path parameter.
207 *
208 * The comparison function sorts static segments before dynamic ones.
209 */
210 UrlMatcher.compare = function (a, b) {
211 /**
212 * Turn a UrlMatcher and all its parent matchers into an array
213 * of slash literals '/', string literals, and Param objects
214 *
215 * This example matcher matches strings like "/foo/:param/tail":
216 * var matcher = $umf.compile("/foo").append($umf.compile("/:param")).append($umf.compile("/")).append($umf.compile("tail"));
217 * var result = segments(matcher); // [ '/', 'foo', '/', Param, '/', 'tail' ]
218 *
219 * Caches the result as `matcher._cache.segments`
220 */
221 var segments = function (matcher) {
222 return (matcher._cache.segments =
223 matcher._cache.segments ||
224 matcher._cache.path
225 .map(UrlMatcher.pathSegmentsAndParams)
226 .reduce(common_1.unnestR, [])
227 .reduce(strings_1.joinNeighborsR, [])
228 .map(function (x) { return (predicates_1.isString(x) ? splitOnSlash(x) : x); })
229 .reduce(common_1.unnestR, []));
230 };
231 /**
232 * Gets the sort weight for each segment of a UrlMatcher
233 *
234 * Caches the result as `matcher._cache.weights`
235 */
236 var weights = function (matcher) {
237 return (matcher._cache.weights =
238 matcher._cache.weights ||
239 segments(matcher).map(function (segment) {
240 // Sort slashes first, then static strings, the Params
241 if (segment === '/')
242 return 1;
243 if (predicates_1.isString(segment))
244 return 2;
245 if (segment instanceof param_1.Param)
246 return 3;
247 }));
248 };
249 /**
250 * Pads shorter array in-place (mutates)
251 */
252 var padArrays = function (l, r, padVal) {
253 var len = Math.max(l.length, r.length);
254 while (l.length < len)
255 l.push(padVal);
256 while (r.length < len)
257 r.push(padVal);
258 };
259 var weightsA = weights(a), weightsB = weights(b);
260 padArrays(weightsA, weightsB, 0);
261 var _pairs = common_1.arrayTuples(weightsA, weightsB);
262 var cmp, i;
263 for (i = 0; i < _pairs.length; i++) {
264 cmp = _pairs[i][0] - _pairs[i][1];
265 if (cmp !== 0)
266 return cmp;
267 }
268 return 0;
269 };
270 /**
271 * Creates a new concatenated UrlMatcher
272 *
273 * Builds a new UrlMatcher by appending another UrlMatcher to this one.
274 *
275 * @param url A `UrlMatcher` instance to append as a child of the current `UrlMatcher`.
276 */
277 UrlMatcher.prototype.append = function (url) {
278 this._children.push(url);
279 url._cache = {
280 path: this._cache.path.concat(url),
281 parent: this,
282 pattern: null,
283 };
284 return url;
285 };
286 /** @internal */
287 UrlMatcher.prototype.isRoot = function () {
288 return this._cache.path[0] === this;
289 };
290 /** Returns the input pattern string */
291 UrlMatcher.prototype.toString = function () {
292 return this.pattern;
293 };
294 UrlMatcher.prototype._getDecodedParamValue = function (value, param) {
295 if (predicates_1.isDefined(value)) {
296 if (this.config.decodeParams && !param.type.raw) {
297 if (predicates_1.isArray(value)) {
298 value = value.map(function (paramValue) { return decodeURIComponent(paramValue); });
299 }
300 else {
301 value = decodeURIComponent(value);
302 }
303 }
304 value = param.type.decode(value);
305 }
306 return param.value(value);
307 };
308 /**
309 * Tests the specified url/path against this matcher.
310 *
311 * Tests if the given url matches this matcher's pattern, and returns an object containing the captured
312 * parameter values. Returns null if the path does not match.
313 *
314 * The returned object contains the values
315 * of any search parameters that are mentioned in the pattern, but their value may be null if
316 * they are not present in `search`. This means that search parameters are always treated
317 * as optional.
318 *
319 * #### Example:
320 * ```js
321 * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
322 * x: '1', q: 'hello'
323 * });
324 * // returns { id: 'bob', q: 'hello', r: null }
325 * ```
326 *
327 * @param path The URL path to match, e.g. `$location.path()`.
328 * @param search URL search parameters, e.g. `$location.search()`.
329 * @param hash URL hash e.g. `$location.hash()`.
330 * @param options
331 *
332 * @returns The captured parameter values.
333 */
334 UrlMatcher.prototype.exec = function (path, search, hash, options) {
335 var _this = this;
336 if (search === void 0) { search = {}; }
337 if (options === void 0) { options = {}; }
338 var match = memoizeTo(this._cache, 'pattern', function () {
339 return new RegExp([
340 '^',
341 common_1.unnest(_this._cache.path.map(hof_1.prop('_compiled'))).join(''),
342 _this.config.strict === false ? '/?' : '',
343 '$',
344 ].join(''), _this.config.caseInsensitive ? 'i' : undefined);
345 }).exec(path);
346 if (!match)
347 return null;
348 // options = defaults(options, { isolate: false });
349 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 = {};
350 if (nPathSegments !== match.length - 1)
351 throw new Error("Unbalanced capture group in route '" + this.pattern + "'");
352 function decodePathArray(paramVal) {
353 var reverseString = function (str) { return str.split('').reverse().join(''); };
354 var unquoteDashes = function (str) { return str.replace(/\\-/g, '-'); };
355 var split = reverseString(paramVal).split(/-(?!\\)/);
356 var allReversed = common_1.map(split, reverseString);
357 return common_1.map(allReversed, unquoteDashes).reverse();
358 }
359 for (var i = 0; i < nPathSegments; i++) {
360 var param = pathParams[i];
361 var value = match[i + 1];
362 // if the param value matches a pre-replace pair, replace the value before decoding.
363 for (var j = 0; j < param.replace.length; j++) {
364 if (param.replace[j].from === value)
365 value = param.replace[j].to;
366 }
367 if (value && param.array === true)
368 value = decodePathArray(value);
369 values[param.id] = this._getDecodedParamValue(value, param);
370 }
371 searchParams.forEach(function (param) {
372 var value = search[param.id];
373 for (var j = 0; j < param.replace.length; j++) {
374 if (param.replace[j].from === value)
375 value = param.replace[j].to;
376 }
377 values[param.id] = _this._getDecodedParamValue(value, param);
378 });
379 if (hash)
380 values['#'] = hash;
381 return values;
382 };
383 /**
384 * @internal
385 * Returns all the [[Param]] objects of all path and search parameters of this pattern in order of appearance.
386 *
387 * @returns {Array.<Param>} An array of [[Param]] objects. Must be treated as read-only. If the
388 * pattern has no parameters, an empty array is returned.
389 */
390 UrlMatcher.prototype.parameters = function (opts) {
391 if (opts === void 0) { opts = {}; }
392 if (opts.inherit === false)
393 return this._params;
394 return common_1.unnest(this._cache.path.map(function (matcher) { return matcher._params; }));
395 };
396 /**
397 * @internal
398 * Returns a single parameter from this UrlMatcher by id
399 *
400 * @param id
401 * @param opts
402 * @returns {T|Param|any|boolean|UrlMatcher|null}
403 */
404 UrlMatcher.prototype.parameter = function (id, opts) {
405 var _this = this;
406 if (opts === void 0) { opts = {}; }
407 var findParam = function () {
408 for (var _i = 0, _a = _this._params; _i < _a.length; _i++) {
409 var param = _a[_i];
410 if (param.id === id)
411 return param;
412 }
413 };
414 var parent = this._cache.parent;
415 return findParam() || (opts.inherit !== false && parent && parent.parameter(id, opts)) || null;
416 };
417 /**
418 * Validates the input parameter values against this UrlMatcher
419 *
420 * Checks an object hash of parameters to validate their correctness according to the parameter
421 * types of this `UrlMatcher`.
422 *
423 * @param params The object hash of parameters to validate.
424 * @returns Returns `true` if `params` validates, otherwise `false`.
425 */
426 UrlMatcher.prototype.validates = function (params) {
427 var validParamVal = function (param, val) { return !param || param.validates(val); };
428 params = params || {};
429 // I'm not sure why this checks only the param keys passed in, and not all the params known to the matcher
430 var paramSchema = this.parameters().filter(function (paramDef) { return params.hasOwnProperty(paramDef.id); });
431 return paramSchema.map(function (paramDef) { return validParamVal(paramDef, params[paramDef.id]); }).reduce(common_1.allTrueR, true);
432 };
433 /**
434 * Given a set of parameter values, creates a URL from this UrlMatcher.
435 *
436 * Creates a URL that matches this pattern by substituting the specified values
437 * for the path and search parameters.
438 *
439 * #### Example:
440 * ```js
441 * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
442 * // returns '/user/bob?q=yes'
443 * ```
444 *
445 * @param values the values to substitute for the parameters in this pattern.
446 * @returns the formatted URL (path and optionally search part).
447 */
448 UrlMatcher.prototype.format = function (values) {
449 if (values === void 0) { values = {}; }
450 // Build the full path of UrlMatchers (including all parent UrlMatchers)
451 var urlMatchers = this._cache.path;
452 // Extract all the static segments and Params (processed as ParamDetails)
453 // into an ordered array
454 var pathSegmentsAndParams = urlMatchers
455 .map(UrlMatcher.pathSegmentsAndParams)
456 .reduce(common_1.unnestR, [])
457 .map(function (x) { return (predicates_1.isString(x) ? x : getDetails(x)); });
458 // Extract the query params into a separate array
459 var queryParams = urlMatchers
460 .map(UrlMatcher.queryParams)
461 .reduce(common_1.unnestR, [])
462 .map(getDetails);
463 var isInvalid = function (param) { return param.isValid === false; };
464 if (pathSegmentsAndParams.concat(queryParams).filter(isInvalid).length) {
465 return null;
466 }
467 /**
468 * Given a Param, applies the parameter value, then returns detailed information about it
469 */
470 function getDetails(param) {
471 // Normalize to typed value
472 var value = param.value(values[param.id]);
473 var isValid = param.validates(value);
474 var isDefaultValue = param.isDefaultValue(value);
475 // Check if we're in squash mode for the parameter
476 var squash = isDefaultValue ? param.squash : false;
477 // Allow the Parameter's Type to encode the value
478 var encoded = param.type.encode(value);
479 return { param: param, value: value, isValid: isValid, isDefaultValue: isDefaultValue, squash: squash, encoded: encoded };
480 }
481 // Build up the path-portion from the list of static segments and parameters
482 var pathString = pathSegmentsAndParams.reduce(function (acc, x) {
483 // The element is a static segment (a raw string); just append it
484 if (predicates_1.isString(x))
485 return acc + x;
486 // Otherwise, it's a ParamDetails.
487 var squash = x.squash, encoded = x.encoded, param = x.param;
488 // If squash is === true, try to remove a slash from the path
489 if (squash === true)
490 return acc.match(/\/$/) ? acc.slice(0, -1) : acc;
491 // If squash is a string, use the string for the param value
492 if (predicates_1.isString(squash))
493 return acc + squash;
494 if (squash !== false)
495 return acc; // ?
496 if (encoded == null)
497 return acc;
498 // If this parameter value is an array, encode the value using encodeDashes
499 if (predicates_1.isArray(encoded))
500 return acc + common_1.map(encoded, UrlMatcher.encodeDashes).join('-');
501 // If the parameter type is "raw", then do not encodeURIComponent
502 if (param.raw)
503 return acc + encoded;
504 // Encode the value
505 return acc + encodeURIComponent(encoded);
506 }, '');
507 // Build the query string by applying parameter values (array or regular)
508 // then mapping to key=value, then flattening and joining using "&"
509 var queryString = queryParams
510 .map(function (paramDetails) {
511 var param = paramDetails.param, squash = paramDetails.squash, encoded = paramDetails.encoded, isDefaultValue = paramDetails.isDefaultValue;
512 if (encoded == null || (isDefaultValue && squash !== false))
513 return;
514 if (!predicates_1.isArray(encoded))
515 encoded = [encoded];
516 if (encoded.length === 0)
517 return;
518 if (!param.raw)
519 encoded = common_1.map(encoded, encodeURIComponent);
520 return encoded.map(function (val) { return param.id + "=" + val; });
521 })
522 .filter(common_1.identity)
523 .reduce(common_1.unnestR, [])
524 .join('&');
525 // Concat the pathstring with the queryString (if exists) and the hashString (if exists)
526 return pathString + (queryString ? "?" + queryString : '') + (values['#'] ? '#' + values['#'] : '');
527 };
528 /** @internal */
529 UrlMatcher.nameValidator = /^\w+([-.]+\w+)*(?:\[\])?$/;
530 return UrlMatcher;
531}());
532exports.UrlMatcher = UrlMatcher;
533//# sourceMappingURL=urlMatcher.js.map
\No newline at end of file