diff --git a/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/LexerTests.scala b/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/LexerTests.scala index dabb570b7c..c75a296ffa 100644 --- a/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/LexerTests.scala +++ b/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/LexerTests.scala @@ -19,12 +19,6 @@ final class LexerTests extends AnyFunSuite with Matchers { ) } - test("string literal") { - shouldParseAs("`a`", TsLexer.stringLiteral)( - TsLexer.StringLit("a"), - ) - } - test("-readonly") { shouldParseAs("-readonly", TsLexer.token)( TsLexer.Keyword("-readonly"), @@ -42,4 +36,48 @@ final class LexerTests extends AnyFunSuite with Matchers { TsLexer.Shebang("#!/bin/bash"), ) } + + test("template") { + shouldParseAs("`a`", TsLexer.stringTemplateLiteral)( + TsLexer.StringTemplateLiteral(List(Left('a'))), + ) + shouldParseAs("`a${'b'}c${d}`", TsLexer.stringTemplateLiteral)( + TsLexer.StringTemplateLiteral( + List(Left('a'), Right(List(TsLexer.StringLit("b"))), Left('c'), Right(List(TsLexer.Identifier("d")))), + ), + ) + } + + test("nested template strings") { + val nested = TsLexer.StringTemplateLiteral( + List( + Left('b'), + Right(List(TsLexer.Identifier("c"))), + ), + ) + shouldParseAs("`a${`b${c}`}`", TsLexer.stringTemplateLiteral)( + TsLexer.StringTemplateLiteral(List(Left('a'), Right(List(nested)))), + ) + } + + test("nested template strings (sample)") { + val nested = TsLexer.StringTemplateLiteral( + List( + Left('['), + Right(List(TsLexer.Identifier("Middle"))), + Left(']'), + Right(List(TsLexer.Identifier("Tail"))), + ), + ) + + shouldParseAs("`${Head}.${FixPathSquareBrackets<`[${Middle}]${Tail}`>}`", TsLexer.stringTemplateLiteral)( + TsLexer.StringTemplateLiteral( + List( + Right(List(TsLexer.Identifier("Head"))), + Left('.'), + Right(List(TsLexer.Identifier("FixPathSquareBrackets"), TsLexer.Keyword("<"), nested, TsLexer.Keyword(">"))), + ), + ), + ) + } } diff --git a/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/ParserTests.scala b/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/ParserTests.scala index a127f65468..493b681218 100644 --- a/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/ParserTests.scala +++ b/importer/src/test/scala/org/scalablytyped/converter/internal/ts/parser/ParserTests.scala @@ -3100,4 +3100,13 @@ export {}; ), ) } + + test("nested template strings") { + shouldParseAs( + "`${Head}.${FixPathSquareBrackets<`[${Middle}]${Tail}`>}`", + TsParser.tsType, + )( + TsTypeLiteral(TsLiteral.Str("${Head}.${FixPathSquareBrackets<[${Middle}]${Tail}>}")), + ) + } } diff --git a/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsLexer.scala b/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsLexer.scala index 52ebddee4b..9aa7a267e3 100644 --- a/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsLexer.scala +++ b/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsLexer.scala @@ -22,6 +22,15 @@ object TsLexer extends Lexical with StdTokens with ParserHelpers with ImplicitCo } final case class Shebang(chars: String) extends Token + final case class StringTemplateLiteral(tokens: List[Either[Char, List[Token]]]) extends Token { + def chars = + tokens + .map { + case Left(str) => str.toString + case Right(tokens) => tokens.flatMap(_.chars).mkString("${", "", "}") + } + .mkString("") + } implicit def FromString[T](p: Parser[T]): String => ParseResult[T] = (str: String) => p.apply(new CharSequenceReader(str)) @@ -108,7 +117,20 @@ object TsLexer extends Lexical with StdTokens with ParserHelpers with ImplicitCo def quoted(quoteChar: Char): Parser[String] = quoteChar ~> stringOf(inQuoteChar(quoteChar)) <~ quoteChar - (quoted('\"') | quoted('\'') | quoted('`')) ^^ StringLit + (quoted('\"') | quoted('\'')) ^^ StringLit + } + + lazy val stringTemplateLiteral: Parser[StringTemplateLiteral] = { + val templateQuote = '`' + val interpolationStart: Parser[Char] = '$' ~> '{' + val interpolationEnd: Parser[Char] = '}' + val nonInterpolationEndToken = token.filter { case Keyword("}") => false; case _ => true } + + val either: Parser[Either[Char, List[Token]]] = + interpolationStart.flatMap(_ => (rep(nonInterpolationEndToken) <~ interpolationEnd).map(Right.apply)) | + chrExcept(templateQuote).map(Left.apply) + + templateQuote ~> rep(either) <~ templateQuote ^^ StringTemplateLiteral.apply } val delim: Parser[Keyword] = { @@ -182,8 +204,8 @@ object TsLexer extends Lexical with StdTokens with ParserHelpers with ImplicitCo not(directive) ~> oneLine | block } - override val token: Parser[Token] = { - val base = identifier | directive | comment | numericLiteral | stringLiteral | delim | shebang | EofCh ^^^ EOF + override lazy val token: Parser[Token] = { + val base = identifier | directive | comment | numericLiteral | stringLiteral | stringTemplateLiteral | delim | shebang | EofCh ^^^ EOF val ignore = (newLine | whitespaceChar).* diff --git a/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsParser.scala b/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsParser.scala index 7639dc4b39..d9973da620 100644 --- a/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsParser.scala +++ b/ts/src/main/scala/org/scalablytyped/converter/internal/ts/parser/TsParser.scala @@ -652,7 +652,11 @@ class TsParser(path: Option[(os.Path, Int)]) extends StdTokenParsers with Parser } lazy val tsLiteralString: Parser[TsLiteral.Str] = - stringLit ^^ TsLiteral.Str.apply + elem("string literal", { + case _: TsLexer.StringLit => true + case _: TsLexer.StringTemplateLiteral => true + case _ => false + }) ^^ (lit => TsLiteral.Str(lit.chars)) lazy val tsIdentModule: Parser[TsIdentModule] = tsLiteralString ^^ ModuleNameParser.apply