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

Unecessary Whitespace #336

Merged
merged 13 commits into from
Oct 27, 2013
35 changes: 27 additions & 8 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import Exception from "../exception";

export function ProgramNode(statements, inverse) {
export function ProgramNode(statements, inverseStrip, inverse) {
this.type = "program";
this.statements = statements;
if(inverse) { this.inverse = new ProgramNode(inverse); }
this.strip = {};

if(inverse) {
this.inverse = new ProgramNode(inverse, inverseStrip);
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
}

export function MustacheNode(rawParams, hash, unescaped) {
export function MustacheNode(rawParams, hash, open, strip) {
this.type = "mustache";
this.escaped = !unescaped;
this.hash = hash;
this.strip = strip;

var escapeFlag = open[3] || open[2];
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';

var id = this.id = rawParams[0];
var params = this.params = rawParams.slice(1);
Expand All @@ -28,23 +38,32 @@ export function MustacheNode(rawParams, hash, unescaped) {
// pass or at runtime.
}

export function PartialNode(partialName, context) {
export function PartialNode(partialName, context, strip) {
this.type = "partial";
this.partialName = partialName;
this.context = context;
this.strip = strip;
}

export function BlockNode(mustache, program, inverse, close) {
if(mustache.id.original !== close.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.original);
if(mustache.id.original !== close.path.original) {
throw new Exception(mustache.id.original + " doesn't match " + close.path.original);
}

this.type = "block";
this.mustache = mustache;
this.program = program;
this.inverse = inverse;

if (this.inverse && !this.program) {
this.strip = {
left: mustache.strip.left,
right: close.strip.right
};

(program || inverse).strip.left = mustache.strip.right;
(inverse || program).strip.right = close.strip.left;

if (inverse && !program) {
this.isInverse = true;
}
}
Expand Down
23 changes: 17 additions & 6 deletions lib/handlebars/compiler/compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Compiler.prototype = {
guid: 0,

compile: function(program, options) {
this.opcodes = [];
this.children = [];
this.depths = {list: []};
this.options = options;
Expand All @@ -93,20 +94,30 @@ Compiler.prototype = {
}
}

return this.program(program);
return this.accept(program);
},

accept: function(node) {
return this[node.type](node);
var strip = node.strip || {},
ret;
if (strip.left) {
this.opcode('strip');
}

ret = this[node.type](node);

if (strip.right) {
this.opcode('strip');
}

return ret;
},

program: function(program) {
var statements = program.statements, statement;
this.opcodes = [];
var statements = program.statements;

for(var i=0, l=statements.length; i<l; i++) {
statement = statements[i];
this[statement.type](statement);
this.accept(statements[i]);
}
this.isSimple = l === 1;

Expand Down
71 changes: 51 additions & 20 deletions lib/handlebars/compiler/javascript-compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,17 @@ JavaScriptCompiler.prototype = {
} else {
this[opcode.opcode].apply(this, opcode.args);
}
}

return this.createFunctionContext(asObject);
},
// Reset the stripNext flag if it was not set by this operation.
if (opcode.opcode !== this.stripNext) {
this.stripNext = false;
}
}

nextOpcode: function() {
var opcodes = this.environment.opcodes;
return opcodes[this.i + 1];
},
// Flush any trailing content that might be pending.
this.pushSource('');

eat: function() {
this.i = this.i + 1;
return this.createFunctionContext(asObject);
},

preamble: function() {
Expand Down Expand Up @@ -141,7 +140,7 @@ JavaScriptCompiler.prototype = {
}

if (!this.environment.isSimple) {
this.source.push("return buffer;");
this.pushSource("return buffer;");
}

var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"];
Expand Down Expand Up @@ -232,7 +231,7 @@ JavaScriptCompiler.prototype = {
// Use the options value generated from the invocation
params[params.length-1] = 'options';

this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
},

// [appendContent]
Expand All @@ -242,7 +241,28 @@ JavaScriptCompiler.prototype = {
//
// Appends the string value of `content` to the current buffer
appendContent: function(content) {
this.source.push(this.appendToBuffer(this.quotedString(content)));
if (this.pendingContent) {
content = this.pendingContent + content;
}
if (this.stripNext) {
content = content.replace(/^\s+/, '');
}

this.pendingContent = content;
},

// [strip]
//
// On stack, before: ...
// On stack, after: ...
//
// Removes any trailing whitespace from the prior content node and flags
// the next operation for stripping if it is a content node.
strip: function() {
if (this.pendingContent) {
this.pendingContent = this.pendingContent.replace(/\s+$/, '');
}
this.stripNext = 'strip';
},

// [append]
Expand All @@ -259,9 +279,9 @@ JavaScriptCompiler.prototype = {
// when we examine local
this.flushInline();
var local = this.popStack();
this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
this.pushSource("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
if (this.environment.isSimple) {
this.source.push("else { " + this.appendToBuffer("''") + " }");
this.pushSource("else { " + this.appendToBuffer("''") + " }");
}
},

Expand All @@ -274,7 +294,7 @@ JavaScriptCompiler.prototype = {
appendEscaped: function() {
this.context.aliases.escapeExpression = 'this.escapeExpression';

this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
},

// [getContext]
Expand Down Expand Up @@ -498,8 +518,8 @@ JavaScriptCompiler.prototype = {
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
var nextStack = this.nextStack();

this.source.push('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }');
this.pushSource('if (' + nextStack + ' = ' + helperName + ') { ' + nextStack + ' = ' + nextStack + '.call(' + helper.callParams + '); }');
this.pushSource('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '.call(' + helper.callParams + ') : ' + nextStack + '; }');
},

// [invokePartial]
Expand Down Expand Up @@ -606,7 +626,7 @@ JavaScriptCompiler.prototype = {

register: function(name, val) {
this.useRegister(name);
this.source.push(name + " = " + val + ";");
this.pushSource(name + " = " + val + ";");
},

useRegister: function(name) {
Expand All @@ -620,12 +640,23 @@ JavaScriptCompiler.prototype = {
return this.push(new Literal(item));
},

pushSource: function(source) {
if (this.pendingContent) {
this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent)));
this.pendingContent = undefined;
}

if (source) {
this.source.push(source);
}
},

pushStack: function(item) {
this.flushInline();

var stack = this.incrStack();
if (item) {
this.source.push(stack + " = " + item + ";");
this.pushSource(stack + " = " + item + ";");
}
this.compileStack.push(stack);
return stack;
Expand Down Expand Up @@ -668,7 +699,7 @@ JavaScriptCompiler.prototype = {
stack = this.nextStack();
}

this.source.push(stack + " = (" + prefix + item + ");");
this.pushSource(stack + " = (" + prefix + item + ");");
}
return stack;
},
Expand Down
62 changes: 62 additions & 0 deletions spec/whitespace-control.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
describe('whitespace control', function() {
it('should strip whitespace around mustache calls', function() {
var hash = {foo: 'bar<'};

shouldCompileTo(' {{~foo~}} ', hash, 'bar&lt;');
shouldCompileTo(' {{~foo}} ', hash, 'bar&lt; ');
shouldCompileTo(' {{foo~}} ', hash, ' bar&lt;');

shouldCompileTo(' {{~&foo~}} ', hash, 'bar<');
shouldCompileTo(' {{~{foo}~}} ', hash, 'bar<');
});

describe('blocks', function() {
it('should strip whitespace around simple block calls', function() {
var hash = {foo: 'bar<'};

shouldCompileTo(' {{~#if foo~}} bar {{~/if~}} ', hash, 'bar');
shouldCompileTo(' {{#if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~#if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{#if foo}} bar {{/if}} ', hash, ' bar ');
});
it('should strip whitespace around inverse block calls', function() {
var hash = {};

shouldCompileTo(' {{~^if foo~}} bar {{~/if~}} ', hash, 'bar');
shouldCompileTo(' {{^if foo~}} bar {{/if~}} ', hash, ' bar ');
shouldCompileTo(' {{~^if foo}} bar {{~/if}} ', hash, ' bar ');
shouldCompileTo(' {{^if foo}} bar {{/if}} ', hash, ' bar ');
});
it('should strip whitespace around complex block calls', function() {
var hash = {foo: 'bar<'};

shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'bar');
shouldCompileTo('{{#if foo~}} bar {{^~}} baz {{/if}}', hash, 'bar ');
shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{~/if}}', hash, ' bar');
shouldCompileTo('{{#if foo}} bar {{^~}} baz {{/if}}', hash, ' bar ');

shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'bar');

hash = {};

shouldCompileTo('{{#if foo~}} bar {{~^~}} baz {{~/if}}', hash, 'baz');
shouldCompileTo('{{#if foo}} bar {{~^~}} baz {{/if}}', hash, 'baz ');
shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{~/if}}', hash, ' baz');
shouldCompileTo('{{#if foo~}} bar {{~^}} baz {{/if}}', hash, ' baz ');

shouldCompileTo('{{#if foo~}} bar {{~else~}} baz {{~/if}}', hash, 'baz');
});
});

it('should strip whitespace around partials', function() {
shouldCompileToWithPartials('foo {{~> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foobar');
shouldCompileToWithPartials('foo {{> dude~}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar');
shouldCompileToWithPartials('foo {{> dude}} ', [{}, {}, {dude: 'bar'}], true, 'foo bar ');
});

it('should only strip whitespace once', function() {
var hash = {foo: 'bar'};

shouldCompileTo(' {{~foo~}} {{foo}} {{foo}} ', hash, 'barbar bar ');
});
});
35 changes: 20 additions & 15 deletions src/handlebars.l
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ function strip(start, end) {

%}

LEFT_STRIP "~"
RIGHT_STRIP "~"

LOOKAHEAD [=~}\s\/.]
LITERAL_LOOKAHEAD [~}\s]

/*
ID is the inverse of control characters.
Expand All @@ -19,7 +24,7 @@ Control characters ranges:
[\[-\^`] [, \, ], ^, `, Exceptions in range: _
[\{-~] {, |, }, ~
*/
ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]
ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}

%%

Expand All @@ -46,30 +51,30 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]

<com>[\s\S]*?"--}}" strip(0,4); this.popState(); return 'COMMENT';

<mu>"{{>" return 'OPEN_PARTIAL';
<mu>"{{#" return 'OPEN_BLOCK';
<mu>"{{/" return 'OPEN_ENDBLOCK';
<mu>"{{^" return 'OPEN_INVERSE';
<mu>"{{"\s*"else" return 'OPEN_INVERSE';
<mu>"{{{" return 'OPEN_UNESCAPED';
<mu>"{{&" return 'OPEN';
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
<mu>"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED';
<mu>"{{"{LEFT_STRIP}?"&" return 'OPEN';
<mu>"{{!--" this.popState(); this.begin('com');
<mu>"{{!"[\s\S]*?"}}" strip(3,5); this.popState(); return 'COMMENT';
<mu>"{{" return 'OPEN';
<mu>"{{"{LEFT_STRIP}? return 'OPEN';

<mu>"=" return 'EQUALS';
<mu>"."/[}\/ ] return 'ID';
<mu>".." return 'ID';
<mu>"."/{LOOKAHEAD} return 'ID';
<mu>[\/.] return 'SEP';
<mu>\s+ /*ignore whitespace*/
<mu>"}}}" this.popState(); return 'CLOSE_UNESCAPED';
<mu>"}}" this.popState(); return 'CLOSE';
<mu>"}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED';
<mu>{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE';
<mu>'"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING';
<mu>"'"("\\"[']|[^'])*"'" yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING';
<mu>"@" return 'DATA';
<mu>"true"/[}\s] return 'BOOLEAN';
<mu>"false"/[}\s] return 'BOOLEAN';
<mu>\-?[0-9]+/[}\s] return 'INTEGER';
<mu>"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN';
<mu>\-?[0-9]+/{LITERAL_LOOKAHEAD} return 'INTEGER';

<mu>{ID} return 'ID';

Expand Down
Loading