Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

""" interpolations require coffeeInterpolation, /// respects coffeeInterpolation, coffeeComment, coffeeDiv #1635

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions civet.dev/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
26 changes: 23 additions & 3 deletions civet.dev/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ console.log '''
<Playground>
console.log """
<div>
Civet #{version}
Civet
</div>
"""
</Playground>
Expand All @@ -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:

<Playground>
phoneNumber := ///
Expand All @@ -564,6 +565,10 @@ phoneNumber := ///
///
</Playground>

<Playground>
r := /// ${prefix} \s+ ${suffix} ///
</Playground>

:::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).
Expand Down Expand Up @@ -3231,11 +3236,19 @@ do (url) ->
await fetch url
</Playground>

### Double-Quoted Strings
### CoffeeScript Interpolation

<Playground>
"civet coffeeInterpolation"
console.log "Hello #{name}!"
console.log """
Goodbye #{name}!
"""
</Playground>

<Playground>
"civet coffeeInterpolation"
r = /// #{prefix} \s+ #{suffix} ///
</Playground>

### CoffeeScript Operators
Expand Down Expand Up @@ -3308,6 +3321,13 @@ you can enable `#` for single-line comments:
# one-line comment
</Playground>

<Playground>
"civet coffeeComment"
r = ///
\s+ # whitespace
///
</Playground>

[`###...###` block comments](#block-comments) are always available.

### CoffeeScript Line Continuations
Expand Down
15 changes: 8 additions & 7 deletions notes/Comparison-to-CoffeeScript.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -109,20 +108,20 @@ 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()`
- Civet tries to keep the transpiled output verbatim as much as possible.
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
Expand Down Expand Up @@ -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"`.
Expand Down
24 changes: 17 additions & 7 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -6200,7 +6208,7 @@ HeregexBody
HeregexPart
RegularExpressionClass

CoffeeStringSubstitution -> { type: "Substitution", children: $1 }
CoffeeInterpolationEnabled CoffeeStringSubstitution -> { type: "Substitution", children: $2 }
TemplateSubstitution -> { type: "Substitution", children: $1 }

/(?:\\.)/ ->
Expand All @@ -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 <PS>, <LS>
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion source/parser/string.civet
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 2 additions & 16 deletions test/block-strings.civet
Original file line number Diff line number Diff line change
Expand Up @@ -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", ->
Expand Down
50 changes: 25 additions & 25 deletions test/helper.civet
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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}
Expand All @@ -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"
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion test/integration.civet
Original file line number Diff line number Diff line change
Expand Up @@ -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///
1 change: 1 addition & 0 deletions test/object.civet
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ describe "object", ->
testCase '''
triple-quoted template literal key shorthand
---
"civet coffeeInterpolation"
{"""x#{y}z""": value}
---
({[`x${y}z`]: value})
Expand Down
Loading
Loading