Skip to content

Commit

Permalink
[[FIX]] Parse nested templates
Browse files Browse the repository at this point in the history
ES6 template literals are nestable, jshint's parser is capable, but it's lexer isn't. This patch adds the concept of a stack of contexts to the lexer, replacing the previous boolean flag. This context stack pattern is borrowed from the awesome Acorn parser project, where it's used to enable more than just template literals.

Closes #2151
Closes #2152
  • Loading branch information
leebyron authored and caitp committed Feb 6, 2015
1 parent 13ae519 commit 3da1eaf
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 24 deletions.
49 changes: 28 additions & 21 deletions src/lex.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ var Token = {
TemplateTail: 12
};

var Context = {
Template: 1
};

// Object that handles postponed lexing verifications that checks the parsed
// environment state.

Expand Down Expand Up @@ -111,7 +115,7 @@ function Lexer(source) {
this.from = 1;
this.input = "";
this.inComment = false;
this.inTemplate = false;
this.context = [];
this.templateLine = null;
this.templateChar = null;

Expand All @@ -123,6 +127,10 @@ function Lexer(source) {
Lexer.prototype = {
_lines: [],

inContext: function(ctxType) {
return this.context.length > 0 && this.context[this.context.length - 1] === ctxType;
},

getLines: function() {
this._lines = state.lines;
return this._lines;
Expand Down Expand Up @@ -975,28 +983,33 @@ Lexer.prototype = {
*/
scanTemplateLiteral: function(checks) {
var tokenType;
var value = '';
var value = "";
var ch;

// String must start with a backtick.
if (!this.inTemplate) {
if (!state.option.esnext || this.peek() !== "`") {
return null;
}
if (!state.option.esnext) {
// Only lex template strings in ESNext mode.
return null;
} else if (this.peek() === "`") {
// Template must start with a backtick.
tokenType = Token.TemplateHead;
this.templateLine = this.line;
this.templateChar = this.char;
this.skip(1);
} else if (this.peek() !== '}') {
// If we're in a template, and we don't have a '}', lex something else instead.
this.context.push(Context.Template);
} else if (this.inContext(Context.Template) && this.peek() === "}") {
// If we're in a template context, and we have a '}', lex a TemplateMiddle.
tokenType = Token.TemplateMiddle;
} else {
// Go lex something else.
return null;
}

while (this.peek() !== "`") {
while ((ch = this.peek()) === "") {
value += "\n";
if (!this.nextLine()) {
// Unclosed template literal --- point to the starting line, or the EOF?
tokenType = this.inTemplate ? Token.TemplateHead : Token.TemplateMiddle;
this.inTemplate = false;
this.context.pop();
this.trigger("error", {
code: "E052",
line: this.templateLine,
Expand All @@ -1012,11 +1025,7 @@ Lexer.prototype = {

if (ch === '$' && this.peek(1) === '{') {
value += '${';
tokenType = value.charAt(0) === '}' ? Token.TemplateMiddle : Token.TemplateHead;
// Either TokenHead or TokenMiddle --- depending on if the initial value
// is '}' or not.
this.skip(2);
this.inTemplate = true;
return {
type: tokenType,
value: value,
Expand All @@ -1026,19 +1035,17 @@ Lexer.prototype = {
var escape = this.scanEscapeSequence(checks);
value += escape.char;
this.skip(escape.jump);
} else if (ch === '`') {
break;
} else {
} else if (ch !== '`') {
// Otherwise, append the value and continue.
value += ch;
this.skip(1);
}
}

// Final value is either TokenTail or NoSubstititionTemplate --- essentially a string
tokenType = this.inTemplate ? Token.TemplateTail : Token.StringLiteral;
this.inTemplate = false;
// Final value is either StringLiteral or TemplateTail
tokenType = tokenType === Token.TemplateHead ? Token.StringLiteral : Token.TemplateTail;
this.skip(1);
this.context.pop();

return {
type: tokenType,
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -846,9 +846,9 @@ exports.testES6TemplateLiterals = function (test) {
var src = fs.readFileSync(__dirname + "/fixtures/es6-template-literal.js", "utf8");
TestRun(test)
.addError(14, "Octal literals are not allowed in strict mode.")
.addError(17, "Unclosed template literal.")
.addError(18, "Expected an identifier and instead saw '(end)'.")
.addError(18, "Missing semicolon.")
.addError(19, "Unclosed template literal.")
.addError(20, "Expected an identifier and instead saw '(end)'.")
.addError(20, "Missing semicolon.")
.test(src, { esnext: true });
test.done();
};
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/fixtures/es6-template-literal.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ function octal_strictmode() {
var test = `\033\t`;
}

var nested = `Look and ${ `Nested ${ `whoaaa` } template` } listen`;

var unterminated = `${one}

0 comments on commit 3da1eaf

Please sign in to comment.