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

Triple backticks to allow creation of JavaScript blocks #4357

Merged
merged 9 commits into from
Nov 19, 2016
15 changes: 10 additions & 5 deletions lib/coffee-script/lexer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions src/lexer.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -292,9 +292,16 @@ exports.Lexer = class Lexer

# Matches JavaScript interpolated directly into the source via backticks.
jsToken: ->
return 0 unless @chunk.charAt(0) is '`' and match = JSTOKEN.exec @chunk
@token 'JS', (script = match[0])[1...-1], 0, script.length
script.length
return 0 unless @chunk.charAt(0) is '`' and
(match = HERE_JSTOKEN.exec(@chunk) or JSTOKEN.exec(@chunk))
# Convert escaped backticks to backticks, and escaped backslashes
# just before escaped backticks to backslashes
script = match[1].replace /\\+(`|$)/g, (string) ->
# `string` is always a value like '\`', '\\\`', '\\\\\`', etc.
# By reducing it to its latter half, we turn '\`' to '`', '\\\`' to '\`', etc.
string[-Math.ceil(string.length / 2)..]
@token 'JS', script, 0, match[0].length
match[0].length

# Matches regular expression literals, as well as multiline extended ones.
# Lexing regular expressions is difficult to distinguish from division, so we
Expand Down Expand Up @@ -900,7 +907,8 @@ CODE = /^[-=]>/

MULTI_DENT = /^(?:\n[^\n\S]*)+/

JSTOKEN = /^`[^\\`]*(?:\\.[^\\`]*)*`/
JSTOKEN = ///^ `(?!``) ((?: [^`\\] | \\[\s\S] )*) ` ///
HERE_JSTOKEN = ///^ ``` ((?: [^`\\] | \\[\s\S] | `(?!``) )*) ``` ///

# String-matching-regexes.
STRING_START = /^(?:'''|"""|'|")/
Expand Down
74 changes: 67 additions & 7 deletions test/javascript_literals.coffee
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
# Javascript Literals
# JavaScript Literals
# -------------------

# TODO: refactor javascript literal tests
# TODO: add indexing and method invocation tests: `[1]`[0] is 1, `function(){}`.call()
test "inline JavaScript is evaluated", ->
eq '\\`', `
// Inline JS
"\\\\\`"
`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my proposed escaping rules, this test needs to be written like this:

eq '\\`', `
  // Inline JS
  "\\\\\`"
`

or like this:

eq '\\a`', `
  // Inline JS
  "\\a\`"
`


eq '\\`', `
// Inline JS
"\\\`"
`
test "escaped backticks are output correctly", ->
`var a = \`2 + 2 = ${4}\``
eq a, '2 + 2 = 4'

test "backslashes before a newline don’t break JavaScript blocks", ->
`var a = \`To be, or not\\
to be.\``
eq a, '''
To be, or not\\
to be.'''

test "block inline JavaScript is evaluated", ->
```
var a = 1;
var b = 2;
```
c = 3
```var d = 4;```
eq a + b + c + d, 10

test "block inline JavaScript containing backticks", ->
```
// This is a comment with `backticks`
var a = 42;
var b = `foo ${'bar'}`;
Copy link
Contributor

@greghuc greghuc Nov 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no 'eq' for variable b in the ""block inline JavaScript containing backticks" test. Is this correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as I didn't want to rely on ES2015 in the 1.x branch. Though I guess I am already for it to parse the b line. Hmm...

var c = 3;
var d = 'foo`bar`';
```
eq a + c, 45
eq b, 'foo bar'
eq d, 'foo`bar`'

test "block JavaScript can end with an escaped backtick character", ->
```var a = \`hello\````
```
var b = \`world${'!'}\````
eq a, 'hello'
eq b, 'world!'

test "JavaScript block only escapes backslashes followed by backticks", ->
eq `'\\\n'`, '\\\n'

test "escaped JavaScript blocks speed round", ->
# The following has escaped backslashes because they’re required in strings, but the intent is this:
# `hello` → hello;
# `\`hello\`` → `hello`;
# `\`Escaping backticks in JS: \\\`hello\\\`\`` → `Escaping backticks in JS: \`hello\``;
# `Single backslash: \ ` → Single backslash: \ ;
# `Double backslash: \\ ` → Double backslash: \\ ;
# `Single backslash at EOS: \\` → Single backslash at EOS: \;
# `Double backslash at EOS: \\\\` → Double backslash at EOS: \\;
for [input, output] in [
['`hello`', 'hello;']
['`\\`hello\\``', '`hello`;']
['`\\`Escaping backticks in JS: \\\\\\`hello\\\\\\`\\``', '`Escaping backticks in JS: \\`hello\\``;']
['`Single backslash: \\ `', 'Single backslash: \\ ;']
['`Double backslash: \\\\ `', 'Double backslash: \\\\ ;']
['`Single backslash at EOS: \\\\`', 'Single backslash at EOS: \\;']
['`Double backslash at EOS: \\\\\\\\`', 'Double backslash at EOS: \\\\;']
]
eq CoffeeScript.compile(input, bare: yes), "#{output}\n\n"