You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1036 lines
39 KiB

  1. var $$UMFP; // reference to $UrlMatcherFactoryProvider
  2. /**
  3. * @ngdoc object
  4. * @name ui.router.util.type:UrlMatcher
  5. *
  6. * @description
  7. * Matches URLs against patterns and extracts named parameters from the path or the search
  8. * part of the URL. A URL pattern consists of a path pattern, optionally followed by '?' and a list
  9. * of search parameters. Multiple search parameter names are separated by '&'. Search parameters
  10. * do not influence whether or not a URL is matched, but their values are passed through into
  11. * the matched parameters returned by {@link ui.router.util.type:UrlMatcher#methods_exec exec}.
  12. *
  13. * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace
  14. * syntax, which optionally allows a regular expression for the parameter to be specified:
  15. *
  16. * * `':'` name - colon placeholder
  17. * * `'*'` name - catch-all placeholder
  18. * * `'{' name '}'` - curly placeholder
  19. * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the
  20. * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash.
  21. *
  22. * Parameter names may contain only word characters (latin letters, digits, and underscore) and
  23. * must be unique within the pattern (across both path and search parameters). For colon
  24. * placeholders or curly placeholders without an explicit regexp, a path parameter matches any
  25. * number of characters other than '/'. For catch-all placeholders the path parameter matches
  26. * any number of characters.
  27. *
  28. * Examples:
  29. *
  30. * * `'/hello/'` - Matches only if the path is exactly '/hello/'. There is no special treatment for
  31. * trailing slashes, and patterns have to match the entire path, not just a prefix.
  32. * * `'/user/:id'` - Matches '/user/bob' or '/user/1234!!!' or even '/user/' but not '/user' or
  33. * '/user/bob/details'. The second path segment will be captured as the parameter 'id'.
  34. * * `'/user/{id}'` - Same as the previous example, but using curly brace syntax.
  35. * * `'/user/{id:[^/]*}'` - Same as the previous example.
  36. * * `'/user/{id:[0-9a-fA-F]{1,8}}'` - Similar to the previous example, but only matches if the id
  37. * parameter consists of 1 to 8 hex digits.
  38. * * `'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the
  39. * path into the parameter 'path'.
  40. * * `'/files/*path'` - ditto.
  41. * * `'/calendar/{start:date}'` - Matches "/calendar/2014-11-12" (because the pattern defined
  42. * in the built-in `date` Type matches `2014-11-12`) and provides a Date object in $stateParams.start
  43. *
  44. * @param {string} pattern The pattern to compile into a matcher.
  45. * @param {Object} config A configuration object hash:
  46. * @param {Object=} parentMatcher Used to concatenate the pattern/config onto
  47. * an existing UrlMatcher
  48. *
  49. * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`.
  50. * * `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`.
  51. *
  52. * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any
  53. * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns
  54. * non-null) will start with this prefix.
  55. *
  56. * @property {string} source The pattern that was passed into the constructor
  57. *
  58. * @property {string} sourcePath The path portion of the source property
  59. *
  60. * @property {string} sourceSearch The search portion of the source property
  61. *
  62. * @property {string} regex The constructed regex that will be used to match against the url when
  63. * it is time to determine which url will match.
  64. *
  65. * @returns {Object} New `UrlMatcher` object
  66. */
  67. function UrlMatcher(pattern, config, parentMatcher) {
  68. config = extend({ params: {} }, isObject(config) ? config : {});
  69. // Find all placeholders and create a compiled pattern, using either classic or curly syntax:
  70. // '*' name
  71. // ':' name
  72. // '{' name '}'
  73. // '{' name ':' regexp '}'
  74. // The regular expression is somewhat complicated due to the need to allow curly braces
  75. // inside the regular expression. The placeholder regexp breaks down as follows:
  76. // ([:*])([\w\[\]]+) - classic placeholder ($1 / $2) (search version has - for snake-case)
  77. // \{([\w\[\]]+)(?:\:( ... ))?\} - curly brace placeholder ($3) with optional regexp/type ... ($4) (search version has - for snake-case
  78. // (?: ... | ... | ... )+ - the regexp consists of any number of atoms, an atom being either
  79. // [^{}\\]+ - anything other than curly braces or backslash
  80. // \\. - a backslash escape
  81. // \{(?:[^{}\\]+|\\.)*\} - a matched set of curly braces containing other atoms
  82. var placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
  83. searchPlaceholder = /([:]?)([\w\[\]-]+)|\{([\w\[\]-]+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g,
  84. compiled = '^', last = 0, m,
  85. segments = this.segments = [],
  86. parentParams = parentMatcher ? parentMatcher.params : {},
  87. params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(),
  88. paramNames = [];
  89. function addParameter(id, type, config, location) {
  90. paramNames.push(id);
  91. if (parentParams[id]) return parentParams[id];
  92. if (!/^\w+(-+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'");
  93. if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'");
  94. params[id] = new $$UMFP.Param(id, type, config, location);
  95. return params[id];
  96. }
  97. function quoteRegExp(string, pattern, squash) {
  98. var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
  99. if (!pattern) return result;
  100. switch(squash) {
  101. case false: surroundPattern = ['(', ')']; break;
  102. case true: surroundPattern = ['?(', ')?']; break;
  103. default: surroundPattern = ['(' + squash + "|", ')?']; break;
  104. }
  105. return result + surroundPattern[0] + pattern + surroundPattern[1];
  106. }
  107. this.source = pattern;
  108. // Split into static segments separated by path parameter placeholders.
  109. // The number of segments is always 1 more than the number of parameters.
  110. function matchDetails(m, isSearch) {
  111. var id, regexp, segment, type, cfg, arrayMode;
  112. id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
  113. cfg = config.params[id];
  114. segment = pattern.substring(last, m.index);
  115. regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null);
  116. type = $$UMFP.type(regexp || "string") || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp) });
  117. return {
  118. id: id, regexp: regexp, segment: segment, type: type, cfg: cfg
  119. };
  120. }
  121. var p, param, segment;
  122. while ((m = placeholder.exec(pattern))) {
  123. p = matchDetails(m, false);
  124. if (p.segment.indexOf('?') >= 0) break; // we're into the search part
  125. param = addParameter(p.id, p.type, p.cfg, "path");
  126. compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash);
  127. segments.push(p.segment);
  128. last = placeholder.lastIndex;
  129. }
  130. segment = pattern.substring(last);
  131. // Find any search parameter names and remove them from the last segment
  132. var i = segment.indexOf('?');
  133. if (i >= 0) {
  134. var search = this.sourceSearch = segment.substring(i);
  135. segment = segment.substring(0, i);
  136. this.sourcePath = pattern.substring(0, last + i);
  137. if (search.length > 0) {
  138. last = 0;
  139. while ((m = searchPlaceholder.exec(search))) {
  140. p = matchDetails(m, true);
  141. param = addParameter(p.id, p.type, p.cfg, "search");
  142. last = placeholder.lastIndex;
  143. // check if ?&
  144. }
  145. }
  146. } else {
  147. this.sourcePath = pattern;
  148. this.sourceSearch = '';
  149. }
  150. compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$';
  151. segments.push(segment);
  152. this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined);
  153. this.prefix = segments[0];
  154. this.$$paramNames = paramNames;
  155. }
  156. /**
  157. * @ngdoc function
  158. * @name ui.router.util.type:UrlMatcher#concat
  159. * @methodOf ui.router.util.type:UrlMatcher
  160. *
  161. * @description
  162. * Returns a new matcher for a pattern constructed by appending the path part and adding the
  163. * search parameters of the specified pattern to this pattern. The current pattern is not
  164. * modified. This can be understood as creating a pattern for URLs that are relative to (or
  165. * suffixes of) the current pattern.
  166. *
  167. * @example
  168. * The following two matchers are equivalent:
  169. * <pre>
  170. * new UrlMatcher('/user/{id}?q').concat('/details?date');
  171. * new UrlMatcher('/user/{id}/details?q&date');
  172. * </pre>
  173. *
  174. * @param {string} pattern The pattern to append.
  175. * @param {Object} config An object hash of the configuration for the matcher.
  176. * @returns {UrlMatcher} A matcher for the concatenated pattern.
  177. */
  178. UrlMatcher.prototype.concat = function (pattern, config) {
  179. // Because order of search parameters is irrelevant, we can add our own search
  180. // parameters to the end of the new pattern. Parse the new pattern by itself
  181. // and then join the bits together, but it's much easier to do this on a string level.
  182. var defaultConfig = {
  183. caseInsensitive: $$UMFP.caseInsensitive(),
  184. strict: $$UMFP.strictMode(),
  185. squash: $$UMFP.defaultSquashPolicy()
  186. };
  187. return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this);
  188. };
  189. UrlMatcher.prototype.toString = function () {
  190. return this.source;
  191. };
  192. /**
  193. * @ngdoc function
  194. * @name ui.router.util.type:UrlMatcher#exec
  195. * @methodOf ui.router.util.type:UrlMatcher
  196. *
  197. * @description
  198. * Tests the specified path against this matcher, and returns an object containing the captured
  199. * parameter values, or null if the path does not match. The returned object contains the values
  200. * of any search parameters that are mentioned in the pattern, but their value may be null if
  201. * they are not present in `searchParams`. This means that search parameters are always treated
  202. * as optional.
  203. *
  204. * @example
  205. * <pre>
  206. * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
  207. * x: '1', q: 'hello'
  208. * });
  209. * // returns { id: 'bob', q: 'hello', r: null }
  210. * </pre>
  211. *
  212. * @param {string} path The URL path to match, e.g. `$location.path()`.
  213. * @param {Object} searchParams URL search parameters, e.g. `$location.search()`.
  214. * @returns {Object} The captured parameter values.
  215. */
  216. UrlMatcher.prototype.exec = function (path, searchParams) {
  217. var m = this.regexp.exec(path);
  218. if (!m) return null;
  219. searchParams = searchParams || {};
  220. var paramNames = this.parameters(), nTotal = paramNames.length,
  221. nPath = this.segments.length - 1,
  222. values = {}, i, j, cfg, paramName;
  223. if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'");
  224. function decodePathArray(string) {
  225. function reverseString(str) { return str.split("").reverse().join(""); }
  226. function unquoteDashes(str) { return str.replace(/\\-/, "-"); }
  227. var split = reverseString(string).split(/-(?!\\)/);
  228. var allReversed = map(split, reverseString);
  229. return map(allReversed, unquoteDashes).reverse();
  230. }
  231. for (i = 0; i < nPath; i++) {
  232. paramName = paramNames[i];
  233. var param = this.params[paramName];
  234. var paramVal = m[i+1];
  235. // if the param value matches a pre-replace pair, replace the value before decoding.
  236. for (j = 0; j < param.replace; j++) {
  237. if (param.replace[j].from === paramVal) paramVal = param.replace[j].to;
  238. }
  239. if (paramVal && param.array === true) paramVal = decodePathArray(paramVal);
  240. values[paramName] = param.value(paramVal);
  241. }
  242. for (/**/; i < nTotal; i++) {
  243. paramName = paramNames[i];
  244. values[paramName] = this.params[paramName].value(searchParams[paramName]);
  245. }
  246. return values;
  247. };
  248. /**
  249. * @ngdoc function
  250. * @name ui.router.util.type:UrlMatcher#parameters
  251. * @methodOf ui.router.util.type:UrlMatcher
  252. *
  253. * @description
  254. * Returns the names of all path and search parameters of this pattern in an unspecified order.
  255. *
  256. * @returns {Array.<string>} An array of parameter names. Must be treated as read-only. If the
  257. * pattern has no parameters, an empty array is returned.
  258. */
  259. UrlMatcher.prototype.parameters = function (param) {
  260. if (!isDefined(param)) return this.$$paramNames;
  261. return this.params[param] || null;
  262. };
  263. /**
  264. * @ngdoc function
  265. * @name ui.router.util.type:UrlMatcher#validate
  266. * @methodOf ui.router.util.type:UrlMatcher
  267. *
  268. * @description
  269. * Checks an object hash of parameters to validate their correctness according to the parameter
  270. * types of this `UrlMatcher`.
  271. *
  272. * @param {Object} params The object hash of parameters to validate.
  273. * @returns {boolean} Returns `true` if `params` validates, otherwise `false`.
  274. */
  275. UrlMatcher.prototype.validates = function (params) {
  276. return this.params.$$validates(params);
  277. };
  278. /**
  279. * @ngdoc function
  280. * @name ui.router.util.type:UrlMatcher#format
  281. * @methodOf ui.router.util.type:UrlMatcher
  282. *
  283. * @description
  284. * Creates a URL that matches this pattern by substituting the specified values
  285. * for the path and search parameters. Null values for path parameters are
  286. * treated as empty strings.
  287. *
  288. * @example
  289. * <pre>
  290. * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
  291. * // returns '/user/bob?q=yes'
  292. * </pre>
  293. *
  294. * @param {Object} values the values to substitute for the parameters in this pattern.
  295. * @returns {string} the formatted URL (path and optionally search part).
  296. */
  297. UrlMatcher.prototype.format = function (values) {
  298. values = values || {};
  299. var segments = this.segments, params = this.parameters(), paramset = this.params;
  300. if (!this.validates(values)) return null;
  301. var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0];
  302. function encodeDashes(str) { // Replace dashes with encoded "\-"
  303. return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); });
  304. }
  305. for (i = 0; i < nTotal; i++) {
  306. var isPathParam = i < nPath;
  307. var name = params[i], param = paramset[name], value = param.value(values[name]);
  308. var isDefaultValue = param.isOptional && param.type.equals(param.value(), value);
  309. var squash = isDefaultValue ? param.squash : false;
  310. var encoded = param.type.encode(value);
  311. if (isPathParam) {
  312. var nextSegment = segments[i + 1];
  313. if (squash === false) {
  314. if (encoded != null) {
  315. if (isArray(encoded)) {
  316. result += map(encoded, encodeDashes).join("-");
  317. } else {
  318. result += encodeURIComponent(encoded);
  319. }
  320. }
  321. result += nextSegment;
  322. } else if (squash === true) {
  323. var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/;
  324. result += nextSegment.match(capture)[1];
  325. } else if (isString(squash)) {
  326. result += squash + nextSegment;
  327. }
  328. } else {
  329. if (encoded == null || (isDefaultValue && squash !== false)) continue;
  330. if (!isArray(encoded)) encoded = [ encoded ];
  331. encoded = map(encoded, encodeURIComponent).join('&' + name + '=');
  332. result += (search ? '&' : '?') + (name + '=' + encoded);
  333. search = true;
  334. }
  335. }
  336. return result;
  337. };
  338. /**
  339. * @ngdoc object
  340. * @name ui.router.util.type:Type
  341. *
  342. * @description
  343. * Implements an interface to define custom parameter types that can be decoded from and encoded to
  344. * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`}
  345. * objects when matching or formatting URLs, or comparing or validating parameter values.
  346. *
  347. * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more
  348. * information on registering custom types.
  349. *
  350. * @param {Object} config A configuration object which contains the custom type definition. The object's
  351. * properties will override the default methods and/or pattern in `Type`'s public interface.
  352. * @example
  353. * <pre>
  354. * {
  355. * decode: function(val) { return parseInt(val, 10); },
  356. * encode: function(val) { return val && val.toString(); },
  357. * equals: function(a, b) { return this.is(a) && a === b; },
  358. * is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
  359. * pattern: /\d+/
  360. * }
  361. * </pre>
  362. *
  363. * @property {RegExp} pattern The regular expression pattern used to match values of this type when
  364. * coming from a substring of a URL.
  365. *
  366. * @returns {Object} Returns a new `Type` object.
  367. */
  368. function Type(config) {
  369. extend(this, config);
  370. }
  371. /**
  372. * @ngdoc function
  373. * @name ui.router.util.type:Type#is
  374. * @methodOf ui.router.util.type:Type
  375. *
  376. * @description
  377. * Detects whether a value is of a particular type. Accepts a native (decoded) value
  378. * and determines whether it matches the current `Type` object.
  379. *
  380. * @param {*} val The value to check.
  381. * @param {string} key Optional. If the type check is happening in the context of a specific
  382. * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the
  383. * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects.
  384. * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`.
  385. */
  386. Type.prototype.is = function(val, key) {
  387. return true;
  388. };
  389. /**
  390. * @ngdoc function
  391. * @name ui.router.util.type:Type#encode
  392. * @methodOf ui.router.util.type:Type
  393. *
  394. * @description
  395. * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the
  396. * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it
  397. * only needs to be a representation of `val` that has been coerced to a string.
  398. *
  399. * @param {*} val The value to encode.
  400. * @param {string} key The name of the parameter in which `val` is stored. Can be used for
  401. * meta-programming of `Type` objects.
  402. * @returns {string} Returns a string representation of `val` that can be encoded in a URL.
  403. */
  404. Type.prototype.encode = function(val, key) {
  405. return val;
  406. };
  407. /**
  408. * @ngdoc function
  409. * @name ui.router.util.type:Type#decode
  410. * @methodOf ui.router.util.type:Type
  411. *
  412. * @description
  413. * Converts a parameter value (from URL string or transition param) to a custom/native value.
  414. *
  415. * @param {string} val The URL parameter value to decode.
  416. * @param {string} key The name of the parameter in which `val` is stored. Can be used for
  417. * meta-programming of `Type` objects.
  418. * @returns {*} Returns a custom representation of the URL parameter value.
  419. */
  420. Type.prototype.decode = function(val, key) {
  421. return val;
  422. };
  423. /**
  424. * @ngdoc function
  425. * @name ui.router.util.type:Type#equals
  426. * @methodOf ui.router.util.type:Type
  427. *
  428. * @description
  429. * Determines whether two decoded values are equivalent.
  430. *
  431. * @param {*} a A value to compare against.
  432. * @param {*} b A value to compare against.
  433. * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`.
  434. */
  435. Type.prototype.equals = function(a, b) {
  436. return a == b;
  437. };
  438. Type.prototype.$subPattern = function() {
  439. var sub = this.pattern.toString();
  440. return sub.substr(1, sub.length - 2);
  441. };
  442. Type.prototype.pattern = /.*/;
  443. Type.prototype.toString = function() { return "{Type:" + this.name + "}"; };
  444. /*
  445. * Wraps an existing custom Type as an array of Type, depending on 'mode'.
  446. * e.g.:
  447. * - urlmatcher pattern "/path?{queryParam[]:int}"
  448. * - url: "/path?queryParam=1&queryParam=2
  449. * - $stateParams.queryParam will be [1, 2]
  450. * if `mode` is "auto", then
  451. * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1
  452. * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2]
  453. */
  454. Type.prototype.$asArray = function(mode, isSearch) {
  455. if (!mode) return this;
  456. if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only");
  457. return new ArrayType(this, mode);
  458. function ArrayType(type, mode) {
  459. function bindTo(type, callbackName) {
  460. return function() {
  461. return type[callbackName].apply(type, arguments);
  462. };
  463. }
  464. // Wrap non-array value as array
  465. function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); }
  466. // Unwrap array value for "auto" mode. Return undefined for empty array.
  467. function arrayUnwrap(val) {
  468. switch(val.length) {
  469. case 0: return undefined;
  470. case 1: return mode === "auto" ? val[0] : val;
  471. default: return val;
  472. }
  473. }
  474. function falsey(val) { return !val; }
  475. // Wraps type (.is/.encode/.decode) functions to operate on each value of an array
  476. function arrayHandler(callback, allTruthyMode) {
  477. return function handleArray(val) {
  478. val = arrayWrap(val);
  479. var result = map(val, callback);
  480. if (allTruthyMode === true)
  481. return filter(result, falsey).length === 0;
  482. return arrayUnwrap(result);
  483. };
  484. }
  485. // Wraps type (.equals) functions to operate on each value of an array
  486. function arrayEqualsHandler(callback) {
  487. return function handleArray(val1, val2) {
  488. var left = arrayWrap(val1), right = arrayWrap(val2);
  489. if (left.length !== right.length) return false;
  490. for (var i = 0; i < left.length; i++) {
  491. if (!callback(left[i], right[i])) return false;
  492. }
  493. return true;
  494. };
  495. }
  496. this.encode = arrayHandler(bindTo(type, 'encode'));
  497. this.decode = arrayHandler(bindTo(type, 'decode'));
  498. this.is = arrayHandler(bindTo(type, 'is'), true);
  499. this.equals = arrayEqualsHandler(bindTo(type, 'equals'));
  500. this.pattern = type.pattern;
  501. this.$arrayMode = mode;
  502. }
  503. };
  504. /**
  505. * @ngdoc object
  506. * @name ui.router.util.$urlMatcherFactory
  507. *
  508. * @description
  509. * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory
  510. * is also available to providers under the name `$urlMatcherFactoryProvider`.
  511. */
  512. function $UrlMatcherFactory() {
  513. $$UMFP = this;
  514. var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
  515. function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; }
  516. function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; }
  517. // TODO: in 1.0, make string .is() return false if value is undefined by default.
  518. // function regexpMatches(val) { /*jshint validthis:true */ return isDefined(val) && this.pattern.test(val); }
  519. function regexpMatches(val) { /*jshint validthis:true */ return this.pattern.test(val); }
  520. var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = {
  521. string: {
  522. encode: valToString,
  523. decode: valFromString,
  524. is: regexpMatches,
  525. pattern: /[^/]*/
  526. },
  527. int: {
  528. encode: valToString,
  529. decode: function(val) { return parseInt(val, 10); },
  530. is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; },
  531. pattern: /\d+/
  532. },
  533. bool: {
  534. encode: function(val) { return val ? 1 : 0; },
  535. decode: function(val) { return parseInt(val, 10) !== 0; },
  536. is: function(val) { return val === true || val === false; },
  537. pattern: /0|1/
  538. },
  539. date: {
  540. encode: function (val) {
  541. if (!this.is(val))
  542. return undefined;
  543. return [ val.getFullYear(),
  544. ('0' + (val.getMonth() + 1)).slice(-2),
  545. ('0' + val.getDate()).slice(-2)
  546. ].join("-");
  547. },
  548. decode: function (val) {
  549. if (this.is(val)) return val;
  550. var match = this.capture.exec(val);
  551. return match ? new Date(match[1], match[2] - 1, match[3]) : undefined;
  552. },
  553. is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); },
  554. equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); },
  555. pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,
  556. capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/
  557. },
  558. json: {
  559. encode: angular.toJson,
  560. decode: angular.fromJson,
  561. is: angular.isObject,
  562. equals: angular.equals,
  563. pattern: /[^/]*/
  564. },
  565. any: { // does not encode/decode
  566. encode: angular.identity,
  567. decode: angular.identity,
  568. is: angular.identity,
  569. equals: angular.equals,
  570. pattern: /.*/
  571. }
  572. };
  573. function getDefaultConfig() {
  574. return {
  575. strict: isStrictMode,
  576. caseInsensitive: isCaseInsensitive
  577. };
  578. }
  579. function isInjectable(value) {
  580. return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1])));
  581. }
  582. /**
  583. * [Internal] Get the default value of a parameter, which may be an injectable function.
  584. */
  585. $UrlMatcherFactory.$$getDefaultValue = function(config) {
  586. if (!isInjectable(config.value)) return config.value;
  587. if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
  588. return injector.invoke(config.value);
  589. };
  590. /**
  591. * @ngdoc function
  592. * @name ui.router.util.$urlMatcherFactory#caseInsensitive
  593. * @methodOf ui.router.util.$urlMatcherFactory
  594. *
  595. * @description
  596. * Defines whether URL matching should be case sensitive (the default behavior), or not.
  597. *
  598. * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`;
  599. * @returns {boolean} the current value of caseInsensitive
  600. */
  601. this.caseInsensitive = function(value) {
  602. if (isDefined(value))
  603. isCaseInsensitive = value;
  604. return isCaseInsensitive;
  605. };
  606. /**
  607. * @ngdoc function
  608. * @name ui.router.util.$urlMatcherFactory#strictMode
  609. * @methodOf ui.router.util.$urlMatcherFactory
  610. *
  611. * @description
  612. * Defines whether URLs should match trailing slashes, or not (the default behavior).
  613. *
  614. * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`.
  615. * @returns {boolean} the current value of strictMode
  616. */
  617. this.strictMode = function(value) {
  618. if (isDefined(value))
  619. isStrictMode = value;
  620. return isStrictMode;
  621. };
  622. /**
  623. * @ngdoc function
  624. * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy
  625. * @methodOf ui.router.util.$urlMatcherFactory
  626. *
  627. * @description
  628. * Sets the default behavior when generating or matching URLs with default parameter values.
  629. *
  630. * @param {string} value A string that defines the default parameter URL squashing behavior.
  631. * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL
  632. * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the
  633. * parameter is surrounded by slashes, squash (remove) one slash from the URL
  634. * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove)
  635. * the parameter value from the URL and replace it with this string.
  636. */
  637. this.defaultSquashPolicy = function(value) {
  638. if (!isDefined(value)) return defaultSquashPolicy;
  639. if (value !== true && value !== false && !isString(value))
  640. throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string");
  641. defaultSquashPolicy = value;
  642. return value;
  643. };
  644. /**
  645. * @ngdoc function
  646. * @name ui.router.util.$urlMatcherFactory#compile
  647. * @methodOf ui.router.util.$urlMatcherFactory
  648. *
  649. * @description
  650. * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern.
  651. *
  652. * @param {string} pattern The URL pattern.
  653. * @param {Object} config The config object hash.
  654. * @returns {UrlMatcher} The UrlMatcher.
  655. */
  656. this.compile = function (pattern, config) {
  657. return new UrlMatcher(pattern, extend(getDefaultConfig(), config));
  658. };
  659. /**
  660. * @ngdoc function
  661. * @name ui.router.util.$urlMatcherFactory#isMatcher
  662. * @methodOf ui.router.util.$urlMatcherFactory
  663. *
  664. * @description
  665. * Returns true if the specified object is a `UrlMatcher`, or false otherwise.
  666. *
  667. * @param {Object} object The object to perform the type check against.
  668. * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by
  669. * implementing all the same methods.
  670. */
  671. this.isMatcher = function (o) {
  672. if (!isObject(o)) return false;
  673. var result = true;
  674. forEach(UrlMatcher.prototype, function(val, name) {
  675. if (isFunction(val)) {
  676. result = result && (isDefined(o[name]) && isFunction(o[name]));
  677. }
  678. });
  679. return result;
  680. };
  681. /**
  682. * @ngdoc function
  683. * @name ui.router.util.$urlMatcherFactory#type
  684. * @methodOf ui.router.util.$urlMatcherFactory
  685. *
  686. * @description
  687. * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to
  688. * generate URLs with typed parameters.
  689. *
  690. * @param {string} name The type name.
  691. * @param {Object|Function} definition The type definition. See
  692. * {@link ui.router.util.type:Type `Type`} for information on the values accepted.
  693. * @param {Object|Function} definitionFn (optional) A function that is injected before the app
  694. * runtime starts. The result of this function is merged into the existing `definition`.
  695. * See {@link ui.router.util.type:Type `Type`} for information on the values accepted.
  696. *
  697. * @returns {Object} Returns `$urlMatcherFactoryProvider`.
  698. *
  699. * @example
  700. * This is a simple example of a custom type that encodes and decodes items from an
  701. * array, using the array index as the URL-encoded value:
  702. *
  703. * <pre>
  704. * var list = ['John', 'Paul', 'George', 'Ringo'];
  705. *
  706. * $urlMatcherFactoryProvider.type('listItem', {
  707. * encode: function(item) {
  708. * // Represent the list item in the URL using its corresponding index
  709. * return list.indexOf(item);
  710. * },
  711. * decode: function(item) {
  712. * // Look up the list item by index
  713. * return list[parseInt(item, 10)];
  714. * },
  715. * is: function(item) {
  716. * // Ensure the item is valid by checking to see that it appears
  717. * // in the list
  718. * return list.indexOf(item) > -1;
  719. * }
  720. * });
  721. *
  722. * $stateProvider.state('list', {
  723. * url: "/list/{item:listItem}",
  724. * controller: function($scope, $stateParams) {
  725. * console.log($stateParams.item);
  726. * }
  727. * });
  728. *
  729. * // ...
  730. *
  731. * // Changes URL to '/list/3', logs "Ringo" to the console
  732. * $state.go('list', { item: "Ringo" });
  733. * </pre>
  734. *
  735. * This is a more complex example of a type that relies on dependency injection to
  736. * interact with services, and uses the parameter name from the URL to infer how to
  737. * handle encoding and decoding parameter values:
  738. *
  739. * <pre>
  740. * // Defines a custom type that gets a value from a service,
  741. * // where each service gets different types of values from
  742. * // a backend API:
  743. * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
  744. *
  745. * // Matches up services to URL parameter names
  746. * var services = {
  747. * user: Users,
  748. * post: Posts
  749. * };
  750. *
  751. * return {
  752. * encode: function(object) {
  753. * // Represent the object in the URL using its unique ID
  754. * return object.id;
  755. * },
  756. * decode: function(value, key) {
  757. * // Look up the object by ID, using the parameter
  758. * // name (key) to call the correct service
  759. * return services[key].findById(value);
  760. * },
  761. * is: function(object, key) {
  762. * // Check that object is a valid dbObject
  763. * return angular.isObject(object) && object.id && services[key];
  764. * }
  765. * equals: function(a, b) {
  766. * // Check the equality of decoded objects by comparing
  767. * // their unique IDs
  768. * return a.id === b.id;
  769. * }
  770. * };
  771. * });
  772. *
  773. * // In a config() block, you can then attach URLs with
  774. * // type-annotated parameters:
  775. * $stateProvider.state('users', {
  776. * url: "/users",
  777. * // ...
  778. * }).state('users.item', {
  779. * url: "/{user:dbObject}",
  780. * controller: function($scope, $stateParams) {
  781. * // $stateParams.user will now be an object returned from
  782. * // the Users service
  783. * },
  784. * // ...
  785. * });
  786. * </pre>
  787. */
  788. this.type = function (name, definition, definitionFn) {
  789. if (!isDefined(definition)) return $types[name];
  790. if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined.");
  791. $types[name] = new Type(extend({ name: name }, definition));
  792. if (definitionFn) {
  793. typeQueue.push({ name: name, def: definitionFn });
  794. if (!enqueue) flushTypeQueue();
  795. }
  796. return this;
  797. };
  798. // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s
  799. function flushTypeQueue() {
  800. while(typeQueue.length) {
  801. var type = typeQueue.shift();
  802. if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime.");
  803. angular.extend($types[type.name], injector.invoke(type.def));
  804. }
  805. }
  806. // Register default types. Store them in the prototype of $types.
  807. forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); });
  808. $types = inherit($types, {});
  809. /* No need to document $get, since it returns this */
  810. this.$get = ['$injector', function ($injector) {
  811. injector = $injector;
  812. enqueue = false;
  813. flushTypeQueue();
  814. forEach(defaultTypes, function(type, name) {
  815. if (!$types[name]) $types[name] = new Type(type);
  816. });
  817. return this;
  818. }];
  819. this.Param = function Param(id, type, config, location) {
  820. var self = this;
  821. config = unwrapShorthand(config);
  822. type = getType(config, type, location);
  823. var arrayMode = getArrayMode();
  824. type = arrayMode ? type.$asArray(arrayMode, location === "search") : type;
  825. if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined)
  826. config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to ""
  827. var isOptional = config.value !== undefined;
  828. var squash = getSquashPolicy(config, isOptional);
  829. var replace = getReplace(config, arrayMode, isOptional, squash);
  830. function unwrapShorthand(config) {
  831. var keys = isObject(config) ? objectKeys(config) : [];
  832. var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 &&
  833. indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1;
  834. if (isShorthand) config = { value: config };
  835. config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; };
  836. return config;
  837. }
  838. function getType(config, urlType, location) {
  839. if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
  840. if (urlType) return urlType;
  841. if (!config.type) return (location === "config" ? $types.any : $types.string);
  842. return config.type instanceof Type ? config.type : new Type(config.type);
  843. }
  844. // array config: param name (param[]) overrides default settings. explicit config overrides param name.
  845. function getArrayMode() {
  846. var arrayDefaults = { array: (location === "search" ? "auto" : false) };
  847. var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {};
  848. return extend(arrayDefaults, arrayParamNomenclature, config).array;
  849. }
  850. /**
  851. * returns false, true, or the squash value to indicate the "default parameter url squash policy".
  852. */
  853. function getSquashPolicy(config, isOptional) {
  854. var squash = config.squash;
  855. if (!isOptional || squash === false) return false;
  856. if (!isDefined(squash) || squash == null) return defaultSquashPolicy;
  857. if (squash === true || isString(squash)) return squash;
  858. throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string");
  859. }
  860. function getReplace(config, arrayMode, isOptional, squash) {
  861. var replace, configuredKeys, defaultPolicy = [
  862. { from: "", to: (isOptional || arrayMode ? undefined : "") },
  863. { from: null, to: (isOptional || arrayMode ? undefined : "") }
  864. ];
  865. replace = isArray(config.replace) ? config.replace : [];
  866. if (isString(squash))
  867. replace.push({ from: squash, to: undefined });
  868. configuredKeys = map(replace, function(item) { return item.from; } );
  869. return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace);
  870. }
  871. /**
  872. * [Internal] Get the default value of a parameter, which may be an injectable function.
  873. */
  874. function $$getDefaultValue() {
  875. if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
  876. return injector.invoke(config.$$fn);
  877. }
  878. /**
  879. * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
  880. * default value, which may be the result of an injectable function.
  881. */
  882. function $value(value) {
  883. function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; }
  884. function $replace(value) {
  885. var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; });
  886. return replacement.length ? replacement[0] : value;
  887. }
  888. value = $replace(value);
  889. return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
  890. }
  891. function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; }
  892. extend(this, {
  893. id: id,
  894. type: type,
  895. location: location,
  896. array: arrayMode,
  897. squash: squash,
  898. replace: replace,
  899. isOptional: isOptional,
  900. value: $value,
  901. dynamic: undefined,
  902. config: config,
  903. toString: toString
  904. });
  905. };
  906. function ParamSet(params) {
  907. extend(this, params || {});
  908. }
  909. ParamSet.prototype = {
  910. $$new: function() {
  911. return inherit(this, extend(new ParamSet(), { $$parent: this}));
  912. },
  913. $$keys: function () {
  914. var keys = [], chain = [], parent = this,
  915. ignore = objectKeys(ParamSet.prototype);
  916. while (parent) { chain.push(parent); parent = parent.$$parent; }
  917. chain.reverse();
  918. forEach(chain, function(paramset) {
  919. forEach(objectKeys(paramset), function(key) {
  920. if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key);
  921. });
  922. });
  923. return keys;
  924. },
  925. $$values: function(paramValues) {
  926. var values = {}, self = this;
  927. forEach(self.$$keys(), function(key) {
  928. values[key] = self[key].value(paramValues && paramValues[key]);
  929. });
  930. return values;
  931. },
  932. $$equals: function(paramValues1, paramValues2) {
  933. var equal = true, self = this;
  934. forEach(self.$$keys(), function(key) {
  935. var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key];
  936. if (!self[key].type.equals(left, right)) equal = false;
  937. });
  938. return equal;
  939. },
  940. $$validates: function $$validate(paramValues) {
  941. var result = true, isOptional, val, param, self = this;
  942. forEach(this.$$keys(), function(key) {
  943. param = self[key];
  944. val = paramValues[key];
  945. isOptional = !val && param.isOptional;
  946. result = result && (isOptional || !!param.type.is(val));
  947. });
  948. return result;
  949. },
  950. $$parent: undefined
  951. };
  952. this.ParamSet = ParamSet;
  953. }
  954. // Register as a provider so it's available to other providers
  955. angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory);
  956. angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]);