diff options
author | Linus Groh <mail@linusgroh.de> | 2020-05-06 10:17:35 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-05-06 14:49:53 +0200 |
commit | 4d20cf57dbc32f470faf93b637f01432e3c65696 (patch) | |
tree | 187097d9253db5b64113fe456e48b9c8249b02b6 | |
parent | eea62dd3656e0ba87f0f573009e338e36063b62c (diff) | |
download | serenity-4d20cf57dbc32f470faf93b637f01432e3c65696.zip |
LibJS: Implement tagged template literals (foo`bar`)
To make processing tagged template literals easier, template literals
will now add one empty StringLiteral before and after each template
expression *if* there's no other string - e.g.:
`${foo}` -> "", foo, ""
`test${foo}${bar}test` -> "test", foo, "", bar, "test"
This also matches the behaviour of many other parsers.
-rw-r--r-- | Libraries/LibJS/AST.cpp | 44 | ||||
-rw-r--r-- | Libraries/LibJS/AST.h | 18 | ||||
-rw-r--r-- | Libraries/LibJS/Parser.cpp | 14 | ||||
-rw-r--r-- | Libraries/LibJS/Tests/tagged-template-literals.js | 88 |
4 files changed, 161 insertions, 3 deletions
diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 77250683a0..eb643cc794 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -1277,8 +1277,7 @@ Value ArrayExpression::execute(Interpreter& interpreter) const void TemplateLiteral::dump(int indent) const { ASTNode::dump(indent); - - for (auto& expression : expressions()) + for (auto& expression : m_expressions) expression.dump(indent + 1); } @@ -1286,7 +1285,7 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const { StringBuilder string_builder; - for (auto& expression : expressions()) { + for (auto& expression : m_expressions) { auto expr = expression.execute(interpreter); if (interpreter.exception()) return {}; @@ -1296,6 +1295,45 @@ Value TemplateLiteral::execute(Interpreter& interpreter) const return js_string(interpreter, string_builder.build()); } +void TaggedTemplateLiteral::dump(int indent) const +{ + ASTNode::dump(indent); + print_indent(indent + 1); + printf("(Tag)\n"); + m_tag->dump(indent + 2); + print_indent(indent + 1); + printf("(Template Literal)\n"); + m_template_literal->dump(indent + 2); +} + +Value TaggedTemplateLiteral::execute(Interpreter& interpreter) const +{ + auto tag = m_tag->execute(interpreter); + if (interpreter.exception()) + return {}; + if (!tag.is_function()) { + interpreter.throw_exception<TypeError>(String::format("%s is not a function", tag.to_string().characters())); + return {}; + } + auto& tag_function = tag.as_function(); + auto& expressions = m_template_literal->expressions(); + auto* strings = Array::create(interpreter.global_object()); + MarkedValueList arguments(interpreter.heap()); + arguments.append(strings); + for (size_t i = 0; i < expressions.size(); ++i) { + auto value = expressions[i].execute(interpreter); + if (interpreter.exception()) + return {}; + // tag`${foo}` -> "", foo, "" -> tag(["", ""], foo) + // tag`foo${bar}baz${qux}` -> "foo", bar, "baz", qux, "" -> tag(["foo", "baz", ""], bar, qux) + if (i % 2 == 0) + strings->elements().append(value); + else + arguments.append(value); + } + return interpreter.call(tag_function, js_undefined(), move(arguments)); +} + void TryStatement::dump(int indent) const { ASTNode::dump(indent); diff --git a/Libraries/LibJS/AST.h b/Libraries/LibJS/AST.h index 245fa7ac4d..8ab77d53fb 100644 --- a/Libraries/LibJS/AST.h +++ b/Libraries/LibJS/AST.h @@ -786,6 +786,24 @@ private: const NonnullRefPtrVector<Expression> m_expressions; }; +class TaggedTemplateLiteral final : public Expression { +public: + TaggedTemplateLiteral(NonnullRefPtr<Expression> tag, NonnullRefPtr<TemplateLiteral> template_literal) + : m_tag(move(tag)) + , m_template_literal(move(template_literal)) + { + } + + virtual Value execute(Interpreter&) const override; + virtual void dump(int indent) const override; + +private: + virtual const char* class_name() const override { return "TaggedTemplateLiteral"; } + + const NonnullRefPtr<Expression> m_tag; + const NonnullRefPtr<TemplateLiteral> m_template_literal; +}; + class MemberExpression final : public Expression { public: MemberExpression(NonnullRefPtr<Expression> object, NonnullRefPtr<Expression> property, bool computed = false) diff --git a/Libraries/LibJS/Parser.cpp b/Libraries/LibJS/Parser.cpp index a665ec2872..7c82bd6de3 100644 --- a/Libraries/LibJS/Parser.cpp +++ b/Libraries/LibJS/Parser.cpp @@ -560,6 +560,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal() NonnullRefPtrVector<Expression> expressions; + if (!match(TokenType::TemplateLiteralString)) + expressions.append(create_ast_node<StringLiteral>("")); + while (!match(TokenType::TemplateLiteralEnd) && !match(TokenType::UnterminatedTemplateLiteral)) { if (match(TokenType::TemplateLiteralString)) { expressions.append(create_ast_node<StringLiteral>(consume().string_value())); @@ -576,6 +579,9 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal() return create_ast_node<TemplateLiteral>(expressions); } consume(TokenType::TemplateLiteralExprEnd); + + if (!match(TokenType::TemplateLiteralString)) + expressions.append(create_ast_node<StringLiteral>("")); } } @@ -591,6 +597,10 @@ NonnullRefPtr<TemplateLiteral> Parser::parse_template_literal() NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associativity associativity) { auto expression = parse_primary_expression(); + while (match(TokenType::TemplateLiteralStart)) { + auto template_literal = parse_template_literal(); + expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal)); + } while (match_secondary_expression()) { int new_precedence = operator_precedence(m_parser_state.m_current_token.type()); if (new_precedence < min_precedence) @@ -600,6 +610,10 @@ NonnullRefPtr<Expression> Parser::parse_expression(int min_precedence, Associati Associativity new_associativity = operator_associativity(m_parser_state.m_current_token.type()); expression = parse_secondary_expression(move(expression), new_precedence, new_associativity); + while (match(TokenType::TemplateLiteralStart)) { + auto template_literal = parse_template_literal(); + expression = create_ast_node<TaggedTemplateLiteral>(move(expression), move(template_literal)); + } } return expression; } diff --git a/Libraries/LibJS/Tests/tagged-template-literals.js b/Libraries/LibJS/Tests/tagged-template-literals.js new file mode 100644 index 0000000000..7318ab5e35 --- /dev/null +++ b/Libraries/LibJS/Tests/tagged-template-literals.js @@ -0,0 +1,88 @@ +load("test-common.js"); + +try { + assertThrowsError(() => { + foo`bar${baz}`; + }, { + error: ReferenceError, + message: "'foo' not known" + }); + + assertThrowsError(() => { + function foo() { } + foo`bar${baz}`; + }, { + error: ReferenceError, + message: "'baz' not known" + }); + + assertThrowsError(() => { + undefined``````; + }, { + error: TypeError, + message: "undefined is not a function" + }); + + function test1(strings) { + assert(strings instanceof Array); + assert(strings.length === 1); + assert(strings[0] === ""); + return 42; + } + assert(test1`` === 42); + + function test2(s) { + return function (strings) { + assert(strings instanceof Array); + assert(strings.length === 1); + assert(strings[0] === "bar"); + return s + strings[0]; + } + } + assert(test2("foo")`bar` === "foobar"); + + var test3 = { + foo(strings, p1) { + assert(strings instanceof Array); + assert(strings.length === 2); + assert(strings[0] === ""); + assert(strings[1] === ""); + assert(p1 === "bar"); + } + }; + test3.foo`${"bar"}`; + + function test4(strings, p1) { + assert(strings instanceof Array); + assert(strings.length === 2); + assert(strings[0] === "foo"); + assert(strings[1] === ""); + assert(p1 === 42); + } + var bar = 42; + test4`foo${bar}`; + + function test5(strings, p1, p2) { + assert(strings instanceof Array); + assert(strings.length === 3); + assert(strings[0] === "foo"); + assert(strings[1] === "baz"); + assert(strings[2] === ""); + assert(p1 === 42); + assert(p2 === "qux"); + return (strings, value) => `${value}${strings[0]}`; + } + var bar = 42; + assert(test5`foo${bar}baz${"qux"}``test${123}` === "123test"); + + function review(strings, name, rating) { + return `${strings[0]}**${name}**${strings[1]}_${rating}_${strings[2]}`; + } + var name = "SerenityOS"; + var rating = "great"; + assert(review`${name} is a ${rating} project!` === "**SerenityOS** is a _great_ project!"); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} |