JavaScript template language shootoff (v997)

Revision 997 of this benchmark created on


Description

A limited comparison of some popular JavaScript templating engines on a short template: 6 header tags, and 10 list items. Compared templating engines:

Same as 412 but excludes DOM node generation.

Preparation HTML

<script>
/*
 |-----------------------------------------------------------------------------|
 |                                jsToolkit 2.0                                |
 |-----------------------------------------------------------------------------|
 |                                Developed By:                                |
 |                              Joshua Blackburn                               |
 |                                                                             |
 |-----------------------------------------------------------------------------|
 | JavaScript tools to increase productivity                                   |
 | IE 6                                                                        |
 |-----------------------------------------------------------------------------|
 | September 2004 | jsToolkit 1.0                                              |
 | October   2005 | jsToolkit 2.0.  Added Perl like functions map/grep.        |
 |-----------------------------------------------------------------------------|
 */
var jss = {
    block  : '',
    obj    : '',
    params : {},
    page   : ''
};

function vGetParams() {
    var d  = document.URL.split('?'), pn = window.location.pathname;
    var dn = pn.substring(pn.lastIndexOf('/') + 1);
    jss.page = dn.replace(/[\.\:\\]/g,'');

    if ( d[1] ) {
        var argList = d[1].split('&');
        var vars = [];
        if ( typeof(argList) == 'object' ) {
            for ( var stuff in argList ) {
                vars = argList[stuff].split('=');
                //vars = d[1].split('=');
                jss.params[vars[0]] = vars[1];
            }
        } else {
            vars = d[1].split('=');
            jss.params[vars[0]] = vars[1];
        }
        //jss.page = dn.replace('.', '');
        return jss.params;
    }
}

function vCount() {
    var obj = arguments[0];
    var i = 0;
    for ( var c in obj ) {
        if ( c == 'toXMLRPC' ) continue;
        i++;
    }

    obj = null;
    return i;
}

function stringToHash(str, del1, del2) {
    var argList = str.split(del1);
    var vars = [], obj = {};
    if ( typeof(argList) == 'object' ) {
        for ( var stuff in argList ) {
            vars = argList[stuff].split(del2);
            //vars = d[1].split('=');
            obj[vars[0]] = vars[1];
        }
    } else {
        vars = str.split(del2);
        obj[vars[0]] = vars[1];
    }
    return obj;
}

// Converts a hash to a string with given delimeters.  delimeters default to = (del1) and & (del2)
function hashToString(obj, del1, del2) {
    var str = [];
    var count = vCount(obj), counter = 1;
    for ( var i in obj ) {
        if ( typeof obj[i] == 'string' || typeof obj[i] == 'number' ) str.push(i + (del1 || "=") + obj[i] + (counter != count ? (del2 || "&") : ''));
        ++counter;
    }
    return str.join('');
}

function vTypeof( obj ) {
    var vType = typeof obj;

    if( vType == "function" ) {
        var vFunction = obj.toString();
        if( ( /^\/.*\/[gi]??[gi]??$/ ).test( vFunction ) ) {
            return "regex";
        } else if( ( /^\[object.*\]$/i ).test( vFunction ) ) {
            vType = "object"
        }
    }

    if( vType != "object" ) {
        return vType;
    }

    switch( obj ) {
        case null:
            return "null";
        case window:
            return "window";
        case window.event:
            return "event";
    }

    if( window.event && ( event.type == obj.type ) ) {
        return "event";
    }

    try {
        if ( obj.join(',') ) return "array";
    } catch(e) {}

    var fConstructor = obj.constructor;
    if( fConstructor != null ) {
        switch( fConstructor ) {
            case Array:
                vType = "array";
                break;
            case Date:
                return "date";
            case RegExp:
                return "regex";
            case Object:
                vType = "object";
                break;
            case ReferenceError:
                return "error";
            default:
                var sConstructor = fConstructor.toString();
                var aMatch = sConstructor.match( /\s*function (.*)\(/ );
                if( aMatch != null ) {
                    return 'object';
                }
        }
    }

    var nNodeType = obj.nodeType;
    if( nNodeType != null ) {
        switch( nNodeType ) {
            case 1:
                if( obj.item == null ) {
                    return "domelement";
                }
                break;
            case 3:
                return "textnode";
        }
    }

    if( obj.toString != null ) {
        var sExpression = obj.toString();
        var aMatch = sExpression.match( /^\[object (.*)\]$/i );
        if( aMatch != null ) {
            var sMatch = aMatch[ 1 ];
            switch( sMatch.toLowerCase() ) {
                case "event":
                    return "event";
                case "math":
                    return "math";
                case "error":
                    return "error";
                case "mimetypearray":
                    return "mimetypecollection";
                case "pluginarray":
                    return "plugincollection";
                case "windowcollection":
                    return "window";
                case "nodelist":
                case "htmlcollection":
                case "elementarray":
                    return "domcollection";
            }
        }
    }

    if( obj.moveToBookmark && obj.moveToElementText ) {
        return "textrange";
    } else if( obj.callee != null ) {
        return "array";
    } else if( obj.item != null ) {
        return "domcollection";
    }

    return vType;

}

function rtrim(str) {
    try {
        return str.replace(/\s+$/,'');
    } catch(e) { return ''; }
}

function ltrim(str) {
    try {
        return str.replace(/^\s+/,'');
    } catch(e) { return ''; }
}

function trim(str) {
    try {
        return str.replace(/(^\s+|\s+$)/g, '');
    } catch(e) { return ''; }
}


// Check if a value is in an array
function contains(ary, val) {
    //if ( vTypeof(ary) != 'array' || vTypeof(ary) != 'arguments' ) return false;

    if ( vTypeof(val) == 'object' ) {

        // Extract first key and value from the value and assign to temp array
        var v = [];
        for ( var m in val ) {
            v[0] = m;
            v[1] = val[m];
            break;
        }

        // Iterate over array and check for hashes
        for ( var i = 0; i < ary.length; i++ ) {
            if ( vTypeof(ary[i]) == 'object' ) {
                for ( var j in ary[i] ) {
                    //alert("j: " + j + " val: " + ary[i][j] + " *" + v[0] + "**" + v[1] + "*");
                    if ( j == v[0] ) {
                        if ( trim(ary[i][j]) == trim(v[1]) ) return ( i + 1);
                    }
                }
            }
        }

    } else {
        for ( var i = 0; i < ary.length; i++ ) {
            if ( ary[i] == val ) return (i + 1);
        }
    }

    return -1;
}

function vGet(id) {
    return document.getElementById(id);
}

document.getElementsByClassName = function(cName) {
    var domE;

    if ( arguments[1] ) {
        if ( vTypeof(arguments[1]) == 'string' ) {
            if ( document.getElementById(arguments[1]) ) {
                domE = document.getElementById(arguments[1]).getElementsByTagName("*");
            } else { return false; }
        } else {
            domE = arguments[1];
        }
    } else {
        if ( document.all ) {
            domE = document.all;
        } else {
            domE = document.getElementsByTagName("*");
        }
    }

    var yourElements = [];

    //for ( var a = domE.length - 1; a >= 0; a--) {
    for ( var a in domE ) {
        if ( ( new RegExp("^(?:[^\\s]+\\s)*"+cName+"(?:\\s.+)?$") ).test(domE[a].className ) ) {
            yourElements[yourElements.length] = domE[a];
        }
    }

    domE = null;
    return yourElements;
}

document.getElementsByID = function(cName) {
    var domE;

    arguments[1] ? ( vTypeof(arguments[1]) == 'string' ? domE = (document.getElementById(arguments[1]) ? document.getElementById(arguments[1]).getElementsByTagName("*") : null) : domE = arguments[1] ) : ( document.all ? domE = document.all : domE = document.getElementsByTagName("*") );
    if ( vTypeof(domE) == 'null' ) return false;
    var yourElements = [];
    for ( var a = 0; a < domE.length; a++) {
        if (domE[a].id == cName) {
            yourElements[yourElements.length] = domE[a];
        }
    }

    return yourElements;
}

document.remove = function() {
    $map(arguments, function (i) { $(i).parentNode.removeChild( $(i) ); });
}

function $() {
    //From prototype
    var ary = [];

    for ( var i = 0; i < arguments.length; i++ ) {
        if ( typeof arguments[i] == 'string' ) {
            if ( arguments.length == 1) {
                return document.getElementById(arguments[i]);
            } else {
                ary.push(document.getElementById(arguments[i]));
            }
        }
    }

    return ary;
}

function $map(ary) {
    var mapped  = [];
    var actions = arguments;
    for (var i = 0; i < ary.length; i++ ) {
        for ( var x = 0; x < actions.length; x++ ) {
            if ( x != 0 ) try{mapped.push(actions[x](ary[i], i));}catch(e){return ary;}
        }
    }
    return mapped;
}

function $grep(ary, action) {
    var mapped = [];
    var result;
    for (var i = 0; i < ary.length; i++ ) {
        try {
            result = action(ary[i], i);
            if ( result ) mapped.push(result);
        } catch(e){return ary;};
    }
    return mapped;
}

function $mapHash(obj, action) {
    var mapped = {};
    var actions = arguments, counter = 0, key;
    for (var i in obj ) {
        for ( var x = 0; x < actions.length; x++ ) {
            if ( x != 0 )
                try {
                    key = i;
                    if ( x > 1 ) {
                        while ( mapped[key] ) {
                            key = i + counter;
                            ++counter;
                        }
                        counter = 0;
                    }
                    mapped[key] = actions[x](obj[i], i);
                } catch(e){return obj;}
        }
    }
    return mapped;
}

function $grepHash(obj, action) {
    var mapped = {};
    var result;
    for (var i in obj ) {
        try {
            result = action(obj[i], i);
            if ( result ) mapped[i] = result;
        } catch(e){return obj;};
    }
    return mapped;
}

function $dcopy(obj) {
    var objType = vTypeof(obj);
    var temp;
    switch ( objType ) {
        case 'array':
            temp = [];
            for ( var i = 0; i < obj.length; i++ ) {
                if ( vTypeof(obj[i]) == 'array' || vTypeof(obj[i]) == 'object' ) {
                    temp.push($dcopy(obj[i]));
                } else {
                    temp.push(obj[i]);
                }
            }
            return temp;
            break;
        case 'object':
            temp = {};
            for ( var i in obj ) {
                if ( vTypeof(obj[i]) == 'array' || vTypeof(obj[i]) == 'object' ) {
                    temp[i] = $dcopy(obj[i]);
                } else {
                    temp[i] = obj[i];
                }
            }
            return temp;
        default:
            return obj;
    }
}

function $concat(obj) {
    var mapped = obj;
    var hashes = arguments, counter = 0, key;
    for ( var x = 1; x < hashes.length; x++ ) {
        try {
            for (var i in hashes[x] ) {
                key = i;
                if ( x > 0 ) {
                    while ( mapped[key] ) {
                        key = i + counter;
                        ++counter;
                    }
                    counter = 0;
                }
                mapped[key] = hashes[x][i];
            }
        } catch(e){continue;}
    }
    return mapped;
}

function $renameKey(hash, oldKey, newKey) {
    try {
        if ( hash[oldKey] ) {
            hash[newKey] = hash[oldKey];
            delete hash[oldKey];
            return true;
        } else {
            return false;
        }
    } catch(e) { return false; }
}

/* ============================== END Tools ================================= */

/* =============== Session ====================== */
/*
 |-----------------------------------------------------------------------------|
 |                                vSession 1.0                                 |
 |-----------------------------------------------------------------------------|
 |                                Developed By:                                |
 |                              Joshua Blackburn                               |
 |                                                                             |
 |-----------------------------------------------------------------------------|
 | Client side "session". JavaScript data store.                               |
 | IE 6                                                                        |
 |-----------------------------------------------------------------------------|
 | September 2004 | vSession 1.0                                               |
 |-----------------------------------------------------------------------------|
 */

function vSessionGet() {
    var objRef = "top.vSession.";
    objRef += arguments[0].replace(/\s/g, '1s1');
    try {
        var val = eval(objRef);
        return val;
    } catch(e) {
        //alert("vSessionGet:False");
        return false;
    }

}

function vSessionGetCount() {
    var obj = vSessionGet(arguments[0]);
    var i = 0;
    for ( var c in obj ) {
        if ( c == "toXMLRPC" ) continue;
        i++;
    }

    obj = null;

    return i;
}


function vSessionInsert() {
    if ( ! top.vSession ) top.vSession = {};
    vSessionRandom = new Date();
    arguments[0] ? keyName = arguments[0] : keyName = "v" + Math.floor(vSessionRandom.getTime());
    arguments[1] ? keyValue = arguments[1] : keyValue = "";
    arguments[2] ? skipInsert = true : skipInsert = false;
    if ( top.vSession[keyName] ) {
        if( !skipInsert ) alert('vSessionInsert:\rThe vSession key ' + keyName + ' already exists.\rUse vDump to view all keys in vSession or choose another key name.');
        return false;
    } else {
        top.vSession[keyName] = keyValue;
        return keyName;
    }
}

function vSessionUpdate() {
    if ( ! top.vSession ) top.vSession = {};
    var vSessionRandom = new Date();
    arguments[0] ? keyName  = arguments[0]  : keyName = "v" + vSessionGet("current_time").time();
    keyName = keyName.replace(/\s/g, '1s1');
    arguments[1] ? keyValue = arguments[1]  : keyValue = "";
    if ((/\./).test(keyName)) {
        var sub = true;
        var keyNames = keyName.split(".");
    } else {
        var sub = false;
    }
    if ( sub == true ) {
        var objRef = new Object();
        var kLength = keyNames.length;
        for (i = 0; i < kLength; i++) {
            if ( i == 0 ) {
                if ( ! top.vSession[keyNames[i]] ) { top.vSession[keyNames[i]] = new Object(); }
                if ( typeof(top.vSession[keyNames[i]]) != "object" ) { top.vSession[keyNames[i]] = null; top.vSession[keyNames[i]] = new Object(); }
                objRef = top.vSession[keyNames[i]];
            } else {
                if ( i == kLength - 1 ) {
                    if ( objRef[keyNames[i]] ) { objRef[keyNames[i]] = null; }
                    objRef[keyNames[i]] = keyValue;
                } else {
                    if ( ! objRef[keyNames[i]] ) { objRef[keyNames[i]] = new Object(); }
                    if ( typeof(objRef[keyNames[i]]) != "object" ) { objRef[keyNames[i]] = null; objRef[keyNames[i]] = new Object(); }
                    objRef = objRef[keyNames[i]];
                }
            }
        }
    } else {
        top.vSession[keyName] = null;
        top.vSession[keyName] = keyValue;
    }
    return keyName;
}

function vSessionDelete() {
    if ( arguments[0] ) {
        vKey = arguments[0];
    } else {
        vAlert('vSessionDelete:\rYou must pass the name of the key you\rwish to delete.');
        return false;
    }

    if ( ( /\./ ).test(vKey) ) {
        var objRef = 'top.vSession.';
        objRef += vKey;
        try { eval(objRef + " = null"); eval("delete " + objRef); objRef = null; return true; } catch(e){};
    } else {
        if ( top.vSession[vKey] ) {
            top.vSession[vKey] = null;
            objRef = null;
            return true;
        } else {
            return false;
        }
    }
}

function vSessionFlush() {
    top.vSession = null;
    top.vSession = new Object();
}
/* ============================== END Session ================================= */
/* =============== jsSimple ====================== */
/*
 |-----------------------------------------------------------------------------|
 |                                jsSimple 2.1                                 |
 |-----------------------------------------------------------------------------|
 |                                Developed By:                                |
 |                              Joshua Blackburn                               |
 |                                                                             |
 |-----------------------------------------------------------------------------|
 | JavaScript DOM formatting.                                                  |
 | IE 6+ + Firefox + Safari + Chrome + Opera                                   |
 |-----------------------------------------------------------------------------|
 | September 2004 | jsSimple 1.0                                               |
 | March     2005 | jsSimple 2.0.  Complete recode and optimization.           |
 | July      2009 | jsSimple 2.1.  Cross browser compatibility                 |
 |-----------------------------------------------------------------------------|
 */
/*

 JS.Simple

 ******************
 Example of use tag ( insert directly below <html> )

 <uses tags="replace" ids=""/>

 - ids are only used for tags that cannot be wrapped with <simple:replace> tags.
 *******************

 ********************
 - ALL SIMPLE TAGS MUST HAVE A UNIQUE ID
 ********************

 ********************
 Example of simple replace tag

 <simple:replace structure="object" id="someID" where="key=value">
 <simple:replace structure="{pointer}.object" id="someID2" where="key=value">
 </simple:replace>
 </simple:replace>

 - Structures should only be a hash or an array of hashes.
 Ex 1:
 var hash = {
 key : value,
 key : value
 }

 EX 2:
 var arrayHashes = [
 {
 key : value
 },
 {
 key : value
 }
 ]

 - Usage of where: specify one key with specific value that must be present in the current hash to be displayed. 

 - Usage of {pointer}: used in nested tags.  {pointer} is replaced with the current object path in the replace.  Allows you to use objects within a hash for example:
 var arrayHashes = [
 {
 key : value
 myKey : {
 key : value
 }
 },
 {
 key : value
 }
 ]
 Allows you to use myKey by using {pointer}.myKey

 +++++++++++++++++++++NOTES+++++++++++++

 - If replacing the value of an input text box, HTML parsers will strip the "" off the value if it doesn't contain a space, therefore, after simple:replace, strings are not treated correctly.  To avoid this, insert a space after your tag.  Ex:

 BAD:
 <input type="text" value="{myValue}" />

 GOOD:
 <input type="text" value="{myValue} " />

 This will assure that all characters are replaced correctly.


 */

//var orderBy, ASC, DESC, domMapped = false; // <- Used by _jsSimple

var _jsSimple = {
    debug : false,
    clean : true, // <- Defines if cleanup is called or supressed

    setup : function() {
        vGetParams();
        this._orderBy     = '';
        this.ASC         = '';
        this.DESC        = '';
        this.domMapped   = false;
        this.block       = '';
        this.tag         = '';
        this.page        = jss.page;
        this.tagNames    = (document.getElementsByTagName('uses')[0] ? ( document.getElementsByTagName('uses')[0].tags ? document.getElementsByTagName('uses')[0].tags.split(',') : ["replace"] ) : ["replace"]);
        this.simpleIDS   = (document.getElementsByTagName('uses')[0] ? ( document.getElementsByTagName("uses")[0].getAttribute('ids')  ? document.getElementsByTagName("uses")[0].getAttribute('ids').split(',')  : [] ) : []);
        //sDumper("simpleIDS", this.simpleIDS, this.tagNames);
        this.tags        = [];   // <- Stores all the tags on the page
        this.passedIDS   = [];   // <- Stores all ids passed as arguments
        this.cleanRegex  = /\{[a-zA-Z0-9_-]+\}/ig;
        this.custom      = {};   // <- Allows for extended functionality.  Define custom functions to handle certain data types
        this._override    = {};
        this.mapDOM();

        this.r = {
            nestedRegex  : /\<\?xml[^\>]+\>/g,
            pointerRegex : /\{pointer\}/,
            pathRegex    : /\{path\}/g,
            positionRegex: /\{position\}/g,
            myHTML       : [],
            position     : 0,
            pointer      : '',
            path         : '',
            nestedIDS    : []
        }
    },


    mapDOM : function () {
        //Perform following check to allow stand alone operation
        if ( ! top.vSession ) top.vSession = {};
        var simpleTags, tags = this.tagNames, ids = this.simpleIDS;
        if ( ! this.domMapped && arguments.length < 1 ) {
            // Add all simple tags to DOM map
            for ( var i in tags ) {
                simpleTags = document.getElementsByTagName( trim(tags[i]) );

                for ( var single = 0; single < simpleTags.length; single++ ) {
                    this.tags.push(simpleTags[single]); // Add all tag references to tag array

                    // If tag has id, use that for dom map id, if not, just use the position in the array
                    if ( simpleTags[single].getAttribute('id') ) {
                        vSessionUpdate("DOM_MAP."+this.page+'.'+simpleTags[single].id, simpleTags[single].innerHTML);
                    } else {
                        vSessionUpdate("DOM_MAP."+this.page+'.'+single, simpleTags[single].innerHTML);
                    }
                }

            }
            // Add all regular tags specified as a simple tag
            for ( var j in ids ) {
                try {
                    vSessionUpdate( "DOM_MAP."+this.page+'.'+ trim(ids[j]), document.getElementById( trim(ids[j]) ).innerHTML );
                } catch(e) {}
            }

            this.domMapped = true; // <- Set global variable to signify that the DOM is already mapped and don't do it again.
        }
        if ( arguments.length > 0 ) {
            var myObj;
            for ( var i = 0; i < arguments.length; i++ ) {
                if ( myObj = document.getElementById(arguments[i]) ) { // <- Make sure the element exists first
                    // If this tag has not already been mapped, then map it
                    if ( ! vSessionGet("DOM_MAP."+this.page+'.'+trim(arguments[i])) ) vSessionUpdate( "DOM_MAP."+this.page+'.'+ trim(arguments[i]), myObj.innerHTML );
                }
            }
            myObj = null;
        }
        simpleTags = null;
    },

    format : function () {
        this.passedIDS = [];
        // Check for passed arguments
        for ( var a = 0; a < arguments.length; a++ ) {
            if ( typeof(arguments[a]) == 'number' ) {
                this.clean = false; // Supress cleanup
            } else {
                this.passedIDS.push(arguments[a]);
            }
        }
        if ( this.debug ) alert("Passed " + this.passedIDS.length + ' IDs');
        for ( var i in ( obj = ( this.passedIDS[0] ? this.passedIDS : this.tags.concat(this.simpleIDS) ) ) ) {
            if ( typeof( obj[i] ) == 'string' ) {
                this.tag   = document.getElementById( obj[i] );

                if ( ! this.tag ) { // <- Protect against bad Id's
                    if ( this.debug ) alert('Was passed a bad id: ' + obj[i] );
                    return;
                }

                if ( ! this._override[obj[i]] ) { // <- Allows you to override a skipped tag
                    if ( this.debug ) alert('Tag has no override applied');
                    if ( this.tag.getAttribute('skip') || this.tag.getAttribute('nested') ) { this.simpleCleanup(this.tag.id);continue; } // <- Tag is skipped, but still do cleanup
                } else {
                    delete this.override[this.tag.id]; // <- Delete after being overridden
                }

                this.block = vSessionGet( 'DOM_MAP.'+this.page+"."+(obj[i]) );
                if ( this.debug ) alert("was passed a string.  Block: " + this.block + ' Id being looked up: ' + obj[i] + ' The current page is: ' + this.page);
                this.tag.getAttribute('simple') ? this.replace() : this.replace();
            } else {
                this.tag   = obj[i];

                if ( ! this._override[this.tag.id] ) { // <- Allows you to override a skipped tag
                    if ( this.debug ) alert('Tag has no override applied');
                    if ( this.tag.getAttribute('skip') || this.tag.getAttribute('nested') ) { this.simpleCleanup(this.tag.id);continue; } // <- Tag is skipped, but still do cleanup
                } else {
                    delete this.override[this.tag.id]; // <- Delete after being overridden
                }

                this.block = vSessionGet( 'DOM_MAP.'+this.page+"."+(obj[i].getAttribute('id') ? obj[i].getAttribute('id') : i) );
                if ( this.debug ) alert("was passed an object. block: " + this.block);
                //this[ this.tag.nodeName ]();
                this.replace();
            }
        }
    },

    replace : function () {
        try { // <- Check for invalid structure
            if ( ! top[this.tag.getAttribute('structure')] ) {
                if ( this.debug ) alert('The structure | ' + this.tag.getAttribute('structure') + " | is invalid or does not exist");
                this.simpleCleanup(this.tag.id); // <- Cleanup up block before returning
                return;
            }
        } catch(e) {
            if ( this.debug ) alert('The structure | ' + this.tag.getAttribute('structure') + " | is invalid or does not exist");
            this.simpleCleanup(this.tag.id); // <- Cleanup up block before returning
            return;
        }
        this.r['structure']   = ( this.tag.getAttribute('structure') ? /*eval( this.tag.getAttribute('structure') )*/top[this.tag.getAttribute('structure')] : {} );
        this.r['whereClause'] = ( this.tag.getAttribute('where') ? this.tag.getAttribute('where') : false );
        this._orderBy         = ( this.tag.getAttribute('orderBy') ? this.tag.getAttribute('orderBy') : false );
        this.ASC              = ( this.tag.getAttribute('ASC') ? true : false );
        this.DESC             = ( this.tag.getAttribute('DESC') ? true : false );
        this.r['dynamic']     = ( this.tag.getAttribute('dynamic') ? true : false );

        var wc = this.parseWhereClause(), transformedData, objType, struct, re, tempBlock, nestedTags, tagObj;

        // ------+ Handle Nesting +---------
        if ( arguments[0] ) this.tag.setAttribute("nested", "true"); // <- Replace has been called for a nested tag.  Mark so it is not executed more than once
        if ( ! arguments[0] && this.tag.getAttribute("nested") ) return; // <- Don't continue if the tag has already been marked nested

        // ++++++++++++++++++++- Start Replacing -+++++++++++++++++++++++++++++++++++++++++++++++++++++
        if ( this.debug ) alert("The type of structure is: " + vTypeof(this.r.structure));
        if ( vTypeof(this.r.structure) == 'array' ) {
            // =========================- ARRAY -==========================

            // If there is an orderBy clause, go ahead and sort the array of hashes before starting the loop
            if ( this._orderBy ) {
                if ( this.debug ) alert("The structure is ordered by: " + this._orderBy);
                if ( this.custom['orderBy'] ) { // <- See if there is a custom function defined to handle the order by
                    this.r.structure = this.r.structure.sort(this.custom.orderBy); // <- Sort using custom function if it exists
                } else {
                    this.r.structure = this.r.structure.sort(this.orderBy); // <- Use default sorting
                }
            }

            for ( var c = 0; c < (struct = this.r.structure).length; c++ ) {
                if ( this.debug ) alert("Walking the array and currently on " + c + " with a value of " + struct[c]);
                if ( struct[c] ) { // <- Make sure the key exists

                    // Update pointer and position variables that custom transforms may need
                    this.r.pointer  = this.tag.getAttribute('structure') + "[" + c + "]";
                    this.r.path     = this.tag.getAttribute('structure') + "[" + c + "]";
                    this.r.position = c;

                    // Do pointer replace for nested tags. Then do count replace for nested tags.
                    tempBlock = this.block.replace(this.r.pointerRegex, this.tag.getAttribute('structure') + "[" + c + "]");
                    tempBlock = tempBlock.replace(this.r.positionRegex, c);
                    tempBlock = tempBlock.replace(this.r.pathRegex, this.r.path);

                    // If there is a where clause, make sure it matches, if it does, move on, if not, move on to the next hash
                    try {
                        if ( this.r.whereClause ) {
                            if ( ! eval(wc) ) {
                                if ( this.debug ) alert("The following where clause did not match: " + wc);
                                continue;
                            } else {
                                if ( this.debug ) alert("The following where clause matched: " + wc);
                            }
                        } else {
                            if ( this.debug ) alert("No where clause found. Continuing");
                        }
                    } catch(e) {
                        if ( this.debug ) alert('You have a malformed where clause in the tag ' + this.tag.getAttribute('id') + "\n or a key you checked against does not exist in the structure\n" + wc);
                    }

                    for ( var key in struct[c] ) { // <- Go through all the keys in the hash
                        if ( this.debug ) alert("Type of value is: " + typeof(struct[c][key]) + " vTypeof value is: " + vTypeof(struct[c][key]));
                        if ( typeof(struct[c][key]) == 'string' || typeof(struct[c][key]) == 'number' || vTypeof(struct[c][key]) == 'date' || typeof(struct[c][key]) == 'boolean' ) {
                            if ( this.debug ) alert('key: ' + key + ' value: ' + struct[c][key]);

                            transformedData = struct[c][key];
                            // ------------------------=   Custom Transforms   =--------------------
                            //objType = vTypeof(transformedData);
                            objType = vTypeof(transformedData);
                            if ( typeof(transformedData) != 'boolean' ) if ( this.custom[key] ) transformedData = this.custom[key](transformedData, struct[c], this); // <- Value sent to custom function and returned for replacement. Also pass reference to jsSimple just so they have it.
                            if ( this.custom[objType] && ! this.custom[key] ) transformedData = this.custom[objType](transformedData, struct[c], this); // <- Value sent to custom function and returned for replacement. Also pass reference to jsSimple just so they have it.
                            // ------------------------= End Custom Transforms =--------------------

                            re = new RegExp("\\{" + key + "\\}", "ig");
                            tempBlock = tempBlock.replace(re, transformedData);
                            if ( this.tag.nodeName.toLowerCase() == 'select' ) {
                                if ( this.tag.getAttribute('defaultOption') ) { if ( this.tag.getAttribute('defaultOption') == struct[c][key] && isNaN(parseInt(this.tag.getAttribute('defaultOption')))) { tempBlock = tempBlock.replace(/(.*\<option)(.*\>)/,'$1' + ' selected ' + '$2');} else if ( !isNaN(parseInt(this.tag.getAttribute('defaultOption'))) ) { if ( c == parseInt(this.tag.getAttribute('defaultOption')) ) tempBlock = tempBlock.replace(/(.*\<option)(.*\>)/,'$1' + ' selected ' + '$2');} }
                            }
                            if ( this.debug ) alert('current temp block: ' + tempBlock);
                        }
                    }

                    this.r.myHTML.push( tempBlock );

                }
            }

        } else if ( typeof(this.r.structure) == 'object' ) {
            // =========================- HASH  -==========================
            tempBlock = this.block;
            for ( var key in (struct = this.r.structure) ) {
                try {
                    //if ( struct[key] ) {

                    if ( this.debug ) alert("Type of value is: " + typeof(struct[key]) + " vTypeof value is: " + vTypeof(struct[key]));
                    if ( typeof(struct[key]) == 'string' || typeof(struct[key]) == 'number' || vTypeof(struct[key]) == 'date' || typeof(struct[key]) == 'boolean') {

                        transformedData = struct[key];
                        // ------------------------=   Custom Transforms   =--------------------
                        objType = vTypeof(transformedData);
                        if ( typeof(transformedData) != 'boolean' ) if ( this.custom[key] )     transformedData = this.custom[key](transformedData, struct, this); // <- Value sent to custom function and returned for replacement
                        if ( this.custom[objType] && ! this.custom[key] ) transformedData = this.custom[objType](transformedData, struct, this); // <- Value sent to custom function and returned for replacement
                        // ------------------------= End Custom Transforms =--------------------
                        //Debug
                        if ( this.debug ) alert('The current hash key is: |' + key + '| and the value is: |' + struct[key] + '| and value after transform is: ' + transformedData);

                        re = new RegExp("\\{" + key + "\\}",'ig');
                        tempBlock = tempBlock.replace(re, transformedData);
                        if ( this.tag.nodeName.toLowerCase() == 'select' ) {
                            if ( this.tag.getAttribute('defaultOption') ) if ( this.tag.getAttribute('defaultOption') == struct[key]) tempBlock = tempBlock.replace(/(.*\<option)(.*\>)/,'$1' + ' selected ' + '$2');
                        }
                    }

                    //}
                } catch (e) { continue; }
            }
            this.r.myHTML.push( tempBlock );

        }
        // ++++++++++++++++++++- End Replacing -+++++++++++++++++++++++++++++++++++++++++++++++++++++

        if ( this.debug ) alert("The code before cleanup: " + this.r.myHTML.join(''));
        this.tag.innerHTML = this.cleanup(this.r.myHTML.join('')); // <- Clean the block and put the HTML back inside the tag
        if ( this.tag.nodeName.toLowerCase() == 'select' ) {
            this.simpleCleanup();
            //this.tag.add(new Option());
            //this.tag.options[this.tag.options.length - 1].selected = true;
        }

        this.r.myHTML = null; this.r.myHTML = []; this.r.whereClause = false; // <- Reset HTML holder and whereClause.  Bad things happen if not reset

        // If tag is dynamic, run dynamic piece.  This will redisplay the block of code inside the simple tag.
        if ( this.r.dynamic ) this.tag.style.display = 'block';
        //if ( this.r.dynamic ) this.tag.innerHTML = this.tag.innerHTML.replace( /(\<\!\-\-)|(\-\-\>)/g, '');

        // Handle tag events (Ex: onfinish)
        if ( this.tag.getAttribute('onfinish') ) eval(this.tag.onfinish);

        // +++++++++++++++++++++++++++- Nested Tags     -+++++++++++++++++++++++++++++++++++++++++++++++++++
        nestedTags = this.tag.getElementsByTagName('replace');
        if ( nestedTags.length > 0 ) {
            for ( var j = 0; j < nestedTags.length; j++ ) {
                if ( nestedTags[j].getAttribute('nested') || nestedTags[j].getAttribute('skip') ) continue; // <- If tag has already been executed as nested or is skipped, continue
                if ( this.debug ) alert("Nested tag: " + j);
                this.block = vSessionGet( 'DOM_MAP.'+this.page+"."+(nestedTags[j].getAttribute('id') ? nestedTags[j].getAttribute('id') : j) );
                this.tag   = nestedTags[j];
                this.replace('nested');
            }
            nestedTags = null;
        }

        // +++++++++++++++++++++++++++- End Nested Tags -+++++++++++++++++++++++++++++++++++++++++++++++++++
    },

    parseWhereClause : function () {
        if ( this.r.whereClause ) {
            /*
             * Example where clauses
             * where="keyName = 'string'"
             * where="keyName = 'string' AND keyName = 'string'"
             * where="keyName = 'string' OR keyName = 'string'"
             * where="keyName = 'string' OR keyName = 'string' AND keyName = number"
             * where="keyName ! 'string'"
             * where="[keyName = [0-9]{4}]"
             */
            return this.r.whereClause.replace(/\s?AND\s?/g, "&&").replace(/\s?OR\s?/g, "||").replace(/\s/g, "").replace(/\[([^=]+)\=[\'\"]?([^\'\"]+)[\'\"]?\]/g, "(/$2/i).test(trim(struct[c]['$1']))").replace(/\=/g, " ==").replace(/\!/g," !=").replace(/(\w+)\s/g, "(struct[c]['$1']) && (trim(struct[c]['$1']))");
            //return ( ( trim(this.r.whereClause) ).split('=') ); // <- Only supports one simple where clause.  Ex: where="key=value"
        } else {
            return false;
        }
    },

    orderBy : function (a, b) {

        try {
            if ( (! this.ASC && a[this._orderBy] < b[this._orderBy]) || (this.ASC && a[this._orderBy] > b[this._orderBy] ) || (! a[this._orderBy] ) ) {
                return 1;
            } else if ( a[this._orderBy] == b[this._orderBy] || ( !a[this._orderBy] && !b[this._orderBy]) ) {
                return 0;
            } else {
                return -1;
            }
        } catch (e) {
            return 0;
        }
    },

    override : function () {
        for ( var i = 0; i < arguments.length; i++ ) {
            try { this._override[arguments[i]] = "OVERRIDE"; } catch(e) {}
        }
    },

    cleanup : function () {
        //alert('cleaning');
        if ( arguments.length > 0 ) { // <- Only cleanup tags specified
            return arguments[0].replace(this.cleanRegex, '');
        } else { // <- Cleanup all tags
            return '';
        }
    },

    simpleCleanup : function () {
        //alert('simpleCleanup');
        try {
            if ( arguments.length > 0 ) {
                for ( var i = 0; i < arguments.length; i++ ) {
                    document.getElementById(arguments[i]).innerHTML = document.getElementById(arguments[i]).innerHTML.replace(this.cleanRegex, '');
                }
            } else {
                var dE = document.body.innerHTML;
                document.body.innerHTML = dE;
            }
        } catch(e) {}
    },

    sort : function (tag, key, pos, col) {
        var tagObj;
        if ( (tagObj = document.getElementsByName(tag)).length > 1 ) {
            arguments[2] ? tagObj = tagObj[ parseInt(arguments[2]) ] : tagObj = tagObj[0];
        } else {
            tagObj = tagObj[0];
        }

        //try {((document.getElementById('_jsSimpleSort')).parentNode).removeChild(document.getElementById('_jsSimpleSort'));} catch(e) {};
        try {document.remove('_jsSimpleSort');} catch(e) {};

        if ( tagObj.getAttribute('ASC') ) {
            tagObj.removeAttribute('ASC');
            tagObj.setAttribute('DESC', 'true');
            if ( col ) col.innerHTML = col.innerHTML + "<img id='_jsSimpleSort' src='images/arrow_down.gif'>";
        } else if ( tagObj.getAttribute('DESC') ) {
            tagObj.removeAttribute('DESC');
            tagObj.setAttribute('ASC', 'true');
            if ( col ) col.innerHTML = col.innerHTML + "<img id='_jsSimpleSort' src='images/arrow_up.gif'>";
        } else {
            tagObj.setAttribute('DESC', 'true');
            if ( col ) col.innerHTML = col.innerHTML + "<img id='_jsSimpleSort' src='images/arrow_down.gif'>";
        }

        if ( tagObj.getAttribute('orderBy') ) {
            tagObj.orderBy = key;
        } else {
            tagObj.setAttribute('orderBy', key);
        }

        if ( tagObj.getAttribute('nested') ) tagObj.removeAttribute('nested');
        this.format(tagObj);
    },

    kill : function () {
        try{
            if ( ! top.vSession ||
                    ! top.vSession.DOM_MAP ||
                    ! top.vSession.DOM_MAP[this.page] ) return;
        }catch(Exception){
            return;
        }
        for ( var i in top.vSession.DOM_MAP[this.page] ) {
            top.vSession.DOM_MAP[this.page][i] = null;
        }

        top.vSession.DOM_MAP[this.page] = null;
        delete top.vSession.DOM_MAP[this.page];

        for ( var i in jsSimple ) {
            jsSimple[i] = null;
        }
        jsSimple = null;
    },

    xmlhttp : function() {
        if ( ! this._xmlhttp ) this._xmlhttp = undefined;

        if (window.XMLHttpRequest) {
            this._xmlhttp = new XMLHttpRequest();
            if ( typeof this.xmlhttp.overrideMimeType != 'undefined') {
                this._xmlhttp.overrideMimeType('text/xml');
            }
        } else if (window.ActiveXObject) {
            this._xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        } else {
            alert('Perhaps your browser does not support xmlhttprequests?');
        }

        return this._xmlhttp;
    },

    jsonFetch : function(url) {
    },

    jsonFetchAndUpdate : function(url, id) {
        //alert(url);
        this.xmlhttp();

        this._xmlhttp.onreadystatechange = function() {
            if (this.readyState == 4) {
                // do something with the results
                if ( ! top[(document.getElementById(id)).getAttribute('structure')] ) {
                    return;
                } else {
                    top[(document.getElementById(id)).getAttribute('structure')] = eval(this.responseText);
                    //sDumper(eval(this.responseText));
                    alert(top.struct.length);
                    _jsSimple.format(id);
                }
            }
        };

        //sDumper(this._xmlhttp);
        this._xmlhttp.open('GET', url, true);
        this._xmlhttp.send();
    }
}
/* ============================== END jsSimple ================================= */
</script>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>

<script src="https://raw.github.com/documentcloud/underscore/master/underscore.js"></script>

<script src="http://ajax.cdnjs.com/ajax/libs/mustache.js/0.3.0/mustache.min.js"></script>

<script src="https://github.com/downloads/wycats/handlebars.js/handlebars.1.0.0.beta.3.js"></script>

<script src="http://borismoore.github.com/jsrender/jsrender.js"></script>

<script src="https://github.com/olado/doT/raw/master/doT.js">
</script>
<!--External Template Definitions-->
<script type="text/x-kendo-template" id="kendoUIextTemplate">
    <div>
        <h1 class='header'>#= data.header #</h1>
        <h2 class='header2'>#= data.header2 #</h2>
        <h3 class='header3'>#= data.header3 #</h3>
        <h4 class='header4'>#= data.header4 #</h4>
        <h5 class='header5'>#= data.header5 #</h5>
        <h6 class='header6'>#= data.header6 #</h6>
        <ul class='list'>
            # for (var i = 0, l = data.list.length; i < l; i++) { #
            <li class='item'>#= data.list[i] #</li>
            # } #
        </ul>
    </div>
</script>
<script>
    window.sharedVariables = {
        header: "Header",
        header2: "Header2",
        header3: "Header3",
        header4: "Header4",
        header5: "Header5",
        header6: "Header6",
        list: ['1000000000', '2', '3', '4', '5', '6', '7', '8', '9', '10']
    };


    //JsRender compiled template (no encoding)
    window.jsRenderTemplate = $.templates("<div><h1 class='header'>{{:header}}</h1><h2 class='header2'>{{:header2}}</h2><h3 class='header3'>{{:header3}}</h3><h4 class='header4'>{{:header4}}</h4><h5 class='header5'>{{:header5}}</h5><h6 class='header6'>{{:header6}}</h6><ul class='list'>{{for list}}<li class='item'>{{:#data}}</li>{{/for}}</ul></div>");

    window.mustacheTemplate = "<div><h1 class='header'>{{{header}}}</h1><h2 class='header2'>{{{header2}}}</h2><h3 class='header3'>{{{header3}}}</h3><h4 class='header4'>{{{header4}}}</h4><h5 class='header5'>{{{header5}}}</h5><h6 class='header6'>{{{header6}}}</h6><ul class='list'>{{#list}}<li class='item'>{{{.}}}</li>{{/list}}</ul></div>";

    window.handlebarsTemplate = Handlebars.compile("<div><h1 class='header'>{{header}}</h1><h2 class='header2'>{{header2}}</h2><h3 class='header3'>{{header3}}</h3><h4 class='header4'>{{header4}}</h4><h5 class='header5'>{{header5}}</h5><h6 class='header6'>{{header6}}</h6><ul class='list'>{{#each list}}<li class='item'>{{this}}</li>{{/each}}</ul></div>");



    //Use external template definition

    window.underscoreTemplate = _.template("<div><h1 class='header'><%= header %></h1><h2 class='header2'><%= header2 %></h2><h3 class='header3'><%= header3 %></h3><h4 class='header4'><%= header4 %></h4><h5 class='header5'><%= header5 %></h5><h6 class='header6'><%= header6 %></h6><ul class='list'><% for (var i = 0, l = list.length; i < l; i++) { %><li class='item'><%= list[i] %></li><% } %></ul></div>");

    window.underscoreTemplateNoWith = _.template("<div><h1 class='header'><%= data.header %></h1><h2 class='header2'><%= data.header2 %></h2><h3 class='header3'><%= data.header3 %></h3><h4 class='header4'><%= data.header4 %></h4><h5 class='header5'><%= data.header5 %></h5><h6 class='header6'><%= data.header6 %></h6><ul class='list'><% for (var i = 0, l = data.list.length; i < l; i++) { %><li class='item'><%= data.list[i] %></li><% } %></ul></div>", null, {variable: 'data'});

    window.baseHtml = "<div><h1 class='header'></h1><h2 class='header2'></h2><h3 class='header3'></h3><h4 class='header4'></h4><h5 class='header5'></h5><h6 class='header6'></h6><ul class='list'><li class='item'></li></ul></div>";

    window.doTtemplate = doT.template("<div><h1 class='header'>{{=it.header}}</h1><h2 class='header2'>{{=it.header2}}</h2><h3 class='header3'>{{=it.header3}}</h3><h4 class='header4'>{{=it.header4}}</h4><h5 class='header5'>{{=it.header5}}</h5><h6 class='header6'>{{=it.header6}}</h6><ul class='list'>{{for(var i = 0,l=it.list.length;i<l;i++) { }}<li class='item'>{{=it.list[i]}}</li>{{ } }}</ul></div>");


    //Resig Template Function (modified to support ')
    function tmpl(str) {
        var strFunc =
                "var p=[];" +
                "with(obj){p.push('" +

                str.replace(/[\r\t\n]/g, " ")
                        .replace(/'(?=[^#]*#>)/g, "\t")
                        .split("'").join("\\'")
                        .split("\t").join("'")
                        .replace(/<#=(.+?)#>/g, "',$1,'")
                        .split("<#").join("');")
                        .split("#>").join("p.push('")
                + "');}return p.join('');";

        return new Function("obj", strFunc);
    }

    window.resig = tmpl("<div><h1 class='header'><#= header #></h1><h2 class='header2'><#= header2 #></h2><h3 class='header3'><#= header3 #></h3><h4 class='header4'><#= header4 #></h4><h5 class='header5'><#= header5 #></h5><h6 class='header6'><#= header6 #></h6><ul class='list'><# for (var i = 0, l = list.length; i < l; i++) { #><li class='item'><#= list[i] #></li><# } #></ul></div>");

    //Resig modified template function (no "with" block)
    function tmpl2(str) {
        var strFunc =
                "var p=[];" +
                "p.push('" +

                str.replace(/[\r\t\n]/g, " ")
                        .replace(/'(?=[^#]*#>)/g, "\t")
                        .split("'").join("\\'")
                        .split("\t").join("'")
                        .replace(/<#=(.+?)#>/g, "',$1,'")
                        .split("<#").join("');")
                        .split("#>").join("p.push('")
                + "');return p.join('');";

        return new Function("data", strFunc);
    }

    window.resig2 = tmpl2("<div><h1 class='header'><#= data.header #></h1><h2 class='header2'><#= data.header2 #></h2><h3 class='header3'><#= data.header3 #></h3><h4 class='header4'><#= data.header4 #></h4><h5 class='header5'><#= data.header5 #></h5><h6 class='header6'><#= data.header6 #></h6><ul class='list'><# for (var i = 0, l = data.list.length; i < l; i++) { #><li class='item'><#= data.list[i] #></li><# } #></ul></div>");

</script>

<div id="jsSimpleTemplate" structure="sharedVariables" style="display:none">
    <h1 class='header'>{header}</h1>
    <h2 class='header2'>{header2}</h2>
    <h3 class='header3'>{header3}</h3>
    <h4 class='header4'>{header4}</h4>
    <h5 class='header5'>{header5}</h5>
    <h6 class='header6'>{header6}</h6>
    <ul id="hello" class='list' structure="list">
        <li class='item'>{item}</li>
    </ul>
</div>
<div id="template" style="display:none"></div>
<script>
    window.jsSimple = function() {
        _jsSimple.format('jsSimpleTemplate');
        _jsSimple.format('hello');
    };
    window.list = sharedVariables.list.map(function(item) { return { item: item }; });
    _jsSimple.setup();
</script>

Test runner

Ready to run.

Testing in
TestOps/sec
doT.js
doTtemplate(sharedVariables);
ready
Kendo UI Templates (Default)
 
ready
Kendo UI Templates (No "with" block)
 
ready
Handlebars.js
handlebarsTemplate(sharedVariables);
ready
Underscore.js Template
underscoreTemplate(sharedVariables);
ready
Resig Micro Templates (modified)
resig(sharedVariables);
ready
Resig Micro Templates (No "with" block)
resig2(sharedVariables);
ready
Underscore.js Template (No "with")
underscoreTemplateNoWith(sharedVariables);
ready
Hogan.js
 
ready
JsRender
jsRenderTemplate.render(sharedVariables);
ready
Mustache.js Template
Mustache.to_html(mustacheTemplate, sharedVariables);
 
ready
JSSimple
jsSimple();
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.