diff --git a/lib/complexion-js.js b/lib/complexion-js.js index adf8fa3..f2d35b1 100644 --- a/lib/complexion-js.js +++ b/lib/complexion-js.js @@ -1181,6 +1181,105 @@ }; } + /** + * Matches a template string + * + * @return {Complexion~matcher} + */ + function matchTemplateString() { + function movePastEscape(str, offset) { + var c; + + c = str.charAt(offset); + + // You can't escape a line terminator + if (isLineTerminator(c)) { + return 0; + } + + if (c >= '4' && c <= '7') { + // Octal numbers that can only be two digits + c = str.charAt(offset + 1); + + if (c >= '0' && c <= '7') { + return 2; + } + } else if (c >= '0' && c <= '3') { + // Octal numbers that can be three digits + c = str.charAt(offset + 1); + + if (c >= '0' && c <= '7') { + c = str.charAt(offset + 2); + + if (c >= '0' && c <= '7') { + return 3; + } + + return 2; + } + } else if (c === 'x') { + // Hex + if (isHex(str.charAt(offset + 1)) && isHex(str.charAt(offset + 2))) { + return 3; + } + } else if (c === 'u') { + // Unicode + if (isHex(str.charAt(offset + 1)) && isHex(str.charAt(offset + 2)) && isHex(str.charAt(offset + 3)) && isHex(str.charAt(offset + 4))) { + return 5; + } + } + + // We are just escaping a single character + return 1; + } + + return function (str, offset) { + var c, checkId, inId, len, quote; + + inId = false; + checkId = false; + quote = str.charAt(offset); + + // It must start with single or double quotes + if (quote !== '`') { + return null; + } + + len = 1; + c = str.charAt(offset + len); + + // Strings must not contain CR, LF, LS, nor PS + while (c && c !== quote && !isLineTerminator(c)) { + len += 1; + + if (c === "\\") { + len += movePastEscape(str, offset + len); + } + + if (checkId && c === "{") { + checkId = false; + inId = true; + } + + if (inId && c === '}') { + inId = false; + } + + if (c === "$") { + checkId = true; + } + + c = str.charAt(offset + len); + } + + if (c !== quote || inId) { + return null; + } + + return str.substr(offset, len + 1); + }; + } + /** * Matches any character @@ -1328,6 +1427,7 @@ add('LINE_TERMINATOR', matchLineTerminator); // Can set implicitSemicolonFlag add('KEYWORD', matchKeyword); // Uses keywordFromIdentifierName add('STRING_LITERAL', matchStringLiteral); + add('TEMPLATE_STRING', matchTemplateString); add('SINGLE_LINE_COMMENT', matchSingleLineComment); add('BOOLEAN_LITERAL', matchBooleanLiteral); // Uses keywordFromIdentifierName add('NULL_LITERAL', matchNullLiteral); // Uses keywordFromIdentifierName diff --git a/tests/complexion-js.spec.js b/tests/complexion-js.spec.js index 8d8912c..257a461 100644 --- a/tests/complexion-js.spec.js +++ b/tests/complexion-js.spec.js @@ -401,6 +401,39 @@ }); }); }); + describe('TEMPLATE_STRING', function () { + [ + '``', // Empty + "`\\r\\n`", // Properly escaped newlines in single quotes + "`\\0`", // NULL + "`\\xfF\\u0123\\12\\012\\321`", // Escaped characters + "`random string \\a\\b\\c\\d\\e`", // Letters that do not need escaping + "`${id}`", // Properly closed identifier + "`{literal}`", // Curly braces with no $ + "`{literal`" // Curly braces with no $ + ].forEach(function (str) { + it('matches a valid template string: ' + JSON.stringify(str), function () { + var result; + + result = tokenize(str + 'xyz'); + expect(result[0]).toEqual('TEMPLATE_STRING:' + str); + }); + }); + [ + "`\n`", // Must not have a newline + "`", // Must be closed + "\"`", // Must be properly closed + "`\\\n`", // Can not escape a newline character, + "`${id`" // Unclosed identifier + ].forEach(function (str) { + it('does not match an invalid string: ' + JSON.stringify(str), function () { + var result; + + result = tokenize(str + 'xyz'); + expect(result[0]).toEqual('UNKNOWN:' + str.charAt(0)); + }); + }); + }); describe('UNKNOWN', function () { it('matches odd characters', function () { // Start with a hash but not a shebang.