/*
UriTemplates Template Processor - Version: @VERSION - Dated: @DATE (c) marc.portier@gmail.com - 2011-2012 Licensed under APLv2 (http://opensource.org/licenses/Apache-2.0) */
; 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;
}());