jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.99k forks source link

CS2 Discussion: Features: Tagged template literals #4925

Closed coffeescriptbot closed 6 years ago

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-01 15:55

Current Coffeescript is incompatible with template literals, specifically tagged template literals. As such, direct support for template literals should be considered for coffeescript nextgen.

Specifically, calling a tagged template literal looks this:

var a = 5;
var b = 10;

function tag(strings, ...values) {
  ...
  return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;

Coffeescript already uses backticks to embed javascript:

hi = `function() {
  return [document.title, "Hello JavaScript"].join(": ");
}`

Unless I've missed a trick, it's currently impossible to use a tagged template literal in Coffeescript by embedding the relevant ES6 javascript.

FYI, I ran into this problem when exploring the newish bel DOM templating library. Since it uses tagged template strings, I believe it's inoperable with current Coffeescript. This is the first time I've seen incompatibility between Coffeescript and Javascript..

coffeescriptbot commented 6 years ago

From @JimPanic on 2016-09-01 16:06

Good catch! This is definitely something that needs work.

Would it make sense to replace the current literal-JS syntax? Or should we rather introduce a different syntax for template strings?

coffeescriptbot commented 6 years ago

From @lydell on 2016-09-01 16:07

See also: https://github.com/jashkenas/coffeescript/issues/1504#issuecomment-187814583

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-01 16:13

I also submitted a bug report for this on the Coffeescript repo: https://github.com/jashkenas/coffeescript/issues/4301

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-06 07:30

FYI, I got tagged template literals working in my browser-side Coffeescript project by encapsulating use of this es6 functionality in its own javascript class. So - for the first time - my project contains both coffeescript and javascript files, which are all brought together in a browserify + broccoli build step.

# views/test-view.js:

module.exports = function (bel) {

    var TestView = function () {
        ...
    };

    TestView.prototype.render = function (name) {
        return bel`<div>Hello ${name}</div>`;
    };

    return TestView;

};

# And in Coffeescript-land:

bel = require 'bel'
TestView = require('views/test-view')(bel)
element = (new TestView()).render 'John'
coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-09-10 15:40

CoffeeScript interpolates with " instead of ```.

Currently, code such as someTag"hello #{world}" throws a syntax error in coffeescript, so presumably this syntax could be supported (how hard it would be, I'm not yet sure).

@greghuc is that the feature you're suggesting we build, or do I misunderstand?

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-10 16:04

@rattrayalex yes, I'm imagining that someTag"hello #{world}" should eventually work Coffeescript. That, or there needs to be an alternative to escaping Javascript using backticks, so that this doesn't break:

`tag`Hello ${ a + b } world ${ a * b }`;`

You could argue that alternative escaping is the better solution, given escaping is the root cause of the break.

I haven't done a deep dive on this, but the issue is really with ES6 tagged template literals, not template literals. ES6 template literals are equivalent to CS string interpolation. But tagged template literals are a new kind of beast. They involve all 'arguments' to the interpolated string being passed to the supplied function, which then processes them. Example from MDN:

var a = 5;
var b = 10;

function tag(strings, ...values) {
  console.log(strings[0]); // "Hello "
  console.log(strings[1]); // " world "
  console.log(strings[2]); // ""
  console.log(values[0]);  // 15
  console.log(values[1]);  // 50

  return "Bazinga!";
}

tag`Hello ${ a + b } world ${ a * b }`;
// "Bazinga!"

Might be worth backing away from this, and treating this as an issue with Javascript escaping in Coffeescript.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-09-10 17:14

Sorry, what does this have to do with escaping? Is it just a question of the syntax? I don't see why use of the backtick is required for this feature.

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-10 17:25

To restate, it is currently not possible to use ES6 template literals or tagged template literals inside Coffeescript (in an escaped javascript block). This is because ES6 template laterals use backticks in their syntax, and Coffeescript already uses backticks to define the start and end of the javascript block. As such, the Coffeescript compiler breaks.

So see this, copy and paste the following into "Coffee to JS" http://js2coffee.thomaskalka.de, and view the error:

`
tag`Hello ${ a + b } world ${ a * b }`;
`

So current Coffeescript is broken for use of this ES6 feature.

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-09-10 17:29

Ah, gotcha. So there are two things we could do (non-exclusively):

  1. Add the "tagged" feature to coffeescript string templates. eg, myTag"hello #{'wo'+'rld'}"
  2. Allow backtick-escaped JavaScript to include template literals, perhaps by allowing triple-backtick blocks to contain single-backticks.

They both sound fairly straightforward to me, though I don't know enough about the relevant parts of the coffeescript compiler to say.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-10 23:22

The escaping thing is just an outright bug in CoffeeScript, that’s been open since 2011: https://github.com/jashkenas/coffeescript/issues/1504 But there’s more interest in it lately since the advent of template literals. Basically it seems like there’s a consensus desire to be able to escape backticks, as well as add a triple-backtick delimiter for backticked blocks. That seems like a worthy improvement.

But fixing escaped backticks just enables a hacky workaround to using tagged template literals; it’s a far cry from supporting the feature itself in CoffeeScript syntax. Theoretically, the efforts could proceed in parallel; we could build support for myTag"hello #{'wo'+'rld'}" without necessarily fixing backticks.

@greghuc, would you mind posting a proposal for what the CoffeeScript syntax should be for tagged template literals? Including what the expected JavaScript output would be. Would we support tagged template literal blocks, e.g. myTag"""some multiline string...?

Also, would you like to take the lead in implementing this feature in the current compiler? It’s one of only three items in the Top Priority tier of our features list, as the only ES2015+ features we’ve found so far that imperil interoperability.

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-11 10:53

@GeoffreyBooth I'll start off by posting a proposal for tagged template literals. I'll aim to have this done by next weekend (by Sunday Sept 18th). Initial thoughts:

Regarding taking the lead on implementing this feature, I'm hypothetically up for it. I'll make a decision once it's clearer what the scope is (once we've agreed on the feature proposal), and how much spare time I have for open-source coding.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-12 05:01

@greghuc I agree with everything except I’m neutral on the “compiles to ‘polyfill’ JavaScript” part. I think we need to make a broader philosophical decision on whether newly-built features should still be compiling down to ES5, or should just output ESNext and leave the shimming to a tool like Babel. For modules and classes it hasn’t been an issue, since shimming modules is way beyond our capabilities and the current class keyword is already essentially an ES5 shim; but for tagged template literals we’ll need to make a choice.

I think as a group in this repo we’ve already reached a consensus that a future version of the CoffeeScript compiler should output as much ESNext syntax as possible, leaving the shimming to other tools. I proposed a flag for enabling such output, so it can be built gradually over time and not break backward compatibility. Assuming @jashkenas signs off on such a plan, it would then be CoffeeScript’s stated goal to output modern ECMAScript whenever possible, which implies that new features we add now should just go ahead and output ESNext. Ideally they would output both, an ESNext version if the --ecmascript flag is set and a shimmed version otherwise, if we have the time and motivation to implement both versions. But I would think that the ESNext version would be the default, since we know we need that for future versions of CoffeeScript. Building the shimmed version merely saves people from needing to attach Babel to their build chains, and I’m not sure such a savings is worth the added burden on us to build and support an alternate (much more complicated) output for a new feature.

So I guess would you be okay will building tagged template literals that output at least as ESNext? And if you want to take on the added challenge of outputting ES5, that’s awesome.

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-12 06:53

@GeoffreyBooth I think outputting tagged template literals in ESNext is actually the nicest option:

I considered the polyfill approach, as I didn't think outputting ESNext would be an option. So I'd be happy aiming for ESNext output for first implementation.

But I'll start with the proposal first.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-12 07:06

@greghuc that sounds great. I don’t think this feature even needs a flag; like generators or modules, using the new syntax opts into getting ESNext output. We only need to worry about flags if you wanted to build both possible outputs.

coffeescriptbot commented 6 years ago

From @jashkenas on 2016-09-12 20:35

Assuming @jashkenas signs off on such a plan, it would then be CoffeeScript’s stated goal to output modern ECMAScript whenever possible, which implies that new features we add now should just go ahead and output ESNext.

My preferred plan would be for a CoffeeScript 2.0 to implement and output modern ECMAScript — as soon as those features are shipping in real browsers and Node. I think that to output them earlier than that point (to rely on chains of transpilers, and promises to implement the spec as its written but before its implemented) would be foolish.

coffeescriptbot commented 6 years ago

From @nilskp on 2016-09-14 15:37

Triple quotes is used in a lot of languages to allow not escaping the single quote. Couldn't something be used here? I.e. triple backticks?

coffeescriptbot commented 6 years ago

From @rattrayalex on 2016-09-14 16:22

@nilskp yes, see above.

coffeescriptbot commented 6 years ago

From @nilskp on 2016-09-14 17:26

@rattrayalex Ah, yes. I didn't read the thread carefully enough.

coffeescriptbot commented 6 years ago

From @DomVinyard on 2016-09-15 22:34

@jashkenas

to rely on chains of transpilers, and promises to implement the spec as its written but before its implemented would be foolish

Why foolish? That seems like an odd indictment of this entire effort.

coffeescriptbot commented 6 years ago

From @jcollum on 2016-09-16 02:24

I think what he's saying is that CS2 shouldn't try to confirm to the spec for ES2016 before ES2016 is actually implemented in Node 7 (which would give time for the kinks to be worked out? make sure the feature will actually make it to the language?).

coffeescriptbot commented 6 years ago

From @mrmowgli on 2016-09-16 03:19

You can also put such things behind a different branch, ie esnext. I get it, the idea is really about only putting things into the language that will actually be adopted. However we can "Anticipate" future features by placing it in an unstable or experimental branch.

Perhaps we should do a formal proposal for the release process.

CS2(CS3, CS4 etc.) branches would include breaking changes (for instance classes) and any currently existing (Implemented in Babel?) high priority items that are already shipping in browsers. The ESNext branch would implement soft targets or features that are nearly complete in browsers. The experimental branch would be implementations of low priority ES6/7 targets that aren't fully defined.

I also wouldn't mind having tagged "Stable" versions.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-16 04:58

Based on some of @jashkenas’ other comments, I think that what he means by waiting for browser implementation is not that he feels that people should rely on the coffee command-line tool as their one and only transpiler, but rather that a spec is just words on paper until browsers have implemented it. So if we support a spec before it’s widely supported, we risk supporting something that’s not really final.

Getting back to template literals, support is widespread in evergreen browsers:

image

So I think this feature is safe to implement. Per this comment, unless anyone objects to @jashkenas, we’re going to skip the ES3 version of template literals and go straight to ESNext.

@greghuc are you going to implement both tagged template literals and regular template literals? You’re welcome to do both if you’re up to the challenge, though if we compile "foo#{bar}" to foo${bar} we would need to release this update as part of a proposed 2.0.0-beta release.

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-18 17:28

As discussed, I've written up a proposal for adding ES6 'tagged template literal' support to Coffeescript. Executive summary follows, followed by more detail (code examples, etc). Opinions welcome.

Executive summary

Template literals and tagged template literals are a new feature in ES6:

The current situation with Coffeescript:

  1. ES6 template literals (in an embedded Javascript block) break the Coffeescript compiler, so cannot be used in a Coffeescript file. This is because the template literal syntax uses backticks, which Coffeescript already uses to define the start and end of the Javascript block.
  2. Coffeescript already offers string-interpolation capabilities matching ES6 template literals (barring spacing differences between a CS block-string and a ES6 multi-line template literal).
  3. Coffeescript has nothing equivalent to tagged template literals.

Proposals for Coffeescript to interoperate/adopt ES6 template literals:

  1. Coffeescript should allow embedded Javascript blocks to be delineated with 3-backticks markdown-style, and not escape backticks in the Javascript block. This is the quickest/easiest way for ES6 template literals to be used in a Coffeescript file. Full proposal below.
  2. Coffeescript should not 'natively' adopt tagged literals at present, since CS already offers equivalent string interpolation. In future, the CS compiler could directly output interpolated-strings as ES6 tagged literals where appropriate.
  3. Coffeescript should 'natively' adopt tagged template literals, as it has no equivalent functionality. The CS compiler will output ES6 code. This will enable ES6 functions expecting tagged template literals as input to be called directly from Coffeescript. The implementation will augment existing Coffeescript behaviour for single-line, multi-line and block strings. As such, the implementation will slightly differ from ES6 for multi-line block-strings, due to the spacing differences. Full proposal below.

Three final observations:

  1. To completely align Coffeescript string behaviour with ES6 template literals, the spacing semantics of a Coffeescript block-string would need changing to match a multi-line ES6 template literal (example below). This change would be backwards incompatible, so has not been proposed.
  2. I found an example where ES6 tagged template literals are nested inside one another. To support this, Coffeescript must also be able to nested interpolated strings. I did some testing, and discovered that CS cannot nest block strings (example below). Bug?
  3. There needs to be clarification where tagged template literal support should be added. Should support be added to the current Coffeescript compiler? Or should support only exist for a new 2.0 compiler? This is a purely ES6 feature after all.

Executive summary ends.

Bug-fix: ensure ES6 template literals can be used in CS embedded Javascript blocks

ES6 template literals cannot be used in an embedded Javascript block, since the new syntax uses backticks. Backticks are already used by Coffeescript to define the start and end of the JS block. This example blows up:

`
`3 + 2 = ${3 + 2}`
`

The proposed fix is for Coffeescript to allow embedded Javascript blocks to be delineated with 3-backticks markdown-style, and not escape backticks in the Javascript block. E.g.:

`` (should be 3 backticks, but github can't handle)
`3 + 2 = ${3 + 2}`
`` (should be 3 backticks, but github can't handle)

Reasoning:

New feature: adopt ES6 tagged template literals in CS

Summary of ES6 template literals

An ES6 'template literal' is a string literal that can include interpolated expressions, and can be multi-line. Example:

`3 + 2
 = ${3 + 2}`

//Output string
"3 + 2
 = 5"

An ES6 'tagged template literal' is a template literal, but prefixed with a function reference. The function is called to generate the output string, being supplied with the template literal in the form of expressions and the text between them. Example:

//Uppercase the given expressions
function upperExpr(text, ...expressions) {
  return text.reduce((accumulator, textPart, i) => {
    return accumulator + expressions[i - 1].toUpperCase() + textPart
  })
}

var name = 'Greg'
var food = 'sushi'
upperExpr`Hi ${name}
     Do you like ${food}?`

//Output string
"Hi GREG
     Do you like SUSHI?"

So the simpler 'template literal' can be seen as a tagged template literal with an invisible 'normal' function that just concatenates the given text-parts and expressions together.

Coffeescript proposal

The proposal is for Coffeescript to 'natively' adopt tagged template literals. The implementation will augment existing Coffeescript behaviour for single-line, multi-line and block strings. As such, the implementation will slightly differ from ES6 for multi-line block-strings, due to the spacing differences.

Single-line string:

upperExpr = (text, expressions...) ->
  text.reduce (accumulator, textPart, i) ->
    accumulator + expressions[i - 1].toUpperCase() + textPart

name = 'Greg'
food = 'sushi'

//CS input
upperExpr"Hi #{name} Do you like #{food}?"

//ES6 output
upperExpr`Hi ${name} Do you like ${food}?`;

Multiline string:

upperExpr = (text, expressions...) ->
  text.reduce (accumulator, textPart, i) ->
    accumulator + expressions[i - 1].toUpperCase() + textPart

name = 'Ismael'
food = 'whales'

//CS input
upperExpr"Hi #{name}. Do
 you like #{food}?"

//ES6 output
upperExpr`Hi ${name}. Do you like ${food}?`;

Block string:

upperExpr = (text, expressions...) ->
  text.reduce (accumulator, textPart, i) ->
    accumulator + expressions[i - 1].toUpperCase() + textPart

language = 'coffeescript'

//CS input
upperExpr"""
         <strong>
           cup of #{language}
         </strong>
         """

//ES6 output
upperExpr`<strong>\n  cup of ${language}\n</strong>`;

Differences between Coffeescript strings and ES6 string literals

Coffeescript already supported multi-line strings with 'block strings'. In ES6, multi-line strings are now supported with string literals. But the two differ in terms of spacing. Example:

CS block string:

"""
  <strong>
         cup of coffeescript
       </strong>
       """

outputs JS string of

"<strong>
       cup of coffeescript
     </strong>"

ES6 multi-line string literal:

`
  <strong>
         cup of coffeescript
       </strong>
       `
outputs JS string of

"
  <strong>
         cup of coffeescript
       </strong>
       "

To completely align Coffeescript string behaviour with ES6 template literals, the spacing semantics of a Coffeescript block-string would need changing to match a multi-line ES6 template literal. This change would be backwards incompatible, so has not been proposed.

Coffeescript bug? Can't nest block-strings inside block strings

I found an example where ES6 tagged template literals are nested inside one another. To support this, Coffeescript must also be able to nested interpolated strings. I did some testing, and discovered that CS cannot nest block strings. Bug?

Busted:

# Nested block strings
""" A test of #{"nesting" + func("""
quotes
""")} 
inside each other
"""

Works:

"A test of #{"nesting" + func("quotes")} inside each other"

"A test of #{"nesting" + func("""
quotes
quotes
""")} inside each other"

""" A test of #{"nesting" + func("2")} 
inside each other
"""
coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-18 18:29

@greghuc this is heroic. In the interest of organizing our discussion, would you mind splitting some of these out into separate issues? I’m not sure template literals and tagged template literals should really be discussed separately, so we could keep those here; but the block-backticks operator could get its own issue, as could the bug about nested block strings (that should perhaps be a bug issue on the main coffeescript repo).

BTW if you want to type three backticks in a code block in GitHub, you can use its other code block syntax, which is indenting with four spaces:

console.log(I’m a JS code block embedded in CoffeeScript! The time is ${Date.now()});

coffeescriptbot commented 6 years ago

From @lydell on 2016-09-18 18:31

A few little things here:

  1. I see no value in aligning the spacing semantics with ES2015+. In my opinion, CoffeeScript’s spacing semantics are far superior over ES2015+.
  2. ${} is invalid JavaScript, while "#{}" is valid CoffeeScript. (CoffeeScript allows “empty” interpolations.)
  3. I can’t reproduce the nesting-of-block-string bug:
$ coffee --version
CoffeeScript version 1.10.0
$ coffee -bpe '""" A test of #{"nesting" + func("""
  quotes
  """)} 
  inside each other
  """'
" A test of " + ("nesting" + func("quotes")) + " \ninside each other";
coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-18 18:41

@greghuc To try to answer your questions:

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-18 18:46

@lydell thanks for checking the bug - I can't reproduce it either on 1.10.0. I wasn't near a terminal, so was running Coffeescript with: http://js2coffee.thomaskalka.de/

coffeescriptbot commented 6 years ago

From @lydell on 2016-09-18 18:51

@greghuc Use http://coffeescript.org/#try: next time ;)

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-19 19:38

@greghuc I invited you to become a collaborator on https://github.com/GeoffreyBooth/coffeescript. You’re welcome to use a new branch on that repo to implement this, if you want me or @lydell or @JimPanic to have write access to your branch. Or you’re more than welcome to work in your own fork.

When the branch is ready for a PR, please submit it against the soon-to-be-created 2 branch on https://github.com/jashkenas/coffeescript, unless you implement tagged template literals separately from template literals. If you submit the two separately, tagged template literals could go into master (which is the 1.x, non-breaking-change branch) and template literals would go into 2.

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-20 18:18

@GeoffreyBooth I've spun out 2 new issues from the tagged template literals work:

coffeescriptbot commented 6 years ago

From @greghuc on 2016-09-20 18:22

So I'll make a start on adding tagged template literals to Coffeescript next week (Sept 26 onwards), as per the proposal in this thread. My time for open-source commits is constrained, so I'm not providing an ETA for completion right now.

This will also be my first time changing Coffeescript, so there'll be a ramp-up period whilst I understand how it works.

Are there any pointers for getting an overview of the Coffeescript compiler?

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-20 20:34

Hi @greghuc, I put up https://github.com/GeoffreyBooth/coffeescript/wiki/How-parsing-works which was consolidated from a few helpful comments left for me while I was working on the modules PR. I plan to remove the specific references to modules and move it to the regular coffeescript repo wiki soon. There are other good pages there too: https://github.com/jashkenas/coffeescript/wiki

I also created https://github.com/GeoffreyBooth/coffeescript-gulp to automate compiling and testing.

coffeescriptbot commented 6 years ago

From @jcollum on 2016-09-21 03:42

Hey I don't know where else to put this so:

I'm working on a new Hapi.js project the last few days. Decided to do it in ES2015 since a couple of junior devs are helping me. HOLY CRAP I MISS COFFEESCRIPT. All these damned extra braces and semicolons and such, clutterin' up my code. And the lack of easy key/value list comprehension. Etc.

So, thanks so much for participating here. Know that your work is appreciated.

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-09-21 04:00

Thanks @jcollum 😄 I think coffeescript6/discuss#32 is the issue you want 😉

coffeescriptbot commented 6 years ago

From @GeoffreyBooth on 2016-12-05 01:57

Released in CoffeeScript 1.12.0.