diff --git a/Readme.md b/Readme.md
index 8e398fd2..b89053ae 100644
--- a/Readme.md
+++ b/Readme.md
@@ -18,6 +18,8 @@ Embedded JavaScript templates.
* Filter support for designer-friendly templates
* Client-side support
* Newline slurping with `<% code -%>` or `<% -%>` or `<%= code -%>` or `<%- code -%>`
+ * Layouts with <% extends foo.html %> and <% block block_name %>
+ * Mixins with <% include %>
## Example
@@ -55,6 +57,58 @@ Which would make the following a valid template:
{{= title }}
+## Layouts and Blocks
+
+Layout for a view can be specified by using :
+
+ <% extends layout.html %>
+
+This will insert the contents of layout.html at the location of the extends
+statement. Extends are used with blocks to provide dynamic layout capabilities
+with default behaviour.
+
+For example, consider the following layout.html:
+
+
+ <% block block1%>
+
This is default value
+ <% end %>
+
+
+ <% block block2%>
+ <% end %>
+
+
+In your ejs file, specifying:
+
+ <% extends layout.html %>
+ <% block block2 %>Block 2 value
<%end%>
+
+would cause the following to be rendered.
+
+
+
This is default value
+
+
+
+In the above _block1_ has a default value defined, if _block1_ is not passed in
+the default value would be used. However, if _block1_ was specified, then it
+would override the default value.
+
+Please note that blocks must be terminated with an _end_ statement.
+
+## Mixins
+
+This effectively replace partials() from Express 2.x. You can include partiral
+ejs by specifying:
+
+ <% include component.html %>
+
+This would insert the contents of _component.html_ at the location of the include
+statement.
+
## Filters
EJS conditionally supports the concept of "filters". A "filter chain"
@@ -121,6 +175,7 @@ ejs.filters.last = function(obj) {
};
```
+
## client-side support
include `./ejs.js` or `./ejs.min.js` and `require("ejs").compile(str)`.
diff --git a/ejs.js b/ejs.js
index d910f117..8a9986c3 100644
--- a/ejs.js
+++ b/ejs.js
@@ -61,7 +61,9 @@ require.register("ejs.js", function(module, exports, require){
*/
var utils = require('./utils')
- , fs = require('fs');
+ , fs = require('fs')
+ , path = require('path')
+ , XRegExp = require('xregexp').XRegExp;
/**
* Library version.
@@ -104,7 +106,7 @@ exports.clearCache = function(){
*/
function filtered(js) {
- return js.substr(1).split('|').reduce(function(js, filter){
+ return js.split('|').reduce(function(js, filter){
var parts = filter.split(':')
, name = parts.shift()
, args = parts.shift() || '';
@@ -149,84 +151,248 @@ function rethrow(err, str, filename, lineno){
}
/**
- * Parse the given `str` of ejs, returning the function body.
+ * Token, used to represent a token/node during parsing
+ * and contains parsing info related to each token/node.
+ */
+
+Token = function (type, text, lineno, meta, children){
+ this.type = type;
+ this.text = text;
+ this.lineno = lineno;
+ this.meta = meta;
+ this.children = children;
+}
+
+/**
+ * Parser class groups together functions that are used for
+ * parsing an ejs file.
*
- * @param {String} str
- * @return {String}
+ * Contructor takes configuration options as parameter.
+ * @param options
* @api public
*/
+var Parser = exports.Parser = function(options) {
+ this.options = options || {};
+ this.delimiter = {
+ open : XRegExp.escape(options.open || exports.open || '<%')
+ , close : XRegExp.escape(options.close || exports.close || '%>')
+ };
+ this.keyword = {
+ block : 'block'
+ , end : 'end'
+ , extends : 'extends'
+ , include : 'include'
+ }
+ this.viewsDir = options.viewsDir? options.viewsDir : '';
+}
-var parse = exports.parse = function(str, options){
- var options = options || {}
- , open = options.open || exports.open || '<%'
- , close = options.close || exports.close || '%>';
-
- var buf = [
- "var buf = [];"
- , "\nwith (locals) {"
- , "\n buf.push('"
- ];
-
- var lineno = 1;
+/**
+ * Parse the given `str` of ejs, returning the function body.
+ *
+ * @param {String} str The string to parse.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @param {Boolean} layout Process as a layout file, i.e. no header/footer.
+ * @return {String} String containing javascript.
+ */
+Parser.prototype.parse = function(str, blocks, layout) {
+ var blocks = blocks ? blocks : []
+ , pre = [ "var buf = [];"
+ , "\nwith (locals) {"
+ , "\n buf.push('"]
+ , post = ["');\n}\nreturn buf.join('');"];
+
+ var tokens = this.tokenize(str, blocks);
+ var buf = this.convertTokens(tokens, blocks);
+
+ if (!layout) {
+ buf = pre.concat(buf, post);
+ }
+ return buf.join('');
+}
+
+/**
+ * Process tokenized form of the string to parse, convertingtokens to
+ * javascript.
+ *
+ * @param {String} str The string to parse.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @return {String[]} List of strings corresponding to the input tokens.
+ */
+Parser.prototype.convertTokens = function(tokens, blocks) {
+ var buf = [];
+ var consumeEOL = false;
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i]
+ , res = ''
+ , line = '__stack.lineno=' + token.lineno;
+
+ switch (token.type) {
+ case 'literal':
+ res = token.text
+ if (consumeEOL) {
+ res = XRegExp.replace( res, /\n/, '', 'one');
+ consumeEOL = false;
+ }
+ res = res
+ .replace(/\\/g, "\\\\")
+ .replace(/\'/g, "\\'")
+ .replace(/\r/g, "")
+ .replace(/\n/g, "\\n");
+ break;
+ case 'code':
+ var prefix = "');" + line + ';'
+ , postfix = "; buf.push('"
+ , js = token.text;
+
+ if ( token.meta && token.meta.buffered) {
+ if (token.meta.escaped) {
+ prefix = "', escape((" + line + ', ';
+ postfix = ")), '";
+ } else {
+ prefix = "', (" + line + ', ';
+ postfix = "), '";
+ }
+ if (token.meta.filtered) {
+ js = filtered(js);
+ }
+ }
+
+ res = prefix + js + postfix;
+ break;
+ case 'consume-EOL':
+ consumeEOL = true;
+ break;
+ case this.keyword.extends:
+ var filename = path.join(this.viewsDir, token.text)
+ , js = fs.readFileSync(filename, 'utf8');
+ res = this.parse(js, blocks, true);
+ break;
+ case this.keyword.block:
+ buf.push.apply(buf, this.convertTokens(token.children, blocks));
+ }
+ buf.push(res);
+ }
+ return buf;
+}
+
+/**
+ * Convert the provided string into a list of tokens/nodes.
+ *
+ * @param {String} str The string to tokenize.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @return {Token[]} List of resulting tokens.
+ */
+Parser.prototype.tokenize = function (str, blocks){
+ var tokens = []
+ , myBlocks = []
+ , lineno = 1
+ , stack = []
+ , layout = false
+ , ejsRE = XRegExp.cache('^(?[=-](?:)?)?(?.*?)\\s*(?-)?$', 'mn')
+ , keywordRE = XRegExp.cache('^\\s*((?'
+ + this.keyword.end + ')|((?('
+ + this.keyword.extends + ')|('
+ + this.keyword.block + ')|('
+ + this.keyword.include +'))(\\s+(?.*?))?))\\s*$', 'mn');
+
+
+ var segments = XRegExp.matchRecursive(str, this.delimiter.open, this.delimiter.close, 'g',
+ {valueNames: ['literal', 'open', 'ejs', 'close']});
+ for (var i = 0; i < segments.length; i++) {
+ var segment = segments[i]
+ , slurp = false;
+
+ switch (segment.value) {
+ case 'literal':
+ tokens.push(new Token('literal', segment.name, lineno));
+ break;
+ case 'ejs': // ejs block, tokenize it further
+ var match = XRegExp.exec(segment.name, ejsRE, 'm');
+ if (!match) throw new Error('Incorrect syntax for ejs...'); // should never happen
+ if (match.op) {
+ tokens.push(new Token('code', match.body, lineno, { buffered: true,
+ escaped: match.op[0] == '=' ? true: false,
+ filtered: match.filter? true: false }));
+ } else {
+ var kwMatch = XRegExp.exec(match.body, keywordRE, 'm');
+ if (kwMatch) {
+ if (kwMatch.keyword) {
+ switch (kwMatch.keyword) {
+ case this.keyword.block:
+ if (!kwMatch.name) throw new Error('Must specify name for block...');
+
+ // its a block, collect all susequent segments as children until the match end
+ var isArgument = (layout && stack.length == 0 )? true:false;
+ stack.push({token: new Token(this.keyword.block, kwMatch.name, lineno
+ , {isArgument: isArgument})
+ , tokensBuf: tokens
+ , keyword: this.keyword.block});
+ tokens = [];
+ break;
+ case this.keyword.extends:
+ if (!kwMatch.name) throw new Error('Must specify filename after extends...');
+ tokens.push(new Token(this.keyword.extends, kwMatch.name, lineno));
+ layout = true;
+ break;
+ case this.keyword.include:
+ if (!kwMatch.name) throw new Error('Must specify file to include...');
+ var filename = path.join(this.viewsDir, kwMatch.name);
+ var includedEjs = fs.readFileSync(filename, 'utf8');
+ if (this.options.debug) console.log('Including: \n' + includedEjs);
+ tokens.push.apply(tokens, this.tokenize(includedEjs));
+ break;
+ default: // should never happen...
+ throw new Error('Invalid keyword: ' + kwMatch.keyword);
+ }
+ } else if (kwMatch.end){
+ var parent = stack.pop();
+ if (!parent) throw new Error('Encountered ' + this.keyword.end + ' without matching block statement.');
+ parent.token.children = tokens;
+ tokens = parent.tokensBuf;
+
+ if (blocks[parent.token.text]) {
+ tokens.push(blocks[parent.token.text]);
+ } else if (!parent.token.meta.isArgument) {
+ tokens.push(parent.token);
+ } else {
+ myBlocks[parent.token.text] = parent.token;
+ }
+ } else { // should never happen..
+ throw new Error('Unexpected error, probably a bug in the parser!');
+ }
+ } else { //unbuffered code
+ tokens.push( new Token('code', match.body, lineno));
+ }
+ }
+ if (match.slurp) slurp = true;
+ break;
- var consumeEOL = false;
- for (var i = 0, len = str.length; i < len; ++i) {
- if (str.slice(i, open.length + i) == open) {
- i += open.length
-
- var prefix, postfix, line = '__stack.lineno=' + lineno;
- switch (str.substr(i, 1)) {
- case '=':
- prefix = "', escape((" + line + ', ';
- postfix = ")), '";
- ++i;
- break;
- case '-':
- prefix = "', (" + line + ', ';
- postfix = "), '";
- ++i;
- break;
default:
- prefix = "');" + line + ';';
- postfix = "; buf.push('";
- }
+ // ignore open and close
+ }
+ if (slurp) { tokens.push(new Token('consume-EOL', '', lineno)); }
- var end = str.indexOf(close, i)
- , js = str.substring(i, end)
- , start = i
- , n = 0;
-
- if ('-' == js[js.length-1]){
- js = js.substring(0, js.length - 2);
- consumeEOL = true;
- }
-
- while (~(n = js.indexOf("\n", n))) n++, lineno++;
- if (js.substr(0, 1) == ':') js = filtered(js);
- buf.push(prefix, js, postfix);
- i += end - start + close.length - 1;
-
- } else if (str.substr(i, 1) == "\\") {
- buf.push("\\\\");
- } else if (str.substr(i, 1) == "'") {
- buf.push("\\'");
- } else if (str.substr(i, 1) == "\r") {
- buf.push(" ");
- } else if (str.substr(i, 1) == "\n") {
- if (consumeEOL) {
- consumeEOL = false;
- } else {
- buf.push("\\n");
- lineno++;
- }
- } else {
- buf.push(str.substr(i, 1));
+ var n = 0;
+ while (~(n = segment.name.indexOf("\n", n))) n++, lineno++;
}
- }
- buf.push("');\n}\nreturn buf.join('');");
- return buf.join('');
-};
+ if(stack.length != 0) throw new Error('Unmatched ' + stack.pop().keyword + ' statment...');
+
+ // send blocks back to parent
+ for (var x in myBlocks) { blocks[x] = myBlocks[x];}
+ return tokens;
+}
+
+/**
+ * Parse the given `str` of ejs, returning the function body.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api public
+ */
+var parse = exports.parse = function(str, options){
+ return new Parser(options).parse(str);
+}
/**
* Compile the given `str` of ejs into a `Function`.
diff --git a/ejs.min.js b/ejs.min.js
index 611ee425..61ac83fe 100644
--- a/ejs.min.js
+++ b/ejs.min.js
@@ -1 +1 @@
-ejs=function(){function require(p){if("fs"==p)return{};var path=require.resolve(p),mod=require.modules[path];if(!mod)throw new Error('failed to require "'+p+'"');return mod.exports||(mod.exports={},mod.call(mod.exports,mod,mod.exports,require.relative(path))),mod.exports}return require.modules={},require.resolve=function(path){var orig=path,reg=path+".js",index=path+"/index.js";return require.modules[reg]&®||require.modules[index]&&index||orig},require.register=function(path,fn){require.modules[path]=fn},require.relative=function(parent){return function(p){if("."!=p.substr(0,1))return require(p);var path=parent.split("/"),segs=p.split("/");path.pop();for(var i=0;i> ":" ")+curr+"| "+line}).join("\n");throw err.path=filename,err.message=(filename||"ejs")+":"+lineno+"\n"+context+"\n\n"+err.message,err}var parse=exports.parse=function(str,options){var options=options||{},open=options.open||exports.open||"<%",close=options.close||exports.close||"%>",buf=["var buf = [];","\nwith (locals) {","\n buf.push('"],lineno=1,consumeEOL=!1;for(var i=0,len=str.length;ib?1:a/g,">").replace(/"/g,""")}}),require("ejs")}();
\ No newline at end of file
+ejs=function(){function require(p){if("fs"==p)return{};var path=require.resolve(p),mod=require.modules[path];if(!mod)throw new Error('failed to require "'+p+'"');return mod.exports||(mod.exports={},mod.call(mod.exports,mod,mod.exports,require.relative(path))),mod.exports}return require.modules={},require.resolve=function(path){var orig=path,reg=path+".js",index=path+"/index.js";return require.modules[reg]&®||require.modules[index]&&index||orig},require.register=function(path,fn){require.modules[path]=fn},require.relative=function(parent){return function(p){if("."!=p.substr(0,1))return require(p);var path=parent.split("/"),segs=p.split("/");path.pop();for(var i=0;i> ":" ")+curr+"| "+line}).join("\n");throw err.path=filename,err.message=(filename||"ejs")+":"+lineno+"\n"+context+"\n\n"+err.message,err}var utils=require("./utils"),fs=require("fs"),path=require("path"),XRegExp=require("xregexp").XRegExp;exports.version="0.7.2";var filters=exports.filters=require("./filters"),cache={};exports.clearCache=function(){cache={}},Token=function(type,text,lineno,meta,children){this.type=type,this.text=text,this.lineno=lineno,this.meta=meta,this.children=children};var Parser=exports.Parser=function(options){this.options=options||{},this.delimiter={open:XRegExp.escape(options.open||exports.open||"<%"),close:XRegExp.escape(options.close||exports.close||"%>")},this.keyword={block:"block",end:"end","extends":"extends",include:"include"},this.viewsDir=options.viewsDir?options.viewsDir:""};Parser.prototype.parse=function(str,blocks,layout){var blocks=blocks?blocks:[],pre=["var buf = [];","\nwith (locals) {","\n buf.push('"],post=["');\n}\nreturn buf.join('');"],tokens=this.tokenize(str,blocks),buf=this.convertTokens(tokens,blocks);return layout||(buf=pre.concat(buf,post)),buf.join("")},Parser.prototype.convertTokens=function(tokens,blocks){var buf=[],consumeEOL=!1;for(var i=0;i[=-](?:)?)?(?.*?)\\s*(?-)?$","mn"),keywordRE=XRegExp.cache("^\\s*((?"+this.keyword.end+")|((?("+this.keyword.extends+")|("+this.keyword.block+")|("+this.keyword.include+"))(\\s+(?.*?))?))\\s*$","mn"),segments=XRegExp.matchRecursive(str,this.delimiter.open,this.delimiter.close,"g",{valueNames:["literal","open","ejs","close"]});for(var i=0;ib?1:a/g,">").replace(/"/g,""")}}),require("ejs")}();
\ No newline at end of file
diff --git a/lib/ejs.js b/lib/ejs.js
index 8eac5a5c..aeaf13cc 100644
--- a/lib/ejs.js
+++ b/lib/ejs.js
@@ -10,7 +10,9 @@
*/
var utils = require('./utils')
- , fs = require('fs');
+ , fs = require('fs')
+ , path = require('path')
+ , XRegExp = require('xregexp').XRegExp;
/**
* Library version.
@@ -53,7 +55,7 @@ exports.clearCache = function(){
*/
function filtered(js) {
- return js.substr(1).split('|').reduce(function(js, filter){
+ return js.split('|').reduce(function(js, filter){
var parts = filter.split(':')
, name = parts.shift()
, args = parts.shift() || '';
@@ -98,84 +100,253 @@ function rethrow(err, str, filename, lineno){
}
/**
- * Parse the given `str` of ejs, returning the function body.
+ * Token, used to represent a token/node during parsing
+ * and contains parsing info related to each token/node.
+ */
+
+Token = function (type, text, lineno, meta, children){
+ this.type = type;
+ this.text = text;
+ this.lineno = lineno;
+ this.meta = meta;
+ this.children = children;
+}
+
+/**
+ * Parser class groups together functions that are used for
+ * parsing an ejs file.
*
- * @param {String} str
- * @return {String}
+ * Contructor takes configuration options as parameter.
+ * @param options
* @api public
*/
+var Parser = exports.Parser = function(options) {
+ this.options = options || {};
+ this.delimiter = {
+ open : XRegExp.escape(options.open || exports.open || '<%')
+ , close : XRegExp.escape(options.close || exports.close || '%>')
+ };
+ this.keyword = {
+ block : 'block'
+ , end : 'end'
+ , extends : 'extends'
+ , include : 'include'
+ }
+ this.viewsDir = options.viewsDir? options.viewsDir : '';
+}
-var parse = exports.parse = function(str, options){
- var options = options || {}
- , open = options.open || exports.open || '<%'
- , close = options.close || exports.close || '%>';
-
- var buf = [
- "var buf = [];"
- , "\nwith (locals) {"
- , "\n buf.push('"
- ];
-
- var lineno = 1;
+/**
+ * Parse the given `str` of ejs, returning the function body.
+ *
+ * @param {String} str The string to parse.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @return {String} String containing javascript.
+ */
+Parser.prototype.parse = function(str, blocks) {
+ var blocks = blocks ? blocks : []
+ , buf = [ "var buf = [];"
+ , "\nwith (locals) {"
+ , "\n buf.push('"];
+
+ var tokens = this.tokenize(str, blocks);
+ buf.push.apply(buf, this.convertTokens(tokens, blocks));
+
+ buf.push(["');\n}\nreturn buf.join('');"]);
+ return buf.join('');
+}
+
+/**
+ * Process tokenized form of the string to parse, convertingtokens to
+ * javascript.
+ *
+ * @param {String} str The string to parse.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @return {String[]} List of strings corresponding to the input tokens.
+ */
+Parser.prototype.convertTokens = function(tokens) {
+ var buf = [];
+ var consumeEOL = false;
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i]
+ , res = ''
+ , line = '__stack.lineno=' + token.lineno;
+
+ switch (token.type) {
+ case 'literal':
+ res = token.text
+ if (consumeEOL) {
+ res = XRegExp.replace( res, /\n/, '', 'one');
+ consumeEOL = false;
+ }
+ res = res
+ .replace(/\\/g, "\\\\")
+ .replace(/\'/g, "\\'")
+ .replace(/\r/g, "")
+ .replace(/\n/g, "\\n");
+ break;
+ case 'code':
+ var prefix = "');" + line + ';'
+ , postfix = "; buf.push('"
+ , js = token.text;
+
+ if ( token.meta && token.meta.buffered) {
+ if (token.meta.escaped) {
+ prefix = "', escape((" + line + ', ';
+ postfix = ")), '";
+ } else {
+ prefix = "', (" + line + ', ';
+ postfix = "), '";
+ }
+ if (token.meta.filtered) {
+ js = filtered(js);
+ }
+ }
+
+ res = prefix + js + postfix;
+ break;
+ case 'consume-EOL':
+ consumeEOL = true;
+ break;
+ case 'node':
+ buf.push.apply(buf, this.convertTokens(token.children));
+ break;
+ }
+ buf.push(res);
+ }
+ return buf;
+}
+
+/**
+ * Convert the provided string into a list of tokens/nodes.
+ *
+ * @param {String} str The string to tokenize.
+ * @param {Token[]} blocks Blocks defined in the current scope.
+ * @return {Token[]} List of resulting tokens.
+ */
+Parser.prototype.tokenize = function (str, blocks){
+ var tokens = []
+ , extNodes = []
+ , myBlocks = []
+ , lineno = 1
+ , stack = []
+ , layout = false
+ , ejsRE = XRegExp.cache('^(?[=-](?:)?)?(?.*?)\\s*(?-)?$', 'mn')
+ , keywordRE = XRegExp.cache('^\\s*((?'
+ + this.keyword.end + ')|((?('
+ + this.keyword.extends + ')|('
+ + this.keyword.block + ')|('
+ + this.keyword.include +'))(\\s+(?.*?))?))\\s*$', 'mn');
+
+
+ var segments = XRegExp.matchRecursive(str, this.delimiter.open, this.delimiter.close, 'g',
+ {valueNames: ['literal', 'open', 'ejs', 'close']});
+ for (var i = 0; i < segments.length; i++) {
+ var segment = segments[i]
+ , slurp = false;
+
+ switch (segment.value) {
+ case 'literal':
+ tokens.push(new Token('literal', segment.name, lineno));
+ break;
+ case 'ejs': // ejs block, tokenize it further
+ var match = XRegExp.exec(segment.name, ejsRE, 'm');
+ if (!match) throw new Error('Incorrect syntax for ejs...'); // should never happen
+ if (match.op) {
+ tokens.push(new Token('code', match.body, lineno, { buffered: true,
+ escaped: match.op[0] == '=' ? true: false,
+ filtered: match.filter? true: false }));
+ } else {
+ var kwMatch = XRegExp.exec(match.body, keywordRE, 'm');
+ if (kwMatch) {
+ if (kwMatch.keyword) {
+ switch (kwMatch.keyword) {
+ case this.keyword.block:
+ if (!kwMatch.name) throw new Error('Must specify name for block...');
+
+ // its a block, collect all susequent segments as children until the match end
+ var isArgument = (layout && stack.length == 0 )? true:false;
+ stack.push({token: new Token('node', kwMatch.name, lineno
+ , {isArgument: isArgument})
+ , tokensBuf: tokens
+ , keyword: this.keyword.block});
+ tokens = [];
+ break;
+ case this.keyword.extends:
+ if (!kwMatch.name) throw new Error('Must specify filename after extends...');
+ var filename = path.join(this.viewsDir, kwMatch.name), extendEjs, node;
+
+ extendEjs = fs.readFileSync(filename, 'utf8');
+ node = new Token('node', extendEjs, lineno);
+ tokens.push(node);
+ extNodes.push(node);
+ layout = true;
+ break;
+ case this.keyword.include:
+ if (!kwMatch.name) throw new Error('Must specify file to include...');
+ var includedEjs, filename = path.join(this.viewsDir, kwMatch.name);
+
+ includedEjs = fs.readFileSync(filename, 'utf8');
+ if (this.options.debug) console.log('Including: \n' + includedEjs);
+ tokens.push(new Token('node', includedEjs, lineno, {}, this.tokenize(includedEjs)));
+ break;
+ default: // should never happen...
+ throw new Error('Invalid keyword: ' + kwMatch.keyword);
+ }
+ } else if (kwMatch.end){
+ var parent = stack.pop();
+ if (!parent) throw new Error('Encountered ' + this.keyword.end + ' without matching block statement.');
+ parent.token.children = tokens;
+ tokens = parent.tokensBuf;
+
+ if (blocks[parent.token.text]) {
+ tokens.push(blocks[parent.token.text]);
+ } else if (!parent.token.meta.isArgument) {
+ tokens.push(parent.token);
+ } else {
+ myBlocks[parent.token.text] = parent.token;
+ }
+ } else { // should never happen..
+ throw new Error('Unexpected error, probably a bug in the parser!');
+ }
+ } else { //unbuffered code
+ tokens.push( new Token('code', match.body, lineno));
+ }
+ }
+ if (match.slurp) slurp = true;
+ break;
- var consumeEOL = false;
- for (var i = 0, len = str.length; i < len; ++i) {
- if (str.slice(i, open.length + i) == open) {
- i += open.length
-
- var prefix, postfix, line = '__stack.lineno=' + lineno;
- switch (str.substr(i, 1)) {
- case '=':
- prefix = "', escape((" + line + ', ';
- postfix = ")), '";
- ++i;
- break;
- case '-':
- prefix = "', (" + line + ', ';
- postfix = "), '";
- ++i;
- break;
default:
- prefix = "');" + line + ';';
- postfix = "; buf.push('";
- }
-
- var end = str.indexOf(close, i)
- , js = str.substring(i, end)
- , start = i
- , n = 0;
-
- if ('-' == js[js.length-1]){
- js = js.substring(0, js.length - 2);
- consumeEOL = true;
- }
-
- while (~(n = js.indexOf("\n", n))) n++, lineno++;
- if (js.substr(0, 1) == ':') js = filtered(js);
- buf.push(prefix, js, postfix);
- i += end - start + close.length - 1;
-
- } else if (str.substr(i, 1) == "\\") {
- buf.push("\\\\");
- } else if (str.substr(i, 1) == "'") {
- buf.push("\\'");
- } else if (str.substr(i, 1) == "\r") {
- buf.push(" ");
- } else if (str.substr(i, 1) == "\n") {
- if (consumeEOL) {
- consumeEOL = false;
- } else {
- buf.push("\\n");
- lineno++;
- }
- } else {
- buf.push(str.substr(i, 1));
+ // ignore open and close
+ }
+ if (slurp) { tokens.push(new Token('consume-EOL', '', lineno)); }
+
+ var n = 0;
+ while (~(n = segment.name.indexOf("\n", n))) n++, lineno++;
}
- }
- buf.push("');\n}\nreturn buf.join('');");
- return buf.join('');
-};
+ if(stack.length != 0) throw new Error('Unmatched ' + stack.pop().keyword + ' statment...');
+
+ // send blocks back to parent
+ for (var x in myBlocks) { blocks[x] = myBlocks[x];}
+
+ // tokenize unprocessed nodes as well
+ for (var i=0; i < extNodes.length; i++) {
+ var node = extNodes[i];
+ node.children = this.tokenize(node.text, blocks);
+ }
+ return tokens;
+}
+
+/**
+ * Parse the given `str` of ejs, returning the function body.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api public
+ */
+var parse = exports.parse = function(str, options){
+ return new Parser(options).parse(str);
+}
/**
* Compile the given `str` of ejs into a `Function`.
diff --git a/package.json b/package.json
index 2b3c25d4..fe036239 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,9 @@
"author": "TJ Holowaychuk ",
"keywords": ["template", "engine", "ejs"],
"devDependencies": {
- "mocha": "*"
+ "mocha": "*",
+ "xregexp": "*"
},
"main": "./lib/ejs.js",
"repository": "git://github.com/visionmedia/ejs.git"
-}
\ No newline at end of file
+}
diff --git a/test/ejs.test.js b/test/ejs.test.js
index 7fa7c22d..229444a8 100644
--- a/test/ejs.test.js
+++ b/test/ejs.test.js
@@ -3,7 +3,8 @@
*/
var ejs = require('../')
- , assert = require('assert');
+ , assert = require('assert')
+ , fs = require('fs');
module.exports = {
'test .version': function(){
@@ -38,18 +39,18 @@ module.exports = {
locals = { name: 'tj' };
assert.equal(html, ejs.render(str, { locals: locals }));
},
-
+
'test `scope` option': function(){
var html = 'tj
',
str = '<%= this %>
';
assert.equal(html, ejs.render(str, { scope: 'tj' }));
},
-
+
'test escaping': function(){
assert.equal('<script>', ejs.render('<%= "