Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

{% extends someVar %} support. #95

Merged
merged 3 commits into from

2 participants

@gigafied

Added the option to use context vars when using the extends tag.

The main catch is that we can't cache the render function in cases where we do use a context var with {% extends %}. This is obviously a speed hit, so a disclaimer somewhere saying this is slower than using string literals may be in order.

Further optimizations can be made though. Creating a dictionary of some sort to store render functions based on certain context var values would work.

Taka Kojima added some commits
Taka Kojima {% extends someVar %} support.
Added the option to use context vars when using the `extends` tag.
17a684d
Taka Kojima Adding tests for using extends with context vars d1deac2
@paularmstrong

Looks good, but can you fix the lint issues?

swig gigafied-master ⚒ make lint
./index.js, line 42, character 27: Unexpected space between 'createRenderFunc' and '('.
function createRenderFunc (code) {
./index.js, line 44, character 4: Expected 'return' at column 5, not column 4.
return new Function('_context', '_parents', '_filters', '_', '_ext', [
./index.js, line 99, character 5: Expected exactly one space between '}' and 'else'.
else {
./index.js, line 99, character 5: Expected 'else' at column 9, not column 5.
else {
./index.js, line 100, character 9: Expected 'render' at column 13, not column 9.
render = function (_context, _parents, _filters, _, _ext) {
./index.js, line 101, character 13: Expected 'template' at column 17, not column 13.
template.tokens = tokens;
./index.js, line 102, character 13: Expected 'code' at column 17, not column 13.
code = parser.compile.call(template, null, '', _context);
./index.js, line 103, character 13: Expected 'var' at column 17, not column 13.
var fn = createRenderFunc(code);
./index.js, line 104, character 13: Expected 'return' at column 17, not column 13.
return fn.call(this, _context, _parents, _filters, _, _ext);
./index.js, line 105, character 9: Expected '}' at column 13, not column 9.
}
./index.js, line 105, character 10: Expected ';' and instead saw '}'.
}
./index.js, line 106, character 5: Expected '}' at column 9, not column 5.
}
./index.test.js, line 87, character 29: Missing space between ',' and 'r3'.
test.strictEqual(r1,r3, "this should not throw");
./index.test.js, line 88, character 29: Missing space between ',' and 'r4'.
test.strictEqual(r2,r4, "this should not throw");
./lib/parser.js, line 34, character 38: Missing space between ',' and '1'.
context = context[a.splice(0,1)[0]];
./lib/parser.js, line 328, character 1: Unexpected '(space)'.

./lib/parser.js, line 333, character 29: Unexpected 'else' after 'return'.
return;
./lib/parser.js, line 335, character 25: Expected exactly one space between '}' and 'else'.
else {
./lib/parser.js, line 335, character 25: Expected 'else' at column 29, not column 25.
else {
./lib/parser.js, line 336, character 29: Expected 'filepath' at column 33, not column 29.
filepath = "\"" + getContextVar(filepath, context) + "\"";
./lib/parser.js, line 337, character 25: Expected '}' at column 29, not column 25.
}
21 errors
make: *** [lint] Error 2
@gigafied

Gah, sorry. Fixed.

@paularmstrong

I didn't really read through the code too much, but it looks good for now!

@paularmstrong paularmstrong merged commit 2670e39 into paularmstrong:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 28, 2012
  1. {% extends someVar %} support.

    Taka Kojima authored
    Added the option to use context vars when using the `extends` tag.
  2. Adding tests for using extends with context vars

    Taka Kojima authored
Commits on Jul 30, 2012
  1. Fixing syntax to correct lint errors

    Taka Kojima authored
This page is out of date. Refresh to see the latest.
View
57 index.js
@@ -39,6 +39,28 @@ function TemplateError(error) {
}};
}
+function createRenderFunc(code) {
+ // The compiled render function - this is all we need
+ return new Function('_context', '_parents', '_filters', '_', '_ext', [
+ '_parents = _parents ? _parents.slice() : [];',
+ '_context = _context || {};',
+ // Prevents circular includes (which will crash node without warning)
+ 'var j = _parents.length,',
+ ' _output = "",',
+ ' _this = this;',
+ // Note: this loop averages much faster than indexOf across all cases
+ 'while (j--) {',
+ ' if (_parents[j] === this.id) {',
+ ' return "Circular import of template " + this.id + " in " + _parents[_parents.length-1];',
+ ' }',
+ '}',
+ // Add this template as a parent to all includes in its scope
+ '_parents.push(this.id);',
+ code,
+ 'return _output;',
+ ].join(''));
+}
+
function createTemplate(data, id) {
var template = {
// Allows us to include templates from the compiled code
@@ -56,37 +78,30 @@ function createTemplate(data, id) {
// The template token tree before compiled into javascript
if (_config.allowErrors) {
- template.tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
+ tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
} else {
try {
- template.tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
+ tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
} catch (e) {
return new TemplateError(e);
}
}
+ template.tokens = tokens;
+
// The raw template code
code = parser.compile.call(template);
- // The compiled render function - this is all we need
- render = new Function('_context', '_parents', '_filters', '_', '_ext', [
- '_parents = _parents ? _parents.slice() : [];',
- '_context = _context || {};',
- // Prevents circular includes (which will crash node without warning)
- 'var j = _parents.length,',
- ' _output = "",',
- ' _this = this;',
- // Note: this loop averages much faster than indexOf across all cases
- 'while (j--) {',
- ' if (_parents[j] === this.id) {',
- ' return "Circular import of template " + this.id + " in " + _parents[_parents.length-1];',
- ' }',
- '}',
- // Add this template as a parent to all includes in its scope
- '_parents.push(this.id);',
- code,
- 'return _output;',
- ].join(''));
+ if (code !== false) {
+ render = createRenderFunc(code);
+ } else {
+ render = function (_context, _parents, _filters, _, _ext) {
+ template.tokens = tokens;
+ code = parser.compile.call(template, null, '', _context);
+ var fn = createRenderFunc(code);
+ return fn.call(this, _context, _parents, _filters, _, _ext);
+ };
+ }
template.render = function (context, parents) {
if (_config.allowErrors) {
View
22 index.test.js
@@ -70,6 +70,28 @@ exports.compileFile = testCase({
test.done();
},
+ 'using context var with extends tag' : function (test) {
+ swig.init({
+ root: __dirname + '/tests/templates',
+ allowErrors: true
+ });
+
+ var tpl, r1, r2, r3, r4;
+
+ tpl = swig.compileFile('extends_dynamic.html');
+ r1 = tpl.render({baseTmpl: "extends_base.html"});
+ r2 = tpl.render({baseTmpl: "extends_base2.html"});
+ r3 = tpl.render({baseTmpl: "extends_base.html"});
+ r4 = tpl.render({baseTmpl: "extends_base2.html"});
+
+ test.strictEqual(r1, r3, "rendering the same template with the same context twice, should return identically.");
+ test.strictEqual(r2, r4, "rendering the same template with the same context twice, should return identically.");
+
+ test.notEqual(r1, r2, "these should not be equal, as they use different base templates.");
+
+ test.done();
+ },
+
'absolute path': function (test) {
swig.init({
root: __dirname + '/tests/templates',
View
94 lib/parser.js
@@ -28,6 +28,14 @@ function getArgs(input) {
return doubleEscape(input.replace(/^[\w\.]+\(|\)$/g, ''));
}
+function getContextVar(varName, context) {
+ var a = varName.split(".");
+ while (a.length) {
+ context = context[a.splice(0, 1)[0]];
+ }
+ return context;
+}
+
function getTokenArgs(token, parts) {
parts = _.map(parts, doubleEscape);
@@ -294,7 +302,7 @@ exports.parse = function (data, tags, autoescape) {
return stack[index];
};
-exports.compile = function compile(indent, parentBlock) {
+exports.compile = function compile(indent, parentBlock, context) {
var code = '',
tokens = [],
sets = [],
@@ -302,49 +310,71 @@ exports.compile = function compile(indent, parentBlock) {
filepath,
blockname,
varOutput,
- wrappedInMethod;
+ wrappedInMethod,
+ extendsHasVar;
indent = indent || '';
// Precompile - extract blocks and create hierarchy based on 'extends' tags
// TODO: make block and extends tags accept context variables
if (this.type === TEMPLATE) {
+
_.each(this.tokens, function (token, index) {
- // Load the parent template
- if (token.name === 'extends') {
- filepath = token.args[0];
- if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
- throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
- }
- if (index > 0) {
- throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
- }
- token.template = this.compileFile(filepath.replace(/['"]/g, ''));
- this.parent = token.template;
- } else if (token.name === 'block') { // Make a list of blocks
- blockname = token.args[0];
- if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
- throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
- }
- if (this.type !== TEMPLATE) {
- throw new Error('Block "' + blockname + '" found nested in another block tag on line' + token.line + '.');
- }
- try {
- if (this.hasOwnProperty('parent') && this.parent.blocks.hasOwnProperty(blockname)) {
- this.blocks[blockname] = compile.call(token, indent + ' ', this.parent.blocks[blockname]);
- } else if (this.hasOwnProperty('blocks')) {
- this.blocks[blockname] = compile.call(token, indent + ' ');
+
+ if (!extendsHasVar) {
+ // Load the parent template
+ if (token.name === 'extends') {
+ filepath = token.args[0];
+
+ if (!helpers.isStringLiteral(filepath)) {
+
+ if (!context) {
+ extendsHasVar = true;
+ return;
+ }
+ filepath = "\"" + getContextVar(filepath, context) + "\"";
+ }
+
+ if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
+ throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
+ }
+ if (index > 0) {
+ throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
+ }
+ token.template = this.compileFile(filepath.replace(/['"]/g, ''));
+ this.parent = token.template;
+
+ } else if (token.name === 'block') { // Make a list of blocks
+ blockname = token.args[0];
+ if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
+ throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
+ }
+ if (this.type !== TEMPLATE) {
+ throw new Error('Block "' + blockname + '" found nested in another block tag on line' + token.line + '.');
+ }
+ try {
+ if (this.hasOwnProperty('parent') && this.parent.blocks.hasOwnProperty(blockname)) {
+ this.blocks[blockname] = compile.call(token, indent + ' ', this.parent.blocks[blockname]);
+ } else if (this.hasOwnProperty('blocks')) {
+ this.blocks[blockname] = compile.call(token, indent + ' ');
+ }
+ } catch (error) {
+ throw new Error('Circular extends found on line ' + token.line + ' of "' + this.id + '"!');
}
- } catch (error) {
- throw new Error('Circular extends found on line ' + token.line + ' of "' + this.id + '"!');
+ } else if (token.name === 'set') {
+ sets.push(token);
+ return;
}
- } else if (token.name === 'set') {
- sets.push(token);
- return;
+ tokens.push(token);
}
- tokens.push(token);
}, this);
+ // If extendsHasVar == true, then we know {% extends %} is not using a string literal, thus we can't
+ // compile until render is called, so we return false.
+ if (extendsHasVar) {
+ return false;
+ }
+
if (tokens.length && tokens[0].name === 'extends') {
this.blocks = _.extend({}, this.parent.blocks, this.blocks);
this.tokens = sets.concat(this.parent.tokens);
View
9 tests/templates/extends_base2.html
@@ -0,0 +1,9 @@
+This is from the "extends_base2.html" template.
+
+{% block one %}
+ This is the default content in block 'one'
+{% endblock %}
+
+{% block two %}
+ This is the default content in block 'two'
+{% endblock %}
View
6 tests/templates/extends_dynamic.html
@@ -0,0 +1,6 @@
+{% extends baseTmpl %}
+This is content from "extends_dynamic.html", you should not see it
+
+{% block one %}
+ This is the "extends_dynamic.html" content in block 'one'
+{% endblock %}
Something went wrong with that request. Please try again.