574 lines
18 KiB
JavaScript
574 lines
18 KiB
JavaScript
/*! 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 ? ("<br/><b>Error:</b> <em> " + (e.message || e) + ". </em>"): '""';
|
|
},
|
|
|
|
//===============
|
|
// 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 <msamuel@google.com>
|
|
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 ? "<!--item-->" + content + "<!--/item-->" : content;
|
|
}
|
|
} else {
|
|
result += tmpl( data, new View( context, path, parentView, data, tmpl ));
|
|
}
|
|
|
|
return viewsNs.activeViews
|
|
// If in activeView mode, include annotations
|
|
? "<!--tmpl(" + (path || "") + ") " + (tagName ? "tag=" + tagName : tmpl._name) + "-->" + result + "<!--/tmpl-->"
|
|
// 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 <msamuel@google.com>
|
|
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 );
|