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> /* 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);