/*! 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 );