/*jslint evil: true */ steal('jquery/view', 'jquery/lang/string/rsplit').then(function( $ ) { // HELPER METHODS ============== var myEval = function( script ) { eval(script); }, // removes the last character from a string // this is no longer needed // chop = function( string ) { // return string.substr(0, string.length - 1); //}, rSplit = $.String.rsplit, extend = $.extend, isArray = $.isArray, // regular expressions for caching returnReg = /\r\n/g, retReg = /\r/g, newReg = /\n/g, nReg = /\n/, slashReg = /\\/g, quoteReg = /"/g, singleQuoteReg = /'/g, tabReg = /\t/g, leftBracket = /\{/g, rightBracket = /\}/g, quickFunc = /\s*\(([\$\w]+)\)\s*->([^\n]*)/, // escapes characters starting with \ clean = function( content ) { return content.replace(slashReg, '\\\\').replace(newReg, '\\n').replace(quoteReg, '\\"').replace(tabReg, '\\t'); }, // escapes html // - from prototype http://www.prototypejs.org/ escapeHTML = function( content ) { return content.replace(/&/g, '&').replace(//g, '>').replace(quoteReg, '"').replace(singleQuoteReg, "'"); }, $View = $.View, bracketNum = function(content){ var lefts = content.match(leftBracket), rights = content.match(rightBracket); return (lefts ? lefts.length : 0) - (rights ? rights.length : 0); }, /** * @class jQuery.EJS * * @plugin jquery/view/ejs * @parent jQuery.View * @download http://jmvcsite.heroku.com/pluginify?plugins[]=jquery/view/ejs/ejs.js * @test jquery/view/ejs/qunit.html * * * Ejs provides ERB * style client side templates. Use them with controllers to easily build html and inject * it into the DOM. * * ### Example * * The following generates a list of tasks: * * @codestart html * <ul> * <% for(var i = 0; i < tasks.length; i++){ %> * <li class="task <%= tasks[i].identity %>"><%= tasks[i].name %></li> * <% } %> * </ul> * @codeend * * For the following examples, we assume this view is in 'views\tasks\list.ejs'. * * * ## Use * * ### Loading and Rendering EJS: * * You should use EJS through the helper functions [jQuery.View] provides such as: * * - [jQuery.fn.after after] * - [jQuery.fn.append append] * - [jQuery.fn.before before] * - [jQuery.fn.html html], * - [jQuery.fn.prepend prepend], * - [jQuery.fn.replaceWith replaceWith], and * - [jQuery.fn.text text]. * * or [jQuery.Controller.prototype.view]. * * ### Syntax * * EJS uses 5 types of tags: * * - <% CODE %> - Runs JS Code. * For example: * * <% alert('hello world') %> * * - <%= CODE %> - Runs JS Code and writes the _escaped_ result into the result of the template. * For example: * *

<%= 'hello world' %>

* * - <%== CODE %> - Runs JS Code and writes the _unescaped_ result into the result of the template. * For example: * *

<%== 'hello world' %>

* * - <%%= CODE %> - Writes <%= CODE %> to the result of the template. This is very useful for generators. * * <%%= 'hello world' %> * * - <%# CODE %> - Used for comments. This does nothing. * * <%# 'hello world' %> * * ## Hooking up controllers * * After drawing some html, you often want to add other widgets and plugins inside that html. * View makes this easy. You just have to return the Contoller class you want to be hooked up. * * @codestart * <ul <%= Mxui.Tabs%>>...<ul> * @codeend * * You can even hook up multiple controllers: * * @codestart * <ul <%= [Mxui.Tabs, Mxui.Filler]%>>...<ul> * @codeend * * To hook up a controller with options or any other jQuery plugin use the * [jQuery.EJS.Helpers.prototype.plugin | plugin view helper]: * * @codestart * <ul <%= plugin('mxui_tabs', { option: 'value' }) %>>...<ul> * @codeend * * Don't add a semicolon when using view helpers. * * *

View Helpers

* View Helpers return html code. View by default only comes with * [jQuery.EJS.Helpers.prototype.view view] and [jQuery.EJS.Helpers.prototype.text text]. * You can include more with the view/helpers plugin. But, you can easily make your own! * Learn how in the [jQuery.EJS.Helpers Helpers] page. * * @constructor Creates a new view * @param {Object} options A hash with the following options * * * * * * * * * * * * * * * * * *
OptionDefaultDescription
text uses the provided text as the template. Example:
new View({text: '<%=user%>'}) *
type'<'type of magic tags. Options are '<' or '[' *
namethe element ID or url an optional name that is used for caching. *
*/ EJS = function( options ) { // If called without new, return a function that // renders with data and helpers like // EJS({text: '<%= message %>'})({message: 'foo'}); // this is useful for steal's build system if ( this.constructor != EJS ) { var ejs = new EJS(options); return function( data, helpers ) { return ejs.render(data, helpers); }; } // if we get a function directly, it probably is coming from // a steal-packaged view if ( typeof options == "function" ) { this.template = { fn: options }; return; } //set options on self extend(this, EJS.options, options); this.template = compile(this.text, this.type, this.name); }; // add EJS to jQuery if it exists window.jQuery && (jQuery.EJS = EJS); /** * @Prototype */ EJS.prototype. /** * Renders an object with view helpers attached to the view. * * new EJS({text: "<%= message %>"}).render({ * message: "foo" * },{helper: function(){ ... }}) * * @param {Object} object data to be rendered * @param {Object} [extraHelpers] an object with view helpers * @return {String} returns the result of the string */ render = function( object, extraHelpers ) { object = object || {}; this._extra_helpers = extraHelpers; var v = new EJS.Helpers(object, extraHelpers || {}); return this.template.fn.call(object, object, v); }; /** * @Static */ extend(EJS, { /** * Used to convert what's in <%= %> magic tags to a string * to be inserted in the rendered output. * * Typically, it's a string, and the string is just inserted. However, * if it's a function or an object with a hookup method, it can potentially be * be ran on the element after it's inserted into the page. * * This is a very nice way of adding functionality through the view. * Usually this is done with [jQuery.EJS.Helpers.prototype.plugin] * but the following fades in the div element after it has been inserted: * * @codestart * <%= function(el){$(el).fadeIn()} %> * @codeend * * @param {String|Object|Function} input the value in between the * write magic tags: <%= %> * @return {String} returns the content to be added to the rendered * output. The content is different depending on the type: * * * string - the original string * * null or undefined - the empty string "" * * an object with a hookup method - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View * * a function - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View * * an array - the attribute "data-view-id='XX'", where XX is a hookup number for jQuery.View */ text: function( input ) { // if it's a string, return if ( typeof input == 'string' ) { return input; } // if has no value if ( input === null || input === undefined ) { return ''; } // if it's an object, and it has a hookup method var hook = (input.hookup && // make a function call the hookup method function( el, id ) { input.hookup.call(input, el, id); }) || // or if it's a function, just use the input (typeof input == 'function' && input) || // of it its an array, make a function that calls hookup or the function // on each item in the array (isArray(input) && function( el, id ) { for ( var i = 0; i < input.length; i++ ) { input[i].hookup ? input[i].hookup(el, id) : input[i](el, id); } }); // finally, if there is a funciton to hookup on some dom // pass it to hookup to get the data-view-id back if ( hook ) { return "data-view-id='" + $View.hookup(hook) + "'"; } // finally, if all else false, toString it return input.toString ? input.toString() : ""; }, /** * Escapes the text provided as html if it's a string. * Otherwise, the value is passed to EJS.text(text). * * @param {String|Object|Array|Function} text to escape. Otherwise, * the result of [jQuery.EJS.text] is returned. * @return {String} the escaped text or likely a $.View data-view-id attribute. */ clean: function( text ) { //return sanatized text if ( typeof text == 'string' ) { return escapeHTML(text); } else if ( typeof text == 'number' ) { return text; } else { return EJS.text(text); } }, /** * @attribute options * Sets default options for all views. * * $.EJS.options.type = '[' * * Only one option is currently supported: type. * * Type is the left hand magic tag. */ options: { type: '<', ext: '.ejs' } }); // ========= SCANNING CODE ========= // Given a scanner, and source content, calls block with each token // scanner - an object of magicTagName : values // source - the source you want to scan // block - function(token, scanner), called with each token var scan = function( scanner, source, block ) { // split on /\n/ to have new lines on their own line. var source_split = rSplit(source, nReg), i = 0; for (; i < source_split.length; i++ ) { scanline(scanner, source_split[i], block); } }, scanline = function( scanner, line, block ) { scanner.lines++; var line_split = rSplit(line, scanner.splitter), token; for ( var i = 0; i < line_split.length; i++ ) { token = line_split[i]; if ( token !== null ) { block(token, scanner); } } }, // creates a 'scanner' object. This creates // values for the left and right magic tags // it's splitter property is a regexp that splits content // by all tags makeScanner = function( left, right ) { var scanner = {}; extend(scanner, { left: left + '%', right: '%' + right, dLeft: left + '%%', dRight: '%%' + right, eeLeft: left + '%==', eLeft: left + '%=', cmnt: left + '%#', scan: scan, lines: 0 }); scanner.splitter = new RegExp("(" + [scanner.dLeft, scanner.dRight, scanner.eeLeft, scanner.eLeft, scanner.cmnt, scanner.left, scanner.right + '\n', scanner.right, '\n'].join(")|("). replace(/\[/g, "\\[").replace(/\]/g, "\\]") + ")"); return scanner; }, // compiles a template where // source - template text // left - the left magic tag // name - the name of the template (for debugging) // returns an object like: {out : "", fn : function(){ ... }} where // out - the converted JS source of the view // fn - a function made from the JS source compile = function( source, left, name ) { // make everything only use \n source = source.replace(returnReg, "\n").replace(retReg, "\n"); // if no left is given, assume < left = left || '<'; // put and insert cmds are used for adding content to the template // currently they are identical, I am not sure why var put_cmd = "___v1ew.push(", insert_cmd = put_cmd, // the text that starts the view code (or block function) startTxt = 'var ___v1ew = [];', // the text that ends the view code (or block function) finishTxt = "return ___v1ew.join('')", // initialize a buffer buff = new EJS.Buffer([startTxt], []), // content is used as the current 'processing' string // this is the content between magic tags content = '', // adds something to be inserted into the view template // this comes out looking like __v1ew.push("CONENT") put = function( content ) { buff.push(put_cmd, '"', clean(content), '");'); }, // the starting magic tag startTag = null, // cleans the running content empty = function() { content = '' }, // what comes after clean or text doubleParen = "));", // a stack used to keep track of how we should end a bracket } // once we have a <%= %> with a leftBracket // we store how the file should end here (either '))' or ';' ) endStack =[]; // start going token to token scan(makeScanner(left, left === '[' ? ']' : '>'), source || "", function( token, scanner ) { // if we don't have a start pair var bn; if ( startTag === null ) { switch ( token ) { case '\n': content = content + "\n"; put(content); buff.cr(); empty(); break; // set start tag, add previous content (if there is some) // clean content case scanner.left: case scanner.eLeft: case scanner.eeLeft: case scanner.cmnt: // a new line, just add whatever content w/i a clean // reset everything startTag = token; if ( content.length > 0 ) { put(content); } empty(); break; case scanner.dLeft: // replace <%% with <% content += scanner.left; break; default: content += token; break; } } else { //we have a start tag switch ( token ) { case scanner.right: // %> switch ( startTag ) { case scanner.left: // <% // get the number of { minus } bn = bracketNum(content); // how are we ending this statement var last = // if the stack has value and we are ending a block endStack.length && bn == -1 ? // use the last item in the block stack endStack.pop() : // or use the default ending ";"; // if we are ending a returning block // add the finish text which returns the result of the // block if(last === doubleParen) { buff.push(finishTxt) } // add the remaining content buff.push(content, last); // if we have a block, start counting if(bn === 1 ){ endStack.push(";") } break; case scanner.eLeft: // <%= clean content bn = bracketNum(content); if( bn ) { endStack.push(doubleParen) } if(quickFunc.test(content)){ var parts = content.match(quickFunc) content = "function(__){var "+parts[1]+"=$(__);"+parts[2]+"}" } buff.push(insert_cmd, "jQuery.EJS.clean(", content,bn ? startTxt : doubleParen); break; case scanner.eeLeft: // <%== content // get the number of { minus } bn = bracketNum(content); // if we have more {, it means there is a block if( bn ){ // when we return to the same # of { vs } end wiht a doubleParen endStack.push(doubleParen) } buff.push(insert_cmd, "jQuery.EJS.text(", content, // if we have a block bn ? // start w/ startTxt "var _v1ew = [])" startTxt : // if not, add doubleParent to close push and text doubleParen ); break; } startTag = null; empty(); break; case scanner.dRight: content += scanner.right; break; default: content += token; break; } } }) if ( content.length > 0 ) { // Should be content.dump in Ruby buff.push(put_cmd, '"', clean(content) + '");'); } var template = buff.close(), out = { out: 'try { with(_VIEW) { with (_CONTEXT) {' + template + " "+finishTxt+"}}}catch(e){e.lineNumber=null;throw e;}" }; //use eval instead of creating a function, b/c it is easier to debug myEval.call(out, 'this.fn = (function(_CONTEXT,_VIEW){' + out.out + '});\r\n//@ sourceURL="' + name + '.js"'); return out; }; // A Buffer used to add content to. // This is useful for performance and simplifying the // code above. // We also can use this so we know line numbers when there // is an error. // pre_cmd - code that sets up the buffer // post - code that finalizes the buffer EJS.Buffer = function( pre_cmd, post ) { // the current line we are on this.line = []; // the combined content added to this buffer this.script = []; // content at the end of the buffer this.post = post; // add the pre commands to the first line this.push.apply(this, pre_cmd); }; EJS.Buffer.prototype = { // add content to this line // need to maintain your own semi-colons (for performance) push: function() { this.line.push.apply(this.line, arguments); }, // starts a new line cr: function() { this.script.push(this.line.join(''), "\n"); this.line = []; }, //returns the script too close: function() { // if we have ending line content, add it to the script if ( this.line.length > 0 ) { this.script.push(this.line.join('')); this.line = []; } // if we have ending content, add it this.post.length && this.push.apply(this, this.post); // always end in a ; this.script.push(";"); return this.script.join(""); } }; /** * @class jQuery.EJS.Helpers * @parent jQuery.EJS * By adding functions to jQuery.EJS.Helpers.prototype, those functions will be available in the * views. * * The following helper converts a given string to upper case: * * $.EJS.Helpers.prototype.toUpper = function(params) * { * return params.toUpperCase(); * } * * Use it like this in any EJS template: * * <%= toUpper('javascriptmvc') %> * * To access the current DOM element return a function that takes the element as a parameter: * * $.EJS.Helpers.prototype.upperHtml = function(params) * { * return function(el) { * $(el).html(params.toUpperCase()); * } * } * * In your EJS view you can then call the helper on an element tag: * *
>
* * * @constructor Creates a view helper. This function * is called internally. You should never call it. * @param {Object} data The data passed to the * view. Helpers have access to it through this._data */ EJS.Helpers = function( data, extras ) { this._data = data; this._extras = extras; extend(this, extras); }; /** * @prototype */ EJS.Helpers.prototype = { /** * Hooks up a jQuery plugin on. * @param {String} name the plugin name */ plugin: function( name ) { var args = $.makeArray(arguments), widget = args.shift(); return function( el ) { var jq = $(el); jq[widget].apply(jq, args); }; }, /** * Renders a partial view. This is deprecated in favor of $.View(). */ view: function( url, data, helpers ) { helpers = helpers || this._extras; data = data || this._data; return $View(url, data, helpers); //new EJS(options).render(data, helpers); } }; // options for steal's build $View.register({ suffix: "ejs", //returns a function that renders the view script: function( id, src ) { return "jQuery.EJS(function(_CONTEXT,_VIEW) { " + new EJS({ text: src, name: id }).template.out + " })"; }, renderer: function( id, text ) { return EJS({ text: text, name: id }); } }); });