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

Refactor blocks, programs and inverses #834

Merged
merged 1 commit into from
Aug 23, 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
50 changes: 6 additions & 44 deletions lib/handlebars/compiler/ast.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Exception from "../exception";

function LocationInfo(locInfo){
function LocationInfo(locInfo) {
locInfo = locInfo || {};
this.firstLine = locInfo.first_line;
this.firstColumn = locInfo.first_column;
Expand All @@ -9,38 +9,11 @@ function LocationInfo(locInfo){
}

var AST = {
ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
var inverseLocationInfo, firstInverseNode;
if (arguments.length === 3) {
locInfo = inverse;
inverse = null;
} else if (arguments.length === 2) {
locInfo = inverseStrip;
inverseStrip = null;
}

ProgramNode: function(statements, strip, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "program";
this.statements = statements;
this.strip = {};

if(inverse) {
firstInverseNode = inverse[0];
if (firstInverseNode) {
inverseLocationInfo = {
first_line: firstInverseNode.firstLine,
last_line: firstInverseNode.lastLine,
last_column: firstInverseNode.lastColumn,
first_column: firstInverseNode.firstColumn
};
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
} else {
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
}
this.strip.right = inverseStrip.left;
} else if (inverseStrip) {
this.strip.left = inverseStrip.right;
}
this.strip = strip;
},

MustacheNode: function(rawParams, hash, open, strip, locInfo) {
Expand Down Expand Up @@ -106,25 +79,14 @@ var AST = {
this.strip = strip;
},

BlockNode: function(mustache, program, inverse, close, locInfo) {
BlockNode: function(mustache, program, inverse, strip, locInfo) {
LocationInfo.call(this, locInfo);

if(mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this);
}

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

this.strip = {
left: mustache.strip.left,
right: close.strip.right
};

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

if (inverse && !program) {
this.isInverse = true;
Expand All @@ -142,7 +104,7 @@ var AST = {

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

ContentNode: function(string, locInfo) {
Expand Down
11 changes: 9 additions & 2 deletions lib/handlebars/compiler/base.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import parser from "./parser";
import AST from "./ast";
import { stripFlags, prepareBlock } from "./helpers";

export { parser };

export function parse(input) {
// Just return if an already-compile AST was passed in.
if(input.constructor === AST.ProgramNode) { return input; }
if (input.constructor === AST.ProgramNode) { return input; }

for (var key in AST) {
parser.yy[key] = AST[key];
}

parser.yy.stripFlags = stripFlags;
parser.yy.prepareBlock = prepareBlock;

parser.yy = AST;
return parser.parse(input);
}
41 changes: 41 additions & 0 deletions lib/handlebars/compiler/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Exception from "../exception";
import AST from "./ast";

export function stripFlags(open, close) {
return {
left: open.charAt(2) === '~',
right: close.charAt(close.length-3) === '~'
};
}

export function prepareBlock(mustache, program, inverseAndProgram, close, inverted, locInfo) {
if (mustache.sexpr.id.original !== close.path.original) {
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, mustache);
}

var inverse, strip;

strip = {
left: mustache.strip.left,
right: close.strip.right
};

if (inverseAndProgram) {
inverse = inverseAndProgram.program;
var inverseStrip = inverseAndProgram.strip;

program.strip.left = mustache.strip.right;
program.strip.right = inverseStrip.left;
inverse.strip.left = inverseStrip.right;
inverse.strip.right = close.strip.left;
} else {
program.strip.left = mustache.strip.right;
program.strip.right = close.strip.left;
}

if (inverted) {
return new AST.BlockNode(mustache, inverse, program, strip, locInfo);
} else {
return new AST.BlockNode(mustache, program, inverse, strip, locInfo);
}
}
47 changes: 13 additions & 34 deletions spec/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,9 @@ describe('ast', function() {
});
});
describe('BlockNode', function() {
it('should throw on mustache mismatch (old sexpr-less version)', function() {
shouldThrow(function() {
var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
}, Handlebars.Exception, "foo doesn't match bar");
});
it('should throw on mustache mismatch', function() {
shouldThrow(function() {
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}, {first_line: 2, first_column: 2});
handlebarsEnv.parse("\n {{#foo}}{{/bar}}")
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
});

Expand Down Expand Up @@ -197,32 +189,12 @@ describe('ast', function() {
testLocationInfoStorage(pn);
});
});

describe("ProgramNode", function(){

describe("storing location info", function(){
it("stores when `inverse` argument isn't passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], LOCATION_INFO);
testLocationInfoStorage(pn);
});

it("stores when `inverse` or `stripInverse` arguments passed", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, undefined, LOCATION_INFO);
testLocationInfoStorage(pn);

var clone = {
strip: {},
firstLine: 0,
lastLine: 0,
firstColumn: 0,
lastColumn: 0
};
pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, [ clone ], LOCATION_INFO);
testLocationInfoStorage(pn);

// Assert that the newly created ProgramNode has the same location
// information as the inverse
testLocationInfoStorage(pn.inverse);
});
it("storing location info", function(){
var pn = new handlebarsEnv.AST.ProgramNode([], {}, LOCATION_INFO);
testLocationInfoStorage(pn);
});
});

Expand Down Expand Up @@ -265,11 +237,18 @@ describe('ast', function() {
testColumns(blockHelperNode, 3, 7, 8, 23);
});

it('correctly records the line numbers the program of a block helper', function(){
var blockHelperNode = statements[5],
program = blockHelperNode.program;

testColumns(program, 3, 5, 8, 5);
});

it('correctly records the line numbers of an inverse of a block helper', function(){
var blockHelperNode = statements[5],
inverse = blockHelperNode.inverse;

testColumns(inverse, 5, 6, 13, 0);
testColumns(inverse, 5, 7, 5, 0);
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions spec/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ describe('parser', function() {
});

it('parses empty blocks with empty inverse section', function() {
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
});

it('parses empty blocks with empty inverse (else-style) section', function() {
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n");
});

it('parses non-empty blocks with empty inverse section', function() {
Expand Down
8 changes: 4 additions & 4 deletions spec/tokenizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ describe('Tokenizer', function() {
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
});

it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{else}}"), ['OPEN_INVERSE', 'CLOSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['OPEN_INVERSE', 'CLOSE']);
it('tokenizes inverse sections as "INVERSE"', function() {
shouldMatchTokens(tokenize("{{^}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{else}}"), ['INVERSE']);
shouldMatchTokens(tokenize("{{ else }}"), ['INVERSE']);
});

it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
Expand Down
2 changes: 2 additions & 0 deletions src/handlebars.l
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD}
<mu>"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL';
<mu>"{{"{LEFT_STRIP}?"#" return 'OPEN_BLOCK';
<mu>"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK';
<mu>"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE';
<mu>"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE';
<mu>"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED';
Expand Down
62 changes: 26 additions & 36 deletions src/handlebars.yy
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,68 @@

%ebnf

%{

function stripFlags(open, close) {
return {
left: open.charAt(2) === '~',
right: close.charAt(0) === '~' || close.charAt(1) === '~'
};
}

%}

%%

root
: statements EOF { return new yy.ProgramNode($1, @$); }
| EOF { return new yy.ProgramNode([], @$); }
: program EOF { return $1; }
;

program
: 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([], @$)
;

statements
: statement -> [$1]
| statements statement { $1.push($2); $$ = $1; }
: statement* -> new yy.ProgramNode($1, {}, @$)
;

statement
: openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
| openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3, @$)
| openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $3, @$)
| mustache -> $1
: mustache -> $1
| block -> $1
| rawBlock -> $1
| partial -> $1
| CONTENT -> new yy.ContentNode($1, @$)
| COMMENT -> new yy.CommentNode($1, @$)
;

rawBlock
: openRawBlock CONTENT END_RAW_BLOCK -> new yy.RawBlockNode($1, $2, $3, @$)
;

openRawBlock
: OPEN_RAW_BLOCK sexpr CLOSE_RAW_BLOCK -> new yy.MustacheNode($2, null, '', '', @$)
;

block
: openBlock program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$)
| openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$)
;

openBlock
: OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
: OPEN_BLOCK sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;

openInverse
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
: OPEN_INVERSE sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;

inverseAndProgram
: INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 }
;

closeBlock
: OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: stripFlags($1, $3)}
: OPEN_ENDBLOCK path CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)}
;

mustache
// 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 sexpr CLOSE -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, stripFlags($1, $3), @$)
: OPEN sexpr CLOSE -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
| OPEN_UNESCAPED sexpr CLOSE_UNESCAPED -> new yy.MustacheNode($2, null, $1, yy.stripFlags($1, $3), @$)
;

partial
: OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, stripFlags($1, $5), @$)
| OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, stripFlags($1, $4), @$)
: OPEN_PARTIAL partialName param hash? CLOSE -> new yy.PartialNode($2, $3, $4, yy.stripFlags($1, $5), @$)
| OPEN_PARTIAL partialName hash? CLOSE -> new yy.PartialNode($2, undefined, $3, yy.stripFlags($1, $4), @$)
;

simpleInverse
: OPEN_INVERSE CLOSE -> stripFlags($1, $2)
: INVERSE -> yy.stripFlags($1, $1)
;

sexpr
Expand Down