diff --git a/documentation/index.html.erb b/documentation/index.html.erb index 7a91aeaa..1a3cbcef 100644 --- a/documentation/index.html.erb +++ b/documentation/index.html.erb @@ -287,6 +287,14 @@ Expressions Value "x" + + --nodejs + + The node executable has some useful options you can set, + such as --debug and --max-stack-size. Use this + flag to forward options directly to Node.js. + +

@@ -537,6 +545,11 @@ coffee --bare --print --stdio by adding a meaningful return value, like true, or null, to the bottom of your function.

+

+ To step through a range comprehension in fixed-size chunks, + use by, for example:
+ evens = (x for x in [0..10] by 2) +

Comprehensions can also be used to iterate over the keys and values in an object. Use of to signal comprehension over the properties of diff --git a/index.html b/index.html index aed1eacb..0d1259d2 100644 --- a/index.html +++ b/index.html @@ -239,7 +239,7 @@ cubes = (function() {

- To install, first make sure you have a working copy of the latest tagged version of + To install, first make sure you have a working copy of the latest stable version of Node.js, and NPM (the Node Package Manager). You can then install CoffeeScript with NPM:

@@ -365,6 +365,14 @@ Expressions Value "x" + + --nodejs + + The node executable has some useful options you can set, + such as --debug and --max-stack-size. Use this + flag to forward options directly to Node.js. + +

@@ -913,6 +921,11 @@ countdown = (function() { by adding a meaningful return value, like true, or null, to the bottom of your function.

+

+ To step through a range comprehension in fixed-size chunks, + use by, for example:
+ evens = (x for x in [0..10] by 2) +

Comprehensions can also be used to iterate over the keys and values in an object. Use of to signal comprehension over the properties of diff --git a/lib/coffee-script.js b/lib/coffee-script.js index 0792ccab..475da437 100755 --- a/lib/coffee-script.js +++ b/lib/coffee-script.js @@ -15,7 +15,7 @@ return compile(content); }); } - exports.VERSION = '0.9.6'; + exports.VERSION = '1.0.0-pre'; exports.RESERVED = RESERVED; exports.helpers = require('./helpers'); exports.compile = compile = function(code, options) { diff --git a/lib/command.js b/lib/command.js index d43d2c33..7611ef24 100644 --- a/lib/command.js +++ b/lib/command.js @@ -1,5 +1,5 @@ (function() { - var ALL_SWITCHES, BANNER, CoffeeScript, DEPRECATED_SWITCHES, EventEmitter, SWITCHES, compileOptions, compileScript, compileScripts, compileStdio, exec, fs, helpers, lint, optionParser, optparse, opts, parseOptions, path, printLine, printTokens, printWarn, sources, spawn, usage, util, version, watch, writeJs, _ref; + var BANNER, CoffeeScript, EventEmitter, SWITCHES, compileOptions, compileScript, compileScripts, compileStdio, exec, forkNode, fs, helpers, lint, optionParser, optparse, opts, parseOptions, path, printLine, printTokens, printWarn, sources, spawn, usage, util, version, watch, writeJs, _ref; fs = require('fs'); path = require('path'); util = require('util'); @@ -16,15 +16,15 @@ return process.binding('stdio').writeError(line + '\n'); }; BANNER = 'Usage: coffee [options] path/to/script.coffee'; - SWITCHES = [['-c', '--compile', 'compile to JavaScript and save as .js files'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-o', '--output [DIR]', 'set the directory for compiled JavaScript'], ['-w', '--watch', 'watch scripts for changes, and recompile'], ['-p', '--print', 'print the compiled JavaScript to stdout'], ['-l', '--lint', 'pipe the compiled JavaScript through JSLint'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-e', '--eval', 'compile a string from the command line'], ['-r', '--require [FILE*]', 'require a library before executing your script'], ['-b', '--bare', 'compile without the top-level function wrapper'], ['-t', '--tokens', 'print the tokens that the lexer produces'], ['-n', '--nodes', 'print the parse tree that Jison produces'], ['-v', '--version', 'display CoffeeScript version'], ['-h', '--help', 'display this help message']]; - DEPRECATED_SWITCHES = [['--no-wrap', 'compile without the top-level function wrapper']]; - ALL_SWITCHES = SWITCHES.concat(DEPRECATED_SWITCHES); + SWITCHES = [['-c', '--compile', 'compile to JavaScript and save as .js files'], ['-i', '--interactive', 'run an interactive CoffeeScript REPL'], ['-o', '--output [DIR]', 'set the directory for compiled JavaScript'], ['-w', '--watch', 'watch scripts for changes, and recompile'], ['-p', '--print', 'print the compiled JavaScript to stdout'], ['-l', '--lint', 'pipe the compiled JavaScript through JSLint'], ['-s', '--stdio', 'listen for and compile scripts over stdio'], ['-e', '--eval', 'compile a string from the command line'], ['-r', '--require [FILE*]', 'require a library before executing your script'], ['-b', '--bare', 'compile without the top-level function wrapper'], ['-t', '--tokens', 'print the tokens that the lexer produces'], ['-n', '--nodes', 'print the parse tree that Jison produces'], ['--nodejs [ARGS]', 'pass options through to the "node" binary'], ['-v', '--version', 'display CoffeeScript version'], ['-h', '--help', 'display this help message']]; opts = {}; sources = []; optionParser = null; exports.run = function() { - var flags, separator; parseOptions(); + if (opts.nodejs) { + return forkNode(); + } if (opts.help) { return usage(); } @@ -43,21 +43,16 @@ if (!sources.length) { return require('./repl'); } - separator = sources.indexOf('--'); - flags = []; - if (separator >= 0) { - flags = sources.splice(separator + 1); - sources.pop(); - } if (opts.run) { - flags = sources.splice(1).concat(flags); + opts.literals = sources.splice(1).concat(opts.literals); } - process.ARGV = process.argv = process.argv.slice(0, 2).concat(flags); + process.ARGV = process.argv = process.argv.slice(0, 2).concat(opts.literals); return compileScripts(); }; compileScripts = function() { - var base, compile, source, _fn, _i, _len, _results; + var source, _fn, _i, _len, _results; _fn = function(source) { + var base, compile; base = path.join(source); compile = function(source, topLevel) { return path.exists(source, function(exists) { @@ -226,22 +221,30 @@ }; parseOptions = function() { var o; - optionParser = new optparse.OptionParser(ALL_SWITCHES, BANNER); + optionParser = new optparse.OptionParser(SWITCHES, BANNER); o = opts = optionParser.parse(process.argv.slice(2)); o.compile || (o.compile = !!o.output); o.run = !(o.compile || o.print || o.lint); o.print = !!(o.print || (o.eval || o.stdio && o.compile)); - sources = o.arguments; - if (opts['no-wrap']) { - return printWarn('--no-wrap is deprecated; please use --bare instead.'); - } + return sources = o.arguments; }; compileOptions = function(fileName) { return { fileName: fileName, - bare: opts.bare || opts['no-wrap'] + bare: opts.bare }; }; + forkNode = function() { + var args, nodeArgs; + nodeArgs = opts.nodejs.split(/\s+/); + args = process.argv.slice(1); + args.splice(args.indexOf('--nodejs'), 2); + return spawn(process.execPath, nodeArgs.concat(args), { + cwd: process.cwd(), + env: process.env, + customFds: [0, 1, 2] + }); + }; usage = function() { printLine((new optparse.OptionParser(SWITCHES, BANNER)).help()); return process.exit(0); diff --git a/lib/lexer.js b/lib/lexer.js index 8982974b..e7b2e08f 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -128,7 +128,7 @@ this.token('STRING', (string = match[0]).replace(MULTILINER, '\\\n')); break; case '"': - if (!(string = this.balancedString(this.chunk, [['"', '"'], ['#{', '}']]))) { + if (!(string = this.balancedString(this.chunk, '"'))) { return 0; } if (0 < string.indexOf('#{', 1)) { @@ -447,34 +447,32 @@ Lexer.prototype.assignmentError = function() { throw SyntaxError("Reserved word \"" + (this.value()) + "\" on line " + (this.line + 1) + " can't be assigned"); }; - Lexer.prototype.balancedString = function(str, delimited, options) { - var i, open, pair, stack, _i, _len, _ref; - if (options == null) { - options = {}; - } - stack = [delimited[0]]; + Lexer.prototype.balancedString = function(str, end) { + var i, letter, prev, stack, _ref; + stack = [end]; for (i = 1, _ref = str.length; (1 <= _ref ? i < _ref : i > _ref); (1 <= _ref ? i += 1 : i -= 1)) { - switch (str.charAt(i)) { + switch (letter = str.charAt(i)) { case '\\': i++; continue; - case stack[stack.length - 1][1]: + case end: stack.pop(); if (!stack.length) { return str.slice(0, i + 1); } + end = stack[stack.length - 1]; continue; } - for (_i = 0, _len = delimited.length; _i < _len; _i++) { - pair = delimited[_i]; - if ((open = pair[0]) === str.substr(i, open.length)) { - stack.push(pair); - i += open.length - 1; - break; - } + if (end === '}' && (letter === '"' || letter === "'")) { + stack.push(end = letter); + } else if (end === '}' && letter === '{') { + stack.push(end = '}'); + } else if (end === '"' && prev === '#' && letter === '{') { + stack.push(end = '}'); } + prev = letter; } - throw new Error("unterminated " + (stack.pop()[0]) + " on line " + (this.line + 1)); + throw new Error("missing " + (stack.pop()) + ", starting on line " + (this.line + 1)); }; Lexer.prototype.interpolateString = function(str, options) { var expr, heredoc, i, inner, interpolated, letter, nested, pi, regex, tag, tokens, value, _len, _ref, _ref2, _ref3; @@ -490,7 +488,7 @@ i += 1; continue; } - if (!(letter === '#' && str.charAt(i + 1) === '{' && (expr = this.balancedString(str.slice(i + 1), [['{', '}']])))) { + if (!(letter === '#' && str.charAt(i + 1) === '{' && (expr = this.balancedString(str.slice(i + 1), '}')))) { continue; } if (pi < i) { diff --git a/lib/nodes.js b/lib/nodes.js index 2ce5b1e3..5928eb61 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -1893,7 +1893,6 @@ args.unshift(new Literal('this')); } body.expressions[idx] = new Call(base, args); - o.sharedScope = true; defs += this.tab + new Assign(ref, fn).compile(o, LEVEL_TOP) + ';\n'; } return defs; diff --git a/lib/optparse.js b/lib/optparse.js index 4147a361..6b1bf75f 100755 --- a/lib/optparse.js +++ b/lib/optparse.js @@ -8,11 +8,16 @@ OptionParser.prototype.parse = function(args) { var arg, i, isOption, matchedRule, options, rule, value, _i, _len, _len2, _ref; options = { - arguments: [] + arguments: [], + literals: [] }; args = normalizeArguments(args); for (i = 0, _len = args.length; i < _len; i++) { arg = args[i]; + if (arg === '--') { + options.literals = args.slice(i + 1); + break; + } isOption = !!(arg.match(LONG_FLAG) || arg.match(SHORT_FLAG)); matchedRule = false; _ref = this.rules; diff --git a/package.json b/package.json index 5291ac9e..5e101d46 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Unfancy JavaScript", "keywords": ["javascript", "language", "coffeescript", "compiler"], "author": "Jeremy Ashkenas", - "version": "0.9.6", + "version": "1.0.0-pre", "licenses": [{ "type": "MIT", "url": "http://github.com/jashkenas/coffee-script/raw/master/LICENSE" diff --git a/src/coffee-script.coffee b/src/coffee-script.coffee index b4073ef4..21aa9c55 100755 --- a/src/coffee-script.coffee +++ b/src/coffee-script.coffee @@ -20,7 +20,7 @@ else if require.registerExtension require.registerExtension '.coffee', (content) -> compile content # The current CoffeeScript version number. -exports.VERSION = '0.9.6' +exports.VERSION = '1.0.0-pre' # Words that cannot be used as identifiers in CoffeeScript code exports.RESERVED = RESERVED diff --git a/src/command.coffee b/src/command.coffee index fcf12882..d252af45 100644 --- a/src/command.coffee +++ b/src/command.coffee @@ -39,17 +39,11 @@ SWITCHES = [ ['-b', '--bare', 'compile without the top-level function wrapper'] ['-t', '--tokens', 'print the tokens that the lexer produces'] ['-n', '--nodes', 'print the parse tree that Jison produces'] + [ '--nodejs [ARGS]', 'pass options through to the "node" binary'] ['-v', '--version', 'display CoffeeScript version'] ['-h', '--help', 'display this help message'] ] -# Switches that are still supported, but will cause a warning message. -DEPRECATED_SWITCHES = [ - ['--no-wrap', 'compile without the top-level function wrapper'] -] - -ALL_SWITCHES = SWITCHES.concat DEPRECATED_SWITCHES - # Top-level objects shared by all the functions. opts = {} sources = [] @@ -60,20 +54,16 @@ optionParser = null # `--` will be passed verbatim to your script as arguments in `process.argv` exports.run = -> parseOptions() - return usage() if opts.help - return version() if opts.version - return require './repl' if opts.interactive - return compileStdio() if opts.stdio - return compileScript null, sources[0] if opts.eval - return require './repl' unless sources.length - separator = sources.indexOf '--' - flags = [] - if separator >= 0 - flags = sources.splice separator + 1 - sources.pop() + return forkNode() if opts.nodejs + return usage() if opts.help + return version() if opts.version + return require './repl' if opts.interactive + return compileStdio() if opts.stdio + return compileScript null, sources[0] if opts.eval + return require './repl' unless sources.length if opts.run - flags = sources.splice(1).concat flags - process.ARGV = process.argv = process.argv.slice(0, 2).concat flags + opts.literals = sources.splice(1).concat opts.literals + process.ARGV = process.argv = process.argv.slice(0, 2).concat opts.literals compileScripts() # Asynchronously read in each CoffeeScript in a list of source files and @@ -180,17 +170,26 @@ printTokens = (tokens) -> # Use the [OptionParser module](optparse.html) to extract all options from # `process.argv` that are specified in `SWITCHES`. parseOptions = -> - optionParser = new optparse.OptionParser ALL_SWITCHES, BANNER + optionParser = new optparse.OptionParser SWITCHES, BANNER o = opts = optionParser.parse process.argv.slice 2 o.compile or= !!o.output o.run = not (o.compile or o.print or o.lint) o.print = !! (o.print or (o.eval or o.stdio and o.compile)) sources = o.arguments - if opts['no-wrap'] - printWarn '--no-wrap is deprecated; please use --bare instead.' # The compile-time options to pass to the CoffeeScript compiler. -compileOptions = (fileName) -> {fileName, bare: opts.bare or opts['no-wrap']} +compileOptions = (fileName) -> {fileName, bare: opts.bare} + +# Start up a new Node.js instance with the arguments in `--nodejs` passed to +# the `node` binary, preserving the other options. +forkNode = -> + nodeArgs = opts.nodejs.split /\s+/ + args = process.argv[1..] + args.splice args.indexOf('--nodejs'), 2 + spawn process.execPath, nodeArgs.concat(args), + cwd: process.cwd() + env: process.env + customFds: [0, 1, 2] # Print the `--help` usage message and exit. Deprecated switches are not # shown. diff --git a/src/lexer.coffee b/src/lexer.coffee index 5ba012b8..81862829 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -139,7 +139,7 @@ exports.Lexer = class Lexer return 0 unless match = SIMPLESTR.exec @chunk @token 'STRING', (string = match[0]).replace MULTILINER, '\\\n' when '"' - return 0 unless string = @balancedString @chunk, [['"', '"'], ['#{', '}']] + return 0 unless string = @balancedString @chunk, '"' if 0 < string.indexOf '#{', 1 @interpolateString string.slice 1, -1 else @@ -387,22 +387,27 @@ exports.Lexer = class Lexer # a series of delimiters, all of which must be nested correctly within the # contents of the string. This method allows us to have strings within # interpolations within strings, ad infinitum. - balancedString: (str, delimited, options = {}) -> - stack = [delimited[0]] + balancedString: (str, end) -> + stack = [end] for i in [1...str.length] - switch str.charAt i + switch letter = str.charAt i when '\\' i++ continue - when stack[stack.length - 1][1] + when end stack.pop() - return str.slice 0, i + 1 unless stack.length + unless stack.length + return str.slice 0, i + 1 + end = stack[stack.length - 1] continue - for pair in delimited when (open = pair[0]) is str.substr i, open.length - stack.push pair - i += open.length - 1 - break - throw new Error "unterminated #{ stack.pop()[0] } on line #{ @line + 1 }" + if end is '}' and letter in ['"', "'"] + stack.push end = letter + else if end is '}' and letter is '{' + stack.push end = '}' + else if end is '"' and prev is '#' and letter is '{' + stack.push end = '}' + prev = letter + throw new Error "missing #{ stack.pop() }, starting on line #{ @line + 1 }" # Expand variables and expressions inside double-quoted strings using @@ -423,7 +428,7 @@ exports.Lexer = class Lexer i += 1 continue unless letter is '#' and str.charAt(i+1) is '{' and - (expr = @balancedString str.slice(i+1), [['{', '}']]) + (expr = @balancedString str.slice(i + 1), '}') continue tokens.push ['NEOSTRING', str.slice(pi, i)] if pi < i inner = expr.slice(1, -1) diff --git a/src/nodes.coffee b/src/nodes.coffee index 16a30664..bbbbc756 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1512,7 +1512,6 @@ exports.For = class For extends Base [val.base, base] = [base, val] args.unshift new Literal 'this' body.expressions[idx] = new Call base, args - o.sharedScope = yes defs += @tab + new Assign(ref, fn).compile(o, LEVEL_TOP) + ';\n' defs diff --git a/src/optparse.coffee b/src/optparse.coffee index 3f486783..19a95a71 100644 --- a/src/optparse.coffee +++ b/src/optparse.coffee @@ -18,13 +18,18 @@ exports.OptionParser = class OptionParser # Parse the list of arguments, populating an `options` object with all of the # specified options, and returning it. `options.arguments` will be an array - # containing the remaining non-option arguments. This is a simpler API than - # many option parsers that allow you to attach callback actions for every - # flag. Instead, you're responsible for interpreting the options object. + # containing the remaining non-option arguments. `options.literals` will be + # an array of options that are meant to be passed through directly to the + # executing script. This is a simpler API than many option parsers that allow + # you to attach callback actions for every flag. Instead, you're responsible + # for interpreting the options object. parse: (args) -> - options = arguments: [] + options = arguments: [], literals: [] args = normalizeArguments args for arg, i in args + if arg is '--' + options.literals = args[(i + 1)..] + break isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG)) matchedRule = no for rule in @rules diff --git a/test/test_comprehensions.coffee b/test/test_comprehensions.coffee index 37467962..22690020 100644 --- a/test/test_comprehensions.coffee +++ b/test/test_comprehensions.coffee @@ -249,3 +249,12 @@ for d in a.b?.c e = d eq e, 3 + + +# Issue #948. Capturing loop variables. +funcs = [] +for y in [1, 2, 3] + z = y + funcs.push -> "y is #{y} and z is #{z}" + +eq funcs[1](), "y is 2 and z is 2" diff --git a/test/test_strings.coffee b/test/test_strings.coffee index cbbe29ac..63e2a451 100644 --- a/test/test_strings.coffee +++ b/test/test_strings.coffee @@ -107,3 +107,12 @@ eq 'multiline nested "interpolations" work', """multiline #{ "\"interpolations\"" }" } work""" + + +# Issue #923: Tricky interpolation. +eq "#{ "{" }", "{" + +eq "#{ '#{}}' } }", '#{}} }' + +eq "#{"'#{ ({a: "b#{1}"}['a']) }'"}", "'b1'" +