Skip to content

Commit 18aaaad

Browse files
rhendricerikd
authored andcommitted
Add support for template literals
1 parent 7d2c6d9 commit 18aaaad

File tree

10 files changed

+88
-1
lines changed

10 files changed

+88
-1
lines changed

src/Language/JavaScript/Parser/AST.hs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module Language.JavaScript.Parser.AST
2323
, JSCommaList (..)
2424
, JSCommaTrailingList (..)
2525
, JSArrowParameterList (..)
26+
, JSTemplatePart (..)
2627

2728
-- Modules
2829
, JSModuleItem (..)
@@ -188,6 +189,7 @@ data JSExpression
188189
| JSNewExpression !JSAnnot !JSExpression -- ^new, expr
189190
| JSObjectLiteral !JSAnnot !JSObjectPropertyList !JSAnnot -- ^lbrace contents rbrace
190191
| JSSpreadExpression !JSAnnot !JSExpression
192+
| JSTemplateLiteral !(Maybe JSExpression) !JSAnnot !String ![JSTemplatePart] -- ^optional tag, lquot, head, parts
191193
| JSUnaryExpression !JSUnaryOp !JSExpression
192194
| JSVarInitExpression !JSExpression !JSVarInitializer -- ^identifier, initializer
193195
deriving (Data, Eq, Show, Typeable)
@@ -321,6 +323,10 @@ data JSCommaTrailingList a
321323
| JSCTLNone !(JSCommaList a) -- ^list
322324
deriving (Data, Eq, Show, Typeable)
323325

326+
data JSTemplatePart
327+
= JSTemplatePart !JSExpression !JSAnnot !String -- ^expr, rb, suffix
328+
deriving (Data, Eq, Show, Typeable)
329+
324330
-- -----------------------------------------------------------------------------
325331
-- | Show the AST elements stripped of their JSAnnot data.
326332

@@ -404,6 +410,8 @@ instance ShowStripped JSExpression where
404410
ss (JSUnaryExpression op x) = "JSUnaryExpression (" ++ ss op ++ "," ++ ss x ++ ")"
405411
ss (JSVarInitExpression x1 x2) = "JSVarInitExpression (" ++ ss x1 ++ ") " ++ ss x2
406412
ss (JSSpreadExpression _ x1) = "JSSpreadExpression (" ++ ss x1 ++ ")"
413+
ss (JSTemplateLiteral Nothing _ s ps) = "JSTemplateLiteral (()," ++ singleQuote s ++ "," ++ ss ps ++ ")"
414+
ss (JSTemplateLiteral (Just t) _ s ps) = "JSTemplateLiteral ((" ++ ss t ++ ")," ++ singleQuote s ++ "," ++ ss ps ++ ")"
407415

408416
instance ShowStripped JSArrowParameterList where
409417
ss (JSUnparenthesizedArrowParameter x) = ss x
@@ -545,6 +553,9 @@ instance ShowStripped JSArrayElement where
545553
ss (JSArrayElement e) = ss e
546554
ss (JSArrayComma _) = "JSComma"
547555

556+
instance ShowStripped JSTemplatePart where
557+
ss (JSTemplatePart e _ s) = "(" ++ ss e ++ "," ++ singleQuote s ++ ")"
558+
548559
instance ShowStripped a => ShowStripped (JSCommaList a) where
549560
ss xs = "(" ++ commaJoin (map ss $ fromCommaList xs) ++ ")"
550561

src/Language/JavaScript/Parser/Grammar7.y

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ import qualified Language.JavaScript.Parser.AST as AST
131131
'octal' { OctalToken {} }
132132
'string' { StringToken {} }
133133
'regex' { RegExToken {} }
134+
'tmplnosub' { NoSubstitutionTemplateToken {} }
135+
'tmplhead' { TemplateHeadToken {} }
136+
'tmplmiddle' { TemplateMiddleToken {} }
137+
'tmpltail' { TemplateTailToken {} }
134138

135139
'future' { FutureToken {} }
136140

@@ -481,6 +485,7 @@ PrimaryExpression : 'this' { AST.JSLiteral (mkJSAnnot $1) "thi
481485
| ArrayLiteral { $1 {- 'PrimaryExpression3' -} }
482486
| ObjectLiteral { $1 {- 'PrimaryExpression4' -} }
483487
| SpreadExpression { $1 {- 'PrimaryExpression5' -} }
488+
| TemplateLiteral { mkJSTemplateLiteral Nothing $1 {- 'PrimaryExpression6' -} }
484489
| LParen Expression RParen { AST.JSExpressionParen $1 $2 $3 }
485490

486491
-- Identifier :: See 7.6
@@ -496,6 +501,14 @@ Identifier : 'ident' { AST.JSIdentifier (mkJSAnnot $1) (tokenLiteral $1) }
496501
SpreadExpression :: { AST.JSExpression }
497502
SpreadExpression : Spread Expression { AST.JSSpreadExpression $1 $2 {- 'SpreadExpression' -} }
498503

504+
TemplateLiteral :: { JSUntaggedTemplate }
505+
TemplateLiteral : 'tmplnosub' { JSUntaggedTemplate (mkJSAnnot $1) (tokenLiteral $1) [] }
506+
| 'tmplhead' TemplateParts { JSUntaggedTemplate (mkJSAnnot $1) (tokenLiteral $1) $2 }
507+
508+
TemplateParts :: { [AST.JSTemplatePart] }
509+
TemplateParts : Expression 'tmplmiddle' TemplateParts { AST.JSTemplatePart $1 (mkJSAnnot $2) (tokenLiteral $2) : $3 }
510+
| Expression 'tmpltail' { AST.JSTemplatePart $1 (mkJSAnnot $2) (tokenLiteral $2) : [] }
511+
499512
-- ArrayLiteral : See 11.1.4
500513
-- [ Elisionopt ]
501514
-- [ ElementList ]
@@ -583,6 +596,7 @@ MemberExpression : PrimaryExpression { $1 {- 'MemberExpression1' -} }
583596
| FunctionExpression { $1 {- 'MemberExpression2' -} }
584597
| MemberExpression LSquare Expression RSquare { AST.JSMemberSquare $1 $2 $3 $4 {- 'MemberExpression3' -} }
585598
| MemberExpression Dot IdentifierName { AST.JSMemberDot $1 $2 $3 {- 'MemberExpression4' -} }
599+
| MemberExpression TemplateLiteral { mkJSTemplateLiteral (Just $1) $2 }
586600
| New MemberExpression Arguments { mkJSMemberNew $1 $2 $3 {- 'MemberExpression5' -} }
587601

588602
-- NewExpression : See 11.2
@@ -606,6 +620,8 @@ CallExpression : MemberExpression Arguments
606620
{ AST.JSCallExpressionSquare $1 $2 $3 $4 {- 'CallExpression3' -} }
607621
| CallExpression Dot IdentifierName
608622
{ AST.JSCallExpressionDot $1 $2 $3 {- 'CallExpression4' -} }
623+
| CallExpression TemplateLiteral
624+
{ mkJSTemplateLiteral (Just $1) $2 {- 'CallExpression5' -} }
609625

610626
-- Arguments : See 11.2
611627
-- ()
@@ -1341,7 +1357,7 @@ StatementMain : StatementNoEmpty Eof { AST.JSAstStatement $1 $2 {- 'Statement
13411357

13421358
-- Need this type while build the AST, but is not actually part of the AST.
13431359
data JSArguments = JSArguments AST.JSAnnot (AST.JSCommaList AST.JSExpression) AST.JSAnnot -- ^lb, args, rb
1344-
1360+
data JSUntaggedTemplate = JSUntaggedTemplate !AST.JSAnnot !String ![AST.JSTemplatePart] -- lquot, head, parts
13451361

13461362
blockToStatement :: AST.JSBlock -> AST.JSSemi -> AST.JSStatement
13471363
blockToStatement (AST.JSBlock a b c) s = AST.JSStatementBlock a b c s
@@ -1368,6 +1384,9 @@ parseError = alexError . show
13681384
mkJSAnnot :: Token -> AST.JSAnnot
13691385
mkJSAnnot a = AST.JSAnnot (tokenSpan a) (tokenComment a)
13701386

1387+
mkJSTemplateLiteral :: Maybe AST.JSExpression -> JSUntaggedTemplate -> AST.JSExpression
1388+
mkJSTemplateLiteral tag (JSUntaggedTemplate a h ps) = AST.JSTemplateLiteral tag a h ps
1389+
13711390
-- ---------------------------------------------------------------------
13721391
-- | mkUnary : The parser detects '+' and '-' as the binary version of these
13731392
-- operator. This function converts from the binary version to the unary

src/Language/JavaScript/Parser/Lexer.x

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ $ZWJ = [\x200d]
187187
@IdentifierPart = @IdentifierStart | $UnicodeCombiningMark | $UnicodeDigit | UnicodeConnectorPunctuation
188188
[\\] @UnicodeEscapeSequence | $ZWNJ | $ZWJ
189189
190+
-- TemplateCharacter ::
191+
-- $ [lookahead ≠ { ]
192+
-- \ EscapeSequence
193+
-- LineContinuation
194+
-- LineTerminatorSequence
195+
-- SourceCharacter but not one of ` or \ or $ or LineTerminator
196+
@TemplateCharacters = (\$* ($any_unicode_char # [\$\\`\{] | \\ $any_unicode_char) | \\ $any_unicode_char | \{)* \$*
197+
190198
-- ! ------------------------------------------------- Terminals
191199
tokens :-
192200
@@ -246,6 +254,13 @@ tokens :-
246254
247255
248256
257+
<reg,divide> "`" @TemplateCharacters "`" { adapt (mkString' NoSubstitutionTemplateToken) }
258+
<reg,divide> "`" @TemplateCharacters "${" { adapt (mkString' TemplateHeadToken) }
259+
<reg,divide> "}" @TemplateCharacters "${" { adapt (mkString' TemplateMiddleToken) }
260+
<reg,divide> "}" @TemplateCharacters "`" { adapt (mkString' TemplateTailToken) }
261+
262+
263+
249264
-- TODO: Work in SignedInteger
250265
251266
-- DecimalLiteral= {Non Zero Digits}+ '.' {Digit}* ('e' | 'E' ) {Non Zero Digits}+ {Digit}*

src/Language/JavaScript/Parser/LexerUtils.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Language.JavaScript.Parser.LexerUtils
1414
( StartCode
1515
, symbolToken
1616
, mkString
17+
, mkString'
1718
, commentToken
1819
, wsToken
1920
, regExToken
@@ -37,6 +38,9 @@ symbolToken mkToken location _ _ = return (mkToken location [])
3738
mkString :: (Monad m) => (TokenPosn -> String -> Token) -> TokenPosn -> Int -> String -> m Token
3839
mkString toToken loc len str = return (toToken loc (take len str))
3940

41+
mkString' :: (Monad m) => (TokenPosn -> String -> [CommentAnnotation] -> Token) -> TokenPosn -> Int -> String -> m Token
42+
mkString' toToken loc len str = return (toToken loc (take len str) [])
43+
4044
decimalToken :: TokenPosn -> String -> Token
4145
decimalToken loc str = DecimalToken loc str []
4246

src/Language/JavaScript/Parser/Token.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ data Token
153153
| RightParenToken { tokenSpan :: !TokenPosn, tokenComment :: ![CommentAnnotation] }
154154
| CondcommentEndToken { tokenSpan :: !TokenPosn, tokenComment :: ![CommentAnnotation] }
155155

156+
-- Template literal lexical components
157+
| NoSubstitutionTemplateToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] }
158+
| TemplateHeadToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] }
159+
| TemplateMiddleToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] }
160+
| TemplateTailToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] }
161+
156162
-- Special cases
157163
| AsToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] }
158164
| TailToken { tokenSpan :: !TokenPosn, tokenComment :: ![CommentAnnotation] } -- ^ Stuff between last JS and EOF

src/Language/JavaScript/Pretty/Printer.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ instance RenderJS JSExpression where
8989
(|>) pacc (JSMemberSquare xs als e ars) = pacc |> xs |> als |> "[" |> e |> ars |> "]"
9090
(|>) pacc (JSNewExpression n e) = pacc |> n |> "new" |> e
9191
(|>) pacc (JSObjectLiteral alb xs arb) = pacc |> alb |> "{" |> xs |> arb |> "}"
92+
(|>) pacc (JSTemplateLiteral t a h ps) = pacc |> t |> a |> h |> ps
9293
(|>) pacc (JSUnaryExpression op x) = pacc |> op |> x
9394
(|>) pacc (JSVarInitExpression x1 x2) = pacc |> x1 |> x2
9495
(|>) pacc (JSSpreadExpression a e) = pacc |> a |> "..." |> e
@@ -344,4 +345,10 @@ instance RenderJS JSVarInitializer where
344345
(|>) pacc (JSVarInit a x) = pacc |> a |> "=" |> x
345346
(|>) pacc JSVarInitNone = pacc
346347

348+
instance RenderJS [JSTemplatePart] where
349+
(|>) = foldl' (|>)
350+
351+
instance RenderJS JSTemplatePart where
352+
(|>) pacc (JSTemplatePart e a s) = pacc |> e |> a |> s
353+
347354
-- EOF

src/Language/JavaScript/Process/Minify.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ instance MinifyJS JSExpression where
168168
fix a (JSMemberSquare xs _ e _) = JSMemberSquare (fix a xs) emptyAnnot (fixEmpty e) emptyAnnot
169169
fix a (JSNewExpression _ e) = JSNewExpression a (fixSpace e)
170170
fix _ (JSObjectLiteral _ xs _) = JSObjectLiteral emptyAnnot (fixEmpty xs) emptyAnnot
171+
fix a (JSTemplateLiteral t _ s ps) = JSTemplateLiteral (fmap (fix a) t) emptyAnnot s (map fixEmpty ps)
171172
fix a (JSUnaryExpression op x) = let (ta, fop) = fixUnaryOp a op in JSUnaryExpression fop (fix ta x)
172173
fix a (JSVarInitExpression x1 x2) = JSVarInitExpression (fix a x1) (fixEmpty x2)
173174
fix a (JSSpreadExpression _ e) = JSSpreadExpression a (fixEmpty e)
@@ -399,6 +400,10 @@ instance MinifyJS JSVarInitializer where
399400
fix _ JSVarInitNone = JSVarInitNone
400401

401402

403+
instance MinifyJS JSTemplatePart where
404+
fix _ (JSTemplatePart e _ s) = JSTemplatePart (fixEmpty e) emptyAnnot s
405+
406+
402407
spaceAnnot :: JSAnnot
403408
spaceAnnot = JSAnnot tokenPosnEmpty [WhiteSpace tokenPosnEmpty " "]
404409

test/Test/Language/Javascript/ExpressionParser.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ testExpressionParser = describe "Parse expressions:" $ do
148148
it "spread expression" $
149149
testExpr "... x" `shouldBe` "Right (JSAstExpression (JSSpreadExpression (JSIdentifier 'x')))"
150150

151+
it "template literal" $ do
152+
testExpr "``" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'``',[])))"
153+
testExpr "`$`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`$`',[])))"
154+
testExpr "`$\\n`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`$\\n`',[])))"
155+
testExpr "`\\${x}`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`\\${x}`',[])))"
156+
testExpr "`$ {x}`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`$ {x}`',[])))"
157+
testExpr "`\n\n`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`\n\n`',[])))"
158+
testExpr "`${x+y} ${z}`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`${',[(JSExpressionBinary ('+',JSIdentifier 'x',JSIdentifier 'y'),'} ${'),(JSIdentifier 'z','}`')])))"
159+
testExpr "`<${x} ${y}>`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((),'`<${',[(JSIdentifier 'x','} ${'),(JSIdentifier 'y','}>`')])))"
160+
testExpr "tag `xyz`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((JSIdentifier 'tag'),'`xyz`',[])))"
161+
testExpr "tag()`xyz`" `shouldBe` "Right (JSAstExpression (JSTemplateLiteral ((JSMemberExpression (JSIdentifier 'tag',JSArguments ())),'`xyz`',[])))"
162+
151163

152164
testExpr :: String -> String
153165
testExpr str = showStrippedMaybe (parseUsing parseExpression str "src")

test/Test/Language/Javascript/Minify.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ testMinifyExpr = describe "Minify expressions:" $ do
137137
it "spread exporession" $
138138
minifyExpr " ... x " `shouldBe` "...x"
139139

140+
it "template literal" $ do
141+
minifyExpr " ` a + b + ${ c + d } + ... ` " `shouldBe` "` a + b + ${c+d} + ... `"
142+
minifyExpr " tagger () ` a + b ` " `shouldBe` "tagger()` a + b `"
143+
140144

141145
testMinifyStmt :: Spec
142146
testMinifyStmt = describe "Minify statements:" $ do

test/Test/Language/Javascript/RoundTrip.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ testRoundTrip = describe "Roundtrip:" $ do
7171
testRT "(a, b) => a + b"
7272
testRT "() => { 42 }"
7373

74+
testRT "/*a*/`<${/*b*/x/*c*/}>`/*d*/"
75+
testRT "`\\${}`"
76+
testRT "`\n\n`"
77+
7478

7579
it "statement" $ do
7680
testRT "if (1) {}"

0 commit comments

Comments
 (0)