Repository: mozilla/nunjucks
Pull request #656 (2016-01-30 17:32:38)

Adds set block assignments a la jinja2
I've implemented set block assignments as requested in #582.
I've updated the docs accordingly, taking inspiration by the jinja documentation.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f2f400..b5285bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ Changelog
 master (unreleased)
 -------------------
 
+* Add support for `{% set %}` block assignments as in jinja2. Thanks Daniele
+  Rapagnani. Merge of [#656](https://github.com/mozilla/nunjucks/pull/656)
+
 * Fix `{% set %}` scoping within macros.
   Fixes [#577](https://github.com/mozilla/nunjucks/issues/577) and
   the macro portion of [#561](https://github.com/mozilla/nunjucks/issues/561).
diff --git a/docs/templating.md b/docs/templating.md
index d06b9b0..93edd47 100644
--- a/docs/templating.md
+++ b/docs/templating.md
@@ -110,9 +110,9 @@ This is the default content
 ```
 
 You can store the template to inherit in a variable and use it by
-omitting quotes. This variable can contain a string that points to a 
+omitting quotes. This variable can contain a string that points to a
 template file, or it can contain a compiled Template object that has
-been added to the context. That way you can dynamically change which 
+been added to the context. That way you can dynamically change which
 template is inherited when rendering by setting it in the context.
 
 ```jinja
@@ -348,6 +348,20 @@ You can introduce new variables, and also set multiple at once:
 
 If `set` is used at the top-level, it changes the value of the global template context. If used inside scoped blocks, like `for`, `include`, and others, it only modifies the current scope.
 
+It is also possible to capture the contents of a block into a variable using block assignments.
+The syntax is similar to the standard `set`, except that the `=` is omitted, and
+everything until the `{% endset %}` is captured.
+
+This can be useful in some situations as an alternative for macros:
+
+```jinja
+{% set standardModal %}
+    {% include 'standardModalData.html' %}
+{% endset %}
+
+<div class="js-modal" data-modal="{{standardModal | e}}">
+```
+
 ### extends
 
 `extends` is used to specify template inheritance. The specified
@@ -359,7 +373,7 @@ Inheritance](#template-inheritance).
 ```
 
 You can store the template to inherit in a variable and use it by
-omitting quotes. This variable can contain a string that points to a 
+omitting quotes. This variable can contain a string that points to a
 template file, or it can contain a compiled Template object that has
 been added to the context. That way you can dynamically change which template is
 inherited when rendering by setting it in the context.
@@ -369,8 +383,8 @@ inherited when rendering by setting it in the context.
 ```
 
 In fact, `extends` accepts any arbitrary expression, so you can pass
-anything into it, as long as that expression evaluates to a string or 
-a compiled Template object: 
+anything into it, as long as that expression evaluates to a string or
+a compiled Template object:
 
 ```jinja
 {% extends name + ".html" %}`.
diff --git a/src/compiler.js b/src/compiler.js
index 911b13b..1160512 100644
--- a/src/compiler.js
+++ b/src/compiler.js
@@ -523,9 +523,18 @@ var Compiler = Object.extend({
             ids.push(id);
         }, this);
 
-        this.emit(ids.join(' = ') + ' = ');
-        this._compileExpression(node.value, frame);
-        this.emitLine(';');
+        if (node.value) {
+          this.emit(ids.join(' = ') + ' = ');
+          this._compileExpression(node.value, frame);
+          this.emitLine(';');
+        }
+        else {
+          this.emitLine(ids.join(' = ') + ' = (function() {');
+          this.emitLine('var output = "";');
+          this.compile(node.body, frame);
+          this.emitLine('return output;');
+          this.emitLine('})();');
+        }
 
         lib.each(node.targets, function(target, i) {
             var id = ids[i];
diff --git a/src/parser.js b/src/parser.js
index c042fb0..9dc3221 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -505,13 +505,21 @@ var Parser = Object.extend({
         }
 
         if(!this.skipValue(lexer.TOKEN_OPERATOR, '=')) {
-            this.fail('parseSet: expected = in set tag',
-                      tag.lineno,
-                      tag.colno);
+            if (!this.skip(lexer.TOKEN_BLOCK_END)) {
+                this.fail('parseSet: expected = or block end in set tag',
+                          tag.lineno,
+                          tag.colno);
+            }
+            else {
+                node.body = this.parseUntilBlocks('endset');
+                node.value = null;
+                this.advanceAfterBlockEnd();
+            }
+        }
+        else {
+            node.value = this.parseExpression();
+            this.advanceAfterBlockEnd(tag.value);
         }
-
-        node.value = this.parseExpression();
-        this.advanceAfterBlockEnd(tag.value);
 
         return node;
     },


Test patch:
diff --git a/tests/compiler.js b/tests/compiler.js
index 45e0eef..e0854a1 100644
--- a/tests/compiler.js
+++ b/tests/compiler.js
@@ -923,6 +923,52 @@
             finish(done);
         });
 
+        it('should compile set blocks', function(done) {
+          equal('{% set block_content %}{% endset %}'+
+                '{{ block_content }}',
+                ''
+                );
+
+          equal('{% set block_content %}test string{% endset %}'+
+                '{{ block_content }}',
+                'test string'
+                );
+
+          equal('{% set block_content %}'+
+                '{% for item in [1, 2, 3] %}'+
+                '{% include "item.html" %} '+
+                '{% endfor %}'+
+                '{% endset %}'+
+                '{{ block_content }}',
+                'showing 1 showing 2 showing 3 '
+                );
+
+          equal('{% set block_content %}'+
+                '{% set inner_block_content %}'+
+                '{% for i in [1, 2, 3] %}'+
+                'item {{ i }} '+
+                '{% endfor %}'+
+                '{% endset %}'+
+                '{% for i in [1, 2, 3] %}'+
+                'inner {{i}}: "{{ inner_block_content }}" '+
+                '{% endfor %}'+
+                '{% endset %}'+
+                '{{ block_content | safe }}',
+                'inner 1: "item 1 item 2 item 3 " '+
+                'inner 2: "item 1 item 2 item 3 " '+
+                'inner 3: "item 1 item 2 item 3 " '
+                );
+
+            equal('{% set x,y,z %}'+
+                  'cool'+
+                  '{% endset %}'+
+                  '{{ x }} {{ y }} {{ z }}',
+                  'cool cool cool'
+                  );
+
+            finish(done);
+        });
+
         it('should throw errors', function(done) {
             render('{% from "import.html" import boozle %}',
                    {},