From 18c659e4feb128591aeb7cb8e983180b1fb4b094 Mon Sep 17 00:00:00 2001 From: Erik Demaine Date: Thu, 5 Dec 2024 12:09:06 -0500 Subject: [PATCH] `"""` interpolations require `coffeeInterpolation`, `///` respects `coffeeInterpolation` and `coffeeDiv` --- civet.dev/config.md | 4 +- civet.dev/reference.md | 26 ++++++++- notes/Comparison-to-CoffeeScript.md | 15 ++--- source/parser.hera | 24 +++++--- source/parser/string.civet | 2 +- test/block-strings.civet | 18 +----- test/helper.civet | 50 ++++++++--------- test/integration.civet | 4 +- test/object.civet | 1 + test/regex.civet | 72 ++++++++++++++++++++++-- test/strings.civet | 86 +---------------------------- 11 files changed, 151 insertions(+), 151 deletions(-) diff --git a/civet.dev/config.md b/civet.dev/config.md index 82395b06..857ebbf9 100644 --- a/civet.dev/config.md +++ b/civet.dev/config.md @@ -69,11 +69,11 @@ For now, we have the following related options: | [`coffeeBooleans`](reference#coffeescript-booleans) | `yes`, `no`, `on`, `off` | | [`coffeeClasses`](reference#coffeescript-classes) | CoffeeScript-style `class` methods via `->` functions | | [`coffeeComment`](reference#coffeescript-comments) | `# single line comments` | -| [`coffeeDiv`](reference#coffeescript-comments) | `x // y` integer division | +| [`coffeeDiv`](reference#coffeescript-comments) | `x // y` integer division instead of JS comment | | [`coffeeDo`](reference#coffeescript-do) | `do ->`; disables [ES6 `do...while` loops](reference#do-while-until-loop) and [Civet `do` blocks](reference#do-blocks) | | [`coffeeEq`](reference#coffeescript-operators) | `==` → `===`, `!=` → `!==` | | [`coffeeForLoops`](reference#coffeescript-for-loops) | `for in`/`of`/`from` loops behave like they do in CoffeeScript (like Civet's `for each of`/`in`/`of` respectively) | -| [`coffeeInterpolation`](reference#double-quoted-strings) | `"a string with #{myVar}"` | +| [`coffeeInterpolation`](reference#double-quoted-strings) | `"a string with #{myVar}"`, `///regex #{myVar}///` | | [`coffeeIsnt`](reference#coffeescript-operators) | `isnt` → `!==` | | [`coffeeJSX`](reference#indentation) | JSX children ignore indentation; tags need to be explicitly closed | | [`coffeeLineContinuation`](reference#coffeescript-line-continuations) | `\` at end of line continues to next line | diff --git a/civet.dev/reference.md b/civet.dev/reference.md index 4d723b0c..166dd11b 100644 --- a/civet.dev/reference.md +++ b/civet.dev/reference.md @@ -531,7 +531,7 @@ console.log ''' console.log """
- Civet #{version} + Civet
"""
@@ -552,7 +552,8 @@ first slash is not immediately followed by a space. Instead of `/ x /` write `/\ x /` or `/[ ]x /` (or more escaped forms like `/[ ]x[ ]/`). In addition, you can use `///...///` to write multi-line regular expressions -that ignore top-level whitespace and single-line comments: +that ignore top-level whitespace and single-line comments, and interpolates +`${expression}` like in template literals: phoneNumber := /// @@ -564,6 +565,10 @@ phoneNumber := /// /// + +r := /// ${prefix} \s+ ${suffix} /// + + :::info `///` is treated as a comment if it appears at the top of your file, to support [TypeScript triple-slash directives](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html). @@ -3231,11 +3236,19 @@ do (url) -> await fetch url -### Double-Quoted Strings +### CoffeeScript Interpolation "civet coffeeInterpolation" console.log "Hello #{name}!" +console.log """ + Goodbye #{name}! +""" + + + +"civet coffeeInterpolation" +r = /// #{prefix} \s+ #{suffix} /// ### CoffeeScript Operators @@ -3308,6 +3321,13 @@ you can enable `#` for single-line comments: # one-line comment + +"civet coffeeComment" +r = /// + \s+ # whitespace +/// + + [`###...###` block comments](#block-comments) are always available. ### CoffeeScript Line Continuations diff --git a/notes/Comparison-to-CoffeeScript.md b/notes/Comparison-to-CoffeeScript.md index 8b7ebfe1..332806a2 100644 --- a/notes/Comparison-to-CoffeeScript.md +++ b/notes/Comparison-to-CoffeeScript.md @@ -33,7 +33,6 @@ Things Kept from CoffeeScript - Chained comparisons: `a < b < c` → `a < b && b < c` - Postfix `if/unless/while/until/for` - Block Strings `"""` / `'''` - - `#{exp}` interpolation in `"""` strings - `when` inside `switch` automatically breaks - Multiple `,` separated `case`/`when` expressions - `else` → `default` in `switch` @@ -109,7 +108,7 @@ Things Changed from CoffeeScript - Generators don't implicitly return the last value (as this is rarely useful) - Backtick embedded JS has been replaced with JS template literals. - No longer allowing multiple postfix `if/unless` on the same line (use `&&` or `and` to combine conditions). -- `#{}` interpolation in `""` strings only when `"civet coffeeCompat"` or `"civet coffeeInterpolation"` +- `#{}` interpolation in `"..."` and `"""..."""` strings only when `"civet coffeeCompat"` or `"civet coffeeInterpolation"` - Expanded chained comparisons to work on more operators `a in b instanceof C` → `a in b && b instanceof C` - Postfix iteration/conditionals always wrap the statement [#5431](https://github.com/jashkenas/coffeescript/issues/5431): `try x() if y` → `if (y) try x()` @@ -117,12 +116,12 @@ Things Changed from CoffeeScript In Coffee `(x)` → `x;` but in Civet `(x)` → `(x)`. Spacing and comments are also preserved as much as possible. - Heregex / re.X - Stay closer to the [Python spec](https://docs.python.org/3/library/re.html#re.X) - - Allows both kinds of substitutions `#{..}`, `${..}`. - - Also allows both kinds of single line comments `//`, `#`. + - Allows JS-style substitutions `${..}`. For Coffee-style substitutions `#{..}`, use `"civet coffeeCompat"` or `"civet coffeeInterpolation"`. + - Allows JS-style comments `//` unless `"civet coffeeDiv"` is set (including by `"civet coffeeCompat"`). For Coffee-style comments `#`, use `"civet coffeeCompat"` or `"civet coffeeInterpolation"`. + - With `coffeeComment` on, `#` is always the start of a comment outside of character classes regardless of leading space (CoffeeScript treats + `\s+#` as comment starts inside and outside of character classes). - Keeps non-newline whitespace inside of character classes. - Doesn't require escaping `#` after space inside of character classes. - - `#` is always the start of a comment outside of character classes regardless of leading space (CoffeeScript treats - `\s+#` as comment starts inside and outside of character classes). - Might later add a compat flag to get more CoffeeScript compatibility. - Might also later add a compat flag to only use ES interpolations and comments inside Heregexes. - JSX children need to be properly indented @@ -167,15 +166,17 @@ Civet provides a compatibility prologue directive that aims to be 97+% compatibl | coffeeBooleans | `yes`, `no`, `on`, `off` | | coffeeClasses | CoffeeScript-style `class` methods via `->` functions | | coffeeComment | `# single line comments` | +| coffeeDiv | `x // y` integer division instead of JS comment | | coffeeDo | `do ->`, disables ES6 do/while | | coffeeEq | `==` → `===`, `!=` → `!==` | | coffeeForLoops | for in, of, from loops behave like they do in CoffeeScript | -| coffeeInterpolation | `"a string with #{myVar}"` | +| coffeeInterpolation | `"a string with #{myVar}"`, `///regex #{myVar}///` | | coffeeIsnt | `isnt` → `!==` | | coffeeLineContinuation | `\` at end of line continues to next line | | coffeeNot | `not` → `!`, disabling Civet extensions like `is not` | | coffeeOf | `a of b` → `a in b`, `a not of b` → `!(a in b)`, `a in b` → `b.indexOf(a) >= 0`, `a not in b` → `b.indexOf(a) < 0` | | coffeePrototype | `x::` -> `x.prototype`, `x::y` -> `x.prototype.y` | +| coffeeRange | `[a..b]` increases or decreases depending on whether `a < b` or `a > b` | You can use these with `"civet coffeeCompat"` to opt in to all or use them bit by bit with `"civet coffeeComment coffeeEq coffeeInterpolation"`. Another possibility is to slowly remove them to provide a way to migrate files a little at a time `"civet coffeeCompat -coffeeBooleans -coffeeComment -coffeeEq"`. diff --git a/source/parser.hera b/source/parser.hera index 32ec7fd2..4bd7ef07 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -6123,10 +6123,18 @@ SingleStringCharacters /(?:\\.|[^'])*/ -> return { $loc, token: $0 } -TripleDoubleStringCharacters +TripleDoubleStringContents + CoffeeInterpolationEnabled ( CoffeeTripleDoubleStringCharacters / CoffeeStringSubstitution )* -> $2 + !CoffeeInterpolationEnabled TripleDoubleStringCharacters -> [$2] + +CoffeeTripleDoubleStringCharacters /(?:"(?!"")|#(?!\{)|\\.|[^#"])+/ -> return { $loc, token: $0 } +TripleDoubleStringCharacters + /(?:"(?!"")|\\.|[^"])+/ -> + return { $loc, token: $0 } + TripleSingleStringCharacters /(?:'(?!'')|\\.|[^'])*/ -> return { $loc, token: $0 } @@ -6200,7 +6208,7 @@ HeregexBody HeregexPart RegularExpressionClass - CoffeeStringSubstitution -> { type: "Substitution", children: $1 } + CoffeeInterpolationEnabled CoffeeStringSubstitution -> { type: "Substitution", children: $2 } TemplateSubstitution -> { type: "Substitution", children: $1 } /(?:\\.)/ -> @@ -6223,14 +6231,16 @@ HeregexPart # Escape forward slashes (that aren't part of a triple slash) /\/(?!\/\/)/ -> return { $loc, token: "\\/" } - /[^[\/\s#\\]+/ -> + # Don't swallow up # and $ which might be interpolations, + # but handle them as single characters if they're not + /[^[\/\s#$\\]+|[#$]/ -> return { $loc, token: $0 } HeregexComment # NOTE: CoffeeScript doesn't treat JS comments as regex comments - # TODO: this behavior should be toggled by a coffeeCompat directive - JSSingleLineComment - CoffeeSingleLineComment + # We disable them when coffeeDiv flag (// operator) is enabled + !CoffeeDivEnabled JSSingleLineComment + CoffeeCommentEnabled CoffeeSingleLineComment -> $2 # https://262.ecma-international.org/#prod-RegularExpressionBody # NOTE: Simplified a little from the spec, ignoring , @@ -6265,7 +6275,7 @@ _TemplateLiteral } # NOTE: actual CoffeeScript """ string behaviors are pretty weird, this is simplified - TripleDoubleQuote ( TripleDoubleStringCharacters / CoffeeStringSubstitution )* TripleDoubleQuote -> + TripleDoubleQuote TripleDoubleStringContents TripleDoubleQuote -> return dedentBlockSubstitutions($0, config.tab) # NOTE: ''' don't have interpolation so could be converted into a regular diff --git a/source/parser/string.civet b/source/parser/string.civet index bbb9f42a..199858a4 100644 --- a/source/parser/string.civet +++ b/source/parser/string.civet @@ -61,7 +61,7 @@ function getIndentOfBlockString(str: string, tab: TabConfig) minLevel -function dedentBlockString({ $loc, token: str }: ASTLeaf, tab: TabConfig, dedent: number | undefined, trimStart = true, trimEnd = true) +function dedentBlockString({ $loc, token: str }: ASTLeaf, tab: TabConfig, dedent: number?, trimStart = true, trimEnd = true) // If string begins with a newline then indentation assume that it should be removed for all lines if not dedent? and /^[ \t]*\r?\n/.test str // Remove remaining shared indentation diff --git a/test/block-strings.civet b/test/block-strings.civet index 7af51aa1..645b92c7 100644 --- a/test/block-strings.civet +++ b/test/block-strings.civet @@ -68,27 +68,13 @@ describe "block strings", -> ''' testCase ''' - CoffeeScript compatible interpolation + attempted CoffeeScript interpolation --- x = """ Ahoy #{name} """ --- - x = `Ahoy ${name}` - ''' - - testCase ''' - CoffeeScript compatible interpolation - --- - x = """ - Hi - Ahoy #{name} - - Hello - Mr. #{surname} - """ - --- - x = `Hi\nAhoy ${name}\n\nHello\nMr. ${surname}` + x = `Ahoy #{name}` ''' describe "single quoted", -> diff --git a/test/helper.civet b/test/helper.civet index c993e2cf..19396c70 100644 --- a/test/helper.civet +++ b/test/helper.civet @@ -13,18 +13,18 @@ compare := (src: string, result: string, compilerOpts: CompilerOptionsWithWrappe ...compilerOpts }) - assert.equal compileResult, result, """ - #{filename} + assert.equal compileResult, result, ``` + ${filename} --- Source --- - #{src} + ${src} --- Expected --- - #{result} + ${result} --- Got --- - #{compileResult} + ${compileResult} - """ + ``` jsCode .= compileResult wrapper := compilerOpts.wrapper ?? wrappers.-1 @@ -39,15 +39,15 @@ compare := (src: string, result: string, compilerOpts: CompilerOptionsWithWrappe loader: if compilerOpts.js then 'jsx' else 'tsx' jsx: 'preserve' catch e - assert.fail """ - Failed to parse #{if compilerOpts.js then 'JavaScript' else 'TypeScript'} + assert.fail ``` + Failed to parse ${if compilerOpts.js then 'JavaScript' else 'TypeScript'} --- Code --- - #{jsCode} + ${jsCode} --- Error --- - #{e} - """ + ${e} + ``` /** * Pass a string with the following format: @@ -111,15 +111,15 @@ throws := (text: string, compilerOpts?: CompilerOptions, opt?: "only" | "skip") assert.throws => e && throw e undefined as any - """ + ``` --- Source --- - #{src} + ${src} --- Got --- - #{result!} + ${result!} - """ + ``` // Then check against desired error message if error {name} := e! as {name: string} @@ -131,18 +131,18 @@ throws := (text: string, compilerOpts?: CompilerOptions, opt?: "only" | "skip") s = s.replace /\nExpected:[^]*$/, '' else // just name s = name - assert.equal s, error, """ + assert.equal s, error, ``` --- Source --- - #{src} + ${src} --- Expected Error --- - #{error} + ${error} --- Got Error --- - #{e!.toString()} + ${e!.toString()} - """ + ``` throws.only = (text: string, compilerOpts?: CompilerOptions) -> throws text, compilerOpts, "only" throws.skip = (text: string, compilerOpts?: CompilerOptions) -> throws text, compilerOpts, "skip" @@ -152,18 +152,18 @@ evalsTo := (src: string, value: any) -> js: true sync: true // TODO: consider wrapping in `it` so we can use async API } - assert.deepEqual result, value, """ + assert.deepEqual result, value, ``` --- Source --- - #{src} + ${src} --- Expected --- - #{value} + ${value} --- Got --- - #{result} + ${result} - """ + ``` wrapper := (wrap: string) -> before => wrappers.push wrap diff --git a/test/integration.civet b/test/integration.civet index 0d8f0c98..8862a9e2 100644 --- a/test/integration.civet +++ b/test/integration.civet @@ -50,4 +50,6 @@ describe "integration", -> it `should sourcemap correctly, ${mode} mode`, -> {err, stderr} := await execCmdError `bash -c "(cd integration/example && ../../dist/civet --no-config error-${mode}.civet)"` assert.match err.message, /Command failed/ - assert.match stderr, ///error-#{mode}.civet:6:7/// + // The newline in this /// block is to avoid a bug in Civet <0.9: + assert.match stderr, ///error- + ${mode}.civet:6:7/// diff --git a/test/object.civet b/test/object.civet index b5f0f584..7b3695c3 100644 --- a/test/object.civet +++ b/test/object.civet @@ -988,6 +988,7 @@ describe "object", -> testCase ''' triple-quoted template literal key shorthand --- + "civet coffeeInterpolation" {"""x#{y}z""": value} --- ({[`x${y}z`]: value}) diff --git a/test/regex.civet b/test/regex.civet index 9949799e..1eaeb142 100644 --- a/test/regex.civet +++ b/test/regex.civet @@ -174,15 +174,16 @@ describe "regexp", -> comment in character class --- ; - ///[/*]/// + ///[//*]/// --- ; - /[/*]/ + /[//*]/ """ testCase """ coffee comment in character class --- + "civet coffeeComment" ; ///[ # hey ]/// --- @@ -212,10 +213,69 @@ describe "regexp", -> /abb/ """ + testCase """ + no JS comment + --- + "civet coffeeDiv" + ; + /// + abb // hey + /// + --- + ; + /abb\\/\\/hey/ + """ + + testCase """ + no coffee comment + --- + ; + /// + abb # hey + /// + --- + ; + /abb#hey/ + """ + + testCase """ + coffee comment + --- + "civet coffeeComment" + ; + /// + abb # hey + /// + --- + ; + /abb/ + """ + testCase ''' substitutions --- ; + ///${a}/// + --- + ; + RegExp(`${a}`) + ''' + + testCase ''' + no coffee substitutions + --- + ; + ///#{a}/// + --- + ; + /#{a}/ + ''' + + testCase ''' + coffee substitutions + --- + "civet coffeeInterpolation" + ; ///#{a}/// --- ; @@ -227,7 +287,7 @@ describe "regexp", -> --- ; /// - `#{a} + `${a} /// --- ; @@ -237,6 +297,7 @@ describe "regexp", -> testCase ''' allows both kinds of substitutions --- + "civet coffeeInterpolation" ; /// ${yo} @@ -265,7 +326,7 @@ describe "regexp", -> --- ; /// - \\[#{a}] + \\[${a}] /// --- ; @@ -277,7 +338,7 @@ describe "regexp", -> --- ; /// - x#{a} + x${a} ///g --- ; @@ -288,6 +349,7 @@ describe "regexp", -> testCase ''' coffee script's heregex --- + "civet coffeeCompat" ; /// ^ (?: diff --git a/test/strings.civet b/test/strings.civet index ddf717ab..2bbca89a 100644 --- a/test/strings.civet +++ b/test/strings.civet @@ -96,79 +96,9 @@ describe "strings", -> b" """ - // NOTE: the `a` variable is only to make the string not be interpereted as a directive prologue - testCase ''' - coffee compat string interpolation - --- - "civet coffee-compat" - a - "a#{b}c" + tagged template literal with string --- - a; - `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 - --- - "civet coffee-compat" - a - "a\\#{b}c" - --- - a - "a\\#{b}c" - ''' - - testCase ''' - coffee compat string interpolation with newlines - --- - "civet coffee-compat" - a - "a - #{b}c" - --- - a; - `a\\n${b}c` - ''' - - testCase ''' - coffee compat string interpolation restore indented - --- - "civet coffee-compat" - f (a, b) => "#{[a, b] - .join 'x' - }" - --- - f((a, b) => `${[a, b] - .join('x') - }`) - ''' - - testCase ''' - coffee compat tagged template literal - --- - "civet coffee-compat" - tag"a#{b}c" - --- - tag`a${b}c` - ''' - - testCase ''' - coffee compat tagged template literal without interpolation - --- - "civet coffee-compat" tag"a" --- tag`a` @@ -179,17 +109,5 @@ describe "strings", -> --- tag"""a#{b}c""" --- - tag`a${b}c` - ''' - - testCase ''' - multi-line function call in """ - --- - """ - #{func 1, - 2} - """ - --- - `${func(1, - 2)}` + tag`a#{b}c` '''