/*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
*
* Option | Default | Description |
*
* text |
* |
* uses the provided text as the template. Example:
new View({text: '<%=user%>'})
* |
*
*
* type |
* '<' |
* type of magic tags. Options are '<' or '['
* |
*
*
* name |
* the 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
});
}
});
});