Permalink
Browse files

Whitespace Control

closes gh-46
  • Loading branch information...
1 parent c2a1fed commit 348d326c4c6a74bd041d6664f8ecda5be4db26e0 @paularmstrong committed Feb 12, 2012
Showing with 164 additions and 12 deletions.
  1. +15 −0 docs/syntax.md
  2. +47 −5 lib/parser.js
  3. +102 −7 tests/parser.test.js
View
@@ -1,6 +1,11 @@
Template Syntax <a name="syntax" href="#syntax">#</a>
===============
+* [Variables](#variables)
+* [Comments](#comments)
+* [Logic](#logic)
+* [Whitespace Control](#whitespace)
+
Swig uses the same template syntax as Django Templates
* `{{` opens variable
@@ -44,3 +49,13 @@ Logic tags are operational blocks that have internal logic on how the template s
{% if something %}
This will display if something is truthy.
{% endif %}
+
+### Whitespace Control <a name="whitespace" href="#whitespace">#</a>
+
+Any whitespace in your templates is left in your final output templates. However, you can control the whitespace around logic tags by using whitespace controls. If you put a dash (`-`) at the beginning or end of you tag, it will remove the previous or following whitespace.
+
+ {% for item in seq -%}
+ {{ item }}
+ {%- endfor %}
+
+This will return all `item` objects without any whitespace between them. If `seq` is an array of number `1` through `5`, the output will be `12345`.
View
@@ -171,12 +171,21 @@ exports.parse = function (data, tags, autoescape) {
lastToken,
rawStart = /^\{\% *raw *\%\}/,
rawEnd = /\{\% *endraw *\%\}$/,
- inRaw = false;
+ inRaw = false,
+ stripAfter = false,
+ stripBefore = false,
+ stripStart = false,
+ stripEnd = false;
for (i; i < j; i += 1) {
token = rawtokens[i];
curline = lines;
newlines = token.match(/\n/g);
+ stripAfter = false;
+ stripBefore = false;
+ stripStart = false;
+ stripEnd = false;
+
if (newlines) {
lines += newlines.length;
}
@@ -209,14 +218,24 @@ exports.parse = function (data, tags, autoescape) {
}
parts = token.replace(/^\{%\s*|\s*%\}$/g, '').split(' ');
+ if (parts[0] === '-') {
+ stripBefore = true;
+ parts.shift();
+ }
tagname = parts.shift();
+ if (_.last(parts) === '-') {
+ stripAfter = true;
+ parts.pop();
+ }
if (index > 0 && (/^end/).test(tagname)) {
lastToken = _.last(stack[stack.length - 2]);
if ('end' + lastToken.name === tagname) {
if (lastToken.name === 'autoescape') {
escape = last_escape;
}
+ lastToken.strip.end = stripBefore;
+ lastToken.strip.after = stripAfter;
stack.pop();
index -= 1;
continue;
@@ -239,11 +258,19 @@ exports.parse = function (data, tags, autoescape) {
line: curline,
name: tagname,
compile: tags[tagname],
- parent: _.uniq(stack[stack.length - 2])
+ parent: _.uniq(stack[stack.length - 2]),
+ strip: {
+ before: stripBefore,
+ after: stripAfter,
+ start: false,
+ end: false
+ }
};
token.args = getTokenArgs(token, parts);
if (tags[tagname].ends) {
+ token.strip.after = false;
+ token.strip.start = stripAfter;
stack[index].push(token);
stack.push(token.tokens = []);
index += 1;
@@ -321,7 +348,16 @@ exports.compile = function compile(indent, parentBlock) {
// If this is not a template then just iterate through its tokens
_.each(this.tokens, function (token, index) {
+ var name, key, args, prev, next;
if (typeof token === 'string') {
+ prev = this.tokens[index - 1];
+ next = this.tokens[index + 1];
+ if (prev && prev.strip && prev.strip.after) {
+ token = token.replace(/^\s+/, '');
+ }
+ if (next && next.strip && next.strip.before) {
+ token = token.replace(/\s+$/, '');
+ }
code += '__output += "' + doubleEscape(token).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '";\n';
return code;
}
@@ -331,9 +367,9 @@ exports.compile = function compile(indent, parentBlock) {
}
if (token.type === VAR_TOKEN) {
- var name = token.name.replace(/\W/g, '_'),
- key = (helpers.isLiteral(name)) ? '["' + name + '"]' : '.' + name,
- args = (token.args && token.args.length) ? token.args : '';
+ name = token.name.replace(/\W/g, '_');
+ key = (helpers.isLiteral(name)) ? '["' + name + '"]' : '.' + name;
+ args = (token.args && token.args.length) ? token.args : '';
code += 'if (typeof __context !== "undefined" && typeof __context' + key + ' === "function") {\n';
code += ' __output += ' + helpers.wrapMethod('', { name: name, args: args }, '__context') + ';\n';
@@ -358,6 +394,12 @@ exports.compile = function compile(indent, parentBlock) {
} else if (token.name === 'parent') {
code += indent + ' ' + parentBlock;
} else {
+ if (token.strip.start && token.tokens.length && typeof token.tokens[0] === 'string') {
+ token.tokens[0] = token.tokens[0].replace(/^\s+/, '');
+ }
+ if (token.strip.end && token.tokens.length && typeof _.last(token.tokens) === 'string') {
+ token.tokens[token.tokens.length - 1] = _.last(token.tokens).replace(/\s+$/, '');
+ }
code += token.compile(indent + ' ', parentBlock);
}
View
@@ -1,4 +1,5 @@
var testCase = require('nodeunit').testCase,
+ swig = require('../index'),
tags = require('../lib/tags'),
parser = require('../lib/parser');
@@ -12,32 +13,83 @@ exports.Tags = testCase({
'basic tag': function (test) {
var output = parser.parse('{% blah %}', { blah: {} });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: [], compile: {}, parent: [] }], output);
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: [],
+ compile: {},
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output);
output = parser.parse('{% blah "foobar" %}', { blah: {} });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: ['"foobar"'], compile: {}, parent: [] }], output, 'args appended');
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: ['"foobar"'],
+ compile: {},
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output, 'args appended');
output = parser.parse('{% blah "foobar" barfoo %}', { blah: {} });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: ['"foobar"', 'barfoo'], compile: {}, parent: [] }], output, 'multiple args appended');
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: ['"foobar"', 'barfoo'],
+ compile: {},
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output, 'multiple args appended');
test.done();
},
'basic tag with ends': function (test) {
var output = parser.parse('{% blah %}{% endblah %}', { blah: { ends: true } });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: [], compile: { ends: true }, tokens: [], parent: [] }], output);
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: [],
+ compile: { ends: true },
+ tokens: [],
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output);
test.done();
},
'multi-line tag': function (test) {
var output = parser.parse('{% blah \n arg1 %}{% endblah\n %}', { blah: { ends: true } });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: ['arg1'], compile: { ends: true }, tokens: [], parent: [] }], output);
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: ['arg1'],
+ compile: { ends: true },
+ tokens: [],
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output);
test.done();
},
'line number included in token': function (test) {
var output = parser.parse('hi!\n\n\n{% blah %}{% endblah %}', { blah: { ends: true } });
- test.deepEqual({ type: parser.TOKEN_TYPES.LOGIC, line: 4, name: 'blah', args: [], compile: { ends: true }, tokens: [], parent: [] }, output[1]);
+ test.deepEqual({
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 4,
+ name: 'blah',
+ args: [],
+ compile: { ends: true },
+ tokens: [],
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }, output[1]);
test.done();
},
@@ -64,7 +116,16 @@ exports.Tags = testCase({
'tag with contents': function (test) {
var output = parser.parse('{% blah %}hello{% endblah %}', { blah: { ends: true } });
- test.deepEqual([{ type: parser.TOKEN_TYPES.LOGIC, line: 1, name: 'blah', args: [], compile: { ends: true }, tokens: ['hello'], parent: [] }], output);
+ test.deepEqual([{
+ type: parser.TOKEN_TYPES.LOGIC,
+ line: 1,
+ name: 'blah',
+ args: [],
+ compile: { ends: true },
+ tokens: ['hello'],
+ strip: { before: false, after: false, start: false, end: false },
+ parent: []
+ }], output);
test.done();
},
@@ -113,6 +174,40 @@ exports.Tags = testCase({
}
});
+exports.Whitespace = testCase({
+ setUp: function (callback) {
+ var tags = {
+ foo: function (indent, parentBlock) {
+ return parser.compile.apply(this, [indent + ' ', parentBlock]);
+ },
+ bar: function (indent, parentBlock) {
+ return '';
+ }
+ };
+ tags.foo.ends = true;
+ swig.init({
+ tags: tags,
+ allowErrors: true
+ });
+ callback();
+ },
+
+ basic: function (test) {
+ test.strictEqual(swig.compile('{% foo -%} foo{% endfoo %}')(), 'foo', 'whitespace before');
+ test.strictEqual(swig.compile('{% foo %}foo {%- endfoo %}')(), 'foo', 'whitespace after');
+ test.strictEqual(swig.compile('{% foo -%}\n\r\t foo\n\r\t {%- endfoo %}')(), 'foo', 'whitespace before and after');
+ test.strictEqual(swig.compile('a {%- bar %}b')(), 'ab', 'previous whitespace removed');
+ test.strictEqual(swig.compile('a {%- foo -%} b {%- endfoo -%} c')(), 'abc', 'all whitespace removed');
+ test.done();
+ },
+
+ 'with tags': function (test) {
+ test.strictEqual(swig.compile('{% foo -%} {% bar %} foo{% endfoo %}')(), ' foo', 'whitespace before only up until tag');
+ test.strictEqual(swig.compile('{% foo%}foo {% bar %}{%- endfoo %}')(), 'foo ', 'whitespace after only up until tag');
+ test.done();
+ }
+});
+
exports.Comments = testCase({
'empty strings are ignored': function (test) {
var output = parser.parse('');

0 comments on commit 348d326

Please sign in to comment.