From 9f9fc1bd22d7557794cb513da4a26693b460d365 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Wed, 25 Jan 2023 16:32:38 -0500 Subject: [PATCH 1/2] Template literals as object keys --- civet.dev/cheatsheet.md | 1 + source/parser.hera | 14 ++++++++++++++ test/jsx/attr.civet | 16 ++++++++++++++++ test/object.civet | 8 ++++++++ 4 files changed, 39 insertions(+) diff --git a/civet.dev/cheatsheet.md b/civet.dev/cheatsheet.md index c6d1bff7..e05b8e7c 100644 --- a/civet.dev/cheatsheet.md +++ b/civet.dev/cheatsheet.md @@ -48,6 +48,7 @@ Literal shorthand beyond `{x}`: another := {person.name, obj?.c?.x} computed := {foo(), bar()} named := {lookup[x+y]} +templated := {`${prefix}${suffix}`: result} Flagging shorthand inspired by [LiveScript](https://livescript.net/#literals-objects): diff --git a/source/parser.hera b/source/parser.hera index 121a830d..a68187c1 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -1873,6 +1873,12 @@ ComputedPropertyName type: "ComputedPropertyName", children: $0, } + # NOTE: Extending to allow template literals without brackets + InsertOpenBracket TemplateLiteral InsertCloseBracket -> + return { + type: "ComputedPropertyName", + children: $0, + } Decorator AtAt IdentifierReference Arguments? @@ -4958,6 +4964,14 @@ InsertCloseBrace "" -> return { $loc, token: "}" } +InsertOpenBracket + "" -> + return { $loc, token: "[" } + +InsertCloseBracket + "" -> + return { $loc, token: "]" } + InsertComma "" -> return { $loc, token: "," } diff --git a/test/jsx/attr.civet b/test/jsx/attr.civet index 93a1e2a9..e2fdacf9 100644 --- a/test/jsx/attr.civet +++ b/test/jsx/attr.civet @@ -143,6 +143,22 @@ describe "JSX computed attribute names", -> """ + testCase """ + template literal and value + --- + + --- + + """ + + testCase """ + just template literal + --- + + --- + + """ + describe "JSX id shorthand", -> testCase """ without space diff --git a/test/object.civet b/test/object.civet index c0915578..e87b603b 100644 --- a/test/object.civet +++ b/test/object.civet @@ -428,6 +428,14 @@ describe "object", -> updateSourceMap: function(outputStr, inputPos) { return outputStr }}) """ + testCase """ + template literal key shorthand + --- + {`x${y}z`: value} + --- + ({[`x${y}z`]: value}) + """ + describe "object literal shorthand", -> testCase """ member From c73d6a56e427875034bf485d0b9afac78b7c36f1 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Wed, 25 Jan 2023 17:37:47 -0500 Subject: [PATCH 2/2] Refactor StringLiteral vs. TemplateLiteral * `StringLiteral` is now always a string literal; no more `BasicStringLiteral` * `TemplateLiteral` now contains all CoffeeScript string interpolations (`"` and `"""` and even `'''` which transpiles to a template literal), though occasionally returns `StringLiteral` when there is no interpolation. * Fixes #289 * Important to check for CoffeeScript double-quoted strings via `TemplateLiteral` before regular double-quoted strings via `StringLiteral`. --- source/parser.hera | 103 ++++++++++++++++++++++-------------- test/object.civet | 17 ++++++ test/strings.civet | 28 ++++++++++ test/template-literal.civet | 8 +++ 4 files changed, 116 insertions(+), 40 deletions(-) diff --git a/source/parser.hera b/source/parser.hera index a68187c1..e27a9fd0 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -352,13 +352,15 @@ PipelineTailItem PrimaryExpression ObjectLiteral ThisLiteral + TemplateLiteral + # NOTE: TemplateLiteral must be before Literal, so that CoffeeScript + # interpolated strings get checked first before StringLiteral. Literal ArrayLiteral IdentifierReference # NOTE: Must be below ObjectLiteral for inline objects `a: 1, b: 2` to not be shadowed by matching the first identifier FunctionExpression ClassExpression RegularExpressionLiteral - TemplateLiteral ParenthesizedExpression # https://facebook.github.io/jsx/#sec-jsx-PrimaryExpression # NOTE: Modified to parse multiple JSXElement/JSXFragments as one fragment @@ -646,7 +648,7 @@ MemberBracketContent } # NOTE: Added shorthand x."string" -> x["string"] - Dot:dot ( StringLiteral / TemplateLiteral ):str -> + Dot:dot ( TemplateLiteral / StringLiteral ):str -> return { type: "Index", children: [ @@ -1861,9 +1863,11 @@ SnugNamedProperty PropertyName # https://262.ecma-international.org/#prod-LiteralPropertyName NumericLiteral + ComputedPropertyName + # NOTE: ComputedPropertyName must be before StringLiteral, + # so that CoffeeScript interpolated strings get checked first. StringLiteral IdentifierName - ComputedPropertyName ComputedPropertyName # https://262.ecma-international.org/#prod-ComputedPropertyName @@ -3140,7 +3144,7 @@ ModuleSpecifier return { $loc, token: token.replace(/\.([mc])?ts(['"])$/, ".$1js$2") } UnprocessedModuleSpecifier - BasicStringLiteral + StringLiteral UnquotedSpecifier UnquotedSpecifier @@ -3431,19 +3435,10 @@ DecimalIntegerLiteral /(?:0|[1-9](?:_[0-9]|[0-9])*)/ # https://262.ecma-international.org/#prod-StringLiteral +# NOTE: This includes unprocessed double-quoted strings. If you might want to +# accept a template literal in the form of a CoffeeScript double-quoted string +# interpolation, be sure to check TemplateLiteral before StringLiteral. StringLiteral - # NOTE: actual CoffeeScript """ string behaviors are pretty weird, this is simplifed - TripleDoubleQuote ( TripleDoubleStringCharacters / CoffeeStringSubstitution )* TripleDoubleQuote -> - return module.dedentBlockSubstitutions($0) - - TripleSingleQuote:s TripleSingleStringCharacters:str TripleSingleQuote:e -> - return [s, module.dedentBlockString(str), e] - - # CoffeeScript Interpolation is enabled when "civet coffee-compat" or "civet coffee-interpolation" directive is present - CoffeeInterpolatedDoubleQuotedString - BasicStringLiteral - -BasicStringLiteral DoubleQuote DoubleStringCharacters:str DoubleQuote -> return { token: `"${module.modifyString(str.token)}"`, @@ -3480,6 +3475,7 @@ CoffeeInterpolatedDoubleQuotedString // Check for no interpolations if (parts.length === 0 || (parts.length === 1 && parts[0].token != null)) { return { + type: "StringLiteral", token: parts.length ? `"${module.modifyString(parts[0].token)}"` : '""', $loc, } @@ -3498,7 +3494,10 @@ CoffeeInterpolatedDoubleQuotedString // Convert to backtick enclosed string s.token = e.token = "`" - return [s, parts, e] + return { + type: "TemplateLiteral", + children: [s, parts, e], + } CoffeeDoubleQuotedStringCharacters /(?:\\.|#(?!\{)|[^"#])+/ -> @@ -3604,7 +3603,28 @@ TemplateLiteral TripleTick ( TemplateBlockCharacters / TemplateSubstitution )* TripleTick -> return module.dedentBlockSubstitutions($0) - Backtick ( TemplateCharacters / TemplateSubstitution )* Backtick + Backtick ( TemplateCharacters / TemplateSubstitution )* Backtick -> + return { + type: "TemplateLiteral", + children: $0, + } + + # NOTE: actual CoffeeScript """ string behaviors are pretty weird, this is simplifed + TripleDoubleQuote ( TripleDoubleStringCharacters / CoffeeStringSubstitution )* TripleDoubleQuote -> + return module.dedentBlockSubstitutions($0) + + # NOTE: ''' don't have interpolation so could be converted into a regular + # String, but currently we use `template`s so the output looks similar to + # the input. Also, this enables tagged template literals such as tag'''x''', + # which CoffeeScript also allows. + TripleSingleQuote:s TripleSingleStringCharacters:str TripleSingleQuote:e -> + return { + type: "TemplateLiteral", + children: [s, module.dedentBlockString(str), e] + } + + # CoffeeScript Interpolation is enabled when "civet coffee-compat" or "civet coffee-interpolation" directive is present + CoffeeInterpolatedDoubleQuotedString # NOTE: Simplified grammar TemplateSubstitution @@ -4397,14 +4417,11 @@ JSXAttributeSpace JSXShorthandString /(?:[\w\-:]+|\([^()]*\)|\[[^\[\]]*\])+/ -> return module.quoteString($0) - StringLiteral -> - if (module.isTemplateLiteral($1)) { - return [ "{", $1, "}" ] - } else { - return $1 - } TemplateLiteral -> return [ "{", $1, "}" ] + # NOTE: TemplateLiteral must be before StringLiteral, + # so that CoffeeScript interpolated strings get checked first. + StringLiteral OpenBrace ExtendedExpression Whitespace? CloseBrace # https://facebook.github.io/jsx/#prod-JSXAttributeName @@ -4421,18 +4438,20 @@ JSXAttributeInitializer JSXAttributeValue # https://facebook.github.io/jsx/#prod-JSXDoubleStringCharacters # https://facebook.github.io/jsx/#prod-JSXSingleStringCharacters - # But double-quoted strings may turn into interpolation, - # and some triple-quoted strings may not use interpolation. - # So try to match but skip here if we find interpolation, - # and add parens in InlineJSXAttributeValue otherwise. - StringLiteral -> - if (module.isTemplateLiteral($1)) return $skip - return $1 # NOTE: Using ExtendedExpression to allow If/Switch expressions OpenBrace ExtendedExpression Whitespace? CloseBrace JSXElement JSXFragment - InsertInlineOpenBrace InlineJSXAttributeValue InsertCloseBrace + InsertInlineOpenBrace InlineJSXAttributeValue InsertCloseBrace -> + // Check for string literal resulting from CoffeeScript interpolated + // double-quoted string that didn't end up actually interpolating. + if ($2.children?.length === 1 && $2.children[0].type === "StringLiteral") { + return $2.children[0] + } + return $0 + # NOTE: InlineJSXAttributeValue which contains TemplateLiteral must be before + # StringLiteral, so that CoffeeScript interpolated strings get checked first. + StringLiteral # JSX shorthand to avoid explicit braces when unnecessary (no whitespace) InlineJSXAttributeValue @@ -4512,8 +4531,8 @@ InlineJSXPrimaryExpression NullLiteral BooleanLiteral NumericLiteral - # StringLiteral that doesn't need braces already matched in JSXAttributeValue - StringLiteral + TemplateLiteral + # Omitting StringLiteral already matched in JSXAttributeValue ThisLiteral ArrayLiteral # Requiring braces on ObjectLiteral; this allows {a, b} even though `a, b` doesn't work as an inline object @@ -4521,7 +4540,6 @@ InlineJSXPrimaryExpression IdentifierReference # Omitting FunctionExpression and ClassExpression which have whitespace RegularExpressionLiteral - TemplateLiteral ParenthesizedExpression # Omitting JSXElement and JSXFragment which don't need braces @@ -4781,9 +4799,9 @@ TypePrimary __ OpenParen Type __ CloseParen ImportType - "import" OpenParen __ BasicStringLiteral __ CloseParen ( Dot IdentifierName )? TypeArguments? + "import" OpenParen __ StringLiteral __ CloseParen ( Dot IdentifierName )? TypeArguments? # NOTE: Added implicit import without parens - "import" InsertOpenParen Trimmed_ BasicStringLiteral InsertCloseParen + "import" InsertOpenParen Trimmed_ StringLiteral InsertCloseParen TypeTuple OpenBracket NestedTypeList __ CloseBracket @@ -4807,8 +4825,10 @@ TypeConditional return $1 TypeLiteral - Literal TemplateLiteral + # NOTE: TemplateLiteral must be before Literal, so that CoffeeScript + # interpolated strings get checked first before StringLiteral. + Literal "void" NonIdContinue -> return { $loc, token: "void" } "[]" -> @@ -4919,7 +4939,7 @@ CivetOption UnknownPrologue # NOTE: $ is to keep source verbatim and not insert a semicolon if one was omitted # Can't use $EOS because it will prevent re-writing of coffee style comments - [\t ]* BasicStringLiteral:s $SimpleStatementDelimiter EOS + [\t ]* StringLiteral:s $SimpleStatementDelimiter EOS DirectivePrologue CivetPrologue @@ -5971,7 +5991,10 @@ Init } results.push(e) - return results + return { + type: "TemplateLiteral", + children: results, + } } module.dedentBlockString = function({$loc, token: str}, spacing, trim=true) { diff --git a/test/object.civet b/test/object.civet index e87b603b..9bbdbc71 100644 --- a/test/object.civet +++ b/test/object.civet @@ -436,6 +436,23 @@ describe "object", -> ({[`x${y}z`]: value}) """ + testCase ''' + triple-quoted template literal key shorthand + --- + {"""x#{y}z""": value} + --- + ({[`x${y}z`]: value}) + ''' + + testCase ''' + coffee compat template literal key shorthand + --- + "civet coffee-compat" + {"x#{y}z": value} + --- + ({[`x${y}z`]: value}) + ''' + describe "object literal shorthand", -> testCase """ member diff --git a/test/strings.civet b/test/strings.civet index 573c8ce7..bbff608c 100644 --- a/test/strings.civet +++ b/test/strings.civet @@ -79,6 +79,17 @@ describe "strings", -> `a${b}c` ''' + testCase ''' + coffee compat string interpolation with ${} + --- + "civet coffee-compat" + a + "a#{b}c${d}" + --- + a + `a${b}c\\${d}` + ''' + testCase ''' coffee compat string interpolation with escaped octothorpe --- @@ -101,3 +112,20 @@ describe "strings", -> a `a\\n${b}c` ''' + + testCase ''' + coffee compat tagged template literal + --- + "civet coffee-compat" + tag"a#{b}c" + --- + tag`a${b}c` + ''' + + testCase ''' + triple-quote tagged template literal + --- + tag"""a#{b}c""" + --- + tag`a${b}c` + ''' diff --git a/test/template-literal.civet b/test/template-literal.civet index 8178b082..4b8b95c4 100644 --- a/test/template-literal.civet +++ b/test/template-literal.civet @@ -41,6 +41,14 @@ describe "template literal", -> x`yo` """ + testCase """ + tagged triple-quote + --- + x```yo``` + --- + x`yo` + """ + testCase """ block template literal ---