Skip to content

Commit

Permalink
Merge pull request #336 from wycats/whitespace-control
Browse files Browse the repository at this point in the history
Unecessary Whitespace
  • Loading branch information
kpdecker committed Oct 27, 2013
2 parents 06d94fe + 31f7c25 commit 320c0a6
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 62 deletions.
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

0 comments on commit 320c0a6

Please sign in to comment.