diff options
-rw-r--r-- | lib/handlebars/compiler/ast.js | 70 | ||||
-rw-r--r-- | spec/ast.js | 186 | ||||
-rw-r--r-- | src/handlebars.yy | 52 |
3 files changed, 266 insertions, 42 deletions
diff --git a/lib/handlebars/compiler/ast.js b/lib/handlebars/compiler/ast.js index 64cd3b7..88d64e3 100644 --- a/lib/handlebars/compiler/ast.js +++ b/lib/handlebars/compiler/ast.js @@ -1,20 +1,48 @@ import Exception from "../exception"; +function LocationInfo(locInfo){ + locInfo = locInfo || {}; + this.firstLine = locInfo.first_line; + this.firstColumn = locInfo.first_column; + this.lastColumn = locInfo.last_column; + this.lastLine = locInfo.last_line; +} + var AST = { - ProgramNode: function(statements, inverseStrip, inverse) { + ProgramNode: function(statements, inverseStrip, inverse, locInfo) { + var inverseLocationInfo, firstInverseNode; + if (arguments.length === 3) { + inverse = null; + locInfo = arguments[arguments.length - 1]; + } else if (arguments.length === 2 ) { + locInfo = arguments[1]; + } + LocationInfo.call(this, locInfo); this.type = "program"; this.statements = statements; this.strip = {}; if(inverse) { - this.inverse = new AST.ProgramNode(inverse, inverseStrip); + 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; } }, - MustacheNode: function(rawParams, hash, open, strip) { + MustacheNode: function(rawParams, hash, open, strip, locInfo) { + LocationInfo.call(this, locInfo); this.type = "mustache"; this.hash = hash; this.strip = strip; @@ -45,19 +73,22 @@ var AST = { // pass or at runtime. }, - PartialNode: function(partialName, context, strip) { + PartialNode: function(partialName, context, strip, locInfo) { + LocationInfo.call(this, locInfo); this.type = "partial"; this.partialName = partialName; this.context = context; this.strip = strip; }, - BlockNode: function(mustache, program, inverse, close) { + BlockNode: function(mustache, program, inverse, close, locInfo) { if(mustache.id.original !== close.path.original) { throw new Exception(mustache.id.original + " doesn't match " + close.path.original); } - this.type = "block"; + LocationInfo.call(this, locInfo); + + this.type = 'block'; this.mustache = mustache; this.program = program; this.inverse = inverse; @@ -75,17 +106,20 @@ var AST = { } }, - ContentNode: function(string) { + ContentNode: function(string, locInfo) { + LocationInfo.call(this, locInfo); this.type = "content"; this.string = string; }, - HashNode: function(pairs) { + HashNode: function(pairs, locInfo) { + LocationInfo.call(this, locInfo); this.type = "hash"; this.pairs = pairs; }, - IdNode: function(parts) { + IdNode: function(parts, locInfo) { + LocationInfo.call(this, locInfo); this.type = "ID"; var original = "", @@ -116,37 +150,43 @@ var AST = { this.stringModeValue = this.string; }, - PartialNameNode: function(name) { + PartialNameNode: function(name, locInfo) { + LocationInfo.call(this, locInfo); this.type = "PARTIAL_NAME"; this.name = name.original; }, - DataNode: function(id) { + DataNode: function(id, locInfo) { + LocationInfo.call(this, locInfo); this.type = "DATA"; this.id = id; }, - StringNode: function(string) { + StringNode: function(string, locInfo) { + LocationInfo.call(this, locInfo); this.type = "STRING"; this.original = this.string = this.stringModeValue = string; }, - IntegerNode: function(integer) { + IntegerNode: function(integer, locInfo) { + LocationInfo.call(this, locInfo); this.type = "INTEGER"; this.original = this.integer = integer; this.stringModeValue = Number(integer); }, - BooleanNode: function(bool) { + BooleanNode: function(bool, locInfo) { + LocationInfo.call(this, locInfo); this.type = "BOOLEAN"; this.bool = bool; this.stringModeValue = bool === "true"; }, - CommentNode: function(comment) { + CommentNode: function(comment, locInfo) { + LocationInfo.call(this, locInfo); this.type = "comment"; this.comment = comment; } diff --git a/spec/ast.js b/spec/ast.js index ae10a37..0feb3f5 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -4,6 +4,25 @@ describe('ast', function() { return; } + var LOCATION_INFO = { + last_line: 0, + first_line: 0, + first_column: 0, + last_column: 0 + }; + + function testLocationInfoStorage(node){ + var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ], + property, + propertiesLen = properties.length, + i; + + for (i = 0; i < propertiesLen; i++){ + property = properties[0]; + equals(node[property], 0); + } + } + describe('MustacheNode', function() { function testEscape(open, expected) { var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false); @@ -13,7 +32,7 @@ describe('ast', function() { it('should store args', function() { var id = {isSimple: true}, hash = {}, - mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false); + mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO); equals(mustache.type, 'mustache'); equals(mustache.hash, hash); equals(mustache.escaped, true); @@ -21,6 +40,7 @@ describe('ast', function() { equals(mustache.params.length, 1); equals(mustache.params[0], 'param1'); equals(!!mustache.isHelper, true); + testLocationInfoStorage(mustache); }); it('should accept token for escape', function() { testEscape('{{', true); @@ -53,6 +73,17 @@ describe('ast', function() { new handlebarsEnv.AST.BlockNode({id: {original: 'foo'}}, {}, {}, {path: {original: 'bar'}}); }, Handlebars.Exception, "foo doesn't match bar"); }); + + it('stores location info', function(){ + var block = new handlebarsEnv.AST.BlockNode({strip: {}, id: {original: 'foo'}}, + {strip: {}}, {strip: {}}, + { + strip: {}, + path: {original: 'foo'} + }, + LOCATION_INFO); + testLocationInfoStorage(block); + }); }); describe('IdNode', function() { it('should throw on invalid path', function() { @@ -78,5 +109,158 @@ describe('ast', function() { ]); }, Handlebars.Exception, "Invalid path: foothis"); }); + + it('stores location info', function(){ + var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO); + testLocationInfoStorage(idNode); + }); + }); + + describe("HashNode", function(){ + + it('stores location info', function(){ + var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO); + testLocationInfoStorage(hash); + }); + }); + + describe("ContentNode", function(){ + + it('stores location info', function(){ + var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO); + testLocationInfoStorage(content); + }); + }); + + describe("CommentNode", function(){ + + it('stores location info', function(){ + var comment = new handlebarsEnv.AST.CommentNode("HI", LOCATION_INFO); + testLocationInfoStorage(comment); + }); + }); + + describe("IntegerNode", function(){ + + it('stores location info', function(){ + var integer = new handlebarsEnv.AST.IntegerNode("6", LOCATION_INFO); + testLocationInfoStorage(integer); + }); + }); + + describe("StringNode", function(){ + + it('stores location info', function(){ + var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO); + testLocationInfoStorage(string); + }); + }); + + describe("BooleanNode", function(){ + + it('stores location info', function(){ + var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO); + testLocationInfoStorage(bool); + }); + }); + + describe("DataNode", function(){ + + it('stores location info', function(){ + var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO); + testLocationInfoStorage(data); + }); + }); + + describe("PartialNameNode", function(){ + + it('stores location info', function(){ + var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO); + testLocationInfoStorage(pnn); + }); + }); + + describe("PartialNode", function(){ + + it('stores location info', function(){ + var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, LOCATION_INFO); + 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 + }; + var 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); + }); + }); + }); + + describe("Line Numbers", function(){ + var ast, statements; + + function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){ + equals(node.firstLine, firstLine); + equals(node.lastLine, lastLine); + equals(node.firstColumn, firstColumn); + equals(node.lastColumn, lastColumn); + } + + ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" + + "line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}"); + statements = ast.statements; + + it('gets ContentNode line numbers', function(){ + var contentNode = statements[0]; + testColumns(contentNode, 1, 1, 0, 7); + }); + + it('gets MustacheNode line numbers', function(){ + var mustacheNode = statements[1]; + testColumns(mustacheNode, 1, 1, 7, 21); + }); + + it('gets line numbers correct when newlines appear', function(){ + var secondContentNode = statements[2]; + testColumns(secondContentNode, 1, 2, 21, 8); + }); + + it('gets MustacheNode line numbers correct across newlines', function(){ + var secondMustacheNode = statements[3]; + testColumns(secondMustacheNode, 2, 2, 8, 22); + }); + + it('gets the block helper information correct', function(){ + var blockHelperNode = statements[5]; + testColumns(blockHelperNode, 3, 7, 8, 23); + }); + + 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); + }); }); }); + diff --git a/src/handlebars.yy b/src/handlebars.yy index 93797f5..63de17b 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -16,17 +16,17 @@ function stripFlags(open, close) { %% root - : statements EOF { return new yy.ProgramNode($1); } - | EOF { return new yy.ProgramNode([]); } + : statements EOF { return new yy.ProgramNode($1, @$); } + | EOF { return new yy.ProgramNode([], @$); } ; 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([]) + : 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 @@ -35,20 +35,20 @@ statements ; statement - : openInverse program closeBlock -> new yy.BlockNode($1, $2.inverse, $2, $3) - | openBlock program closeBlock -> new yy.BlockNode($1, $2, $2.inverse, $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 | partial -> $1 - | CONTENT -> new yy.ContentNode($1) - | COMMENT -> new yy.CommentNode($1) + | CONTENT -> new yy.ContentNode($1, @$) + | COMMENT -> new yy.CommentNode($1, @$) ; openBlock - : OPEN_BLOCK inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3)) + : 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], $1, stripFlags($1, $3)) + : OPEN_INVERSE inMustache CLOSE -> new yy.MustacheNode($2[0], $2[1], $1, stripFlags($1, $3), @$) ; closeBlock @@ -58,13 +58,13 @@ closeBlock 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 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)) + : 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, stripFlags($1, $4)) + : OPEN_PARTIAL partialName path? CLOSE -> new yy.PartialNode($2, $3, stripFlags($1, $4), @$) ; simpleInverse @@ -78,14 +78,14 @@ inMustache param : path -> $1 - | STRING -> new yy.StringNode($1) - | INTEGER -> new yy.IntegerNode($1) - | BOOLEAN -> new yy.BooleanNode($1) + | STRING -> new yy.StringNode($1, @$) + | INTEGER -> new yy.IntegerNode($1, @$) + | BOOLEAN -> new yy.BooleanNode($1, @$) | dataName -> $1 ; hash - : hashSegment+ -> new yy.HashNode($1) + : hashSegment+ -> new yy.HashNode($1, @$) ; hashSegment @@ -93,17 +93,17 @@ hashSegment ; partialName - : path -> new yy.PartialNameNode($1) - | STRING -> new yy.PartialNameNode(new yy.StringNode($1)) - | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1)) + : path -> new yy.PartialNameNode($1, @$) + | STRING -> new yy.PartialNameNode(new yy.StringNode($1, @$), @$) + | INTEGER -> new yy.PartialNameNode(new yy.IntegerNode($1, @$)) ; dataName - : DATA path -> new yy.DataNode($2) + : DATA path -> new yy.DataNode($2, @$) ; path - : pathSegments -> new yy.IdNode($1) + : pathSegments -> new yy.IdNode($1, @$) ; pathSegments |