From 337da204eeb5ccc96a5eaeb6aa5b045d531e8c50 Mon Sep 17 00:00:00 2001 From: Yotam Madem Date: Wed, 26 Jun 2019 00:12:35 +0300 Subject: [PATCH 1/5] Preserve indentation when rendering partials --- mustache.js | 41 +++++++++++++++++++++++++++++++++++------ test/render-test.js | 8 ++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/mustache.js b/mustache.js index 8ec1b44cc..7ea98450f 100644 --- a/mustache.js +++ b/mustache.js @@ -528,6 +528,7 @@ */ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { var buffer = ''; + var indentationContext = {spacer: ''}; var token, symbol, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { @@ -537,18 +538,31 @@ if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); - else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); + else if (symbol === '>') value = this.renderPartial(token, context, partials, tags, indentationContext); else if (symbol === '&') value = this.unescapedValue(token, context); else if (symbol === 'name') value = this.escapedValue(token, context); else if (symbol === 'text') value = this.rawValue(token); - if (value !== undefined) + if (value !== undefined) { + this.updateIndentationContext(indentationContext, value); buffer += value; + } } - return buffer; }; + Writer.prototype.updateIndentationContext = function updateIndentationContext (indentationContext, value) { + for (var j = 0; j < value.length; j++) { + if (value[j] == '\n') { + indentationContext.spacer = ''; + } else if (isWhitespace(value[j])) { + indentationContext.spacer += value[j]; + } else { + indentationContext.spacer += ' '; + } + } + }; + Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { var self = this; var buffer = ''; @@ -592,12 +606,27 @@ return this.renderTokens(token[4], context, partials, originalTemplate); }; - Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { + Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags, indentationContext) { 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 renderResult = this.renderTokens(this.parse(value, tags), context, partials, value); + return this.indent(renderResult, indentationContext); + } + }; + + Writer.prototype.indent = function indent (value, indentationContext) { + var indentedValue = ''; + var lines = value.split('\n'); + for (var i=0; i Date: Thu, 27 Jun 2019 00:51:08 +0300 Subject: [PATCH 2/5] make the partials work exactly as the spec --- mustache.js | 34 ++++++++----- test/partial-tests.js | 112 ++++++++++++++++++++++++++++++++++++++++++ test/render-test.js | 8 +-- 3 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 test/partial-tests.js diff --git a/mustache.js b/mustache.js index 7ea98450f..c3ff79090 100644 --- a/mustache.js +++ b/mustache.js @@ -130,7 +130,7 @@ function stripSpace () { if (hasTag && !nonSpace) { while (spaces.length) - delete tokens[spaces.pop()]; + delete tokens[spaces.pop()]; } else { spaces = []; } @@ -168,7 +168,7 @@ chr = value.charAt(i); if (isWhitespace(chr)) { - spaces.push(tokens.length); + spaces.push(tokens.length); } else { nonSpace = true; } @@ -185,11 +185,16 @@ // Match the opening tag. if (!scanner.scan(openingTagRe)) break; - + hasTag = true; // Get the tag type. type = scanner.scan(tagRe) || 'name'; + + if (type == '>') { + spaces = []; + } + scanner.scan(whiteRe); // Get the tag value. @@ -238,7 +243,7 @@ if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - return nestTokens(squashTokens(tokens)); + return nestTokens(squashTokens(tokens)); } /** @@ -528,7 +533,7 @@ */ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { var buffer = ''; - var indentationContext = {spacer: ''}; + var indentationContext = {spacer: '', active: true}; var token, symbol, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { @@ -554,11 +559,15 @@ Writer.prototype.updateIndentationContext = function updateIndentationContext (indentationContext, value) { for (var j = 0; j < value.length; j++) { if (value[j] == '\n') { + indentationContext.active = true; indentationContext.spacer = ''; } else if (isWhitespace(value[j])) { - indentationContext.spacer += value[j]; + if (indentationContext.active) { + indentationContext.spacer += value[j]; + } } else { - indentationContext.spacer += ' '; + indentationContext.active = false; + indentationContext.spacer = ''; } } }; @@ -611,22 +620,25 @@ var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; if (value != null) { - var renderResult = this.renderTokens(this.parse(value, tags), context, partials, value); - return this.indent(renderResult, indentationContext); + var indentedValue = this.indent(value, indentationContext); + return this.renderTokens(this.parse(indentedValue, tags), context, partials, value); } }; Writer.prototype.indent = function indent (value, indentationContext) { var indentedValue = ''; - var lines = value.split('\n'); + var val = value + '$'; + var lines = val.split('\n'); for (var i=0; inode}}{{/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 4922e7a7c..4f1c063b3 100644 --- a/test/render-test.js +++ b/test/render-test.js @@ -18,11 +18,13 @@ describe('Mustache.render', function () { }); describe('preserve indentation when using partials', function() { - it.only ('should preserve indentation', function() { - var template = 'line1\n bla la \t\r\f foo line2{{>p1}}'; + + it ('should preserve indentation with whitespaces', function() { + var template = 'a\n {{>p1}}'; var renderResult = Mustache.render(template, {}, {p1: 'l1\nl2'}); - assert.equal(renderResult, 'line1\n bla la \t\r\f foo line2l1\n \t\r\f l2'); + assert.equal(renderResult, 'a\n l1\n l2'); }); + }); describe('custom tags', function () { From 909480074923b15f0717b900a04b5f4ab30b8dc0 Mon Sep 17 00:00:00 2001 From: Yotam Madem Date: Thu, 27 Jun 2019 01:02:29 +0300 Subject: [PATCH 3/5] rename to partial-test.js so that it will be run --- test/{partial-tests.js => partial-test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{partial-tests.js => partial-test.js} (100%) diff --git a/test/partial-tests.js b/test/partial-test.js similarity index 100% rename from test/partial-tests.js rename to test/partial-test.js From d06d0bf002eee7be56fd46f793d3e9fe3fb26193 Mon Sep 17 00:00:00 2001 From: Yotam Madem Date: Thu, 27 Jun 2019 19:40:40 +0300 Subject: [PATCH 4/5] merge with the previous solution --- mustache.js | 74 +++++++++++++++++++++------------------------- test/parse-test.js | 9 ++++-- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/mustache.js b/mustache.js index c3ff79090..8e400fd52 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. @@ -168,17 +170,23 @@ chr = value.charAt(i); if (isWhitespace(chr)) { - spaces.push(tokens.length); + 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; + } } } @@ -191,10 +199,6 @@ // Get the tag type. type = scanner.scan(tagRe) || 'name'; - if (type == '>') { - spaces = []; - } - scanner.scan(whiteRe); // Get the tag value. @@ -216,6 +220,13 @@ 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 === '^') { @@ -237,6 +248,8 @@ } } + stripSpace(); + // Make sure there are no open sections when we're done. openSection = sections.pop(); @@ -533,7 +546,6 @@ */ Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, tags) { var buffer = ''; - var indentationContext = {spacer: '', active: true}; var token, symbol, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { @@ -543,35 +555,18 @@ if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); - else if (symbol === '>') value = this.renderPartial(token, context, partials, tags, indentationContext); + else if (symbol === '>') value = this.renderPartial(token, context, partials, tags); else if (symbol === '&') value = this.unescapedValue(token, context); else if (symbol === 'name') value = this.escapedValue(token, context); else if (symbol === 'text') value = this.rawValue(token); if (value !== undefined) { - this.updateIndentationContext(indentationContext, value); buffer += value; } } return buffer; }; - Writer.prototype.updateIndentationContext = function updateIndentationContext (indentationContext, value) { - for (var j = 0; j < value.length; j++) { - if (value[j] == '\n') { - indentationContext.active = true; - indentationContext.spacer = ''; - } else if (isWhitespace(value[j])) { - if (indentationContext.active) { - indentationContext.spacer += value[j]; - } - } else { - indentationContext.active = false; - indentationContext.spacer = ''; - } - } - }; - Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { var self = this; var buffer = ''; @@ -615,30 +610,29 @@ return this.renderTokens(token[4], context, partials, originalTemplate); }; - Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags, indentationContext) { + Writer.prototype.renderPartial = function renderPartial (token, context, partials, tags) { if (!partials) return; var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; if (value != null) { - var indentedValue = this.indent(value, indentationContext); - return this.renderTokens(this.parse(indentedValue, tags), context, partials, value); + var indentation = token[4]; + var partialVal = this.indentPartial(value, indentation); + return this.renderTokens(this.parse(partialVal, tags), context, partials, value); } }; - Writer.prototype.indent = function indent (value, indentationContext) { - var indentedValue = ''; - var val = value + '$'; - var lines = val.split('\n'); - for (var i=0; iabc}}' : [ [ '>', '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 ] ], From 395a24df424caeab18e989499f1579762d1cc7e0 Mon Sep 17 00:00:00 2001 From: Yotam Madem Date: Thu, 27 Jun 2019 19:53:26 +0300 Subject: [PATCH 5/5] fix code styling issues --- mustache.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/mustache.js b/mustache.js index 8e400fd52..aeb25a2ca 100644 --- a/mustache.js +++ b/mustache.js @@ -132,7 +132,7 @@ function stripSpace () { if (hasTag && !nonSpace) { while (spaces.length) - delete tokens[spaces.pop()]; + delete tokens[spaces.pop()]; } else { spaces = []; } @@ -182,7 +182,7 @@ start += 1; // Check for whitespace on the current line. - if (chr === '\n'){ + if (chr === '\n') { stripSpace(); indentation = ''; tagIndex = 0; @@ -193,12 +193,11 @@ // Match the opening tag. if (!scanner.scan(openingTagRe)) break; - + hasTag = true; // Get the tag type. type = scanner.scan(tagRe) || 'name'; - scanner.scan(whiteRe); // Get the tag value. @@ -224,9 +223,7 @@ if (type == '>' && tagIndex == 0) { token.push(indentation); } - tagIndex ++; - tokens.push(token); if (type === '#' || type === '^') { @@ -256,7 +253,7 @@ if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); - return nestTokens(squashTokens(tokens)); + return nestTokens(squashTokens(tokens)); } /** @@ -560,10 +557,10 @@ else if (symbol === 'name') value = this.escapedValue(token, context); else if (symbol === 'text') value = this.rawValue(token); - if (value !== undefined) { + if (value !== undefined) buffer += value; - } } + return buffer; };