var $$UMFP; // reference to $UrlMatcherFactoryProvider /** * @ngdoc object * @name ui.router.util.type:UrlMatcher * * @description * 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 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 {@link ui.router.util.type:UrlMatcher#methods_exec exec}. * * Path parameter placeholders can be specified using simple colon/catch-all syntax or curly brace * syntax, which optionally allows a regular expression for the parameter to be specified: * * * `':'` name - colon placeholder * * `'*'` name - catch-all placeholder * * `'{' name '}'` - curly placeholder * * `'{' name ':' regexp|type '}'` - curly placeholder with regexp or type name. Should the * regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. * * 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). For colon * placeholders or curly placeholders without an explicit regexp, 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` Type matches `2014-11-12`) and provides a Date object in $stateParams.start * * @param {string} pattern The pattern to compile into a matcher. * @param {Object} config A configuration object hash: * @param {Object=} parentMatcher Used to concatenate the pattern/config onto * an existing UrlMatcher * * * `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`. * * `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`. * * @property {string} prefix A static prefix of this pattern. The matcher guarantees that any * URL matching this matcher (i.e. any string for which {@link ui.router.util.type:UrlMatcher#methods_exec exec()} returns * non-null) will start with this prefix. * * @property {string} source The pattern that was passed into the constructor * * @property {string} sourcePath The path portion of the source property * * @property {string} sourceSearch The search portion of the source property * * @property {string} regex The constructed regex that will be used to match against the url when * it is time to determine which url will match. * * @returns {Object} New `UrlMatcher` object */ function UrlMatcher(pattern, config, parentMatcher) { config = extend({ params: {} }, isObject(config) ? config : {}); // 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, searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g, compiled = '^', last = 0, m, segments = this.segments = [], parentParams = parentMatcher ? parentMatcher.params : {}, params = this.params = parentMatcher ? parentMatcher.params.$$new() : new $$UMFP.ParamSet(), paramNames = []; function addParameter(id, type, config, location) { paramNames.push(id); if (parentParams[id]) return parentParams[id]; if (!/^\w+([-.]+\w+)*(?:\[\])?$/.test(id)) throw new Error("Invalid parameter name '" + id + "' in pattern '" + pattern + "'"); if (params[id]) throw new Error("Duplicate parameter name '" + id + "' in pattern '" + pattern + "'"); params[id] = new $$UMFP.Param(id, type, config, location); return params[id]; } function quoteRegExp(string, pattern, squash, optional) { var surroundPattern = ['',''], result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&"); if (!pattern) return result; switch(squash) { case false: surroundPattern = ['(', ')' + (optional ? "?" : "")]; break; case true: result = result.replace(/\/$/, ''); surroundPattern = ['(?:\/(', ')|\/)?']; break; default: surroundPattern = ['(' + squash + "|", ')?']; break; } return result + surroundPattern[0] + pattern + surroundPattern[1]; } this.source = pattern; // Split into static segments separated by path parameter placeholders. // The number of segments is always 1 more than the number of parameters. function matchDetails(m, isSearch) { var id, regexp, segment, type, cfg, arrayMode; id = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null cfg = config.params[id]; segment = pattern.substring(last, m.index); regexp = isSearch ? m[4] : m[4] || (m[1] == '*' ? '.*' : null); if (regexp) { type = $$UMFP.type(regexp) || inherit($$UMFP.type("string"), { pattern: new RegExp(regexp, config.caseInsensitive ? 'i' : undefined) }); } return { id: id, regexp: regexp, segment: segment, type: type, cfg: cfg }; } var p, param, segment; while ((m = placeholder.exec(pattern))) { p = matchDetails(m, false); if (p.segment.indexOf('?') >= 0) break; // we're into the search part param = addParameter(p.id, p.type, p.cfg, "path"); compiled += quoteRegExp(p.segment, param.type.pattern.source, param.squash, param.isOptional); segments.push(p.segment); 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 = this.sourceSearch = segment.substring(i); segment = segment.substring(0, i); this.sourcePath = pattern.substring(0, last + i); if (search.length > 0) { last = 0; while ((m = searchPlaceholder.exec(search))) { p = matchDetails(m, true); param = addParameter(p.id, p.type, p.cfg, "search"); last = placeholder.lastIndex; // check if ?& } } } else { this.sourcePath = pattern; this.sourceSearch = ''; } compiled += quoteRegExp(segment) + (config.strict === false ? '\/?' : '') + '$'; segments.push(segment); this.regexp = new RegExp(compiled, config.caseInsensitive ? 'i' : undefined); this.prefix = segments[0]; this.$$paramNames = paramNames; } /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#concat * @methodOf ui.router.util.type:UrlMatcher * * @description * Returns a new matcher for a pattern constructed by appending the path part and adding the * search parameters of the specified pattern to this pattern. The current pattern is not * modified. This can be understood as creating a pattern for URLs that are relative to (or * suffixes of) the current pattern. * * @example * The following two matchers are equivalent: *
 * new UrlMatcher('/user/{id}?q').concat('/details?date');
 * new UrlMatcher('/user/{id}/details?q&date');
 * 
* * @param {string} pattern The pattern to append. * @param {Object} config An object hash of the configuration for the matcher. * @returns {UrlMatcher} A matcher for the concatenated pattern. */ UrlMatcher.prototype.concat = function (pattern, config) { // Because order of search parameters is irrelevant, we can add our own search // parameters to the end of the new pattern. Parse the new pattern by itself // and then join the bits together, but it's much easier to do this on a string level. var defaultConfig = { caseInsensitive: $$UMFP.caseInsensitive(), strict: $$UMFP.strictMode(), squash: $$UMFP.defaultSquashPolicy() }; return new UrlMatcher(this.sourcePath + pattern + this.sourceSearch, extend(defaultConfig, config), this); }; UrlMatcher.prototype.toString = function () { return this.source; }; /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#exec * @methodOf ui.router.util.type:UrlMatcher * * @description * Tests the specified path against this matcher, and returns an object containing the captured * parameter values, or 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 `searchParams`. This means that search parameters are always treated * as optional. * * @example *
 * new UrlMatcher('/user/{id}?q&r').exec('/user/bob', {
 *   x: '1', q: 'hello'
 * });
 * // returns { id: 'bob', q: 'hello', r: null }
 * 
* * @param {string} path The URL path to match, e.g. `$location.path()`. * @param {Object} searchParams URL search parameters, e.g. `$location.search()`. * @returns {Object} The captured parameter values. */ UrlMatcher.prototype.exec = function (path, searchParams) { var m = this.regexp.exec(path); if (!m) return null; searchParams = searchParams || {}; var paramNames = this.parameters(), nTotal = paramNames.length, nPath = this.segments.length - 1, values = {}, i, j, cfg, paramName; if (nPath !== m.length - 1) throw new Error("Unbalanced capture group in route '" + this.source + "'"); function decodePathArray(string) { function reverseString(str) { return str.split("").reverse().join(""); } function unquoteDashes(str) { return str.replace(/\\-/g, "-"); } var split = reverseString(string).split(/-(?!\\)/); var allReversed = map(split, reverseString); return map(allReversed, unquoteDashes).reverse(); } var param, paramVal; for (i = 0; i < nPath; i++) { paramName = paramNames[i]; param = this.params[paramName]; paramVal = m[i+1]; // if the param value matches a pre-replace pair, replace the value before decoding. for (j = 0; j < param.replace.length; j++) { if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; } if (paramVal && param.array === true) paramVal = decodePathArray(paramVal); if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); values[paramName] = param.value(paramVal); } for (/**/; i < nTotal; i++) { paramName = paramNames[i]; values[paramName] = this.params[paramName].value(searchParams[paramName]); param = this.params[paramName]; paramVal = searchParams[paramName]; for (j = 0; j < param.replace.length; j++) { if (param.replace[j].from === paramVal) paramVal = param.replace[j].to; } if (isDefined(paramVal)) paramVal = param.type.decode(paramVal); values[paramName] = param.value(paramVal); } return values; }; /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#parameters * @methodOf ui.router.util.type:UrlMatcher * * @description * Returns the names of all path and search parameters of this pattern in an unspecified order. * * @returns {Array.} An array of parameter names. Must be treated as read-only. If the * pattern has no parameters, an empty array is returned. */ UrlMatcher.prototype.parameters = function (param) { if (!isDefined(param)) return this.$$paramNames; return this.params[param] || null; }; /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#validates * @methodOf ui.router.util.type:UrlMatcher * * @description * Checks an object hash of parameters to validate their correctness according to the parameter * types of this `UrlMatcher`. * * @param {Object} params The object hash of parameters to validate. * @returns {boolean} Returns `true` if `params` validates, otherwise `false`. */ UrlMatcher.prototype.validates = function (params) { return this.params.$$validates(params); }; /** * @ngdoc function * @name ui.router.util.type:UrlMatcher#format * @methodOf ui.router.util.type:UrlMatcher * * @description * Creates a URL that matches this pattern by substituting the specified values * for the path and search parameters. Null values for path parameters are * treated as empty strings. * * @example *
 * new UrlMatcher('/user/{id}?q').format({ id:'bob', q:'yes' });
 * // returns '/user/bob?q=yes'
 * 
* * @param {Object} values the values to substitute for the parameters in this pattern. * @returns {string} the formatted URL (path and optionally search part). */ UrlMatcher.prototype.format = function (values) { values = values || {}; var segments = this.segments, params = this.parameters(), paramset = this.params; if (!this.validates(values)) return null; var i, search = false, nPath = segments.length - 1, nTotal = params.length, result = segments[0]; function encodeDashes(str) { // Replace dashes with encoded "\-" return encodeURIComponent(str).replace(/-/g, function(c) { return '%5C%' + c.charCodeAt(0).toString(16).toUpperCase(); }); } for (i = 0; i < nTotal; i++) { var isPathParam = i < nPath; var name = params[i], param = paramset[name], value = param.value(values[name]); var isDefaultValue = param.isOptional && param.type.equals(param.value(), value); var squash = isDefaultValue ? param.squash : false; var encoded = param.type.encode(value); if (isPathParam) { var nextSegment = segments[i + 1]; var isFinalPathParam = i + 1 === nPath; if (squash === false) { if (encoded != null) { if (isArray(encoded)) { result += map(encoded, encodeDashes).join("-"); } else { result += encodeURIComponent(encoded); } } result += nextSegment; } else if (squash === true) { var capture = result.match(/\/$/) ? /\/?(.*)/ : /(.*)/; result += nextSegment.match(capture)[1]; } else if (isString(squash)) { result += squash + nextSegment; } if (isFinalPathParam && param.squash === true && result.slice(-1) === '/') result = result.slice(0, -1); } else { if (encoded == null || (isDefaultValue && squash !== false)) continue; if (!isArray(encoded)) encoded = [ encoded ]; if (encoded.length === 0) continue; encoded = map(encoded, encodeURIComponent).join('&' + name + '='); result += (search ? '&' : '?') + (name + '=' + encoded); search = true; } } return result; }; /** * @ngdoc object * @name ui.router.util.type:Type * * @description * Implements an interface to define custom parameter types that can be decoded from and encoded to * string parameters matched in a URL. Used by {@link ui.router.util.type:UrlMatcher `UrlMatcher`} * objects when matching or formatting URLs, or comparing or validating parameter values. * * See {@link ui.router.util.$urlMatcherFactory#methods_type `$urlMatcherFactory#type()`} for more * information on registering custom types. * * @param {Object} config A configuration object which contains the custom type definition. The object's * properties will override the default methods and/or pattern in `Type`'s public interface. * @example *
 * {
 *   decode: function(val) { return parseInt(val, 10); },
 *   encode: function(val) { return val && val.toString(); },
 *   equals: function(a, b) { return this.is(a) && a === b; },
 *   is: function(val) { return angular.isNumber(val) isFinite(val) && val % 1 === 0; },
 *   pattern: /\d+/
 * }
 * 
* * @property {RegExp} pattern The regular expression pattern used to match values of this type when * coming from a substring of a URL. * * @returns {Object} Returns a new `Type` object. */ function Type(config) { extend(this, config); } /** * @ngdoc function * @name ui.router.util.type:Type#is * @methodOf ui.router.util.type:Type * * @description * Detects whether a value is of a particular type. Accepts a native (decoded) value * and determines whether it matches the current `Type` object. * * @param {*} val The value to check. * @param {string} key Optional. If the type check is happening in the context of a specific * {@link ui.router.util.type:UrlMatcher `UrlMatcher`} object, this is the name of the * parameter in which `val` is stored. Can be used for meta-programming of `Type` objects. * @returns {Boolean} Returns `true` if the value matches the type, otherwise `false`. */ Type.prototype.is = function(val, key) { return true; }; /** * @ngdoc function * @name ui.router.util.type:Type#encode * @methodOf ui.router.util.type:Type * * @description * Encodes a custom/native type value to a string that can be embedded in a URL. Note that the * return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it * only needs to be a representation of `val` that has been coerced to a string. * * @param {*} val The value to encode. * @param {string} key The name of the parameter in which `val` is stored. Can be used for * meta-programming of `Type` objects. * @returns {string} Returns a string representation of `val` that can be encoded in a URL. */ Type.prototype.encode = function(val, key) { return val; }; /** * @ngdoc function * @name ui.router.util.type:Type#decode * @methodOf ui.router.util.type:Type * * @description * Converts a parameter value (from URL string or transition param) to a custom/native value. * * @param {string} val The URL parameter value to decode. * @param {string} key The name of the parameter in which `val` is stored. Can be used for * meta-programming of `Type` objects. * @returns {*} Returns a custom representation of the URL parameter value. */ Type.prototype.decode = function(val, key) { return val; }; /** * @ngdoc function * @name ui.router.util.type:Type#equals * @methodOf ui.router.util.type:Type * * @description * Determines whether two decoded values are equivalent. * * @param {*} a A value to compare against. * @param {*} b A value to compare against. * @returns {Boolean} Returns `true` if the values are equivalent/equal, otherwise `false`. */ Type.prototype.equals = function(a, b) { return a == b; }; Type.prototype.$subPattern = function() { var sub = this.pattern.toString(); return sub.substr(1, sub.length - 2); }; Type.prototype.pattern = /.*/; Type.prototype.toString = function() { return "{Type:" + this.name + "}"; }; /** Given an encoded string, or a decoded object, returns a decoded object */ Type.prototype.$normalize = function(val) { return this.is(val) ? val : this.decode(val); }; /* * Wraps an existing custom Type as an array of Type, depending on 'mode'. * e.g.: * - urlmatcher pattern "/path?{queryParam[]:int}" * - url: "/path?queryParam=1&queryParam=2 * - $stateParams.queryParam will be [1, 2] * if `mode` is "auto", then * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1 * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2] */ Type.prototype.$asArray = function(mode, isSearch) { if (!mode) return this; if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only"); function ArrayType(type, mode) { function bindTo(type, callbackName) { return function() { return type[callbackName].apply(type, arguments); }; } // Wrap non-array value as array function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); } // Unwrap array value for "auto" mode. Return undefined for empty array. function arrayUnwrap(val) { switch(val.length) { case 0: return undefined; case 1: return mode === "auto" ? val[0] : val; default: return val; } } function falsey(val) { return !val; } // Wraps type (.is/.encode/.decode) functions to operate on each value of an array function arrayHandler(callback, allTruthyMode) { return function handleArray(val) { if (isArray(val) && val.length === 0) return val; val = arrayWrap(val); var result = map(val, callback); if (allTruthyMode === true) return filter(result, falsey).length === 0; return arrayUnwrap(result); }; } // Wraps type (.equals) functions to operate on each value of an array function arrayEqualsHandler(callback) { return function handleArray(val1, val2) { var left = arrayWrap(val1), right = arrayWrap(val2); if (left.length !== right.length) return false; for (var i = 0; i < left.length; i++) { if (!callback(left[i], right[i])) return false; } return true; }; } this.encode = arrayHandler(bindTo(type, 'encode')); this.decode = arrayHandler(bindTo(type, 'decode')); this.is = arrayHandler(bindTo(type, 'is'), true); this.equals = arrayEqualsHandler(bindTo(type, 'equals')); this.pattern = type.pattern; this.$normalize = arrayHandler(bindTo(type, '$normalize')); this.name = type.name; this.$arrayMode = mode; } return new ArrayType(this, mode); }; /** * @ngdoc object * @name ui.router.util.$urlMatcherFactory * * @description * Factory for {@link ui.router.util.type:UrlMatcher `UrlMatcher`} instances. The factory * is also available to providers under the name `$urlMatcherFactoryProvider`. */ function $UrlMatcherFactory() { $$UMFP = this; var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false; // Use tildes to pre-encode slashes. // If the slashes are simply URLEncoded, the browser can choose to pre-decode them, // and bidirectional encoding/decoding fails. // Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character function valToString(val) { return val != null ? val.toString().replace(/~/g, "~~").replace(/\//g, "~2F") : val; } function valFromString(val) { return val != null ? val.toString().replace(/~2F/g, "/").replace(/~~/g, "~") : val; } var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = { "string": { encode: valToString, decode: valFromString, // TODO: in 1.0, make string .is() return false if value is undefined/null by default. // In 0.2.x, string params are optional by default for backwards compat is: function(val) { return val == null || !isDefined(val) || typeof val === "string"; }, pattern: /[^/]*/ }, "int": { encode: valToString, decode: function(val) { return parseInt(val, 10); }, is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; }, pattern: /\d+/ }, "bool": { encode: function(val) { return val ? 1 : 0; }, decode: function(val) { return parseInt(val, 10) !== 0; }, is: function(val) { return val === true || val === false; }, pattern: /0|1/ }, "date": { encode: function (val) { if (!this.is(val)) return undefined; return [ val.getFullYear(), ('0' + (val.getMonth() + 1)).slice(-2), ('0' + val.getDate()).slice(-2) ].join("-"); }, decode: function (val) { if (this.is(val)) return val; var match = this.capture.exec(val); return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; }, is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); }, equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); }, pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ }, "json": { encode: angular.toJson, decode: angular.fromJson, is: angular.isObject, equals: angular.equals, pattern: /[^/]*/ }, "any": { // does not encode/decode encode: angular.identity, decode: angular.identity, equals: angular.equals, pattern: /.*/ } }; function getDefaultConfig() { return { strict: isStrictMode, caseInsensitive: isCaseInsensitive }; } function isInjectable(value) { return (isFunction(value) || (isArray(value) && isFunction(value[value.length - 1]))); } /** * [Internal] Get the default value of a parameter, which may be an injectable function. */ $UrlMatcherFactory.$$getDefaultValue = function(config) { if (!isInjectable(config.value)) return config.value; if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); return injector.invoke(config.value); }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#caseInsensitive * @methodOf ui.router.util.$urlMatcherFactory * * @description * Defines whether URL matching should be case sensitive (the default behavior), or not. * * @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`; * @returns {boolean} the current value of caseInsensitive */ this.caseInsensitive = function(value) { if (isDefined(value)) isCaseInsensitive = value; return isCaseInsensitive; }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#strictMode * @methodOf ui.router.util.$urlMatcherFactory * * @description * Defines whether URLs should match trailing slashes, or not (the default behavior). * * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`. * @returns {boolean} the current value of strictMode */ this.strictMode = function(value) { if (isDefined(value)) isStrictMode = value; return isStrictMode; }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy * @methodOf ui.router.util.$urlMatcherFactory * * @description * Sets the default behavior when generating or matching URLs with default parameter values. * * @param {string} value A string that defines the default parameter URL squashing behavior. * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the * parameter is surrounded by slashes, squash (remove) one slash from the URL * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove) * the parameter value from the URL and replace it with this string. */ this.defaultSquashPolicy = function(value) { if (!isDefined(value)) return defaultSquashPolicy; if (value !== true && value !== false && !isString(value)) throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string"); defaultSquashPolicy = value; return value; }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#compile * @methodOf ui.router.util.$urlMatcherFactory * * @description * Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern. * * @param {string} pattern The URL pattern. * @param {Object} config The config object hash. * @returns {UrlMatcher} The UrlMatcher. */ this.compile = function (pattern, config) { return new UrlMatcher(pattern, extend(getDefaultConfig(), config)); }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#isMatcher * @methodOf ui.router.util.$urlMatcherFactory * * @description * Returns true if the specified object is a `UrlMatcher`, or false otherwise. * * @param {Object} object The object to perform the type check against. * @returns {Boolean} Returns `true` if the object matches the `UrlMatcher` interface, by * implementing all the same methods. */ this.isMatcher = function (o) { if (!isObject(o)) return false; var result = true; forEach(UrlMatcher.prototype, function(val, name) { if (isFunction(val)) { result = result && (isDefined(o[name]) && isFunction(o[name])); } }); return result; }; /** * @ngdoc function * @name ui.router.util.$urlMatcherFactory#type * @methodOf ui.router.util.$urlMatcherFactory * * @description * Registers a custom {@link ui.router.util.type:Type `Type`} object that can be used to * generate URLs with typed parameters. * * @param {string} name The type name. * @param {Object|Function} definition The type definition. See * {@link ui.router.util.type:Type `Type`} for information on the values accepted. * @param {Object|Function} definitionFn (optional) A function that is injected before the app * runtime starts. The result of this function is merged into the existing `definition`. * See {@link ui.router.util.type:Type `Type`} for information on the values accepted. * * @returns {Object} Returns `$urlMatcherFactoryProvider`. * * @example * This is a simple example of a custom type that encodes and decodes items from an * array, using the array index as the URL-encoded value: * *
   * var list = ['John', 'Paul', 'George', 'Ringo'];
   *
   * $urlMatcherFactoryProvider.type('listItem', {
   *   encode: function(item) {
   *     // Represent the list item in the URL using its corresponding index
   *     return list.indexOf(item);
   *   },
   *   decode: function(item) {
   *     // Look up the list item by index
   *     return list[parseInt(item, 10)];
   *   },
   *   is: function(item) {
   *     // Ensure the item is valid by checking to see that it appears
   *     // in the list
   *     return list.indexOf(item) > -1;
   *   }
   * });
   *
   * $stateProvider.state('list', {
   *   url: "/list/{item:listItem}",
   *   controller: function($scope, $stateParams) {
   *     console.log($stateParams.item);
   *   }
   * });
   *
   * // ...
   *
   * // Changes URL to '/list/3', logs "Ringo" to the console
   * $state.go('list', { item: "Ringo" });
   * 
* * This is a more complex example of a type that relies on dependency injection to * interact with services, and uses the parameter name from the URL to infer how to * handle encoding and decoding parameter values: * *
   * // Defines a custom type that gets a value from a service,
   * // where each service gets different types of values from
   * // a backend API:
   * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
   *
   *   // Matches up services to URL parameter names
   *   var services = {
   *     user: Users,
   *     post: Posts
   *   };
   *
   *   return {
   *     encode: function(object) {
   *       // Represent the object in the URL using its unique ID
   *       return object.id;
   *     },
   *     decode: function(value, key) {
   *       // Look up the object by ID, using the parameter
   *       // name (key) to call the correct service
   *       return services[key].findById(value);
   *     },
   *     is: function(object, key) {
   *       // Check that object is a valid dbObject
   *       return angular.isObject(object) && object.id && services[key];
   *     }
   *     equals: function(a, b) {
   *       // Check the equality of decoded objects by comparing
   *       // their unique IDs
   *       return a.id === b.id;
   *     }
   *   };
   * });
   *
   * // In a config() block, you can then attach URLs with
   * // type-annotated parameters:
   * $stateProvider.state('users', {
   *   url: "/users",
   *   // ...
   * }).state('users.item', {
   *   url: "/{user:dbObject}",
   *   controller: function($scope, $stateParams) {
   *     // $stateParams.user will now be an object returned from
   *     // the Users service
   *   },
   *   // ...
   * });
   * 
*/ this.type = function (name, definition, definitionFn) { if (!isDefined(definition)) return $types[name]; if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined."); $types[name] = new Type(extend({ name: name }, definition)); if (definitionFn) { typeQueue.push({ name: name, def: definitionFn }); if (!enqueue) flushTypeQueue(); } return this; }; // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s function flushTypeQueue() { while(typeQueue.length) { var type = typeQueue.shift(); if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); angular.extend($types[type.name], injector.invoke(type.def)); } } // Register default types. Store them in the prototype of $types. forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); }); $types = inherit($types, {}); /* No need to document $get, since it returns this */ this.$get = ['$injector', function ($injector) { injector = $injector; enqueue = false; flushTypeQueue(); forEach(defaultTypes, function(type, name) { if (!$types[name]) $types[name] = new Type(type); }); return this; }]; this.Param = function Param(id, type, config, location) { var self = this; config = unwrapShorthand(config); type = getType(config, type, location); var arrayMode = getArrayMode(); type = arrayMode ? type.$asArray(arrayMode, location === "search") : type; if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined) config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to "" var isOptional = config.value !== undefined; var squash = getSquashPolicy(config, isOptional); var replace = getReplace(config, arrayMode, isOptional, squash); function unwrapShorthand(config) { var keys = isObject(config) ? objectKeys(config) : []; var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 && indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1; if (isShorthand) config = { value: config }; config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; }; return config; } function getType(config, urlType, location) { if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations."); if (urlType) return urlType; if (!config.type) return (location === "config" ? $types.any : $types.string); if (angular.isString(config.type)) return $types[config.type]; if (config.type instanceof Type) return config.type; return new Type(config.type); } // array config: param name (param[]) overrides default settings. explicit config overrides param name. function getArrayMode() { var arrayDefaults = { array: (location === "search" ? "auto" : false) }; var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {}; return extend(arrayDefaults, arrayParamNomenclature, config).array; } /** * returns false, true, or the squash value to indicate the "default parameter url squash policy". */ function getSquashPolicy(config, isOptional) { var squash = config.squash; if (!isOptional || squash === false) return false; if (!isDefined(squash) || squash == null) return defaultSquashPolicy; if (squash === true || isString(squash)) return squash; throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string"); } function getReplace(config, arrayMode, isOptional, squash) { var replace, configuredKeys, defaultPolicy = [ { from: "", to: (isOptional || arrayMode ? undefined : "") }, { from: null, to: (isOptional || arrayMode ? undefined : "") } ]; replace = isArray(config.replace) ? config.replace : []; if (isString(squash)) replace.push({ from: squash, to: undefined }); configuredKeys = map(replace, function(item) { return item.from; } ); return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace); } /** * [Internal] Get the default value of a parameter, which may be an injectable function. */ function $$getDefaultValue() { if (!injector) throw new Error("Injectable functions cannot be called at configuration time"); var defaultValue = injector.invoke(config.$$fn); if (defaultValue !== null && defaultValue !== undefined && !self.type.is(defaultValue)) throw new Error("Default value (" + defaultValue + ") for parameter '" + self.id + "' is not an instance of Type (" + self.type.name + ")"); return defaultValue; } /** * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the * default value, which may be the result of an injectable function. */ function $value(value) { function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; } function $replace(value) { var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; }); return replacement.length ? replacement[0] : value; } value = $replace(value); return !isDefined(value) ? $$getDefaultValue() : self.type.$normalize(value); } function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; } extend(this, { id: id, type: type, location: location, array: arrayMode, squash: squash, replace: replace, isOptional: isOptional, value: $value, dynamic: undefined, config: config, toString: toString }); }; function ParamSet(params) { extend(this, params || {}); } ParamSet.prototype = { $$new: function() { return inherit(this, extend(new ParamSet(), { $$parent: this})); }, $$keys: function () { var keys = [], chain = [], parent = this, ignore = objectKeys(ParamSet.prototype); while (parent) { chain.push(parent); parent = parent.$$parent; } chain.reverse(); forEach(chain, function(paramset) { forEach(objectKeys(paramset), function(key) { if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key); }); }); return keys; }, $$values: function(paramValues) { var values = {}, self = this; forEach(self.$$keys(), function(key) { values[key] = self[key].value(paramValues && paramValues[key]); }); return values; }, $$equals: function(paramValues1, paramValues2) { var equal = true, self = this; forEach(self.$$keys(), function(key) { var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key]; if (!self[key].type.equals(left, right)) equal = false; }); return equal; }, $$validates: function $$validate(paramValues) { var keys = this.$$keys(), i, param, rawVal, normalized, encoded; for (i = 0; i < keys.length; i++) { param = this[keys[i]]; rawVal = paramValues[keys[i]]; if ((rawVal === undefined || rawVal === null) && param.isOptional) break; // There was no parameter value, but the param is optional normalized = param.type.$normalize(rawVal); if (!param.type.is(normalized)) return false; // The value was not of the correct Type, and could not be decoded to the correct Type encoded = param.type.encode(normalized); if (angular.isString(encoded) && !param.type.pattern.exec(encoded)) return false; // The value was of the correct type, but when encoded, did not match the Type's regexp } return true; }, $$parent: undefined }; this.ParamSet = ParamSet; } // Register as a provider so it's available to other providers angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory); angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]);