Skip to content

Commit

Permalink
parser: understand nested template strings
Browse files Browse the repository at this point in the history
  • Loading branch information
oyvindberg committed Sep 29, 2022
1 parent 46c5867 commit aad3cd9
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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(">"))),
),
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3100,4 +3100,13 @@ export {};
),
)
}

test("nested template strings") {
shouldParseAs(
"`${Head}.${FixPathSquareBrackets<`[${Middle}]${Tail}`>}`",
TsParser.tsType,
)(
TsTypeLiteral(TsLiteral.Str("${Head}.${FixPathSquareBrackets<[${Middle}]${Tail}>}")),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down Expand Up @@ -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] = {
Expand Down Expand Up @@ -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).*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit aad3cd9

Please sign in to comment.