Editing URI Templates This edit will create a new revision. Your details (optional) Name Email (won’t be displayed; might be used for Gravatar) URL Test case details Title * Published (uncheck if you want to fiddle around before making the page public) Description (in case you feel further explanation is needed)(Markdown syntax is allowed) Comparing [uri-templates](https://github.com/marc-portier/uri-templates) (interpreter) and [URI.js](https://github.com/medialize/URI.js) (compiler) implementations of [URI Templates (RFC 6570)](http://tools.ietf.org/html/rfc6570). Are you a spammer? (just answer the question) Preparation code Preparation code HTML (this will be inserted in the <body> of a valid HTML5 document in standards mode) (useful when testing DOM operations or including libraries) <script> (function (exportCallback) { "use strict"; // // helpers // function isArray(value) { return Object.prototype.toString.apply(value) === '[object Array]'; } // performs an array.reduce for objects function objectReduce(object, callback, initialValue) { var propertyName, currentValue = initialValue; for (propertyName in object) { if (object.hasOwnProperty(propertyName)) { currentValue = callback(currentValue, object[propertyName], propertyName, object); } } return currentValue; } // performs an array.reduce, if reduce is not present (older browser...) function arrayReduce(array, callback, initialValue) { var index, currentValue = initialValue; for (index = 0; index < array.length; index += 1) { currentValue = callback(currentValue, array[index], index, array); } return currentValue; } function arrayAll(array, predicate) { var index; for (index = 0; index < array.length; index += 1) { if (!predicate(array[index], index, array)) { return false; } } return true; } function reduce(arrayOrObject, callback, initialValue) { return isArray(arrayOrObject) ? arrayReduce(arrayOrObject, callback, initialValue) : objectReduce(arrayOrObject, callback, initialValue); } /** * Detects, whether a given element is defined in the sense of rfc 6570 * Section 2.3 of the RFC makes clear defintions: * * undefined and null are not defined. * * the empty string is defined * * an array is defined, if it contains at least one defined element * * an object is defined, if it contains at least one defined property * @param object * @return {Boolean} */ function isDefined (object) { var index, propertyName; if (object === null || object === undefined) { return false; } if (isArray(object)) { for (index = 0; index < object.length; index +=1) { if(isDefined(object[index])) { return true; } } return false; } if (typeof object === "string" || typeof object === "number" || typeof object === "boolean") { // even the empty string is considered as defined return true; } // else Object for (propertyName in object) { if (object.hasOwnProperty(propertyName) && isDefined(object[propertyName])) { return true; } } return false; } function isAlpha(chr) { return (chr >= 'a' && chr <= 'z') || ((chr >= 'A' && chr <= 'Z')); } function isDigit(chr) { return chr >= '0' && chr <= '9'; } function isHexDigit(chr) { return isDigit(chr) || (chr >= 'a' && chr <= 'f') || (chr >= 'A' && chr <= 'F'); } function isPctEncoded(chr) { return chr.length === 3 && chr.charAt(0) === '%' && isHexDigit(chr.charAt(1) && isHexDigit(chr.charAt(2))); } /** * Returns if an character is an varchar character according 2.3 of rfc 6570 * @param chr * @return (Boolean) */ function isVarchar(chr) { return isAlpha(chr) || isDigit(chr) || chr === '_' || isPctEncoded(chr); } /** * Returns if chr is an unreserved character according 1.5 of rfc 6570 * @param chr * @return {Boolean} */ function isUnreserved(chr) { return isAlpha(chr) || isDigit(chr) || chr === '-' || chr === '.' || chr === '_' || chr === '~'; } /** * Returns if chr is an reserved character according 1.5 of rfc 6570 * @param chr * @return {Boolean} */ function isReserved(chr) { return chr === ':' || chr === '/' || chr === '?' || chr === '#' || chr === '[' || chr === ']' || chr === '@' || chr === '!' || chr === '$' || chr === '&' || chr === '(' || chr === ')' || chr === '*' || chr === '+' || chr === ',' || chr === ';' || chr === '=' || chr === "'"; } function pctEncode(chr) { return '%' + chr.charCodeAt(0).toString(16).toUpperCase(); } function encode(text, passReserved) { var result = '', index, chr; if (typeof text === "number" || typeof text === "boolean") { text = text.toString(); } for (index = 0; index < text.length; index += 1) { chr = text.charAt(index); if (chr === '%') { if (isHexDigit(text.charAt(index+1)) && isHexDigit(text.charAt(index+2))) { // pass three chars to the result result += text.substr(index, 3); index += 2; } else { result += '%25'; } } else { result += isUnreserved(chr) || (passReserved && isReserved(chr)) ? chr : pctEncode(chr); } } return result; } function encodePassReserved(text) { return encode(text, true); } var operators = (function () { var bySymbol = {}; function create(symbol) { bySymbol[symbol] = { symbol: symbol, separator: (symbol === '?') ? '&' : (symbol === '' || symbol === '+' || symbol === '#') ? ',' : symbol, named: symbol === ';' || symbol === '&' || symbol === '?', ifEmpty: (symbol === '&' || symbol === '?') ? '=' : '', first: (symbol === '+' ) ? '' : symbol, encode: (symbol === '+' || symbol === '#') ? encodePassReserved : encode, toString: function () {return this.symbol;} }; } create(''); create('+'); create('#'); create('.'); create('/'); create(';'); create('?'); create('&'); return {valueOf: function (chr) { if (bySymbol[chr]) { return bySymbol[chr]; } if ("=,!@|".indexOf(chr) >= 0) { throw new Error('Illegal use of reserved operator "' + chr + '"'); } return bySymbol['']; }} }()); function UriTemplate(templateText, expressions) { this.templateText = templateText; this.experssions = expressions; } UriTemplate.prototype.toString = function () { return this.templateText; }; UriTemplate.prototype.expand = function (variables) { var index, result = ''; for (index = 0; index < this.experssions.length; index += 1) { result += this.experssions[index].expand(variables); } return result; }; function encodeLiteral(literal) { var result = '', index, chr; for (index = 0; index < literal.length; index += 1) { chr = literal.charAt(index); if (chr === '%') { if (isHexDigit(literal.charAt(index + 1)) && isHexDigit(literal.charAt(index + 2))) { result += literal.substr(index, 3); index += 2; } else { throw new Error('illegal % found at position ' + index); } } else { result += isReserved(chr) || isUnreserved(chr) ? chr : pctEncode(chr); } } return result; } function LiteralExpression(literal) { this.literal = encodeLiteral(literal); } LiteralExpression.prototype.expand = function () { return this.literal; }; LiteralExpression.prototype.toString = LiteralExpression.prototype.expand; function VariableExpression(templateText, operator, varspecs) { this.templateText = templateText; this.operator = operator; this.varspecs = varspecs; } VariableExpression.prototype.toString = function () { return this.templateText; }; VariableExpression.prototype.expand = function expandExpression(variables) { var result = '', index, varspec, value, valueIsArr, isFirstVarspec = true, operator = this.operator; // callback to be used within array.reduce function reduceUnexploded(result, currentValue, currentKey) { if (isDefined(currentValue)) { if (result.length > 0) { result += ','; } if (!valueIsArr) { result += operator.encode(currentKey) + ','; } result += operator.encode(currentValue); } return result; } function reduceNamedExploded(result, currentValue, currentKey) { if (isDefined(currentValue)) { if (result.length > 0) { result += operator.separator; } result += (valueIsArr) ? encodeLiteral(varspec.varname) : operator.encode(currentKey); result += '=' + operator.encode(currentValue); } return result; } function reduceUnnamedExploded(result, currentValue, currentKey) { if (isDefined(currentValue)) { if (result.length > 0) { result += operator.separator; } if (!valueIsArr) { result += operator.encode(currentKey) + '='; } result += operator.encode(currentValue); } return result; } // expand each varspec and join with operator's separator for (index = 0; index < this.varspecs.length; index += 1) { varspec = this.varspecs[index]; value = variables[varspec.varname]; if (!isDefined(value)) { continue; } if (isFirstVarspec) { result += this.operator.first; isFirstVarspec = false; } else { result += this.operator.separator; } valueIsArr = isArray(value); if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { value = value.toString(); if (this.operator.named) { result += encodeLiteral(varspec.varname); if (value === '') { result += this.operator.ifEmpty; continue; } result += '='; } if (varspec.maxLength && value.length > varspec.maxLength) { value = value.substr(0, varspec.maxLength); } result += this.operator.encode(value); } else if (varspec.maxLength) { // 2.4.1 of the spec says: "Prefix modifiers are not applicable to variables that have composite values." throw new Error('Prefix modifiers are not applicable to variables that have composite values. You tried to expand ' + this + " with " + JSON.stringify(value)); } else if (!varspec.exploded) { if (operator.named) { result += encodeLiteral(varspec.varname); if (!isDefined(value)) { result += this.operator.ifEmpty; continue; } result += '='; } result += reduce(value, reduceUnexploded, ''); } else { // exploded and not string result += reduce(value, operator.named ? reduceNamedExploded : reduceUnnamedExploded, ''); } } return result; }; function parseExpression(outerText) { var text, operator, varspecs = [], varspec = null, varnameStart = null, maxLengthStart = null, index, chr; function closeVarname() { varspec = {varname: text.substring(varnameStart, index), exploded: false, maxLength: null}; varnameStart = null; } function closeMaxLength() { if (maxLengthStart === index) { throw new Error("after a ':' you have to specify the length. position = " + index); } varspec.maxLength = parseInt(text.substring(maxLengthStart, index), 10); maxLengthStart = null; } text = outerText.substr(1, outerText.length - 2); for (index = 0; index < text.length; index += chr.length) { chr = text[index]; if (index === 0) { operator = operators.valueOf(chr); if (operator.symbol !== '') { // first char is operator symbol. so we can continue varnameStart = 1; continue; } // the first char was a regular varname char. We have simple strings and must go on. varnameStart = 0; } if (varnameStart !== null) { // Within varnames pct encoded values are allowed. looks strange to me. if (chr === '%') { if (index > text.length - 2 || !isDigit(text[index + 1] || !isDigit(text[index + 2]))) { throw new Error('illegal char "%" at position ' + index); } chr += text[index + 1] + text[index + 2]; } // the spec says: varname = varchar *( ["."] varchar ) // so a dot is allowed except for the first char if (chr === '.') { if (varnameStart === index) { throw new Error('a varname MUST NOT start with a dot -- see position ' + index); } continue; } if (isVarchar(chr)) { continue; } closeVarname(); } if (maxLengthStart !== null) { if (isDigit(chr)) { continue; } closeMaxLength(); } if (chr === ':') { if (varspec.maxLength !== null) { throw new Error('only one :maxLength is allowed per varspec at position ' + index); } maxLengthStart = index + 1; continue; } if (chr === '*') { if (varspec === null) { throw new Error('explode exploded at position ' + index); } if (varspec.exploded) { throw new Error('explode exploded twice at position ' + index); } if (varspec.maxLength) { throw new Error('an explode (*) MUST NOT follow to a prefix, see position ' + index); } varspec.exploded = true; continue; } // the only legal character now is the comma if (chr === ',') { varspecs.push(varspec); varspec = null; varnameStart = index + 1; continue; } throw new Error("illegal character '" + chr + "' at position " + index); } // for chr if (varnameStart !== null) { closeVarname(); } if (maxLengthStart !== null) { closeMaxLength(); } varspecs.push(varspec); return new VariableExpression(outerText, operator, varspecs); } UriTemplate.parse = function parse(uriTemplateText) { // assert filled string var index, chr, expressions = [], braceOpenIndex = null, literalStart = 0; for (index = 0; index < uriTemplateText.length; index += 1) { chr = uriTemplateText.charAt(index); if (literalStart !== null) { if (chr === '}') { throw new Error('brace was closed in position ' + index + " but never opened"); } if (chr === '{') { if (literalStart < index) { expressions.push(new LiteralExpression(uriTemplateText.substring(literalStart, index))); } literalStart = null; braceOpenIndex = index; } // TODO if (chr === ';') // In a regular URI a colon is only allowed as separator after a uri scheme (e.g. 'http:') and between host and port // (e.g. 'example.com:443'). So the only slash allowed in front of a colon is the '//' after the scheme separator // throw new Error('":" not allowed after a "/" in a regular uri'); continue; } if (braceOpenIndex !== null) { // here just { is forbidden if (chr === '{') { throw new Error('brace was opened in position ' + braceOpenIndex + " and cannot be reopened in position " + index); } if (chr === '}') { if (braceOpenIndex + 1 === index) { throw new Error("empty braces on position " + braceOpenIndex); } expressions.push(parseExpression(uriTemplateText.substring(braceOpenIndex, index + 1))); braceOpenIndex = null; literalStart = index + 1; } continue; } throw new Error('reached unreachable code'); } if (braceOpenIndex !== null) { throw new Error("brace was opened on position " + braceOpenIndex + ", but never closed"); } if (literalStart < uriTemplateText.length) { expressions.push(new LiteralExpression(uriTemplateText.substr(literalStart))); } return new UriTemplate(uriTemplateText, expressions); }; exportCallback(UriTemplate); }(function (UriTemplate) { "use strict"; // export UriTemplate, when module is present, or pass it to window or global if (typeof module !== "undefined") { module.exports = UriTemplate; } else if (typeof window !== "undefined") { window.UriTemplate = UriTemplate; } else { global.UriTemplate = UriTemplate; } } )); /* UriTemplates Template Processor - Version: @VERSION - Dated: @DATE (c) marc.portier@gmail.com - 2011-2012 Licensed under ALPv2 */ ; var uritemplate = (function() { // Below are the functions we originally used from jQuery. // The implementations below are often more naive then what is inside jquery, but they suffice for our needs. function isFunction(fn) { return typeof fn == 'function'; } function isEmptyObject (obj) { for(var name in obj){ return false; } return true; } function extend(base, newprops) { for (var name in newprops) { base[name] = newprops[name]; } return base; } /** * Create a runtime cache around retrieved values from the context. * This allows for dynamic (function) results to be kept the same for multiple * occuring expansions within one template. * Note: Uses key-value tupples to be able to cache null values as well. */ //TODO move this into prep-processing function CachingContext(context) { this.raw = context; this.cache = {}; } CachingContext.prototype.get = function(key) { var val = this.lookupRaw(key); var result = val; if (isFunction(val)) { // check function-result-cache var tupple = this.cache[key]; if (tupple !== null && tupple !== undefined) { result = tupple.val; } else { result = val(this.raw); this.cache[key] = {key: key, val: result}; // NOTE: by storing tupples we make sure a null return is validly consistent too in expansions } } return result; }; CachingContext.prototype.lookupRaw = function(key) { return CachingContext.lookup(this, this.raw, key); }; CachingContext.lookup = function(me, context, key) { var result = context[key]; if (result !== undefined) { return result; } else { var keyparts = key.split('.'); var i = 0, keysplits = keyparts.length - 1; for (i = 0; i<keysplits; i++) { var leadKey = keyparts.slice(0, keysplits - i).join('.'); var trailKey = keyparts.slice(-i-1).join('.'); var leadContext = context[leadKey]; if (leadContext !== undefined) { return CachingContext.lookup(me, leadContext, trailKey); } } return undefined; } }; function UriTemplate(set) { this.set = set; } UriTemplate.prototype.expand = function(context) { var cache = new CachingContext(context); var res = ""; var i = 0, cnt = this.set.length; for (i = 0; i<cnt; i++ ) { res += this.set[i].expand(cache); } return res; }; //TODO: change since draft-0.6 about characters in literals /* extract: The characters outside of expressions in a URI Template string are intended to be copied literally to the URI-reference if the character is allowed in a URI (reserved / unreserved / pct-encoded) or, if not allowed, copied to the URI-reference in its UTF-8 pct-encoded form. */ function Literal(txt ) { this.txt = txt; } Literal.prototype.expand = function() { return this.txt; }; var RESERVEDCHARS_RE = new RegExp("[:/?#\\[\\]@!$&()*+,;=']","g"); function encodeNormal(val) { return encodeURIComponent(val).replace(RESERVEDCHARS_RE, function(s) {return escape(s);} ); } //var SELECTEDCHARS_RE = new RegExp("[]","g"); function encodeReserved(val) { //return encodeURI(val).replace(SELECTEDCHARS_RE, function(s) {return escape(s)} ); return encodeURI(val); // no need for additional replace if selected-chars is empty } function addUnNamed(name, key, val) { return key + (key.length > 0 ? "=" : "") + val; } function addNamed(name, key, val, noName) { noName = noName || false; if (noName) { name = ""; } if (!key || key.length === 0) { key = name; } return key + (key.length > 0 ? "=" : "") + val; } function addLabeled(name, key, val, noName) { noName = noName || false; if (noName) { name = ""; } if (!key || key.length === 0) { key = name; } return key + (key.length > 0 && val ? "=" : "") + val; } var simpleConf = { prefix : "", joiner : ",", encode : encodeNormal, builder : addUnNamed }; var reservedConf = { prefix : "", joiner : ",", encode : encodeReserved, builder : addUnNamed }; var fragmentConf = { prefix : "#", joiner : ",", encode : encodeReserved, builder : addUnNamed }; var pathParamConf = { prefix : ";", joiner : ";", encode : encodeNormal, builder : addLabeled }; var formParamConf = { prefix : "?", joiner : "&", encode : encodeNormal, builder : addNamed }; var formContinueConf = { prefix : "&", joiner : "&", encode : encodeNormal, builder : addNamed }; var pathHierarchyConf = { prefix : "/", joiner : "/", encode : encodeNormal, builder : addUnNamed }; var labelConf = { prefix : ".", joiner : ".", encode : encodeNormal, builder : addUnNamed }; function Expression(conf, vars ) { extend(this, conf); this.vars = vars; } Expression.build = function(ops, vars) { var conf; switch(ops) { case '' : conf = simpleConf; break; case '+' : conf = reservedConf; break; case '#' : conf = fragmentConf; break; case ';' : conf = pathParamConf; break; case '?' : conf = formParamConf; break; case '&' : conf = formContinueConf; break; case '/' : conf = pathHierarchyConf; break; case '.' : conf = labelConf; break; default : throw "Unexpected operator: '"+ops+"'"; } return new Expression(conf, vars); }; Expression.prototype.expand = function(context) { var joiner = this.prefix; var nextjoiner = this.joiner; var buildSegment = this.builder; var res = ""; var i = 0, cnt = this.vars.length; for (i = 0 ; i< cnt; i++) { var varspec = this.vars[i]; varspec.addValues(context, this.encode, function(key, val, noName) { var segm = buildSegment(varspec.name, key, val, noName); if (segm !== null && segm !== undefined) { res += joiner + segm; joiner = nextjoiner; } }); } return res; }; var UNBOUND = {}; /** * Helper class to help grow a string of (possibly encoded) parts until limit is reached */ function Buffer(limit) { this.str = ""; if (limit === UNBOUND) { this.appender = Buffer.UnboundAppend; } else { this.len = 0; this.limit = limit; this.appender = Buffer.BoundAppend; } } Buffer.prototype.append = function(part, encoder) { return this.appender(this, part, encoder); }; Buffer.UnboundAppend = function(me, part, encoder) { part = encoder ? encoder(part) : part; me.str += part; return me; }; Buffer.BoundAppend = function(me, part, encoder) { part = part.substring(0, me.limit - me.len); me.len += part.length; part = encoder ? encoder(part) : part; me.str += part; return me; }; function arrayToString(arr, encoder, maxLength) { var buffer = new Buffer(maxLength); var joiner = ""; var i = 0, cnt = arr.length; for (i=0; i<cnt; i++) { if (arr[i] !== null && arr[i] !== undefined) { buffer.append(joiner).append(arr[i], encoder); joiner = ","; } } return buffer.str; } function objectToString(obj, encoder, maxLength) { var buffer = new Buffer(maxLength); var joiner = ""; var k; for (k in obj) { if (obj.hasOwnProperty(k) ) { if (obj[k] !== null && obj[k] !== undefined) { buffer.append(joiner + k + ',').append(obj[k], encoder); joiner = ","; } } } return buffer.str; } function simpleValueHandler(me, val, valprops, encoder, adder) { var result; if (valprops.isArr) { result = arrayToString(val, encoder, me.maxLength); } else if (valprops.isObj) { result = objectToString(val, encoder, me.maxLength); } else { var buffer = new Buffer(me.maxLength); result = buffer.append(val, encoder).str; } adder("", result); } function explodeValueHandler(me, val, valprops, encoder, adder) { if (valprops.isArr) { var i = 0, cnt = val.length; for (i = 0; i<cnt; i++) { adder("", encoder(val[i]) ); } } else if (valprops.isObj) { var k; for (k in val) { if (val.hasOwnProperty(k)) { adder(k, encoder(val[k]) ); } } } else { // explode-requested, but single value adder("", encoder(val)); } } function valueProperties(val) { var isArr = false; var isObj = false; var isUndef = true; //note: "" is empty but not undef if (val !== null && val !== undefined) { isArr = (val.constructor === Array); isObj = (val.constructor === Object); isUndef = (isArr && val.length === 0) || (isObj && isEmptyObject(val)); } return {isArr: isArr, isObj: isObj, isUndef: isUndef}; } function VarSpec (name, vhfn, nums) { this.name = unescape(name); this.valueHandler = vhfn; this.maxLength = nums; } VarSpec.build = function(name, expl, part, nums) { var valueHandler, valueModifier; if (!!expl) { //interprete as boolean valueHandler = explodeValueHandler; } else { valueHandler = simpleValueHandler; } if (!part) { nums = UNBOUND; } return new VarSpec(name, valueHandler, nums); }; VarSpec.prototype.addValues = function(context, encoder, adder) { var val = context.get(this.name); var valprops = valueProperties(val); if (valprops.isUndef) { return; } // ignore empty values this.valueHandler(this, val, valprops, encoder, adder); }; //----------------------------------------------parsing logic // How each varspec should look like var VARSPEC_RE=/([^*:]*)((\*)|(:)([0-9]+))?/; var match2varspec = function(m) { var name = m[1]; var expl = m[3]; var part = m[4]; var nums = parseInt(m[5], 10); return VarSpec.build(name, expl, part, nums); }; // Splitting varspecs in list with: var LISTSEP=","; // How each template should look like var TEMPL_RE=/(\{([+#.;?&\/])?(([^.*:,{}|@!=$()][^*:,{}$()]*)(\*|:([0-9]+))?(,([^.*:,{}][^*:,{}]*)(\*|:([0-9]+))?)*)\})/g; // Note: reserved operators: |!@ are left out of the regexp in order to make those templates degrade into literals // (as expected by the spec - see tests.html "reserved operators") var match2expression = function(m) { var expr = m[0]; var ops = m[2] || ''; var vars = m[3].split(LISTSEP); var i = 0, len = vars.length; for (i = 0; i<len; i++) { var match; if ( (match = vars[i].match(VARSPEC_RE)) === null) { throw "unexpected parse error in varspec: " + vars[i]; } vars[i] = match2varspec(match); } return Expression.build(ops, vars); }; var pushLiteralSubstr = function(set, src, from, to) { if (from < to) { var literal = src.substr(from, to - from); set.push(new Literal(literal)); } }; var parse = function(str) { var lastpos = 0; var comp = []; var match; var pattern = TEMPL_RE; pattern.lastIndex = 0; // just to be sure while ((match = pattern.exec(str)) !== null) { var newpos = match.index; pushLiteralSubstr(comp, str, lastpos, newpos); comp.push(match2expression(match)); lastpos = pattern.lastIndex; } pushLiteralSubstr(comp, str, lastpos, str.length); return new UriTemplate(comp); }; //-------------------------------------------comments and ideas //TODO: consider building cache of previously parsed uris or even parsed expressions? return parse; }()); /*! * URI.js - Mutating URLs * URI Template Support - http://tools.ietf.org/html/rfc6570 * * Version: 1.6.3 * * Author: Rodney Rehm * Web: http://medialize.github.com/URI.js/ * * Licensed under * MIT License http://www.opensource.org/licenses/mit-license * GPL v3 http://opensource.org/licenses/GPL-3.0 * */ (function(undefined) { var hasOwn = Object.prototype.hasOwnProperty, _use_module = typeof module !== "undefined" && module.exports, _load_module = function(module) { return _use_module ? require('./' + module) : window[module]; }, URI = {}, URITemplate = function(expression) { // serve from cache where possible if (URITemplate._cache[expression]) { return URITemplate._cache[expression]; } // Allow instantiation without the 'new' keyword if (!(this instanceof URITemplate)) { return new URITemplate(expression); } this.expression = expression; URITemplate._cache[expression] = this; return this; }, Data = function(data) { this.data = data; this.cache = {}; }, p = URITemplate.prototype, operators = { // Simple string expansion '' : { prefix: "", separator: ",", named: false, empty_name_separator: false, encode : "encode" }, // Reserved character strings '+' : { prefix: "", separator: ",", named: false, empty_name_separator: false, encode : "encodeReserved" }, // Fragment identifiers prefixed by "#" '#' : { prefix: "#", separator: ",", named: false, empty_name_separator: false, encode : "encodeReserved" }, // Name labels or extensions prefixed by "." '.' : { prefix: ".", separator: ".", named: false, empty_name_separator: false, encode : "encode" }, // Path segments prefixed by "/" '/' : { prefix: "/", separator: "/", named: false, empty_name_separator: false, encode : "encode" }, // Path parameter name or name=value pairs prefixed by ";" ';' : { prefix: ";", separator: ";", named: true, empty_name_separator: false, encode : "encode" }, // Query component beginning with "?" and consisting // of name=value pairs separated by "&"; an '?' : { prefix: "?", separator: "&", named: true, empty_name_separator: true, encode : "encode" }, // Continuation of query-style &name=value pairs // within a literal query component. '&' : { prefix: "&", separator: "&", named: true, empty_name_separator: true, encode : "encode" } // The operator characters equals ("="), comma (","), exclamation ("!"), // at sign ("@"), and pipe ("|") are reserved for future extensions. }; URITemplate._cache = {}; URI.encode = function(string) { return encodeURIComponent(string).replace(/[!'()*]/g, escape); }; URI.encodeReserved = function(string) { return encodeURIComponent(string).replace(URI.characters.reserved.encode.expression, function(c) { return URI.characters.reserved.encode.map[c]; }); }; URI.characters = { reserved: { encode: { // RFC3986 2.1: For consistency, URI producers and normalizers should // use uppercase hexadecimal digits for all percent-encodings. expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig, map: { // gen-delims "%3A": ":", "%2F": "/", "%3F": "?", "%23": "#", "%5B": "[", "%5D": "]", "%40": "@", // sub-delims "%21": "!", "%24": "$", "%26": "&", "%27": "'", "%28": "(", "%29": ")", "%2A": "*", "%2B": "+", "%2C": ",", "%3B": ";", "%3D": "=" } } } }; window.URI = URI; window.URITemplate = URITemplate; // storage for already parsed templates URITemplate._cache = {}; // pattern to identify expressions [operator, variable-list] in template URITemplate.EXPRESSION_PATTERN = /\{([^a-zA-Z0-9%_]?)([^\}]+)(\}|$)/g; // pattern to identify variables [name, explode, maxlength] in variable-list URITemplate.VARIABLE_PATTERN = /^([^*:]+)((\*)|:(\d+))?$/; // pattern to verify variable name integrity URITemplate.VARIABLE_NAME_PATTERN = /[^a-zA-Z0-9%_]/; URITemplate.expand = function(expression, data) { var options = operators[expression.operator], variables = expression.variables, buffer = [], d, variable, i, l, value, type; for (i = 0; variable = variables[i]; i++) { // fetch simplified data source d = data.get(variable.name); if (!d.val.length) { if (d.type) { // empty variable buffer.push(""); } continue; } type = options.named ? "Named" : "Unnamed"; buffer.push(URITemplate["expand" + type]( d, options, variable.explode, variable.explode && options.separator || ",", variable.maxlength, variable.name )); } if (buffer.length) { return options.prefix + buffer.join(options.separator); } else { return ""; } }; URITemplate.expandNamed = function(d, options, explode, separator, length, name) { var result = "", encode = options.encode, empty_name_separator = options.empty_name_separator, _encode = !d[encode].length, _name = d.type === 2 ? '': URI[encode](name), _value, i, l; for (i = 0, l = d.val.length; i < l; i++) { if (length) { _value = URI[encode](d.val[i][1].substring(0, length)); if (d.type === 2) { _name = URI[encode](d.val[i][0].substring(0, length)); } } else if (_encode) { _value = URI[encode](d.val[i][1]); if (d.type === 2) { _name = URI[encode](d.val[i][0]); d[encode].push([_name, _value]); } else { d[encode].push([undefined, _value]); } } else { _value = d[encode][i][1]; if (d.type === 2) { _name = d.name[encode][i][0]; } } if (result) { result += separator; } if (!explode) { if (!i) { // first element, so prepend variable name result += URI[encode](name) + (empty_name_separator || _value ? "=" : ""); } if (d.type === 2) { result += _name + ","; } result += _value; } else { result += _name + (empty_name_separator || _value ? "=" : "") + _value; } } return result; }; URITemplate.expandUnnamed = function(d, options, explode, separator, length, name) { var result = "", encode = options.encode, empty_name_separator = options.empty_name_separator, _encode = !d[encode].length, _name, _value, i, l; for (i = 0, l = d.val.length; i < l; i++) { if (length) { _value = URI[encode](d.val[i][1].substring(0, length)); } else if (_encode) { _value = URI[encode](d.val[i][1]); d[encode].push([ d.type === 2 ? URI[encode](d.val[i][0]) : undefined, _value ]); } else { _value = d[encode][i][1]; } if (result) { result += separator; } if (d.type === 2) { if (length) { _name = URI[encode](d.val[i][0].substring(0, length)); } else { _name = d[encode][i][0]; } result += _name; if (explode) { result += (empty_name_separator || _value ? "=" : ""); } else { result += ","; } } result += _value; } return result; }; p.expand = function(data) { var result = ""; if (!this.parts || !this.parts.length) { this.parse(); } if (!(data instanceof Data)) { data = new Data(data); } for (var i = 0, l = this.parts.length; i < l; i++) { result += typeof this.parts[i] === "string" ? this.parts[i] : URITemplate.expand(this.parts[i], data); } return result; }; p.parse = function() { var expression = this.expression, ePattern = URITemplate.EXPRESSION_PATTERN, vPattern = URITemplate.VARIABLE_PATTERN, nPattern = URITemplate.VARIABLE_NAME_PATTERN, parts = [], pos = 0, variables, eMatch, vMatch; ePattern.lastIndex = 0; while (true) { eMatch = ePattern.exec(expression); if (eMatch === null) { // push trailing literal parts.push(expression.substring(pos)); break; } else { // push leading literal parts.push(expression.substring(pos, eMatch.index)); pos = eMatch.index + eMatch[0].length; } if (!operators[eMatch[1]]) { throw new Error('Unknown Operator "' + eMatch[1] + '" in "' + eMatch[0] + '"'); } else if (!eMatch[3]) { throw new Error('Unclosed Expression "' + eMatch[0] + '"'); } // parse variable-list variables = eMatch[2].split(','); for (var i = 0, l = variables.length; i < l; i++) { vMatch = variables[i].match(vPattern); if (vMatch === null) { throw new Error('Invalid Variable "' + variables[i] + '" in "' + eMatch[0] + '"'); } if (vMatch[1].match(nPattern)) { throw new Error('Invalid Variable Name "' + vMatch[1] + '" in "' + eMatch[0] + '"'); } variables[i] = { name: vMatch[1], explode: !!vMatch[3], maxlength: vMatch[4] && parseInt(vMatch[4], 10) }; } if (!variables.length) { throw new Error('Expression Missing Variable(s) "' + eMatch[0] + '"'); } parts.push({ expression: eMatch[0], operator: eMatch[1], variables: variables }); } if (!parts.length) { // template doesn't contain any expressions parts.push(expression); } this.parts = parts; return this; }; // simplify data structures Data.prototype.get = function(key) { var data = this.data, d = { type: 0, val: [], encode: [], encodeReserved: [] }, i, l, value; if (this.cache[key] !== undefined) { // we've already processed this key return this.cache[key]; } this.cache[key] = d; if (String(Object.prototype.toString.call(data)) === "[object Function]") { // data itself is a callback value = data(key); } else if (String(Object.prototype.toString.call(data[key])) === "[object Function]") { // data is a map of callbacks value = data[key](key); } else { // data is a map of data value = data[key]; } // generalize input if (value === undefined || value === null) { return d; } else if (String(Object.prototype.toString.call(value)) === "[object Array]") { for (i = 0, l = value.length; i < l; i++) { if (value[i] !== undefined && value[i] !== null) { d.val.push([undefined, String(value[i])]); } } if (d.val.length) { d.type = 3; // array } } else if (String(Object.prototype.toString.call(value)) === "[object Object]") { for (i in value) { if (hasOwn.call(value, i) && value[i] !== undefined && value[i] !== null) { d.val.push([i, String(value[i])]); } } if (d.val.length) { d.type = 2; // array } } else { d.type = 1; // primitive d.val.push([undefined, String(value)]); } return d; }; })(); window._values = { "count" : ["one", "two", "three"], "dom" : ["example", "com"], "dub" : "me/too", "hello" : "Hello World!", "half" : "50%", "var" : "value", "who" : "fred", "base" : "http://example.com/home/", "path" : "/foo/bar", "list" : ["red", "green", "blue"], "keys" : { "semi" : ";", "dot" : ".", "comma" : "," }, "v" : "6", "x" : "1024", "y" : "768", "empty" : "", "empty_keys" : [], "undef" : null }; </script> Include JavaScript libraries as follows: <script src="//cdn.ext/library.js"></script> Define setup for all tests (variables, functions, arrays or other objects that will be used in the tests) (runs before each clocked test loop, outside of the timed code region) (e.g. define local test variables, reset global variables, clear canvas, etc.) (see FAQ) Define teardown for all tests (runs after each clocked test loop, outside of the timed code region) (see FAQ) Code snippets to compare Test 1 Title Async (check if this is an asynchronous test) Code var expression = "{count}"; var template = new URITemplate(expression); var expanded = template.expand(window._values); delete URITemplate._cache[expression]; Test 2 Title Async (check if this is an asynchronous test) Code var expression = "{count}"; var template = uritemplate(expression); var expanded = template.expand(window._values); Test 3 Title Async (check if this is an asynchronous test) Code var expression = "{var}string{hello}string{half}stringO{empty}XstringO{undef}Xstring{x,y}string{x,hello,y}string?{x,empty}string?{x,undef}string?{undef,y}string{var:3}string{var:30}string{list}string{list*}string{keys}string{keys*}string{count}string{count*}string{/count}string{/count*}string{;count}string{;count*}string{?count}string{?count*}string{&count*}string"; var template = new URITemplate(expression); var expanded = template.expand(window._values); delete URITemplate._cache[expression]; Test 4 Title Async (check if this is an asynchronous test) Code var expression = "{var}string{hello}string{half}stringO{empty}XstringO{undef}Xstring{x,y}string{x,hello,y}string?{x,empty}string?{x,undef}string?{undef,y}string{var:3}string{var:30}string{list}string{list*}string{keys}string{keys*}string{count}string{count*}string{/count}string{/count*}string{;count}string{;count*}string{?count}string{?count*}string{&count*}string"; var template = uritemplate(expression); var expanded = template.expand(window._values); Test 5 Title Async (check if this is an asynchronous test) Code var expression = "{count}"; var template = UriTemplate.parse(expression); var expanded = template.expand(window._values); Test 6 Title Async (check if this is an asynchronous test) Code var expression = "{var}string{hello}string{half}stringO{empty}XstringO{undef}Xstring{x,y}string{x,hello,y}string?{x,empty}string?{x,undef}string?{undef,y}string{var:3}string{var:30}string{list}string{list*}string{keys}string{keys*}string{count}string{count*}string{/count}string{/count*}string{;count}string{;count*}string{?count}string{?count*}string{&count*}string"; var template = UriTemplate.parse(expression); var expanded = template.expand(window._values);