Skip to content
This repository

{% extends someVar %} support. #95

Merged
merged 3 commits into from over 1 year ago

2 participants

Taka Kojima Paul Armstrong
Taka Kojima

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.

added some commits July 27, 2012
{% extends someVar %} support.
Added the option to use context vars when using the `extends` tag.
17a684d
Adding tests for using extends with context vars d1deac2
Paul Armstrong
Owner

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
Taka Kojima

Gah, sorry. Fixed.

Paul Armstrong
Owner

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

Paul Armstrong paularmstrong merged commit 2670e39 into from August 03, 2012
Paul Armstrong paularmstrong closed this August 03, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 1 author.

Jul 27, 2012
{% extends someVar %} support.
Added the option to use context vars when using the `extends` tag.
17a684d
Adding tests for using extends with context vars d1deac2
Jul 30, 2012
Fixing syntax to correct lint errors 28ec144
This page is out of date. Refresh to see the latest.
57  index.js
@@ -39,6 +39,28 @@ function TemplateError(error) {
39 39
     }};
40 40
 }
41 41
 
  42
+function createRenderFunc(code) {
  43
+    // The compiled render function - this is all we need
  44
+    return new Function('_context', '_parents', '_filters', '_', '_ext', [
  45
+        '_parents = _parents ? _parents.slice() : [];',
  46
+        '_context = _context || {};',
  47
+        // Prevents circular includes (which will crash node without warning)
  48
+        'var j = _parents.length,',
  49
+        '    _output = "",',
  50
+        '    _this = this;',
  51
+        // Note: this loop averages much faster than indexOf across all cases
  52
+        'while (j--) {',
  53
+        '   if (_parents[j] === this.id) {',
  54
+        '         return "Circular import of template " + this.id + " in " + _parents[_parents.length-1];',
  55
+        '   }',
  56
+        '}',
  57
+        // Add this template as a parent to all includes in its scope
  58
+        '_parents.push(this.id);',
  59
+        code,
  60
+        'return _output;',
  61
+    ].join(''));
  62
+}
  63
+
42 64
 function createTemplate(data, id) {
43 65
     var template = {
44 66
             // Allows us to include templates from the compiled code
@@ -56,37 +78,30 @@ function createTemplate(data, id) {
56 78
 
57 79
     // The template token tree before compiled into javascript
58 80
     if (_config.allowErrors) {
59  
-        template.tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
  81
+        tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
60 82
     } else {
61 83
         try {
62  
-            template.tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
  84
+            tokens = parser.parse.call(template, data, _config.tags, _config.autoescape);
63 85
         } catch (e) {
64 86
             return new TemplateError(e);
65 87
         }
66 88
     }
67 89
 
  90
+    template.tokens = tokens;
  91
+
68 92
     // The raw template code
69 93
     code = parser.compile.call(template);
70 94
 
71  
-    // The compiled render function - this is all we need
72  
-    render = new Function('_context', '_parents', '_filters', '_', '_ext', [
73  
-        '_parents = _parents ? _parents.slice() : [];',
74  
-        '_context = _context || {};',
75  
-        // Prevents circular includes (which will crash node without warning)
76  
-        'var j = _parents.length,',
77  
-        '    _output = "",',
78  
-        '    _this = this;',
79  
-        // Note: this loop averages much faster than indexOf across all cases
80  
-        'while (j--) {',
81  
-        '   if (_parents[j] === this.id) {',
82  
-        '         return "Circular import of template " + this.id + " in " + _parents[_parents.length-1];',
83  
-        '   }',
84  
-        '}',
85  
-        // Add this template as a parent to all includes in its scope
86  
-        '_parents.push(this.id);',
87  
-        code,
88  
-        'return _output;',
89  
-    ].join(''));
  95
+    if (code !== false) {
  96
+        render = createRenderFunc(code);
  97
+    } else {
  98
+        render = function (_context, _parents, _filters, _, _ext) {
  99
+            template.tokens = tokens;
  100
+            code = parser.compile.call(template, null, '', _context);
  101
+            var fn = createRenderFunc(code);
  102
+            return fn.call(this, _context, _parents, _filters, _, _ext);
  103
+        };
  104
+    }
90 105
 
91 106
     template.render = function (context, parents) {
92 107
         if (_config.allowErrors) {
22  index.test.js
@@ -70,6 +70,28 @@ exports.compileFile = testCase({
70 70
         test.done();
71 71
     },
72 72
 
  73
+    'using context var with extends tag' : function (test) {
  74
+        swig.init({
  75
+            root: __dirname + '/tests/templates',
  76
+            allowErrors: true
  77
+        });
  78
+
  79
+        var tpl, r1, r2, r3, r4;
  80
+
  81
+        tpl = swig.compileFile('extends_dynamic.html');
  82
+        r1 = tpl.render({baseTmpl: "extends_base.html"});
  83
+        r2 = tpl.render({baseTmpl: "extends_base2.html"});
  84
+        r3 = tpl.render({baseTmpl: "extends_base.html"});
  85
+        r4 = tpl.render({baseTmpl: "extends_base2.html"});
  86
+
  87
+        test.strictEqual(r1, r3, "rendering the same template with the same context twice, should return identically.");
  88
+        test.strictEqual(r2, r4, "rendering the same template with the same context twice, should return identically.");
  89
+
  90
+        test.notEqual(r1, r2, "these should not be equal, as they use different base templates.");
  91
+
  92
+        test.done();
  93
+    },
  94
+
73 95
     'absolute path': function (test) {
74 96
         swig.init({
75 97
             root: __dirname + '/tests/templates',
94  lib/parser.js
@@ -28,6 +28,14 @@ function getArgs(input) {
28 28
     return doubleEscape(input.replace(/^[\w\.]+\(|\)$/g, ''));
29 29
 }
30 30
 
  31
+function getContextVar(varName, context) {
  32
+    var a = varName.split(".");
  33
+    while (a.length) {
  34
+        context = context[a.splice(0, 1)[0]];
  35
+    }
  36
+    return context;
  37
+}
  38
+
31 39
 function getTokenArgs(token, parts) {
32 40
     parts = _.map(parts, doubleEscape);
33 41
 
@@ -294,7 +302,7 @@ exports.parse = function (data, tags, autoescape) {
294 302
     return stack[index];
295 303
 };
296 304
 
297  
-exports.compile = function compile(indent, parentBlock) {
  305
+exports.compile = function compile(indent, parentBlock, context) {
298 306
     var code = '',
299 307
         tokens = [],
300 308
         sets = [],
@@ -302,49 +310,71 @@ exports.compile = function compile(indent, parentBlock) {
302 310
         filepath,
303 311
         blockname,
304 312
         varOutput,
305  
-        wrappedInMethod;
  313
+        wrappedInMethod,
  314
+        extendsHasVar;
306 315
 
307 316
     indent = indent || '';
308 317
 
309 318
     // Precompile - extract blocks and create hierarchy based on 'extends' tags
310 319
     // TODO: make block and extends tags accept context variables
311 320
     if (this.type === TEMPLATE) {
  321
+
312 322
         _.each(this.tokens, function (token, index) {
313  
-            // Load the parent template
314  
-            if (token.name === 'extends') {
315  
-                filepath = token.args[0];
316  
-                if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
317  
-                    throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
318  
-                }
319  
-                if (index > 0) {
320  
-                    throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
321  
-                }
322  
-                token.template = this.compileFile(filepath.replace(/['"]/g, ''));
323  
-                this.parent = token.template;
324  
-            } else if (token.name === 'block') { // Make a list of blocks
325  
-                blockname = token.args[0];
326  
-                if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
327  
-                    throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
328  
-                }
329  
-                if (this.type !== TEMPLATE) {
330  
-                    throw new Error('Block "' + blockname + '" found nested in another block tag on line' + token.line + '.');
331  
-                }
332  
-                try {
333  
-                    if (this.hasOwnProperty('parent') && this.parent.blocks.hasOwnProperty(blockname)) {
334  
-                        this.blocks[blockname] = compile.call(token, indent + '  ', this.parent.blocks[blockname]);
335  
-                    } else if (this.hasOwnProperty('blocks')) {
336  
-                        this.blocks[blockname] = compile.call(token, indent + '  ');
  323
+
  324
+            if (!extendsHasVar) {
  325
+                // Load the parent template
  326
+                if (token.name === 'extends') {
  327
+                    filepath = token.args[0];
  328
+
  329
+                    if (!helpers.isStringLiteral(filepath)) {
  330
+
  331
+                        if (!context) {
  332
+                            extendsHasVar = true;
  333
+                            return;
  334
+                        }
  335
+                        filepath = "\"" + getContextVar(filepath, context) + "\"";
  336
+                    }
  337
+
  338
+                    if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
  339
+                        throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
  340
+                    }
  341
+                    if (index > 0) {
  342
+                        throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
  343
+                    }
  344
+                    token.template = this.compileFile(filepath.replace(/['"]/g, ''));
  345
+                    this.parent = token.template;
  346
+
  347
+                } else if (token.name === 'block') { // Make a list of blocks
  348
+                    blockname = token.args[0];
  349
+                    if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
  350
+                        throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
  351
+                    }
  352
+                    if (this.type !== TEMPLATE) {
  353
+                        throw new Error('Block "' + blockname + '" found nested in another block tag on line' + token.line + '.');
  354
+                    }
  355
+                    try {
  356
+                        if (this.hasOwnProperty('parent') && this.parent.blocks.hasOwnProperty(blockname)) {
  357
+                            this.blocks[blockname] = compile.call(token, indent + '  ', this.parent.blocks[blockname]);
  358
+                        } else if (this.hasOwnProperty('blocks')) {
  359
+                            this.blocks[blockname] = compile.call(token, indent + '  ');
  360
+                        }
  361
+                    } catch (error) {
  362
+                        throw new Error('Circular extends found on line ' + token.line + ' of "' + this.id + '"!');
337 363
                     }
338  
-                } catch (error) {
339  
-                    throw new Error('Circular extends found on line ' + token.line + ' of "' + this.id + '"!');
  364
+                } else if (token.name === 'set') {
  365
+                    sets.push(token);
  366
+                    return;
340 367
                 }
341  
-            } else if (token.name === 'set') {
342  
-                sets.push(token);
343  
-                return;
  368
+                tokens.push(token);
344 369
             }
345  
-            tokens.push(token);
346 370
         }, this);
347 371
 
  372
+        // If extendsHasVar == true, then we know {% extends %} is not using a string literal, thus we can't
  373
+        // compile until render is called, so we return false.
  374
+        if (extendsHasVar) {
  375
+            return false;
  376
+        }
  377
+
348 378
         if (tokens.length && tokens[0].name === 'extends') {
349 379
             this.blocks = _.extend({}, this.parent.blocks, this.blocks);
350 380
             this.tokens = sets.concat(this.parent.tokens);
9  tests/templates/extends_base2.html
... ...
@@ -0,0 +1,9 @@
  1
+This is from the "extends_base2.html" template.
  2
+
  3
+{% block one %}
  4
+  This is the default content in block 'one'
  5
+{% endblock %}
  6
+
  7
+{% block two %}
  8
+  This is the default content in block 'two'
  9
+{% endblock %}
6  tests/templates/extends_dynamic.html
... ...
@@ -0,0 +1,6 @@
  1
+{% extends baseTmpl %}
  2
+This is content from "extends_dynamic.html", you should not see it
  3
+
  4
+{% block one %}
  5
+  This is the "extends_dynamic.html" content in block 'one'
  6
+{% endblock %}
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.