diff --git a/src/jsrender.js b/src/jsrender.js new file mode 100644 index 0000000..ef618de --- /dev/null +++ b/src/jsrender.js @@ -0,0 +1,570 @@ +/*! JsRender v1.0pre - (jsrender.js version: does not require jQuery): http://github.com/BorisMoore/jsrender */ +/* + * Optimized version of jQuery Templates, for rendering to string, using 'codeless' markup. + */ +window.JsViews || window.jQuery && jQuery.views || (function( window, undefined ) { + +var $, viewsNs, tmplEncode, render, + FALSE = false, TRUE = true, + jQuery = window.jQuery, document = window.document; + htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /, + stack = [], + autoName = 0, + escapeMapForHtml = { + "&": "&", + "<": "<", + ">": ">" + }, + htmlSpecialChar = /[\x00"&'<>]/g, + slice = Array.prototype.slice; + +if ( jQuery ) { + + //////////////////////////////////////////////////////////////////////////////////////////////// + // jQuery is loaded, so make $ the jQuery object + $ = jQuery; + + $.fn.extend({ + // Use first wrapped element as template markup. + // Return string obtained by rendering the template against data. + render: function( data, context, parentView, path ) { + return render( this[0], data, context, parentView, path ); + }, + + // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template. + template: function( name, context ) { + return $.template( name, this[0], context ); + } + }); + +} else { + + //////////////////////////////////////////////////////////////////////////////////////////////// + // jQuery is not loaded. Make $ the JsViews object + window.JsViews = window.$ = $ = { + extend: function( target, source ) { + if ( source === undefined ) { + source = target; + target = $; + } + for ( var name in source ) { + target[ name ] = source[ name ]; + } + return target; + }, + map: function( elems, callback ) { + var value, ret = [], + i = 0, + length = elems.length; + + if ( $.isArray( elems )) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i ); + + if ( value != null ) { + ret.push( value ); + } + } + } + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + isArray: Array.isArray || function( obj ) { + return Object.prototype.toString.call( obj ) === "[object Array]"; + } + } +} + +//================= +// View constructor +//================= + +function View( context, path, parentView, data, template ) { + // Returns a view data structure for a new rendered instance of a template. + // The content field is a hierarchical array of strings and nested views. + + var self, content, + parentContext = parentView && parentView.ctx; + + parentView = parentView || { viewsCount:0 }; + + self = { + jsViews: "v1.0pre", + path: path || "", + // inherit context from parentView, merged with new context. + itemNumber: ++parentView.viewsCount || 1, + viewsCount: 0, + tmpl: template, + data: data || parentView.data || {}, + // Set additional context on this view (which will modify the context inherited from the parent, and be inherited by child views) + ctx : context && context === parentContext + ? parentContext + : (parentContext ? $.extend( {}, parentContext, context ) : context), + parent: parentView + }; + return self; +} + +$.extend({ + views: viewsNs = { + templates: {}, + tags: {}, + allowCode: FALSE, + +//=============== +// renderTag +//=============== + + renderTag: function( tagName ) { + // This is a tag call, with arguments: "tagName", [params, ...], [content,] [params.toString,] view, encoding, [hash,] [nestedTemplateFnIndex] + var content, ret, key, view, encoding, hash, l, + json = "", + path = "", + keyCount = 0, + args = slice.call( arguments, 1 ), + tagFn = viewsNs.tags[ tagName ]; + + function getValue( val ) { // TODO optimize in case whether this a simple path on an object - no bindings etc. + var result, object, varName; + + if ( /^(['"]).*\1$/.test( val )) { + // If parameter is quoted text ('text' or "text") - replace by string: text + result = val.slice( 1,-1 ); + } else if ( "" + val !== val ) { + // Otherwise, treat as path to be evaluated + result = val; + } else { + val = val.split("."); + object = val[ 0 ].charAt( 0 ) === "$" + ? (varName = val.shift().slice( 1 ), varName === "view" ? view : view[ varName ]) + : view.data; + + // If 'from' val points to a property of a descendant 'leaf object', + // link not only from leaf object, but also from intermediate objects + while ( object && val.length > 1 ) { + object = object[ val.shift() ]; + } + val = val[ 0 ]; + result = val ? object && object[ val ] : object; + } + return [ result ]; + } + + encoding = args.pop(); + if ( +encoding === encoding ) { + // Last arg is a number, so this is a block tagFn and last arg is the nested template index (integer key) + // assign the sub-content template function as last arg + content = encoding; + encoding = args.pop(); // In this case, encoding is the next to last arg + } + if ( "" + encoding !== encoding ) { + // Last arg is a number, so this is a block tagFn and last arg is the nested template index (integer key) + // assign the sub-content template function as last arg + hash = encoding; + encoding = args.pop(); // In this case, encoding is the next to last arg + } + view = args.pop(); + content = content && view.tmpl.nested[ content - 1 ]; + l = args.length; + if ( l ) { + path = args.toString() + args = $.map( args, getValue ); + } + + if ( hash ) { + json = hash.json; + delete hash.json; + if ( hash.content ) { + content = content || getValue( hash.content )[0]; + delete hash.content; + } + for ( key in hash ) { + keyCount++; + hash[ key ] = getValue( hash[ key ])[0]; + } + if ( keyCount ) { + args.push( hash ); + } + } + if ( content ) { + args.push( content ); + } + if ( l ) { + args.push( path ); + } + if ( keyCount ) { + args.push( json ); + } + args.push( encoding ); + + // Parameters are params..., hash, content, layout, path, json + ret = tagFn && (tagFn.apply( view, args ) || ""); + + return encoding === "string" ? ('"' + ret + '"') : ret; + // Useful to force chained tags to return results as string values, + // (wrapped as quoted string) for passing as arguments to calling tag + }, + +//=============== +// registerTags +//=============== + + // Register declarative tag. + registerTags: function registerTags( name, tag ) { + if ( typeof name === "object" ) { + // Object representation where property name is path and property value is value. + // TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges. + for ( var key in name ) { + registerTags( key, name[ key ]) + } + } else { + // Simple single property case. + viewsNs.tags[ name ] = tag; + } + return this; + }, + + +//=============== +// tmpl.encode +//=============== + + encode: tmplEncode = { + "none": function( text ) { + return text; + }, + "html": function( text ) { + // HTML encoding helper: Replace < > & and ' and " by corresponding entities. + // Implementation, from Mike Samuel + return String( text ).replace( htmlSpecialChar, replacerForHtml ); + }, + "string": function( text ) { + return '"' + text + '"'; // Used for chained helpers to return quoted strings + } + //TODO add URL encoding, and perhaps other encoding helpers... + } + }, + +//=============== +// render +//=============== + + render: render = function( tmpl, data, context, parentView, path ) { + // Render template against data as a tree of subviews (nested template), or as a string (top-level template). + var i, l, dataItem, arrayView, content, result = ""; + + if ( arguments.length === 2 && data.jsViews ) { + parentView = data; + context = parentView.ctx; + data = parentView.data; + } + tmpl = $.template( tmpl ); + if ( !tmpl ) { + return ""; // Could throw... + } + + if ( $.isArray( data )) { + // Create a view item for the array, whose child views correspond to each data item. + arrayView = View( context, path, parentView, data); + l = data.length; + for ( i = 0, l = data.length; i < l; i++ ) { + dataItem = data[ i ]; + content = dataItem ? tmpl( dataItem, View( context, path, arrayView, dataItem, tmpl )) : ""; + result += viewsNs.activeViews ? "" + content + "" : content; + } + } else { + result += tmpl( data, View( context, path, parentView, data, tmpl )); + } + + return viewsNs.activeViews + // If in activeView mode, include annotations + ? "" + result + "" + // else return just the string result + : result; + }, + +//=============== +// template +//=============== + + // Set: + // Use $.template( name, tmpl ) to cache a named template, + // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc. + // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration. + + // Get: + // Use $.template( name ) to access a cached template. + // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString ) + // will return the compiled template, without adding a name reference. + // If templateString is not a selector, $.template( templateString ) is equivalent + // to $.template( null, templateString ). To ensure a string is treated as a template, + // include an HTML element, an HTML comment, or a template comment tag. + template: function( name, tmpl ) { + if (tmpl) { + // Compile template and associate with name + if ( "" + tmpl === tmpl ) { + // This is an HTML string being passed directly in. + tmpl = compile( tmpl ); + } else if ( jQuery && tmpl instanceof $ ) { + tmpl = tmpl[0]; + } + if ( tmpl ) { + if ( jQuery && tmpl.nodeType ) { + // If this is a template block, use cached copy, or generate tmpl function and cache. + tmpl = $.data( tmpl, "tmpl" ) || $.data( tmpl, "tmpl", compile( tmpl.innerHTML )); + } + viewsNs.templates[ tmpl._name = tmpl._name || name || "_" + autoName++ ] = tmpl; + } + return tmpl; + } + // Return named compiled template + return name + ? "" + name !== name + ? (name._name + ? name // already compiled + : $.template( null, name )) + : viewsNs.templates[ name ] || + // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec) + $.template( null, htmlExpr.test( name ) ? name : try$( name )) + : null; + } +}); + +//=============== +// Built-in tags +//=============== + +viewsNs.registerTags({ + "if": function() { + function ifArgs( args ) { + var i = 0, + l = args.length - 3; // number of parameters, since args are: (parameters..., content, params.toString, encoding) + while ( l > -1 && !args[ i++ ]) { + // Only render content if args.length < 3 (i.e. this is an else with no condition) or if a condition argument is truey + if ( i === l ) { + return ""; + } + } + self.onElse = undefined; + return render( args[ l < 0 ? 0 : l ], self.data, self.context, self);//, l > 0 && args[ l + 1 ] ); + } + var self = this; + self.onElse = function() { + return ifArgs( arguments ); + }; + return ifArgs( arguments ); + }, + "else": function() { + return this.onElse ? this.onElse.apply( this, arguments ) : ""; + }, + each: function() { + var result = "", + args = arguments, + i = 0, + l = args.length - 1, + content = args[ l - 2 ], + path = args[ l - 1 ]; + for ( ; i < l - 2; i++ ) { + result += args[ i ] ? render( content, args[ i ], this.context, this, path ) : ""; + } + return result; + }, + "*": function( value ) { + return value; + } +}); + +//================= +// compile template +//================= + +// Generate a reusable function that will serve to render a template against data +// (Compile AST then build template function) +function compile( markup ) { + var loc = 0, + inBlock = TRUE, + stack = [], + top = [], + content = top, + current = [,,top]; + + function pushPreceedingContent( shift ) { + shift -= loc; + if ( shift ) { + var text = markup.substr( loc, shift ).replace(/\n/g,"\\n"); + if ( inBlock ) { + content.push( text ); + } else { + if ( !text.split('"').length%2 ) { + // This is a {{ or }} within a string parameter, so skip parsing. (Leave in string) + return TRUE; + } + //( path or \"string\" ) or ( path = ( path or \"string\" ) + (text + " ").replace( /([\w\$\.\[\]]+|(\\?['"])(.*?)\2)(?=\s)|([\w\$\.\[\]]+)\=([\w\$\.\[\]]+|\\(['"]).*?\\\6)(?=\s)/g, + function( all, path, quot, string, lhs, rhs, quot2 ) { + content.push( path ? path : [ lhs, rhs ] ); // lhs and rhs are for named params + } + ); + } + } + } + + // Build abstract syntax tree: [ tag, params, content, encoding ] + markup = markup + .replace( /\\'|'/g, "\\\'" ).replace( /\\"|"/g, "\\\"" ) //escape ', and " + .split( /\s+/g ).join( " " ) // collapse white-space + .replace( /^\s+/, "" ) // trim left + .replace( /\s+$/, "" ); // trim right; + + // {{ # tag singleCharTag code !encoding endTag {{/closeBlock}} + markup.replace( /(?:\{\{(?:(\#)?([\w\$\.\[\]]+(?=[\s\}!]))|([^\/\*\>$\w\s\d\x7f\x00-\x1f](?=[\s\w\$\[]))|\*((?:[^\}]|\}(?!\}))+)\}\}))|(!(\w*))?(\}\})|(?:\{\{\/([\w\$\.\[\]]+)\}\})/g, + function( all, isBlock, tagName, singleCharTag, code, useEncode, encoding, endTag, closeBlock, index ) { + tagName = tagName || singleCharTag; + if ( inBlock && endTag || pushPreceedingContent( index )) { + return; + } + if ( code ) { + if ( viewsNs.allowCode ) { + content.push([ "*", code.replace( /\\(['"])/g, "$1" )]); // unescape ', and " + } + } else if ( tagName ) { + if ( tagName === "else" ) { + current = stack.pop(); + content = current[ 2 ]; + isBlock = TRUE; + } + stack.push( current ); + content.push( current = [ tagName, [], isBlock && 1] ); + } else if ( endTag ) { + current[ 3 ] = useEncode ? encoding || "none" : ""; + if ( current[ 2 ] ) { + current[ 2 ] = []; + } else { + current = stack.pop(); + } + } else if ( closeBlock ) { + current = stack.pop(); + } + loc = index + all.length; // location marker - parsed up to here + inBlock = !tagName && current[ 2 ] && current[ 2 ] !== 1; + content = current[ inBlock ? 2 : 1 ]; + }); + + pushPreceedingContent( markup.length ); + + return buildTmplFunction( top ); +} + +// Build javascript compiled template function, from AST +function buildTmplFunction( nodes ) { + var ret, content, node, + endsInPlus = TRUE, + chainingDepth = 0, + nested = [], + i = 0, + l = nodes.length, + code = 'var tag=$.views.renderTag,html=$.views.encode.html,\nresult=""+'; + + function nestedCall( node, outParams ) { + if ( "" + node === node ) { + return '"' + node + '"'; + } + if ( node.length < 3 ) { + // Named parameter + key = (outParams[ 0 ] && ",") + node[ 0 ] + ":"; + outParams[ 0 ] += key + nestedCall( node[ 1 ]); + outParams[ 1 ] += key + node[ 1 ]; + return FALSE; + } + var codeFrag, tokens, j, k, ctx, val, hash, key, out, + tag = node[ 0 ], + params = node[ 1 ], + encoding = node[ 3 ]; + if ( tag === "=" ) { + if ( chainingDepth > 0 || params.length !== 1 ) { + // Using {{= }} at depth>0 is an error. + return ""; // Could throw... + } + params = params[ 0 ]; + if ( tokens = /^((?:\$view|\$data|\$(itemNumber)|\$(ctx))(?:$|\.))?[\w\.]*$/.exec( params )) { + // Can optimize for perf and not go through call to renderTag() + codeFrag = tokens[ 1 ] + ? tokens[ 2 ] || tokens[ 3 ] + ? ('$view.' + params.slice( 1 )) // $itemNumber, $ctx -> $view.itemNumber, $view.ctx + : params // $view, $data - unchanged + : '$data.' + params; // other paths -> $data.path + if ( encoding !== "none" ) { + codeFrag = 'html(' + codeFrag + ')'; + } + } else { + // Cannot optimize here. Must call renderTag() for processing, encoding etc. + codeFrag = 'tag("=","' + params + '",$view,"' + encoding + '")'; // Not able + } + } else { + codeFrag = 'tag("' + tag + '",'; + chainingDepth++; + out = [ "", "" ]; // out param + for ( j = 0, k = params.length; j < k; j++ ) { + val = nestedCall( params[ j ], out ); + codeFrag += val ? (val + ',') : ""; + } + hash = out[ 0 ]; + chainingDepth--; + content = node[ 2 ]; + if( content ) { + nested.push( buildTmplFunction( content )); + } + codeFrag += '$view,"' + + ( encoding + ? encoding + : chainingDepth + ? "string" // Default encoding for chained tags is "string" + : "" ) + '"' + + (hash ? ",{ json:'{" + out[ 1 ] + "}'," + hash + "}" : "") + + (content ? "," + nested.length : ""); // For block tags, pass in the key to the nested content template + codeFrag += ')'; + } + return codeFrag; + } + + for ( ; i < l; i++ ) { + endsInPlus = TRUE; + node = nodes[ i ]; + if ( node[ 0 ] === "*" ) { + code = code.slice( 0, -1 ) + ";" + node[ 1 ] + "result+="; + } else { + code += nestedCall( node ) + "+"; + endsInPlus = TRUE; + } + } + ret = new Function( "$data, $view", code.slice( 0, endsInPlus ? -1 : -8 ) + ";\nreturn result;" ); + ret.nested = nested; + return ret; +} + +//========================== Private helper functions, used by code above ========================== + +function encode( encoding, text ) { + return text + ? encoding + ? ( tmplEncode[ encoding ] || tmplEncode.html)( text ) // HTML encoding is the default + : '"' + text + '"' + : ""; +} + +function replacerForHtml( ch ) { + // Original code from Mike Samuel + return escapeMapForHtml[ ch ] + // Intentional assignment that caches the result of encoding ch. + || ( escapeMapForHtml[ ch ] = "&#" + ch.charCodeAt( 0 ) + ";" ); +} + +function try$( selector ) { + // If selector is valid, return jQuery object, otherwise return (invalid) selector string + try { + return $( selector ); + } catch( e) {} + return selector; +} + +})( window ); diff --git a/tests/benchmarks/bm_dot.html b/tests/benchmarks/bm_dot.html deleted file mode 100644 index 5382a78..0000000 --- a/tests/benchmarks/bm_dot.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark doT - - - - - -

doT

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_jquery-tmpl-encode.html b/tests/benchmarks/bm_jquery-tmpl-encode.html deleted file mode 100644 index cc22024..0000000 --- a/tests/benchmarks/bm_jquery-tmpl-encode.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark jQuery Templates - - - - - -

jQuery Templates

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_jquery-tmpl.html b/tests/benchmarks/bm_jquery-tmpl.html deleted file mode 100644 index deef728..0000000 --- a/tests/benchmarks/bm_jquery-tmpl.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark jQuery Templates - - - - - -

jQuery Templates

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_jsrender-encode.html b/tests/benchmarks/bm_jsrender-encode.html deleted file mode 100644 index 38a3ed4..0000000 --- a/tests/benchmarks/bm_jsrender-encode.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark JsRender - - - - - -

JsRender

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_jsrender.html b/tests/benchmarks/bm_jsrender.html deleted file mode 100644 index 6ad11f4..0000000 --- a/tests/benchmarks/bm_jsrender.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark JsRender - - - - - -

JsRender

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_nest_jsrender.html b/tests/benchmarks/bm_nest_jsrender.html deleted file mode 100644 index ebd6e7c..0000000 --- a/tests/benchmarks/bm_nest_jsrender.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - Benchmark JsRender - - - - - -

JsRender

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_nest_strappend2.html b/tests/benchmarks/bm_nest_strappend2.html deleted file mode 100644 index b449335..0000000 --- a/tests/benchmarks/bm_nest_strappend2.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - Benchmark Strappend2 - - - - - -

Strappend2

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend-auto-encode.html b/tests/benchmarks/bm_strappend-auto-encode.html deleted file mode 100644 index 37913da..0000000 --- a/tests/benchmarks/bm_strappend-auto-encode.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - BenchMark Strappend - - - - - -

Strappend

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend-auto.html b/tests/benchmarks/bm_strappend-auto.html deleted file mode 100644 index 5531b32..0000000 --- a/tests/benchmarks/bm_strappend-auto.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - BenchMark Strappend - - - - - -

Strappend

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend-encode.html b/tests/benchmarks/bm_strappend-encode.html deleted file mode 100644 index 6e88ed3..0000000 --- a/tests/benchmarks/bm_strappend-encode.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - BenchMark Strappend - - - - - -

Strappend

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend.html b/tests/benchmarks/bm_strappend.html deleted file mode 100644 index 26b694e..0000000 --- a/tests/benchmarks/bm_strappend.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - BenchMark Strappend - - - - - -

Strappend

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend2-auto.html b/tests/benchmarks/bm_strappend2-auto.html deleted file mode 100644 index 78c0f35..0000000 --- a/tests/benchmarks/bm_strappend2-auto.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark Strappend2 - - - - - -

Strappend2

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/bm_strappend2.html b/tests/benchmarks/bm_strappend2.html deleted file mode 100644 index 4b60ba0..0000000 --- a/tests/benchmarks/bm_strappend2.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - Benchmark Strappend2 - - - - - -

Strappend2

- - - - - - - -
-
- - - - - - - - - - - - - diff --git a/tests/benchmarks/dot.html b/tests/benchmarks/dot.html index 5382a78..37c978f 100644 --- a/tests/benchmarks/dot.html +++ b/tests/benchmarks/dot.html @@ -27,14 +27,15 @@

doT

- + @@ -13,10 +13,10 @@

JsRender

@@ -44,7 +44,7 @@

JsRender

count = times; startTime = +new Date; while (count--) { - res = tmpl($, { data: movie }).join(""); + res = $.render( tmpl, movie ); } endTime = +new Date; result = ( endTime-startTime )/times; @@ -55,9 +55,5 @@

JsRender

- - - - diff --git a/tests/benchmarks/jsrender.html b/tests/benchmarks/jsrender.html index 6ad11f4..51ba7a4 100644 --- a/tests/benchmarks/jsrender.html +++ b/tests/benchmarks/jsrender.html @@ -3,7 +3,7 @@ Benchmark JsRender - + @@ -13,10 +13,10 @@

JsRender

@@ -27,14 +27,15 @@

JsRender

- + @@ -13,25 +13,23 @@

JsRender

- -