Skip to content

Commit 9300003

Browse files
committed
Add support for template literals
1 parent 5582f95 commit 9300003

File tree

10 files changed

+88
-1
lines changed

10 files changed

+88
-1
lines changed

src/Language/JavaScript/Parser/AST.hs

+11
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 (..)
@@ -185,6 +186,7 @@ data JSExpression
185186
| JSNewExpression !JSAnnot !JSExpression -- ^new, expr
186187
| JSObjectLiteral !JSAnnot !JSObjectPropertyList !JSAnnot -- ^lbrace contents rbrace
187188
| JSSpreadExpression !JSAnnot !JSExpression
189+
| JSTemplateLiteral !(Maybe JSExpression) !JSAnnot !String ![JSTemplatePart] -- ^optional tag, lquot, head, parts
188190
| JSUnaryExpression !JSUnaryOp !JSExpression
189191
| JSVarInitExpression !JSExpression !JSVarInitializer -- ^identifier, initializer
190192
deriving (Data, Eq, Show, Typeable)
@@ -317,6 +319,10 @@ data JSCommaTrailingList a
317319
| JSCTLNone !(JSCommaList a) -- ^list
318320
deriving (Data, Eq, Show, Typeable)
319321

322+
data JSTemplatePart
323+
= JSTemplatePart !JSExpression !JSAnnot !String -- ^expr, rb, suffix
324+
deriving (Data, Eq, Show, Typeable)
325+
320326
-- -----------------------------------------------------------------------------
321327
-- | Show the AST elements stripped of their JSAnnot data.
322328

@@ -397,6 +403,8 @@ instance ShowStripped JSExpression where
397403
ss (JSUnaryExpression op x) = "JSUnaryExpression (" ++ ss op ++ "," ++ ss x ++ ")"
398404
ss (JSVarInitExpression x1 x2) = "JSVarInitExpression (" ++ ss x1 ++ ") " ++ ss x2
399405
ss (JSSpreadExpression _ x1) = "JSSpreadExpression (" ++ ss x1 ++ ")"
406+
ss (JSTemplateLiteral Nothing _ s ps) = "JSTemplateLiteral (()," ++ singleQuote s ++ "," ++ ss ps ++ ")"
407+
ss (JSTemplateLiteral (Just t) _ s ps) = "JSTemplateLiteral ((" ++ ss t ++ ")," ++ singleQuote s ++ "," ++ ss ps ++ ")"
400408

401409
instance ShowStripped JSArrowParameterList where
402410
ss (JSUnparenthesizedArrowParameter x) = ss x
@@ -537,6 +545,9 @@ instance ShowStripped JSArrayElement where
537545
ss (JSArrayElement e) = ss e
538546
ss (JSArrayComma _) = "JSComma"
539547

548+
instance ShowStripped JSTemplatePart where
549+
ss (JSTemplatePart e _ s) = "(" ++ ss e ++ "," ++ singleQuote s ++ ")"
550+
540551
instance ShowStripped a => ShowStripped (JSCommaList a) where
541552
ss xs = "(" ++ commaJoin (map ss $ fromCommaList xs) ++ ")"
542553

src/Language/JavaScript/Parser/Grammar7.y

+20-1
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

@@ -433,6 +437,7 @@ PrimaryExpression : 'this' { AST.JSLiteral (mkJSAnnot $1) "thi
433437
| ArrayLiteral { $1 {- 'PrimaryExpression3' -} }
434438
| ObjectLiteral { $1 {- 'PrimaryExpression4' -} }
435439
| SpreadExpression { $1 {- 'PrimaryExpression5' -} }
440+
| TemplateLiteral { mkJSTemplateLiteral Nothing $1 {- 'PrimaryExpression6' -} }
436441
| LParen Expression RParen { AST.JSExpressionParen $1 $2 $3 }
437442

438443
-- Identifier :: See 7.6
@@ -494,6 +499,14 @@ IdentifierName : Identifier {$1}
494499
SpreadExpression :: { AST.JSExpression }
495500
SpreadExpression : Spread Expression { AST.JSSpreadExpression $1 $2 {- 'SpreadExpression' -} }
496501

502+
TemplateLiteral :: { JSUntaggedTemplate }
503+
TemplateLiteral : 'tmplnosub' { JSUntaggedTemplate (mkJSAnnot $1) (tokenLiteral $1) [] }
504+
| 'tmplhead' TemplateParts { JSUntaggedTemplate (mkJSAnnot $1) (tokenLiteral $1) $2 }
505+
506+
TemplateParts :: { [AST.JSTemplatePart] }
507+
TemplateParts : Expression 'tmplmiddle' TemplateParts { AST.JSTemplatePart $1 (mkJSAnnot $2) (tokenLiteral $2) : $3 }
508+
| Expression 'tmpltail' { AST.JSTemplatePart $1 (mkJSAnnot $2) (tokenLiteral $2) : [] }
509+
497510
-- ArrayLiteral : See 11.1.4
498511
-- [ Elisionopt ]
499512
-- [ ElementList ]
@@ -580,6 +593,7 @@ MemberExpression : PrimaryExpression { $1 {- 'MemberExpression1' -} }
580593
| FunctionExpression { $1 {- 'MemberExpression2' -} }
581594
| MemberExpression LSquare Expression RSquare { AST.JSMemberSquare $1 $2 $3 $4 {- 'MemberExpression3' -} }
582595
| MemberExpression Dot IdentifierName { AST.JSMemberDot $1 $2 $3 {- 'MemberExpression4' -} }
596+
| MemberExpression TemplateLiteral { mkJSTemplateLiteral (Just $1) $2 }
583597
| New MemberExpression Arguments { mkJSMemberNew $1 $2 $3 {- 'MemberExpression5' -} }
584598

585599
-- NewExpression : See 11.2
@@ -603,6 +617,8 @@ CallExpression : MemberExpression Arguments
603617
{ AST.JSCallExpressionSquare $1 $2 $3 $4 {- 'CallExpression3' -} }
604618
| CallExpression Dot IdentifierName
605619
{ AST.JSCallExpressionDot $1 $2 $3 {- 'CallExpression4' -} }
620+
| CallExpression TemplateLiteral
621+
{ mkJSTemplateLiteral (Just $1) $2 {- 'CallExpression5' -} }
606622

607623
-- Arguments : See 11.2
608624
-- ()
@@ -1332,7 +1348,7 @@ StatementMain : StatementNoEmpty Eof { AST.JSAstStatement $1 $2 {- 'Statement
13321348

13331349
-- Need this type while build the AST, but is not actually part of the AST.
13341350
data JSArguments = JSArguments AST.JSAnnot (AST.JSCommaList AST.JSExpression) AST.JSAnnot -- ^lb, args, rb
1335-
1351+
data JSUntaggedTemplate = JSUntaggedTemplate !AST.JSAnnot !String ![AST.JSTemplatePart] -- lquot, head, parts
13361352

13371353
blockToStatement :: AST.JSBlock -> AST.JSSemi -> AST.JSStatement
13381354
blockToStatement (AST.JSBlock a b c) s = AST.JSStatementBlock a b c s
@@ -1359,6 +1375,9 @@ parseError = alexError . show
13591375
mkJSAnnot :: Token -> AST.JSAnnot
13601376
mkJSAnnot a = AST.JSAnnot (tokenSpan a) (tokenComment a)
13611377

1378+
mkJSTemplateLiteral :: Maybe AST.JSExpression -> JSUntaggedTemplate -> AST.JSExpression
1379+
mkJSTemplateLiteral tag (JSUntaggedTemplate a h ps) = AST.JSTemplateLiteral tag a h ps
1380+
13621381
-- ---------------------------------------------------------------------
13631382
-- | mkUnary : The parser detects '+' and '-' as the binary version of these
13641383
-- operator. This function converts from the binary version to the unary

src/Language/JavaScript/Parser/Lexer.x

+15
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

+4
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

+6
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

+7
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
@@ -340,4 +341,10 @@ instance RenderJS JSVarInitializer where
340341
(|>) pacc (JSVarInit a x) = pacc |> a |> "=" |> x
341342
(|>) pacc JSVarInitNone = pacc
342343

344+
instance RenderJS [JSTemplatePart] where
345+
(|>) = foldl' (|>)
346+
347+
instance RenderJS JSTemplatePart where
348+
(|>) pacc (JSTemplatePart e a s) = pacc |> e |> a |> s
349+
343350
-- EOF

src/Language/JavaScript/Process/Minify.hs

+5
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ instance MinifyJS JSExpression where
165165
fix a (JSMemberSquare xs _ e _) = JSMemberSquare (fix a xs) emptyAnnot (fixEmpty e) emptyAnnot
166166
fix a (JSNewExpression _ e) = JSNewExpression a (fixSpace e)
167167
fix _ (JSObjectLiteral _ xs _) = JSObjectLiteral emptyAnnot (fixEmpty xs) emptyAnnot
168+
fix a (JSTemplateLiteral t _ s ps) = JSTemplateLiteral (fmap (fix a) t) emptyAnnot s (map fixEmpty ps)
168169
fix a (JSUnaryExpression op x) = let (ta, fop) = fixUnaryOp a op in JSUnaryExpression fop (fix ta x)
169170
fix a (JSVarInitExpression x1 x2) = JSVarInitExpression (fix a x1) (fixEmpty x2)
170171
fix a (JSSpreadExpression _ e) = JSSpreadExpression a (fixEmpty e)
@@ -395,6 +396,10 @@ instance MinifyJS JSVarInitializer where
395396
fix _ JSVarInitNone = JSVarInitNone
396397

397398

399+
instance MinifyJS JSTemplatePart where
400+
fix _ (JSTemplatePart e _ s) = JSTemplatePart (fixEmpty e) emptyAnnot s
401+
402+
398403
spaceAnnot :: JSAnnot
399404
spaceAnnot = JSAnnot tokenPosnEmpty [WhiteSpace tokenPosnEmpty " "]
400405

test/Test/Language/Javascript/ExpressionParser.hs

+12
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ testExpressionParser = describe "Parse expressions:" $ do
146146
it "spread expression" $
147147
testExpr "... x" `shouldBe` "Right (JSAstExpression (JSSpreadExpression (JSIdentifier 'x')))"
148148

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

150162
testExpr :: String -> String
151163
testExpr str = showStrippedMaybe (parseUsing parseExpression str "src")

test/Test/Language/Javascript/Minify.hs

+4
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ testMinifyExpr = describe "Minify expressions:" $ do
136136
it "spread exporession" $
137137
minifyExpr " ... x " `shouldBe` "...x"
138138

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

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

test/Test/Language/Javascript/RoundTrip.hs

+4
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ testRoundTrip = describe "Roundtrip:" $ do
6969
testRT "(a, b) => a + b"
7070
testRT "() => { 42 }"
7171

72+
testRT "/*a*/`<${/*b*/x/*c*/}>`/*d*/"
73+
testRT "`\\${}`"
74+
testRT "`\n\n`"
75+
7276

7377
it "statement" $ do
7478
testRT "if (1) {}"

0 commit comments

Comments
 (0)