Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of extend and block in EJS #142

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 31 additions & 3 deletions Readme.md
Expand Up @@ -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 -%>`
Expand Down Expand Up @@ -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
Expand All @@ -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)`.
Expand Down
2 changes: 1 addition & 1 deletion benchmark.js → benchmark/benchmark.js
@@ -1,6 +1,6 @@


var ejs = require('./lib/ejs'),
var ejs = require('../lib/ejs'),
str = '<% if (foo) { %><p><%= foo %></p><% } %>',
times = 50000;

Expand Down
32 changes: 32 additions & 0 deletions 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');
7 changes: 7 additions & 0 deletions benchmark/father.ejs
@@ -0,0 +1,7 @@
this is head

<%block body%>
main part
<%/block%>

this is footer
1 change: 1 addition & 0 deletions benchmark/footer.ejs
@@ -0,0 +1 @@
this is footer
1 change: 1 addition & 0 deletions benchmark/head.ejs
@@ -0,0 +1 @@
this is head
5 changes: 5 additions & 0 deletions benchmark/include.ejs
@@ -0,0 +1,5 @@
<%include head.ejs%>

this is body

<%include footer.ejs%>
4 changes: 4 additions & 0 deletions benchmark/son.ejs
@@ -0,0 +1,4 @@
<%+ father%>
<%block body%>
this is body
<%/block%>
10 changes: 10 additions & 0 deletions 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%>
8 changes: 8 additions & 0 deletions 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%>
1 change: 1 addition & 0 deletions examples/extend/include-in.ejs
@@ -0,0 +1 @@
the content in include file also work in the extend file, <%= pets.length%> pet(s) here
7 changes: 7 additions & 0 deletions 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%>
8 changes: 8 additions & 0 deletions 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);
});
91 changes: 86 additions & 5 deletions lib/ejs.js
Expand Up @@ -44,6 +44,13 @@ exports.clearCache = function(){
cache = {};
};

/**
* the blocks in the inheritance.
*
* @type Object
*/
var blocks = {};

/**
* Translate filtered code into function calls.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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"){
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the rest looks fine and the tests are good but we'd definitely want to try and make this chunk more declarative it's not super obvious and it's a lot to cram into the one function

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you are right. I used too many temp variables in the function. I'll spend my weekend to optimize the code, make it more declarative. we can talt about it later.
The extend feature is very useful and necessary. So I advise strongly that it could be merged into the trunk sooner or later. thanks!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's also #147 which is similar, it would be great if you could maybe check out that PR and help review :D at a glance they both look really similar

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I'd like to :D

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(\'';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -306,7 +387,7 @@ exports.renderFile = function(path, options, fn){
}

options.filename = path;

var str;
try {
str = options.cache
Expand All @@ -320,15 +401,15 @@ 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
* @return {String}
* @api private
*/

function resolveInclude(name, filename) {
function resolveFilename(name, filename) {
var path = join(dirname(filename), name);
var ext = extname(name);
if (!ext) path += '.ejs';
Expand Down
26 changes: 26 additions & 0 deletions test/ejs.js
Expand Up @@ -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'));
})
})
4 changes: 4 additions & 0 deletions test/fixtures/extend-ignored.ejs
@@ -0,0 +1,4 @@
<%+ father%>
<%block body%>
this is the child, has <%=pets.length -%> pets.
<%/block%>
4 changes: 4 additions & 0 deletions test/fixtures/extend-include.ejs
@@ -0,0 +1,4 @@
<%+ father-include%>
<%block body%>
this is the child, has <%=pets.length -%> pets.
<%/block%>
4 changes: 4 additions & 0 deletions 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
<p>hey</p>
4 changes: 4 additions & 0 deletions test/fixtures/extend-nested.ejs
@@ -0,0 +1,4 @@
<%+ extend/father%>
<%block body%>
this is the child, has <%=pets.length -%> pets.
<%/block%>
3 changes: 3 additions & 0 deletions 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
4 changes: 4 additions & 0 deletions test/fixtures/extend.ejs
@@ -0,0 +1,4 @@
<%+ father%>
<%block body%>
this is the child, has <%=pets.length -%> pets.
<%/block%>
5 changes: 5 additions & 0 deletions 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
7 changes: 7 additions & 0 deletions 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%>