diff --git a/mustache.js b/mustache.js index 8ec1b44cc..aeb25a2ca 100644 --- a/mustache.js +++ b/mustache.js @@ -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. @@ -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; + } } } @@ -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 === '^') { @@ -232,6 +245,8 @@ } } + stripSpace(); + // Make sure there are no open sections when we're done. openSection = sections.pop(); @@ -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) { diff --git a/test/parse-test.js b/test/parse-test.js index 959699bc2..4fcfbb654 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -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 ] ], diff --git a/test/partial-test.js b/test/partial-test.js new file mode 100644 index 000000000..b76fb5850 --- /dev/null +++ b/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>'; + 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); + }); +}); \ No newline at end of file diff --git a/test/render-test.js b/test/render-test.js index 1041dd40b..4f1c063b3 100644 --- a/test/render-test.js +++ b/test/render-test.js @@ -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 = '<>bar{{placeholder}}';