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

+
+
+

Block 2 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('<%= "