1 /*! JsRender v1.0pre - (jsrender.js version: does not require jQuery): http://github.com/BorisMoore/jsrender */
3 * Optimized version of jQuery Templates, fosr rendering to string, using 'codeless' markup.
5 * Copyright 2011, Boris Moore
6 * Released under the MIT License.
8 window.JsViews || window.jQuery && jQuery.views || (function( window, undefined ) {
10 var $, _$, JsViews, viewsNs, tmplEncode, render, rTag, registerTags, registerHelpers, extend,
11 FALSE = false, TRUE = true,
12 jQuery = window.jQuery, document = window.document,
13 htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
14 rPath = /^(true|false|null|[\d\.]+)|(\w+|\$(view|data|ctx|(\w+)))([\w\.]*)|((['"])(?:\\\1|.)*\7)$/g,
15 rParams = /(\$?[\w\.\[\]]+)(?:(\()|\s*(===|!==|==|!=|<|>|<=|>=)\s*|\s*(\=)\s*)?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g,
17 rUnescapeQuotes = /\\(['"])/g,
18 rEscapeQuotes = /\\?(['"])/g,
19 rBuildHash = /\x08([^\x08]+)\x08/g,
26 htmlSpecialChar = /[\x00"&'<>]/g,
27 slice = Array.prototype.slice;
31 ////////////////////////////////////////////////////////////////////////////////////////////////
32 // jQuery is loaded, so make $ the jQuery object
36 // Use first wrapped element as template markup.
37 // Return string obtained by rendering the template against data.
38 render: function( data, context, parentView, path ) {
39 return render( data, this[0], context, parentView, path );
42 // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
43 template: function( name, context ) {
44 return $.template( name, this[0], context );
50 ////////////////////////////////////////////////////////////////////////////////////////////////
51 // jQuery is not loaded. Make $ the JsViews object
53 // Map over the $ in case of overwrite
56 window.JsViews = JsViews = window.$ = $ = {
57 extend: function( target, source ) {
59 for ( name in source ) {
60 target[ name ] = source[ name ];
64 isArray: Array.isArray || function( obj ) {
65 return Object.prototype.toString.call( obj ) === "[object Array]";
67 noConflict: function() {
68 if ( window.$ === JsViews ) {
82 function View( context, path, parentView, data, template ) {
83 // Returns a view data structure for a new rendered instance of a template.
84 // The content field is a hierarchical array of strings and nested views.
86 parentView = parentView || { viewsCount:0, ctx: viewsNs.helpers };
88 var parentContext = parentView && parentView.ctx;
93 // inherit context from parentView, merged with new context.
94 itemNumber: ++parentView.viewsCount || 1,
97 data: data || parentView.data || {},
98 // Set additional context on this view (which will modify the context inherited from the parent, and be inherited by child views)
99 ctx : context && context === parentContext
101 : (parentContext ? extend( extend( {}, parentContext ), context ) : context||{}),
102 // If no jQuery, extend does not support chained copies - so limit to two parameters
113 view.onElse = function( presenter, args ) {
116 while ( l && !args[ i++ ]) {
117 // Only render content if args.length === 0 (i.e. this is an else with no condition) or if a condition argument is truey
122 view.onElse = undefined; // If condition satisfied, so won't run 'else'.
123 return render( view.data, presenter.tmpl, view.ctx, view);
125 return view.onElse( this, arguments );
128 var view = this._view;
129 return view.onElse ? view.onElse( this, arguments ) : "";
139 for ( i = 0; i < l; i++ ) {
140 result += args[ i ] ? render( args[ i ], content, self.ctx || view.ctx, view, self._path, self._ctor ) : "";
143 // If no data parameter, use the current $data from view, and render once
144 : result + render( view.data, content, view.ctx, view, self._path, self.tag );
146 "=": function( value ) {
149 "*": function( value ) {
154 not: function( value ) {
161 return viewsNs.debugMode ? ("<br/><b>Error:</b> <em> " + (e.message || e) + ". </em>"): '""';
168 setDelimiters: function( openTag, closeTag ) {
169 // Set or modify the delimiter characters for tags: "{{" and "}}"
170 var firstCloseChar = closeTag.charAt( 0 ),
171 secondCloseChar = closeTag.charAt( 1 );
172 openTag = "\\" + openTag.charAt( 0 ) + "\\" + openTag.charAt( 1 );
173 closeTag = "\\" + firstCloseChar + "\\" + secondCloseChar;
175 // Build regex with new delimiters
178 // # tag (followed by space,! or }) or equals or code
179 + "(?:(?:(\\#)?(\\w+(?=[!\\s\\" + firstCloseChar + "]))" + "|(?:(\\=)|(\\*)))"
181 + "\\s*((?:[^\\" + firstCloseChar + "]|\\" + firstCloseChar + "(?!\\" + secondCloseChar + "))*?)"
185 + "|(?:\\/([\\w\\$\\.\\[\\]]+)))"
189 // Default rTag: # tag equals code params encoding closeBlock
190 // /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
192 rTag = new RegExp( rTag, "g" );
200 // Register declarative tag.
201 registerTags: registerTags = function( name, tagFn ) {
203 if ( typeof name === "object" ) {
204 for ( key in name ) {
205 registerTags( key, name[ key ]);
208 // Simple single property case.
209 viewsNs.tags[ name ] = tagFn;
218 // Register helper function for use in markup.
219 registerHelpers: registerHelpers = function( name, helper ) {
220 if ( typeof name === "object" ) {
221 // Object representation where property name is path and property value is value.
222 // TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
224 for ( key in name ) {
225 registerHelpers( key, name[ key ]);
228 // Simple single property case.
229 viewsNs.helpers[ name ] = helper;
238 encode: function( encoding, text ) {
240 ? ( tmplEncode[ encoding || "html" ] || tmplEncode.html)( text ) // HTML encoding is the default
244 encoders: tmplEncode = {
245 "none": function( text ) {
248 "html": function( text ) {
249 // HTML encoding helper: Replace < > & and ' and " by corresponding entities.
250 // Implementation, from Mike Samuel <msamuel@google.com>
251 return String( text ).replace( htmlSpecialChar, replacerForHtml );
253 //TODO add URL encoding, and perhaps other encoding helpers...
260 renderTag: function( tag, view, encode, content, tagProperties ) {
261 // This is a tag call, with arguments: "tag", view, encode, content, presenter [, params...]
264 presenters = viewsNs.presenters;
265 hash = tagProperties._hash,
266 tagFn = viewsNs.tags[ tag ];
272 content = content && view.tmpl.nested[ content - 1 ];
273 tagProperties.tmpl = tagProperties.tmpl || content || undefined;
274 // Set the tmpl property to the content of the block tag, unless set as an override property on the tag
276 if ( presenters && presenters[ tag ]) {
277 ctx = extend( extend( {}, tagProperties.ctx ), tagProperties );
281 tagProperties.ctx = ctx;
282 tagProperties._ctor = tag + (hash ? "=" + hash.slice( 0, -1 ) : "");
284 tagProperties = extend( extend( {}, tagFn ), tagProperties );
285 tagFn = viewsNs.tags.each; // Use each to render the layout template against the data
288 tagProperties._encode = encode;
289 tagProperties._view = view;
290 ret = tagFn.apply( tagProperties, args.length > 5 ? slice.call( args, 5 ) : [view.data] );
291 return ret || (ret === undefined ? "" : ret.toString()); // (If ret is the value 0 or false or null, will render to string)
299 render: render = function( data, tmpl, context, parentView, path, tagName ) {
300 // Render template against data as a tree of subviews (nested template), or as a string (top-level template).
301 // tagName parameter for internal use only. Used for rendering templates registered as tags (which may have associated presenter objects)
302 var i, l, dataItem, arrayView, content, result = "";
304 if ( arguments.length === 2 && data.jsViews ) {
306 context = parentView.ctx;
307 data = parentView.data;
309 tmpl = $.template( tmpl );
311 return ""; // Could throw...
314 if ( $.isArray( data )) {
315 // Create a view item for the array, whose child views correspond to each data item.
316 arrayView = new View( context, path, parentView, data);
318 for ( i = 0, l = data.length; i < l; i++ ) {
319 dataItem = data[ i ];
320 content = dataItem ? tmpl( dataItem, new View( context, path, arrayView, dataItem, tmpl, this )) : "";
321 result += viewsNs.activeViews ? "<!--item-->" + content + "<!--/item-->" : content;
324 result += tmpl( data, new View( context, path, parentView, data, tmpl ));
327 return viewsNs.activeViews
328 // If in activeView mode, include annotations
329 ? "<!--tmpl(" + (path || "") + ") " + (tagName ? "tag=" + tagName : tmpl._name) + "-->" + result + "<!--/tmpl-->"
330 // else return just the string result
338 template: function( name, tmpl ) {
340 // Use $.template( name, tmpl ) to cache a named template,
341 // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
342 // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
345 // Use $.template( name ) to access a cached template.
346 // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
347 // will return the compiled template, without adding a name reference.
348 // If templateString is not a selector, $.template( templateString ) is equivalent
349 // to $.template( null, templateString ). To ensure a string is treated as a template,
350 // include an HTML element, an HTML comment, or a template comment tag.
353 // Compile template and associate with name
354 if ( "" + tmpl === tmpl ) { // type string
355 // This is an HTML string being passed directly in.
356 tmpl = compile( tmpl );
357 } else if ( jQuery && tmpl instanceof $ ) {
361 if ( jQuery && tmpl.nodeType ) {
362 // If this is a template block, use cached copy, or generate tmpl function and cache.
363 tmpl = $.data( tmpl, "tmpl" ) || $.data( tmpl, "tmpl", compile( tmpl.innerHTML ));
365 viewsNs.templates[ tmpl._name = tmpl._name || name || "_" + autoName++ ] = tmpl;
369 // Return named compiled template
371 ? "" + name !== name // not type string
373 ? name // already compiled
374 : $.template( null, name ))
375 : viewsNs.templates[ name ] ||
376 // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
377 $.template( null, htmlExpr.test( name ) ? name : try$( name ))
382 viewsNs.setDelimiters( "{{", "}}" );
388 // Generate a reusable function that will serve to render a template against data
389 // (Compile AST then build template function)
391 function parsePath( all, comp, object, viewDataCtx, viewProperty, path, string, quot ) {
395 ? ("$view." + viewProperty)
399 : string || (comp || "");
402 function compile( markup ) {
408 current = [,,topNode];
410 function pushPreceedingContent( shift ) {
413 content.push( markup.substr( loc, shift ).replace( rNewLine,"\\n"));
417 function parseTag( all, isBlock, tagName, equals, code, params, useEncode, encode, closeBlock, index ) {
418 // rTag : # tagName equals code params encode closeBlock
419 // /\{\{(?:(?:(\#)?(\w+(?=[\s\}!]))|(?:(\=)|(\*)))((?:[^\}]|\}(?!\}))*?)(!(\w*))?|(?:\/([\w\$\.\[\]]+)))\}\}/g;
421 // Build abstract syntax tree: [ tagName, params, content, encode ]
425 quoted = FALSE, // boolean for string content in double qoutes
426 aposed = FALSE; // or in single qoutes
428 function parseParams( all, path, paren, comp, eq, comma, apos, quot, rightParen, space, index ) {
429 // path paren eq comma apos quot rtPrn space
430 // /(\$?[\w\.\[\]]+)(?:(\()|(===)|(\=))?|(\,\s*)|\\?(\')|\\?(\")|(\))|(\s+)/g
433 // within single-quoted string
434 ? ( aposed = !apos, (aposed ? all : '"'))
436 // within double-quoted string
437 ? ( quoted = !quot, (quoted ? all : '"'))
440 ? ( path.replace( rPath, parsePath ) + comp)
443 ? parenDepth ? "" :( named = TRUE, '\b' + path + ':')
446 ? (parenDepth++, path.replace( rPath, parsePath ) + '(')
449 ? (parenDepth--, ")")
452 ? path.replace( rPath, parsePath )
459 ? ( named = FALSE, "\b")
462 : (aposed = apos, quoted = quot, '"');
465 tagName = tagName || equals;
466 pushPreceedingContent( index );
468 if ( viewsNs.allowCode ) {
469 content.push([ "*", params.replace( rUnescapeQuotes, "$1" )]);
471 } else if ( tagName ) {
472 if ( tagName === "else" ) {
473 current = stack.pop();
474 content = current[ 2 ];
479 .replace( rParams, parseParams )
480 .replace( rBuildHash, function( all, keyValue, index ) {
481 hash += keyValue + ",";
485 params = params.slice( 0, -1 );
488 useEncode ? encode || "none" : "",
490 "{" + hash + "_hash:'" + hash + "',_path:'" + params + "'}",
495 stack.push( current );
498 content.push( newNode );
499 } else if ( closeBlock ) {
500 current = stack.pop();
502 loc = index + all.length; // location marker - parsed up to here
504 throw "Expected block tag";
506 content = current[ 2 ];
508 markup = markup.replace( rEscapeQuotes, "\\$1" );
509 markup.replace( rTag, parseTag );
510 pushPreceedingContent( markup.length );
511 return buildTmplFunction( topNode );
514 // Build javascript compiled template function, from AST
515 function buildTmplFunction( nodes ) {
519 code = "try{var views="
520 + (jQuery ? "jQuery" : "JsViews")
521 + '.views,tag=views.renderTag,enc=views.encode,html=views.encoders.html,$ctx=$view && $view.ctx,result=""+\n\n';
523 for ( i = 0; i < l; i++ ) {
525 if ( node[ 0 ] === "*" ) {
526 code = code.slice( 0, i ? -1 : -3 ) + ";" + node[ 1 ] + ( i + 1 < l ? "result+=" : "" );
527 } else if ( "" + node === node ) { // type string
528 code += '"' + node + '"+';
535 paramsOrEmptyString = params + '||"")+';
538 nested.push( buildTmplFunction( content ));
541 ? (!encode || encode === "html"
542 ? "html(" + paramsOrEmptyString
544 ? ("(" + paramsOrEmptyString)
545 : ('enc("' + encode + '",' + paramsOrEmptyString)
547 : 'tag("' + tag + '",$view,"' + ( encode || "" ) + '",'
548 + (content ? nested.length : '""') // For block tags, pass in the key (nested.length) to the nested content template
549 + "," + obj + (params ? "," : "") + params + ")+";
552 ret = new Function( "$data, $view", code.slice( 0, -1) + ";return result;\n\n}catch(e){return views.err(e);}" );
557 //========================== Private helper functions, used by code above ==========================
559 function replacerForHtml( ch ) {
560 // Original code from Mike Samuel <msamuel@google.com>
561 return escapeMapForHtml[ ch ]
562 // Intentional assignment that caches the result of encoding ch.
563 || ( escapeMapForHtml[ ch ] = "&#" + ch.charCodeAt( 0 ) + ";" );
566 function try$( selector ) {
567 // If selector is valid, return jQuery object, otherwise return (invalid) selector string
569 return $( selector );