/*! JsRender v1.0pre - (jsrender.js version: does not require jQuery): http://github.com/BorisMoore/jsrender */
/*
* Optimized version of jQuery Templates, fosr rendering to string, using 'codeless' markup.
*
* Copyright 2011, Boris Moore
* Released under the MIT License.
*/
window.JsViews || window.jQuery && jQuery.views || (function( window, undefined ) {
var $, _$, JsViews, viewsNs, tmplEncode, render, rTag, registerTags, registerHelpers, extend,
FALSE = false, TRUE = true,
jQuery = window.jQuery, document = window.document,
htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
rPath = /^(true|false|null|[\d\.]+)|(\w+|\$(view|data|ctx|(\w+)))([\w\.]*)|((['"])(?:\\\1|.)*\7)$/g,
rParams = /(\$?[\w\.\[\]]+)(?:(\()|\s*(===|!==|==|!=|<|>|<=|>=)\s*|\s*(\=)\s*)?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g,
rNewLine = /\r?\n/g,
rUnescapeQuotes = /\\(['"])/g,
rEscapeQuotes = /\\?(['"])/g,
rBuildHash = /\x08([^\x08]+)\x08/g,
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( data, this[0], 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
// Map over the $ in case of overwrite
_$ = window.$;
window.JsViews = JsViews = window.$ = $ = {
extend: function( target, source ) {
var name;
for ( name in source ) {
target[ name ] = source[ name ];
}
return target;
},
isArray: Array.isArray || function( obj ) {
return Object.prototype.toString.call( obj ) === "[object Array]";
},
noConflict: function() {
if ( window.$ === JsViews ) {
window.$ = _$;
}
return JsViews;
}
};
}
extend = $.extend;
//=================
// 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.
parentView = parentView || { viewsCount:0, ctx: viewsNs.helpers };
var parentContext = parentView && parentView.ctx;
return {
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( extend( {}, parentContext ), context ) : context||{}),
// If no jQuery, extend does not support chained copies - so limit to two parameters
parent: parentView
};
}
extend( $, {
views: viewsNs = {
templates: {},
tags: {
"if": function() {
var ifTag = this,
view = ifTag._view;
view.onElse = function( presenter, args ) {
var i = 0,
l = args.length;
while ( l && !args[ i++ ]) {
// Only render content if args.length === 0 (i.e. this is an else with no condition) or if a condition argument is truey
if ( i === l ) {
return "";
}
}
view.onElse = undefined; // If condition satisfied, so won't run 'else'.
return render( view.data, presenter.tmpl, view.ctx, view);
};
return view.onElse( this, arguments );
},
"else": function() {
var view = this._view;
return view.onElse ? view.onElse( this, arguments ) : "";
},
each: function() {
var i,
self = this,
result = "",
args = arguments,
l = args.length,
content = self.tmpl,
view = self._view;
for ( i = 0; i < l; i++ ) {
result += args[ i ] ? render( args[ i ], content, self.ctx || view.ctx, view, self._path, self._ctor ) : "";
}
return l ? result
// If no data parameter, use the current $data from view, and render once
: result + render( view.data, content, view.ctx, view, self._path, self.tag );
},
"=": function( value ) {
return value;
},
"*": function( value ) {
return value;
}
},
helpers: {
not: function( value ) {
return !value;
}
},
allowCode: FALSE,
debugMode: TRUE,
err: function( e ) {
return viewsNs.debugMode ? ("
Error: " + (e.message || e) + ". "): '""';
},
//===============
// setDelimiters
//===============
setDelimiters: function( openTag, closeTag ) {
// Set or modify the delimiter characters for tags: "{{" and "}}"
var firstCloseChar = closeTag.charAt( 0 ),
secondCloseChar = closeTag.charAt( 1 );
openTag = "\\" + openTag.charAt( 0 ) + "\\" + openTag.charAt( 1 );
closeTag = "\\" + firstCloseChar + "\\" + secondCloseChar;
// Build regex with new delimiters
// {{
rTag = openTag
// # tag (followed by space,! or }) or equals or code
+ "(?:(?:(\\#)?(\\w+(?=[!\\s\\" + firstCloseChar + "]))" + "|(?:(\\=)|(\\*)))"
// params
+ "\\s*((?:[^\\" + firstCloseChar + "]|\\" + firstCloseChar + "(?!\\" + secondCloseChar + "))*?)"
// encoding
+ "(!(\\w*))?"
// closeBlock
+ "|(?:\\/([\\w\\$\\.\\[\\]]+)))"
// }}
+ closeTag;
// Default rTag: # tag equals code params encoding closeBlock
// /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
rTag = new RegExp( rTag, "g" );
},
//===============
// registerTags
//===============
// Register declarative tag.
registerTags: registerTags = function( name, tagFn ) {
var key;
if ( typeof name === "object" ) {
for ( key in name ) {
registerTags( key, name[ key ]);
}
} else {
// Simple single property case.
viewsNs.tags[ name ] = tagFn;
}
return this;
},
//===============
// registerHelpers
//===============
// Register helper function for use in markup.
registerHelpers: registerHelpers = function( name, helper ) {
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.
var key;
for ( key in name ) {
registerHelpers( key, name[ key ]);
}
} else {
// Simple single property case.
viewsNs.helpers[ name ] = helper;
}
return this;
},
//===============
// tmpl.encode
//===============
encode: function( encoding, text ) {
return text
? ( tmplEncode[ encoding || "html" ] || tmplEncode.html)( text ) // HTML encoding is the default
: "";
},
encoders: 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 );
}
//TODO add URL encoding, and perhaps other encoding helpers...
},
//===============
// renderTag
//===============
renderTag: function( tag, view, encode, content, tagProperties ) {
// This is a tag call, with arguments: "tag", view, encode, content, presenter [, params...]
var ret, ctx, name,
args = arguments,
presenters = viewsNs.presenters;
hash = tagProperties._hash,
tagFn = viewsNs.tags[ tag ];
if ( !tagFn ) {
return "";
}
content = content && view.tmpl.nested[ content - 1 ];
tagProperties.tmpl = tagProperties.tmpl || content || undefined;
// Set the tmpl property to the content of the block tag, unless set as an override property on the tag
if ( presenters && presenters[ tag ]) {
ctx = extend( extend( {}, tagProperties.ctx ), tagProperties );
delete ctx.ctx;
delete ctx._path;
delete ctx.tmpl;
tagProperties.ctx = ctx;
tagProperties._ctor = tag + (hash ? "=" + hash.slice( 0, -1 ) : "");
tagProperties = extend( extend( {}, tagFn ), tagProperties );
tagFn = viewsNs.tags.each; // Use each to render the layout template against the data
}
tagProperties._encode = encode;
tagProperties._view = view;
ret = tagFn.apply( tagProperties, args.length > 5 ? slice.call( args, 5 ) : [view.data] );
return ret || (ret === undefined ? "" : ret.toString()); // (If ret is the value 0 or false or null, will render to string)
}
},
//===============
// render
//===============
render: render = function( data, tmpl, context, parentView, path, tagName ) {
// Render template against data as a tree of subviews (nested template), or as a string (top-level template).
// tagName parameter for internal use only. Used for rendering templates registered as tags (which may have associated presenter objects)
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 = new View( context, path, parentView, data);
l = data.length;
for ( i = 0, l = data.length; i < l; i++ ) {
dataItem = data[ i ];
content = dataItem ? tmpl( dataItem, new View( context, path, arrayView, dataItem, tmpl, this )) : "";
result += viewsNs.activeViews ? "" + content + "" : content;
}
} else {
result += tmpl( data, new View( context, path, parentView, data, tmpl ));
}
return viewsNs.activeViews
// If in activeView mode, include annotations
? "" + result + ""
// else return just the string result
: result;
},
//===============
// template
//===============
template: function( name, tmpl ) {
// 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.
if (tmpl) {
// Compile template and associate with name
if ( "" + tmpl === tmpl ) { // type string
// 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 // not type string
? (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;
}
});
viewsNs.setDelimiters( "{{", "}}" );
//=================
// compile template
//=================
// Generate a reusable function that will serve to render a template against data
// (Compile AST then build template function)
function parsePath( all, comp, object, viewDataCtx, viewProperty, path, string, quot ) {
return object
? ((viewDataCtx
? viewProperty
? ("$view." + viewProperty)
: object
:("$data." + object)
) + ( path || "" ))
: string || (comp || "");
}
function compile( markup ) {
var newNode,
loc = 0,
stack = [],
topNode = [],
content = topNode,
current = [,,topNode];
function pushPreceedingContent( shift ) {
shift -= loc;
if ( shift ) {
content.push( markup.substr( loc, shift ).replace( rNewLine,"\\n"));
}
}
function parseTag( all, isBlock, tagName, equals, code, params, useEncode, encode, closeBlock, index ) {
// rTag : # tagName equals code params encode closeBlock
// /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
// Build abstract syntax tree: [ tagName, params, content, encode ]
var named,
hash = "",
parenDepth = 0,
quoted = FALSE, // boolean for string content in double qoutes
aposed = FALSE; // or in single qoutes
function parseParams( all, path, paren, comp, eq, comma, apos, quot, rightParen, space, index ) {
// path paren eq comma apos quot rtPrn space
// /(\$?[\w\.\[\]]+)(?:(\()|(===)|(\=))?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g
return aposed
// within single-quoted string
? ( aposed = !apos, (aposed ? all : '"'))
: quoted
// within double-quoted string
? ( quoted = !quot, (quoted ? all : '"'))
: comp
// comparison
? ( path.replace( rPath, parsePath ) + comp)
: eq
// named param
? parenDepth ? "" :( named = TRUE, '\b' + path + ':')
: paren
// function
? (parenDepth++, path.replace( rPath, parsePath ) + '(')
: rightParen
// function
? (parenDepth--, ")")
: path
// path
? path.replace( rPath, parsePath )
: comma
? ","
: space
? (parenDepth
? ""
: named
? ( named = FALSE, "\b")
: ","
)
: (aposed = apos, quoted = quot, '"');
}
tagName = tagName || equals;
pushPreceedingContent( index );
if ( code ) {
if ( viewsNs.allowCode ) {
content.push([ "*", params.replace( rUnescapeQuotes, "$1" )]);
}
} else if ( tagName ) {
if ( tagName === "else" ) {
current = stack.pop();
content = current[ 2 ];
isBlock = TRUE;
}
params = (params
? (params + " ")
.replace( rParams, parseParams )
.replace( rBuildHash, function( all, keyValue, index ) {
hash += keyValue + ",";
return "";
})
: "");
params = params.slice( 0, -1 );
newNode = [
tagName,
useEncode ? encode || "none" : "",
isBlock && [],
"{" + hash + "_hash:'" + hash + "',_path:'" + params + "'}",
params
];
if ( isBlock ) {
stack.push( current );
current = newNode;
}
content.push( newNode );
} else if ( closeBlock ) {
current = stack.pop();
}
loc = index + all.length; // location marker - parsed up to here
if ( !current ) {
throw "Expected block tag";
}
content = current[ 2 ];
}
markup = markup.replace( rEscapeQuotes, "\\$1" );
markup.replace( rTag, parseTag );
pushPreceedingContent( markup.length );
return buildTmplFunction( topNode );
}
// Build javascript compiled template function, from AST
function buildTmplFunction( nodes ) {
var ret, node, i,
nested = [],
l = nodes.length,
code = "try{var views="
+ (jQuery ? "jQuery" : "JsViews")
+ '.views,tag=views.renderTag,enc=views.encode,html=views.encoders.html,$ctx=$view && $view.ctx,result=""+\n\n';
for ( i = 0; i < l; i++ ) {
node = nodes[ i ];
if ( node[ 0 ] === "*" ) {
code = code.slice( 0, i ? -1 : -3 ) + ";" + node[ 1 ] + ( i + 1 < l ? "result+=" : "" );
} else if ( "" + node === node ) { // type string
code += '"' + node + '"+';
} else {
var tag = node[ 0 ],
encode = node[ 1 ],
content = node[ 2 ],
obj = node[ 3 ],
params = node[ 4 ],
paramsOrEmptyString = params + '||"")+';
if( content ) {
nested.push( buildTmplFunction( content ));
}
code += tag === "="
? (!encode || encode === "html"
? "html(" + paramsOrEmptyString
: encode === "none"
? ("(" + paramsOrEmptyString)
: ('enc("' + encode + '",' + paramsOrEmptyString)
)
: 'tag("' + tag + '",$view,"' + ( encode || "" ) + '",'
+ (content ? nested.length : '""') // For block tags, pass in the key (nested.length) to the nested content template
+ "," + obj + (params ? "," : "") + params + ")+";
}
}
ret = new Function( "$data, $view", code.slice( 0, -1) + ";return result;\n\n}catch(e){return views.err(e);}" );
ret.nested = nested;
return ret;
}
//========================== Private helper functions, used by code above ==========================
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 );