URI Templates
JavaScript performance comparison
Info
Comparing uri-templates (interpreter) and URI.js (compiler) implementations of URI Templates (RFC 6570).
Preparation code
<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>
Test runner
Warning! For accurate results, please disable Firebug before running the tests. (Why?)
Java applet disabled.
| Test | Ops/sec | |
|---|---|---|
Simple URITemplate |
|
pending… |
Simple uritemplate |
|
pending… |
Complex URITemplate |
|
pending… |
Complex uritemplate |
|
pending… |
Simple UriTemplate |
|
pending… |
Complex UriTemplate |
|
pending… |
Compare results of other browsers
Revisions
You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:
- Revision 1: published by Rodney
- Revision 2: published by Rodney
- Revision 3: published by Rodney
0 comments