diff --git a/Readme.md b/Readme.md index ab0a3dd1..1fb6ca61 100644 --- a/Readme.md +++ b/Readme.md @@ -17,6 +17,8 @@ Embedded JavaScript templates. * Unescaped buffering with `<%- code %>` * Supports tag customization * Filter support for designer-friendly templates + * Extend file with `<%+ file-to-be-extended %>` + * Blocks `<%block blockname%>content comes here<%/block%>`, cooperate with extend * Includes * Client-side support * Newline slurping with `<% code -%>` or `<% -%>` or `<%= code -%>` or `<%- code -%>` @@ -145,10 +147,9 @@ ejs.filters.last = function(obj) { }; ``` -## Layouts +## Layouts without blocks - Currently EJS has no notion of blocks, only compile-time `include`s, - however you may still utilize this feature to implement "layouts" by + You may utilize compile-time `include`s without blocks to implement "layouts" by simply including a header and footer like so: ```html @@ -158,6 +159,33 @@ ejs.filters.last = function(obj) { <% include foot %> ``` +## extend and blocks + + Currently EJS has come up with extend and blocks, they can support: + + - Multilayer inheritance, that is the child can extend the parent file, the father coulud still extend the grandfather + - the blocks in the clild will replace the block with the same name in the parent file, the conent outside blocks in the clild will be ignored, and the content in the blocks that are not replaced in the parent file will run directly, as no blocks surround it + - faster, the test in benchmark shows that the layout using extend is 33% faster than that using include + - the include could also work in the file or blocks + - notice: no space between the open tag and `+` or `block` + +```parent.ejs +<%block head%> + head +<%/block%> +<%block body%> + body +<%/block%> +``` + +```child.ejs +<%+ parent %> +<%block head%> + the clild's head +<%/block%> +``` + + ## client-side support include `./ejs.js` or `./ejs.min.js` and `require("ejs").compile(str)`. diff --git a/benchmark.js b/benchmark/benchmark.js similarity index 74% rename from benchmark.js rename to benchmark/benchmark.js index 7b267e16..0b752879 100644 --- a/benchmark.js +++ b/benchmark/benchmark.js @@ -1,6 +1,6 @@ -var ejs = require('./lib/ejs'), +var ejs = require('../lib/ejs'), str = '<% if (foo) { %>

<%= foo %>

<% } %>', times = 50000; diff --git a/benchmark/bm-extend-include.js b/benchmark/bm-extend-include.js new file mode 100644 index 00000000..2e6310a6 --- /dev/null +++ b/benchmark/bm-extend-include.js @@ -0,0 +1,32 @@ + + +var ejs = require('../lib/ejs'), + times = 50000; + +console.log('rendering include ' + times + ' times'); + +var start = new Date; +while (times--) { + ejs.renderFile("./include.ejs", { cache: true, filename: 'test', locals: { foo: 'bar' }}, function(err, data){ + if(times == 0){ + console.log(data); + } + }); +} + +console.log('took ' + (new Date - start) + 'ms'); + +times = 50000; + +console.log('rendering extend ' + times + ' times'); + +var start = new Date; +while (times--) { + ejs.renderFile("./son.ejs", { cache: true, filename: 'test-ext-bm', locals: { foo: 'bar' }}, function(err, data){ + if(times == 0){ + console.log(data); + } + }); +} + +console.log('took ' + (new Date - start) + 'ms'); \ No newline at end of file diff --git a/benchmark/father.ejs b/benchmark/father.ejs new file mode 100644 index 00000000..77e4a33c --- /dev/null +++ b/benchmark/father.ejs @@ -0,0 +1,7 @@ +this is head + +<%block body%> + main part +<%/block%> + +this is footer \ No newline at end of file diff --git a/benchmark/footer.ejs b/benchmark/footer.ejs new file mode 100644 index 00000000..7995d25d --- /dev/null +++ b/benchmark/footer.ejs @@ -0,0 +1 @@ +this is footer \ No newline at end of file diff --git a/benchmark/head.ejs b/benchmark/head.ejs new file mode 100644 index 00000000..069250a5 --- /dev/null +++ b/benchmark/head.ejs @@ -0,0 +1 @@ +this is head \ No newline at end of file diff --git a/benchmark/include.ejs b/benchmark/include.ejs new file mode 100644 index 00000000..e3ee5dae --- /dev/null +++ b/benchmark/include.ejs @@ -0,0 +1,5 @@ +<%include head.ejs%> + +this is body + +<%include footer.ejs%> \ No newline at end of file diff --git a/benchmark/son.ejs b/benchmark/son.ejs new file mode 100644 index 00000000..416a4d04 --- /dev/null +++ b/benchmark/son.ejs @@ -0,0 +1,4 @@ +<%+ father%> +<%block body%> + this is body +<%/block%> \ No newline at end of file diff --git a/examples/extend/father.ejs b/examples/extend/father.ejs new file mode 100644 index 00000000..38259266 --- /dev/null +++ b/examples/extend/father.ejs @@ -0,0 +1,10 @@ +<%block head%> +this is the grandfather head +<%/block%> +<%block body%> +this is the grandfather body +<%/block%> +<%block footer%> +this is the grandfather footer +<%include include-in%> +<%/block%> \ No newline at end of file diff --git a/examples/extend/grandson.ejs b/examples/extend/grandson.ejs new file mode 100644 index 00000000..43239f51 --- /dev/null +++ b/examples/extend/grandson.ejs @@ -0,0 +1,8 @@ +<%+ son%> +<%block body%> + this is the child, has <%=pets.length -%> pets. +<%/block%> +this will be ingored +<%block another%> + this will be ingored too +<%/block%> \ No newline at end of file diff --git a/examples/extend/include-in.ejs b/examples/extend/include-in.ejs new file mode 100644 index 00000000..23abe979 --- /dev/null +++ b/examples/extend/include-in.ejs @@ -0,0 +1 @@ +the content in include file also work in the extend file, <%= pets.length%> pet(s) here \ No newline at end of file diff --git a/examples/extend/son.ejs b/examples/extend/son.ejs new file mode 100644 index 00000000..300fe3b8 --- /dev/null +++ b/examples/extend/son.ejs @@ -0,0 +1,7 @@ +<%+ father%> +<%block head%> +this is the father head +<%/block%> +<%block body%> +this is the father body +<%/block%> \ No newline at end of file diff --git a/examples/extend/test-extend.js b/examples/extend/test-extend.js new file mode 100644 index 00000000..c125db37 --- /dev/null +++ b/examples/extend/test-extend.js @@ -0,0 +1,8 @@ +var ejs = require("../../"); + +var pets = [{name: "tiger"}]; + + +ejs.renderFile("./grandson.ejs", {debug: true, pets: pets}, function(err, data){ + console.log(data); +}); diff --git a/lib/ejs.js b/lib/ejs.js index fb7323fd..d622c436 100644 --- a/lib/ejs.js +++ b/lib/ejs.js @@ -44,6 +44,13 @@ exports.clearCache = function(){ cache = {}; }; +/** + * the blocks in the inheritance. + * + * @type Object + */ +var blocks = {}; + /** * Translate filtered code into function calls. * @@ -97,6 +104,64 @@ function rethrow(err, str, filename, lineno){ throw err; } +/** + * Parse the blocks in the `str` + * + * @param {String} str + * @param {Object} options + * @param {Boolean} isChild indicate the `str` is from child or parent + * @api private + */ +function block(str, options, isChild){ + var open = options.open || exports.open || '<%' + , close = options.close || exports.close || '%>' + , start = end = 0 + , blockName = "" + , blockPatt = new RegExp(open + "block","g"); + while (blockPatt.exec(str) != null){ + //to match and parse <%block name%> + start = blockPatt.lastIndex; + end = str.indexOf(close, start); + blockName = str.substring(start, end).trim(); + if(isChild){ //if in clild file, push the block content into the blocks map + start = end + close.length; + end = str.indexOf(open + "/block", start); + blocks[blockName] = str.substring(start, end).trim(); + options.debug && console.log("the block name: " + blockName + "\nthe block str: " + blocks[blockName] + "\n"); + }else{ //if in parent, check if it has been extended + if(blocks[blockName]){ + end = str.indexOf(open + "/block", end); + end = str.indexOf(close, end); + str = str.replace(str.substring(start - open.length - 5, end + close.length), blocks[blockName]); + } + } + } + if(!isChild) return str; +} + +/** + * Parse the content `str` in extend file, returning the parsed content and new options.filename. + * + * @param {String} str + * @return {Object} + * @api private + */ +function extend(str, options){ + if (!options.filename) throw new Error('filename option is required for extend'); + //to process the blocks in the clild + block(str, options, true); + //to process the content in the parent + //to get the parent filename, e.g. <%+ par %> + var start = str.indexOf(options.open + "+") + , parName = str.substring(start + options.open.length + 1 ,str.indexOf(options.close, start)).trim(); + parName = resolveFilename(parName, options.filename); + options.filename = parName; + return { + str: block(read(parName, 'utf8'), options, false), + filename: parName + }; +} + /** * Parse the given `str` of ejs, returning the function body. * @@ -112,7 +177,13 @@ var parse = exports.parse = function(str, options){ , filename = options.filename , compileDebug = options.compileDebug !== false , buf = ""; - + + //the extend symbol must be in the first place of the file if exist + if(str.trim().indexOf(open + "+") == "0"){ + var extendObj = extend(str, {open: open, close: close, debug: options.debug, filename: filename}); + options.filename = extendObj.filename; + return exports.parse(extendObj.str, options); + } buf += 'var buf = [];'; if (false !== options._with) buf += '\nwith (locals || {}) { (function(){ '; buf += '\n buf.push(\''; @@ -156,12 +227,22 @@ var parse = exports.parse = function(str, options){ if (0 == js.trim().indexOf('include')) { var name = js.trim().slice(7).trim(); if (!filename) throw new Error('filename option is required for includes'); - var path = resolveInclude(name, filename); + var path = resolveFilename(name, filename); include = read(path, 'utf8'); include = exports.parse(include, { filename: path, _with: false, open: open, close: close, compileDebug: compileDebug }); buf += "' + (function(){" + include + "})() + '"; js = ''; } + // the blocks that not been extended will be parsed as normal ejs + if (0 == js.trim().indexOf('block')) { + var tmpStart = str.indexOf(close, start) + close.length; + end = str.indexOf(open + "/block", start); + tmpBlockStr = str.substring(tmpStart, end); + tmpBlockStr = exports.parse(tmpBlockStr, { filename: filename, _with: false, open: open, close: close, compileDebug: compileDebug }); + buf += "' + (function(){" + tmpBlockStr + "})() + '"; + js = ''; + end = str.indexOf(close, end); + } while (~(n = js.indexOf("\n", n))) n++, lineno++; if (js.substr(0, 1) == ':') js = filtered(js); @@ -306,7 +387,7 @@ exports.renderFile = function(path, options, fn){ } options.filename = path; - + var str; try { str = options.cache @@ -320,7 +401,7 @@ exports.renderFile = function(path, options, fn){ }; /** - * Resolve include `name` relative to `filename`. + * Resolve include or extend `name` relative to `filename`. * * @param {String} name * @param {String} filename @@ -328,7 +409,7 @@ exports.renderFile = function(path, options, fn){ * @api private */ -function resolveInclude(name, filename) { +function resolveFilename(name, filename) { var path = join(dirname(filename), name); var ext = extname(name); if (!ext) path += '.ejs'; diff --git a/test/ejs.js b/test/ejs.js index 70f16197..93896bd7 100644 --- a/test/ejs.js +++ b/test/ejs.js @@ -273,3 +273,29 @@ describe('require', function() { .should.equal(fixture('menu.html')); }) }) + +describe('extend and block', function(){ + it('should extend ejs', function(){ + var file = 'test/fixtures/extend.ejs'; + ejs.render(fixture('extend.ejs'), { filename: file, pets: users }) + .should.equal(fixture('extend.html')); + }) + + it('should extend ejs, the content outside the blocks should be ignored', function(){ + var file = 'test/fixtures/extend-ignored.ejs'; + ejs.render(fixture('extend-ignored.ejs'), { filename: file, pets: users }) + .should.equal(fixture('extend.html')); + }) + + it('should work when nested', function(){ + var file = 'test/fixtures/extend-nested.ejs'; + ejs.render(fixture('extend-nested.ejs'), { filename: file, pets: users }) + .should.equal(fixture('extend-nested.html')); + }) + + it('should work with include', function(){ + var file = 'test/fixtures/extend-include.ejs'; + ejs.render(fixture('extend-include.ejs'), { filename: file, pets: users }) + .should.equal(fixture('extend-include.html')); + }) +}) \ No newline at end of file diff --git a/test/fixtures/extend-ignored.ejs b/test/fixtures/extend-ignored.ejs new file mode 100644 index 00000000..bc7a5d84 --- /dev/null +++ b/test/fixtures/extend-ignored.ejs @@ -0,0 +1,4 @@ +<%+ father%> +<%block body%> +this is the child, has <%=pets.length -%> pets. +<%/block%> \ No newline at end of file diff --git a/test/fixtures/extend-include.ejs b/test/fixtures/extend-include.ejs new file mode 100644 index 00000000..5026e41c --- /dev/null +++ b/test/fixtures/extend-include.ejs @@ -0,0 +1,4 @@ +<%+ father-include%> +<%block body%> +this is the child, has <%=pets.length -%> pets. +<%/block%> \ No newline at end of file diff --git a/test/fixtures/extend-include.html b/test/fixtures/extend-include.html new file mode 100644 index 00000000..a52df274 --- /dev/null +++ b/test/fixtures/extend-include.html @@ -0,0 +1,4 @@ +this is the father head +this is the child, has 3 pets. +this is the father footer +

hey

diff --git a/test/fixtures/extend-nested.ejs b/test/fixtures/extend-nested.ejs new file mode 100644 index 00000000..bbe78d76 --- /dev/null +++ b/test/fixtures/extend-nested.ejs @@ -0,0 +1,4 @@ +<%+ extend/father%> +<%block body%> +this is the child, has <%=pets.length -%> pets. +<%/block%> \ No newline at end of file diff --git a/test/fixtures/extend-nested.html b/test/fixtures/extend-nested.html new file mode 100644 index 00000000..13b537f6 --- /dev/null +++ b/test/fixtures/extend-nested.html @@ -0,0 +1,3 @@ +this is the father head +this is the child, has 3 pets. +this is the grandfather footer diff --git a/test/fixtures/extend.ejs b/test/fixtures/extend.ejs new file mode 100644 index 00000000..bc7a5d84 --- /dev/null +++ b/test/fixtures/extend.ejs @@ -0,0 +1,4 @@ +<%+ father%> +<%block body%> +this is the child, has <%=pets.length -%> pets. +<%/block%> \ No newline at end of file diff --git a/test/fixtures/extend.html b/test/fixtures/extend.html new file mode 100644 index 00000000..ba1863fe --- /dev/null +++ b/test/fixtures/extend.html @@ -0,0 +1,5 @@ + +this is the father head + +this is the child, has 3 pets. +this is the father footer diff --git a/test/fixtures/extend/father.ejs b/test/fixtures/extend/father.ejs new file mode 100644 index 00000000..fbf29d66 --- /dev/null +++ b/test/fixtures/extend/father.ejs @@ -0,0 +1,7 @@ +<%+ grandfather/grandfather%> +<%block head%> +this is the father head +<%/block%> +<%block body%> +this is the father body +<%/block%> \ No newline at end of file diff --git a/test/fixtures/extend/grandfather/grandfather.ejs b/test/fixtures/extend/grandfather/grandfather.ejs new file mode 100644 index 00000000..17b2e0d4 --- /dev/null +++ b/test/fixtures/extend/grandfather/grandfather.ejs @@ -0,0 +1,9 @@ +<%block head%> +this is the grandfather head +<%/block%> +<%block body%> +this is the grandfather body +<%/block%> +<%block footer%> +this is the grandfather footer +<%/block%> \ No newline at end of file diff --git a/test/fixtures/father-include.ejs b/test/fixtures/father-include.ejs new file mode 100644 index 00000000..0b0d8865 --- /dev/null +++ b/test/fixtures/father-include.ejs @@ -0,0 +1,10 @@ +<%block head%> +this is the father head +<%/block%> +<%block body%> +this is the father body +<%/block%> +<%block footer%> +this is the father footer +<%include para%> +<%/block%> \ No newline at end of file diff --git a/test/fixtures/father.ejs b/test/fixtures/father.ejs new file mode 100644 index 00000000..3ef08233 --- /dev/null +++ b/test/fixtures/father.ejs @@ -0,0 +1,9 @@ +<%block head%> +this is the father head +<%/block%> +<%block body%> +this is the father body +<%/block%> +<%block footer%> +this is the father footer +<%/block%> \ No newline at end of file