summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Decker <kpdecker@gmail.com>2013-10-27 09:54:25 -0700
committerKevin Decker <kpdecker@gmail.com>2013-10-27 09:54:25 -0700
commit320c0a6fb54489606bec0d607658c0d0be0f03f6 (patch)
tree1f8e672b06c7d9be52f4237cec3f0b6df455cca4
parent06d94fed56b43bdf0c824bdce966596e551d3324 (diff)
parent31f7c25a8faac32e4996d95ccec509ead41f3f3c (diff)
downloadhandlebars.js-320c0a6fb54489606bec0d607658c0d0be0f03f6.zip
handlebars.js-320c0a6fb54489606bec0d607658c0d0be0f03f6.tar.gz
handlebars.js-320c0a6fb54489606bec0d607658c0d0be0f03f6.tar.bz2
Merge pull request #336 from wycats/whitespace-control
Unecessary Whitespace
-rw-r--r--lib/handlebars/compiler/ast.js35
-rw-r--r--lib/handlebars/compiler/compiler.js23
-rw-r--r--lib/handlebars/compiler/javascript-compiler.js71
-rw-r--r--spec/whitespace-control.js62
-rw-r--r--src/handlebars.l35
-rw-r--r--src/handlebars.yy36
6 files changed, 200 insertions, 62 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js
index 336492d..f6229e2 100644
--- a/lib/handlebars/compiler/ast.js
+++ b/lib/handlebars/compiler/ast.js
@@ -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);
@@ -28,15 +38,16 @@ 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";
@@ -44,7 +55,15 @@ export function BlockNode(mustache, program, inverse, close) {
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;
}
}
diff --git a/lib/handlebars/compiler/compiler.js b/lib/handlebars/compiler/compiler.js
index 50195e3..4f232eb 100644
--- a/lib/handlebars/compiler/compiler.js
+++ b/lib/handlebars/compiler/compiler.js
@@ -72,6 +72,7 @@ Compiler.prototype = {
guid: 0,
compile: function(program, options) {
+ this.opcodes = [];
this.children = [];
this.depths = {list: []};
this.options = options;
@@ -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;
diff --git a/lib/handlebars/compiler/javascript-compiler.js b/lib/handlebars/compiler/javascript-compiler.js
index 283c20c..b04ef1a 100644
--- a/lib/handlebars/compiler/javascript-compiler.js
+++ b/lib/handlebars/compiler/javascript-compiler.js
@@ -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() {
@@ -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"];
@@ -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]
@@ -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]
@@ -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("''") + " }");
}
},
@@ -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]
@@ -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]
@@ -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) {
@@ -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;
@@ -668,7 +699,7 @@ JavaScriptCompiler.prototype = {
stack = this.nextStack();
}
- this.source.push(stack + " = (" + prefix + item + ");");
+ this.pushSource(stack + " = (" + prefix + item + ");");
}
return stack;
},
diff --git a/spec/whitespace-control.js b/spec/whitespace-control.js
new file mode 100644
index 0000000..2088ed8
--- /dev/null
+++ b/spec/whitespace-control.js
@@ -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 ');
+ });
+});
diff --git a/src/handlebars.l b/src/handlebars.l
index 7593189..ddb7fe9 100644
--- a/src/handlebars.l
+++ b/src/handlebars.l
@@ -9,6 +9,11 @@ function strip(start, end) {
%}
+LEFT_STRIP "~"
+RIGHT_STRIP "~"
+
+LOOKAHEAD [=~}\s\/.]
+LITERAL_LOOKAHEAD [~}\s]
/*
ID is the inverse of control characters.
@@ -19,7 +24,7 @@ Control characters ranges:
[\[-\^`] [, \, ], ^, `, Exceptions in range: _
[\{-~] {, |, }, ~
*/
-ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]
+ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
%%
@@ -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';
diff --git a/src/handlebars.yy b/src/handlebars.yy
index d2f24c4..0afd2cb 100644
--- a/src/handlebars.yy
+++ b/src/handlebars.yy
@@ -2,6 +2,17 @@
%ebnf
+%{
+
+function stripFlags(open, close) {
+ return {
+ left: open[2] === '~',
+ right: close[0] === '~' || close[1] === '~'
+ };
+}
+
+%}
+
%%
root
@@ -9,9 +20,9 @@ root
;
program
- : simpleInverse statements -> new yy.ProgramNode([], $2)
- | statements simpleInverse statements -> new yy.ProgramNode($1, $3)
- | statements simpleInverse -> new yy.ProgramNode($1, [])
+ : simpleInverse statements -> new yy.ProgramNode([], $1, $2)
+ | statements simpleInverse statements -> new yy.ProgramNode($1, $2, $3)
+ | statements simpleInverse -> new yy.ProgramNode($1, $2, [])
| statements -> new yy.ProgramNode($1)
| simpleInverse -> new yy.ProgramNode([])
| "" -> new yy.ProgramNode([])
@@ -32,32 +43,31 @@ statement
;
openBlock
- : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1])
+ : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
;
openInverse
- : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1])
+ : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
;
closeBlock
- : OPEN_ENDBLOCK path CLOSE -> $2
+ : OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)}
;
mustache
- : OPEN inMustache CLOSE {
- // Parsing out the '&' escape token at this level saves ~500 bytes after min due to the removal of one parser node.
- $$ = new yy.MustacheNode($2[0], $2[1], $1[2] === '&');
- }
- | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], true)
+ // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node.
+ // This also allows for handler unification as all mustache node instances can utilize the same handler
+ : OPEN inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
+ | OPEN_UNESCAPED inMustache CLOSE_UNESCAPED -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3))
;
partial
- : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3)
+ : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4))
;
simpleInverse
- : OPEN_INVERSE CLOSE { }
+ : OPEN_INVERSE CLOSE -> stripFlags($1, $2)
;
inMustache