Skip to content

Commit

Permalink
Triple backticks to allow creation of JavaScript blocks (#4357)
Browse files Browse the repository at this point in the history
* Support JavaScript code blocks set apart by triple backticks (``` ... ```)

* Add test for escaped backticks

* Remove TODOs for things we’re never going to support

* Convert escaped backticks to backticks; update tests

* Block inline JavaScript can end with an escaped backtick character

* Updated JavaScript token regexes per @lydell

* In JavaScript blocks, escape backslashes when they immediately precede backticks; additional tests

* Test that we don’t break backslash escaping in JavaScript literals
  • Loading branch information
GeoffreyBooth authored Nov 19, 2016
1 parent 78e1f43 commit 073e147
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 16 deletions.
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
"\\\\\`"
`

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'}`;
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"

0 comments on commit 073e147

Please sign in to comment.