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

Preserve indentation when rendering partials #703

Closed
wants to merge 5 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
38 changes: 35 additions & 3 deletions mustache.js
Expand Up @@ -124,6 +124,8 @@
var spaces = []; // Indices of whitespace tokens on the current line
var hasTag = false; // Is there a {{tag}} on the current line?
var nonSpace = false; // Is there a non-space char on the current line?
var indentation = ''; // Tracks indentation for tags that use it
var tagIndex = 0; // Stores a count of number of tags encountered on a line

// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
Expand Down Expand Up @@ -169,16 +171,22 @@

if (isWhitespace(chr)) {
spaces.push(tokens.length);
if (!nonSpace)
indentation += chr;
} else {
nonSpace = true;
indentation = '';
}

tokens.push([ 'text', chr, start, start + 1 ]);
start += 1;

// Check for whitespace on the current line.
if (chr === '\n')
if (chr === '\n') {
stripSpace();
indentation = '';
tagIndex = 0;
}
}
}

Expand Down Expand Up @@ -211,6 +219,11 @@
throw new Error('Unclosed tag at ' + scanner.pos);

token = [ type, value, start, scanner.pos ];

if (type == '>' && tagIndex == 0) {
token.push(indentation);
}
tagIndex ++;
tokens.push(token);

if (type === '#' || type === '^') {
Expand All @@ -232,6 +245,8 @@
}
}

stripSpace();

// Make sure there are no open sections when we're done.
openSection = sections.pop();

Expand Down Expand Up @@ -596,8 +611,25 @@
if (!partials) return;

var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
return this.renderTokens(this.parse(value, tags), context, partials, value);
if (value != null) {
var indentation = token[4];
var partialVal = this.indentPartial(value, indentation);
return this.renderTokens(this.parse(partialVal, tags), context, partials, value);
}
};

Writer.prototype.indentPartial = function indentPartial (value, indentation) {
if (!indentation || indentation.length == 0) {
return value;
}
var filteredIndentation = indentation.replace(/[^ \t]/g, '');
var partialByNl = value.split('\n');
for (var i = 0; i < partialByNl.length; i++) {
if (partialByNl[i].length) {
partialByNl[i] = filteredIndentation + partialByNl[i];
}
}
return partialByNl.join('\n');
};

Writer.prototype.unescapedValue = function unescapedValue (token, context) {
Expand Down
9 changes: 6 additions & 3 deletions test/parse-test.js
Expand Up @@ -40,9 +40,12 @@ var expectations = {
'a\n{{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 2, 8, [ [ '#', 'b', 9, 15, [], 16 ] ], 23 ], [ 'text', 'b', 30, 31 ] ],
'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}}\nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 31, 32 ] ],
'a\n {{#a}}\n{{#b}}\n{{/b}}\n{{/a}} \nb' : [ [ 'text', 'a\n', 0, 2 ], [ '#', 'a', 3, 9, [ [ '#', 'b', 10, 16, [], 17 ] ], 24 ], [ 'text', 'b', 32, 33 ] ],
'{{>abc}}' : [ [ '>', 'abc', 0, 8 ] ],
'{{> abc }}' : [ [ '>', 'abc', 0, 10 ] ],
'{{ > abc }}' : [ [ '>', 'abc', 0, 11 ] ],
'{{>abc}}' : [ [ '>', 'abc', 0, 8, ''] ],
'{{> abc }}' : [ [ '>', 'abc', 0, 10, ''] ],
'{{ > abc }}' : [ [ '>', 'abc', 0, 11, ''] ],
' {{> abc }}\n' : [ [ '>', 'abc', 2, 12, ' '] ],
' {{> abc }} {{> abc }}\n' : [ [ '>', 'abc', 2, 12, ' '], [ '>', 'abc', 13, 23] ],
'{{ > abc }}' : [ [ '>', 'abc', 0, 11, ''] ],
'{{=<% %>=}}' : [ [ '=', '<% %>', 0, 11 ] ],
'{{= <% %> =}}' : [ [ '=', '<% %>', 0, 13 ] ],
'{{=<% %>=}}<%={{ }}=%>' : [ [ '=', '<% %>', 0, 11 ], [ '=', '{{ }}', 11, 22 ] ],
Expand Down
112 changes: 112 additions & 0 deletions test/partial-test.js
@@ -0,0 +1,112 @@
/* eslint-disable func-names */
require('./helper');

describe('Partials spec', function () {
beforeEach(function () {
Mustache.clearCache();
});


it('The greater-than operator should expand to the named partial.', function () {
var template = '"{{>text}}"';
var data = {};
var partials = {'text':'from partial'};
var expected = '"from partial"';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('The empty string should be used when the named partial is not found.', function () {
var template = '"{{>text}}"';
var data = {};
var partials = {};
var expected = '""';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('The greater-than operator should operate within the current context.', function () {
var template = '"{{>partial}}"';
var data = {'text':'content'};
var partials = {'partial':'*{{text}}*'};
var expected = '"*content*"';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('The greater-than operator should properly recurse.', function () {
var template = '{{>node}}';
var data = {'content':'X','nodes':[{'content':'Y','nodes':[]}]};
var partials = {'node':'{{content}}<{{#nodes}}{{>node}}{{/nodes}}>'};
var expected = 'X<Y<>>';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('The greater-than operator should not alter surrounding whitespace.', function () {
var template = '| {{>partial}} |';
var data = {};
var partials = {'partial':'\t|\t'};
var expected = '| \t|\t |';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('"\r\n" should be considered a newline for standalone tags.', function () {
var template = '|\r\n{{>partial}}\r\n|';
var data = {};
var partials = {'partial':'>'};
var expected = '|\r\n>|';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('Standalone tags should not require a newline to precede them.', function () {
var template = ' {{>partial}}\n>';
var data = {};
var partials = {'partial':'>\n>'};
var expected = ' >\n >>';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('Superfluous in-tag whitespace should be ignored.', function () {
var template = '|{{> partial }}|';
var data = {'boolean':true};
var partials = {'partial':'[]'};
var expected = '|[]|';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
it('Each line of the partial should be indented before rendering.', function () {
var template = '\\\n {{>partial}}\n/\n';
var data = {
'content': '<\n->'
};
var partials = {
'partial': '|\n{{{content}}}\n|\n'
};
var expected = '\\\n |\n <\n->\n |\n/\n';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});

it('Standalone tags should not require a newline to follow them.', function () {
var template = '>\n {{>partial}}';
var data = {

};
var partials = {
'partial': '>\n>'
};
var expected = '>\n >\n >';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});

it('Whitespace should be left untouched.', function () {
var template = ' {{data}} {{> partial}}\n';
var data = {
'data': '|'
};
var partials = {
'partial': '>\n>'
};
var expected = ' | >\n>\n';
var renderResult = Mustache.render(template, data, partials);
assert.equal(renderResult, expected);
});
});
10 changes: 10 additions & 0 deletions test/render-test.js
Expand Up @@ -17,6 +17,16 @@ describe('Mustache.render', function () {
'for mustache#render(template, view, partials)');
});

describe('preserve indentation when using partials', function() {

it ('should preserve indentation with whitespaces', function() {
var template = 'a\n {{>p1}}';
var renderResult = Mustache.render(template, {}, {p1: 'l1\nl2'});
assert.equal(renderResult, 'a\n l1\n l2');
});

});

describe('custom tags', function () {
it('uses tags argument instead of Mustache.tags when given', function () {
var template = '<<placeholder>>bar{{placeholder}}';
Expand Down