diff options
author | kpdecker <kpdecker@gmail.com> | 2014-07-13 08:25:28 -0500 |
---|---|---|
committer | kpdecker <kpdecker@gmail.com> | 2014-08-12 14:41:58 -0500 |
commit | 50c657f138d01c90d2dee19c7c15ca81bb0ee165 (patch) | |
tree | 4fd44fa61546dfbd15c98f387d7fa1571094f2ac | |
parent | 60701790099d3c4b30c32d498b05c46fa323df30 (diff) | |
download | handlebars.js-50c657f138d01c90d2dee19c7c15ca81bb0ee165.zip handlebars.js-50c657f138d01c90d2dee19c7c15ca81bb0ee165.tar.gz handlebars.js-50c657f138d01c90d2dee19c7c15ca81bb0ee165.tar.bz2 |
Flag standalone nodes in the AST
-rw-r--r-- | lib/handlebars/compiler/ast.js | 141 | ||||
-rw-r--r-- | spec/ast.js | 148 |
2 files changed, 286 insertions, 3 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 74f276e..ae5ea63 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -41,6 +41,9 @@ var AST = { } else if (inverseStrip) { this.strip.left = inverseStrip.right; } + + // Scan all children to complete the standalone analysis + checkStandalone(this, isRoot, statements); }, MustacheNode: function(rawParams, hash, open, strip, locInfo) { @@ -104,6 +107,8 @@ var AST = { this.context = context; this.hash = hash; this.strip = strip; + + this.strip.inlineStandalone = true; }, BlockNode: function(mustache, program, inverse, close, locInfo) { @@ -118,13 +123,31 @@ var AST = { this.program = program; this.inverse = inverse; + var firstChild = program || inverse, + lastChild = inverse || program; + this.strip = { left: mustache.strip.left, - right: close.strip.right + right: close.strip.right, + + // Determine the standalone candiacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(firstChild), + closeStandalone: isPrevWhitespace(lastChild) }; - (program || inverse).strip.left = mustache.strip.right; - (inverse || program).strip.right = close.strip.left; + // Calculate stripping for any else statements + firstChild.strip.left = mustache.strip.right; + lastChild.strip.right = close.strip.left; + + // Find standalone else statments + if (program && inverse + && isPrevWhitespace(program) + && isNextWhitespace(inverse)) { + + omitLeft(program); + omitRight(inverse); + } if (inverse && !program) { this.isInverse = true; @@ -238,9 +261,121 @@ var AST = { LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; + + this.strip = { + inlineStandalone: true + }; } }; + +function checkStandalone(program, isRoot, statements) { + for (var i = 0, l = statements.length; i < l; i++) { + var current = statements[i], + strip = current.strip; + + if (!strip) { + continue; + } + + var _isPrevWhitespace = isPrevWhitespace(program, i, isRoot, current.type === 'partial'), + _isNextWhitespace = isNextWhitespace(program, i, isRoot); + strip.openStandalone = strip.openStandalone && _isPrevWhitespace; + strip.closeStandalone = strip.closeStandalone && _isNextWhitespace; + strip.inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; + + if (strip.inlineStandalone) { + omitRight(program, i); + + if (omitLeft(program, i)) { + // If we are on a standalone node, save the indent info for partials + if (current.type === 'partial') { + current.indent = statements[i-1].string; + } + } + } + if (strip.openStandalone) { + omitRight(current.program || current.inverse); + + // Strip out the previous content node if it's whitespace only + omitLeft(program, i); + } + if (strip.closeStandalone) { + // Always strip the next node + omitRight(program, i); + + omitLeft(current.inverse || current.program); + } + } +} +function isPrevWhitespace(parent, i, isRoot, disallowIndent) { + var statements = parent.statements; + if (i === undefined) { + i = statements.length; + } + + // Nodes that end with newlines are considered whitespace (but are special + // cased for strip operations) + var prev = statements[i-1]; + if (prev && /\n$/.test(prev.string)) { + return true; + } + + return checkWhitespace(isRoot, prev, statements[i-2]); +} +function isNextWhitespace(parent, i, isRoot) { + var statements = parent.statements; + if (i === undefined) { + i = -1; + } + + return checkWhitespace(isRoot, statements[i+1], statements[i+2]); +} +function checkWhitespace(isRoot, next1, next2, disallowIndent) { + if (!next1) { + return isRoot; + } else if (next1.type === 'content') { + // Check if the previous node is empty or whitespace only + if (disallowIndent ? !next1.string : /^[\s]*$/.test(next1.string)) { + if (next2) { + return next2.type === 'content' || /\n$/.test(next1.string); + } else { + return isRoot || (next1.string.indexOf('\n') >= 0); + } + } + } +} + +// Marks the node to the right of the position as omitted. +// I.e. " "{{foo}} will mark the " " node as omitted. +// +// If i is undefined, then the first child will be marked as such. +function omitRight(program, i) { + var first = program.statements[i == null ? 0 : i + 1]; + if (first) { + first.omit = true; + } +} + +// Marks the node to the left of the position as omitted. +// I.e. " "{{foo}} will mark the " " node as omitted. +// +// If i is undefined then the last child will be marked as such. +function omitLeft(program, i) { + var statements = program.statements; + if (i === undefined) { + i = statements.length; + } + + var last = statements[i-1], + prev = statements[i-2]; + + // We omit the last node if it's whitespace only and not preceeded by a non-content node. + if (last && /^[\s]*$/.test(last.string) && (!prev || prev.type === 'content')) { + return last.omit = true; + } +} + // Must be exported as an object rather than the root of the module as the jison lexer // most modify the object to operate properly. export default AST; diff --git a/spec/ast.js b/spec/ast.js index cad4b30..316d4a3 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -272,5 +272,153 @@ describe('ast', function() { testColumns(inverse, 5, 6, 13, 0); }); }); + + describe('standalone flags', function(){ + describe('mustache', function() { + it('does not mark mustaches as standalone', function() { + var ast = Handlebars.parse(' {{comment}} '); + equals(ast.statements[0].omit, undefined); + equals(ast.statements[2].omit, undefined); + }); + }); + describe('blocks', function() { + it('marks block mustaches as standalone', function() { + var ast = Handlebars.parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), + block = ast.statements[1]; + + equals(ast.statements[0].omit, true); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(ast.statements[2].omit, true); + }); + it('marks initial block mustaches as standalone', function() { + var ast = Handlebars.parse('{{# comment}} \nfoo\n {{/comment}}'), + block = ast.statements[0]; + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + }); + it('marks mustaches with children as standalone', function() { + var ast = Handlebars.parse('{{# comment}} \n{{foo}}\n {{/comment}}'), + block = ast.statements[0]; + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].id.original, 'foo'); + equals(block.program.statements[2].omit, undefined); + equals(block.program.statements[3].omit, true); + }); + it('marks nested block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[1]; + + equals(statements[0].omit, true); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(statements[0].omit, true); + }); + it('does not mark nested block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[1]; + + equals(statements[0].omit, undefined); + + equals(block.program.statements[0].omit, undefined); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, undefined); + + equals(statements[0].omit, undefined); + }); + it('does not mark nested initial block mustaches as standalone', function() { + var ast = Handlebars.parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'), + statements = ast.statements[0].program.statements, + block = statements[0]; + + equals(block.program.statements[0].omit, undefined); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, undefined); + + equals(statements[0].omit, undefined); + }); + + it('marks column 0 block mustaches as standalone', function() { + var ast = Handlebars.parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), + block = ast.statements[1]; + + equals(ast.statements[0].omit, undefined); + + equals(block.program.statements[0].omit, true); + equals(block.program.statements[1].string, 'foo\n'); + equals(block.program.statements[2].omit, true); + + equals(block.inverse.statements[0].omit, true); + equals(block.inverse.statements[1].string, ' bar \n'); + equals(block.inverse.statements[2].omit, true); + + equals(ast.statements[2].omit, true); + }); + }); + describe('partials', function() { + it('marks partial as standalone', function() { + var ast = Handlebars.parse('{{> partial }} '); + equals(ast.statements[1].omit, true); + }); + it('marks indented partial as standalone', function() { + var ast = Handlebars.parse(' {{> partial }} '); + equals(ast.statements[0].omit, true); + equals(ast.statements[1].indent, ' '); + equals(ast.statements[2].omit, true); + }); + it('marks those around content as not standalone', function() { + var ast = Handlebars.parse('a{{> partial }}'); + equals(ast.statements[0].omit, undefined); + + ast = Handlebars.parse('{{> partial }}a'); + equals(ast.statements[1].omit, undefined); + }); + }); + describe('comments', function() { + it('marks comment as standalone', function() { + var ast = Handlebars.parse('{{! comment }} '); + equals(ast.statements[1].omit, true); + }); + it('marks indented comment as standalone', function() { + var ast = Handlebars.parse(' {{! comment }} '); + equals(ast.statements[0].omit, true); + equals(ast.statements[2].omit, true); + }); + it('marks those around content as not standalone', function() { + var ast = Handlebars.parse('a{{! comment }}'); + equals(ast.statements[0].omit, undefined); + + ast = Handlebars.parse('{{! comment }}a'); + equals(ast.statements[1].omit, undefined); + }); + }); + }); }); |