summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkpdecker <kpdecker@gmail.com>2014-07-13 08:25:28 -0500
committerkpdecker <kpdecker@gmail.com>2014-08-12 14:41:58 -0500
commit50c657f138d01c90d2dee19c7c15ca81bb0ee165 (patch)
tree4fd44fa61546dfbd15c98f387d7fa1571094f2ac
parent60701790099d3c4b30c32d498b05c46fa323df30 (diff)
downloadhandlebars.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.js141
-rw-r--r--spec/ast.js148
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);
+ });
+ });
+ });
});