summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYehuda Katz <wycats@gmail.com>2013-12-28 17:12:25 -0800
committerYehuda Katz <wycats@gmail.com>2013-12-28 17:12:25 -0800
commit6c2137a420aa1007dfed85db88021e09ddc66a04 (patch)
tree769c4f821b441a868d174c0a8a4c02f09ee0afc7
parent1c0614bd88b835b7cdfe1fb7f39650c18f645763 (diff)
parente1878050b5e0148947054275b14773cfc054e535 (diff)
downloadhandlebars.js-6c2137a420aa1007dfed85db88021e09ddc66a04.zip
handlebars.js-6c2137a420aa1007dfed85db88021e09ddc66a04.tar.gz
handlebars.js-6c2137a420aa1007dfed85db88021e09ddc66a04.tar.bz2
Merge pull request #692 from fivetanley/line-numbers
add line numbers to nodes when parsing
-rw-r--r--lib/handlebars/compiler/ast.js70
-rw-r--r--spec/ast.js186
-rw-r--r--src/handlebars.yy52
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