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

Remove whitespace surrounding standalone statements #787

Merged
merged 6 commits into from
Aug 13, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 144 additions & 9 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ function LocationInfo(locInfo){
}

var AST = {
ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
ProgramNode: function(isRoot, statements, inverseStrip, inverse, locInfo) {
var inverseLocationInfo, firstInverseNode;
if (arguments.length === 3) {
if (arguments.length === 4) {
locInfo = inverse;
inverse = null;
} else if (arguments.length === 2) {
} else if (arguments.length === 3) {
locInfo = inverseStrip;
inverseStrip = null;
}
Expand All @@ -33,14 +33,17 @@ var AST = {
last_column: firstInverseNode.lastColumn,
first_column: firstInverseNode.firstColumn
};
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
this.inverse = new AST.ProgramNode(isRoot, inverse, inverseStrip, inverseLocationInfo);
} else {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
this.inverse = new AST.ProgramNode(isRoot, inverse, inverseStrip);
}
this.strip.right = inverseStrip.left;
} 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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -142,7 +165,7 @@ var AST = {

this.type = 'block';
this.mustache = mustache;
this.program = new AST.ProgramNode([content], locInfo);
this.program = new AST.ProgramNode(false, [content], locInfo);
},

ContentNode: function(string, locInfo) {
Expand Down Expand Up @@ -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;
6 changes: 4 additions & 2 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,14 @@ Compiler.prototype = {
this.opcode('push', 'depth0');
}

this.opcode('invokePartial', partialName.name);
this.opcode('invokePartial', partialName.name, partial.indent || '');
this.opcode('append');
},

content: function(content) {
this.opcode('appendContent', content.string);
if (!content.omit) {
this.opcode('appendContent', content.string);
}
},

mustache: function(mustache) {
Expand Down
4 changes: 2 additions & 2 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,8 @@ JavaScriptCompiler.prototype = {
//
// This operation pops off a context, invokes a partial with that context,
// and pushes the result of the invocation back.
invokePartial: function(name) {
var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"];
invokePartial: function(name, indent) {
var params = [this.nameLookup('partials', name, 'partial'), "'" + indent + "'", "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"];

if (this.options.data) {
params.push("data");
Expand Down
21 changes: 17 additions & 4 deletions lib/handlebars/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,31 @@ export function template(templateSpec, env) {
// for external users to override these as psuedo-supported APIs.
env.VM.checkRevision(templateSpec.compiler);

var invokePartialWrapper = function(partial, name, context, hash, helpers, partials, data) {
var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data) {
if (hash) {
context = Utils.extend({}, context, hash);
}

var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data);
if (result != null) { return result; }

if (env.compile) {
if (result == null && env.compile) {
var options = { helpers: helpers, partials: partials, data: data };
partials[name] = env.compile(partial, { data: data !== undefined }, env);
return partials[name](context, options);
result = partials[name](context, options);
}
if (result != null) {
if (indent) {
var lines = result.split('\n');
for (var i = 0, l = lines.length; i < l; i++) {
if (!lines[i] && i + 1 === l) {
break;
}

lines[i] = indent + lines[i];
}
result = lines.join('\n');
}
return result;
} else {
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
}
Expand Down
Loading