URI Templates

JavaScript performance comparison

Revision 4 of this test case created

Info

Comparing uritemplate, UriTemplate, URITemplate (URI.js) and URI.Template 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;
};



})();



// https://github.com/litejs/uri-template-lite
!function(URI) {
        var RESERVED = /[\]\[:\/?#@!$&()*+,;=']/g
        , SEPARATORS = { '':",", '+':",", '#':",", '?':"&" }
        , escapeRe = /[.*+?^=!:${}()|\[\]\/\\]/g
        , expandRe = /\{([+#.\/;?&]?)((?:[\w%.]+(\*|:\d)?,?)+)\}/g
        , parseRe  =  new RegExp(expandRe.source + "|.[^{]*?", "g")

        function encodeNormal(val) {
                return encodeURIComponent(val).replace(RESERVED, escape)
        }

        function addNamed(name, val, sep) {
                return name + (val || sep == "&" ? "=" : "") + val;
        }

        function mapCleanJoin(arr, mapFn, joinStr) {
                arr = arr.map(mapFn).filter(function(s){return typeof s == "string"})
                return arr.length && arr.join(joinStr)
        }

        function expand(template, data) {
                return template.replace(expandRe, function(_, op, vals) {
                        var sep = SEPARATORS[op] || op
                        , enc = op && sep == "," ? encodeURI : encodeNormal
                        , add = (sep == ";" || sep == "&") && addNamed
                        , out = mapCleanJoin(vals.split(","), function(name) {
                                var exp = name != (name = name.split("*")[0])
                                , len = !exp && (len = name.split(":"), name=len[0], len[1])
                                , val = data[name]

                                if (val == null) return

                                if (typeof val == "object") {
                                        if (Array.isArray(val)) {
                                                val = mapCleanJoin(val, enc, exp ? add ? sep + name + "=" : sep : "," )
                                        }
                                        else {
                                                len = exp ? "=" : ","
                                                val = mapCleanJoin(Object.keys(val), function(key) {
                                                        return enc(key) + len + enc(val[key])
                                                }, exp && (add || sep == "/") ? sep : "," )
                                                if (exp) add = null
                                        }
                                        if (!val) return
                                }
                                else {
                                        val = enc( len ? val.slice(0, len) : val )
                                }

                                return add ? add(name, val, sep) : val
                        }, sep)

                        return out ? (op!="+"?op+out:out) : out === "" && (op=="#"||op==".") ? op : ""
                }
        )}

        URI.expand = expand

}(this.URI || (this.URI = {}));






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,
            "inc": 1
        };

</script>
<script>
Benchmark.prototype.setup = function() {
    window._values.inc++
};
</script>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
Simple URITemplate
var expression = "{count,inc}";
var template = new URITemplate(expression);
var expanded = template.expand(window._values);
delete URITemplate._cache[expression];
pending…
Simple uritemplate
var expression = "{count,inc}";
var template = uritemplate(expression);
var expanded = template.expand(window._values);
 
pending…
Complex URITemplate
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];
pending…
Complex uritemplate
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);
pending…
Simple UriTemplate
var expression = "{count,inc}";
var template = UriTemplate.parse(expression);
var expanded = template.expand(window._values);
 
pending…
Complex UriTemplate
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);
pending…
Simple URI.Template
var expression = "{count,inc}";
var expanded = URI.expand(expression, window._values);
 
pending…
Complex URI.Template
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 expanded = URI.expand(expression, window._values);
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:

0 comments

Add a comment