browser.coffee | |
---|---|
Override exported methods for non-Node.js engines. | CoffeeScript = require './coffee-script'
CoffeeScript.require = require |
Use standard JavaScript | CoffeeScript.eval = (code, options = {}) ->
options.bare ?= on
eval CoffeeScript.compile code, options |
Running code does not provide access to this scope. | CoffeeScript.run = (code, options = {}) ->
options.bare = on
Function(CoffeeScript.compile code, options)() |
If we're not in a browser environment, we're finished with the public API. | return unless window? |
Load a remote script from the current domain via XHR. | CoffeeScript.load = (url, callback) ->
xhr = if window.ActiveXObject
new window.ActiveXObject('Microsoft.XMLHTTP')
else
new XMLHttpRequest()
xhr.open 'GET', url, true
xhr.overrideMimeType 'text/plain' if 'overrideMimeType' of xhr
xhr.onreadystatechange = ->
if xhr.readyState is 4
if xhr.status in [0, 200]
CoffeeScript.run xhr.responseText
else
throw new Error "Could not load #{url}"
callback() if callback
xhr.send null |
Activate CoffeeScript in the browser by having it compile and evaluate
all script tags with a content-type of | runScripts = ->
scripts = document.getElementsByTagName 'script'
coffees = (s for s in scripts when s.type is 'text/coffeescript')
index = 0
length = coffees.length
do execute = ->
script = coffees[index++]
if script?.type is 'text/coffeescript'
if script.src
CoffeeScript.load script.src, execute
else
CoffeeScript.run script.innerHTML
execute()
null |
Listen for window load, both in browsers and in IE. | if window.addEventListener
addEventListener 'DOMContentLoaded', runScripts, no
else
attachEvent 'onload', runScripts
|
cake.coffee | |
---|---|
Running | |
External dependencies. | fs = require 'fs'
path = require 'path'
helpers = require './helpers'
optparse = require './optparse'
CoffeeScript = require './coffee-script'
existsSync = fs.existsSync or path.existsSync |
Keep track of the list of defined tasks, the accepted options, and so on. | tasks = {}
options = {}
switches = []
oparse = null |
Mixin the top-level Cake functions for Cakefiles to use directly. | helpers.extend global, |
Define a Cake task with a short name, an optional sentence description, and the function to run as the action itself. | task: (name, description, action) ->
[action, description] = [description, action] unless action
tasks[name] = {name, description, action} |
Define an option that the Cakefile accepts. The parsed options hash, containing all of the command-line options passed, will be made available as the first argument to the action. | option: (letter, flag, description) ->
switches.push [letter, flag, description] |
Invoke another task in the current Cakefile. | invoke: (name) ->
missingTask name unless tasks[name]
tasks[name].action options |
Run | exports.run = ->
global.__originalDirname = fs.realpathSync '.'
process.chdir cakefileDirectory __originalDirname
args = process.argv[2..]
CoffeeScript.run fs.readFileSync('Cakefile').toString(), filename: 'Cakefile'
oparse = new optparse.OptionParser switches
return printTasks() unless args.length
try
options = oparse.parse(args)
catch e
return fatalError "#{e}"
invoke arg for arg in options.arguments |
Display the list of Cake tasks in a format similar to | printTasks = ->
relative = path.relative or path.resolve
cakefilePath = path.join relative(__originalDirname, process.cwd()), 'Cakefile'
console.log "#{cakefilePath} defines the following tasks:\n"
for name, task of tasks
spaces = 20 - name.length
spaces = if spaces > 0 then Array(spaces + 1).join(' ') else ''
desc = if task.description then "# #{task.description}" else ''
console.log "cake #{name}#{spaces} #{desc}"
console.log oparse.help() if switches.length |
Print an error and exit when attempting to use an invalid task/option. | fatalError = (message) ->
console.error message + '\n'
console.log 'To see a list of all tasks/options, run "cake"'
process.exit 1
missingTask = (task) -> fatalError "No such task: #{task}" |
When | cakefileDirectory = (dir) ->
return dir if existsSync path.join dir, 'Cakefile'
parent = path.normalize path.join dir, '..'
return cakefileDirectory parent unless parent is dir
throw new Error "Cakefile not found in #{process.cwd()}"
|
coffee-script.coffee | |
---|---|
CoffeeScript can be used both on the server, as a command-line compiler based on Node.js/V8, or to run CoffeeScripts directly in the browser. This module contains the main entry functions for tokenizing, parsing, and compiling source CoffeeScript into JavaScript. If included on a webpage, it will automatically sniff out, compile, and
execute all scripts present in | fs = require 'fs'
path = require 'path'
{Lexer,RESERVED} = require './lexer'
{parser} = require './parser'
vm = require 'vm'
stripBOM = (content) ->
if content.charCodeAt(0) is 0xFEFF then content.substring 1 else content
if require.extensions
require.extensions['.coffee'] = (module, filename) ->
content = compile stripBOM(fs.readFileSync filename, 'utf8'), {filename}
module._compile content, filename |
The current CoffeeScript version number. | exports.VERSION = '1.4.0' |
Words that cannot be used as identifiers in CoffeeScript code | exports.RESERVED = RESERVED |
Expose helpers for testing. | exports.helpers = require './helpers' |
Compile a string of CoffeeScript code to JavaScript, using the Coffee/Jison compiler. | exports.compile = compile = (code, options = {}) ->
{merge} = exports.helpers
try
js = (parser.parse lexer.tokenize code).compile options
return js unless options.header
catch err
err.message = "In #{options.filename}, #{err.message}" if options.filename
throw err
header = "Generated by CoffeeScript #{@VERSION}"
"// #{header}\n#{js}" |
Tokenize a string of CoffeeScript code, and return the array of tokens. | exports.tokens = (code, options) ->
lexer.tokenize code, options |
Parse a string of CoffeeScript code or an array of lexed tokens, and
return the AST. You can then compile it by calling | exports.nodes = (source, options) ->
if typeof source is 'string'
parser.parse lexer.tokenize source, options
else
parser.parse source |
Compile and execute a string of CoffeeScript (on the server), correctly
setting | exports.run = (code, options = {}) ->
mainModule = require.main |
Set the filename. | mainModule.filename = process.argv[1] =
if options.filename then fs.realpathSync(options.filename) else '.' |
Clear the module cache. | mainModule.moduleCache and= {} |
Assign paths for node_modules loading | mainModule.paths = require('module')._nodeModulePaths path.dirname fs.realpathSync options.filename |
Compile. | if path.extname(mainModule.filename) isnt '.coffee' or require.extensions
mainModule._compile compile(code, options), mainModule.filename
else
mainModule._compile code, mainModule.filename |
Compile and evaluate a string of CoffeeScript (in a Node.js-like environment). The CoffeeScript REPL uses this to run the input. | exports.eval = (code, options = {}) ->
return unless code = code.trim()
Script = vm.Script
if Script
if options.sandbox?
if options.sandbox instanceof Script.createContext().constructor
sandbox = options.sandbox
else
sandbox = Script.createContext()
sandbox[k] = v for own k, v of options.sandbox
sandbox.global = sandbox.root = sandbox.GLOBAL = sandbox
else
sandbox = global
sandbox.__filename = options.filename || 'eval'
sandbox.__dirname = path.dirname sandbox.__filename |
define module/require only if they chose not to specify their own | unless sandbox isnt global or sandbox.module or sandbox.require
Module = require 'module'
sandbox.module = _module = new Module(options.modulename || 'eval')
sandbox.require = _require = (path) -> Module._load path, _module, true
_module.filename = sandbox.__filename
_require[r] = require[r] for r in Object.getOwnPropertyNames require when r isnt 'paths' |
use the same hack node currently uses for their own REPL | _require.paths = _module.paths = Module._nodeModulePaths process.cwd()
_require.resolve = (request) -> Module._resolveFilename request, _module
o = {}
o[k] = v for own k, v of options
o.bare = on # ensure return value
js = compile code, o
if sandbox is global
vm.runInThisContext js
else
vm.runInContext js, sandbox |
Instantiate a Lexer for our use here. | lexer = new Lexer |
The real Lexer produces a generic stream of tokens. This object provides a thin wrapper around it, compatible with the Jison API. We can then pass it directly as a "Jison lexer". | parser.lexer =
lex: ->
[tag, @yytext, @yylineno] = @tokens[@pos++] or ['']
tag
setInput: (@tokens) ->
@pos = 0
upcomingInput: ->
""
parser.yy = require './nodes'
|
command.coffee | |
---|---|
The | |
External dependencies. | fs = require 'fs'
path = require 'path'
helpers = require './helpers'
optparse = require './optparse'
CoffeeScript = require './coffee-script'
{spawn, exec} = require 'child_process'
{EventEmitter} = require 'events'
exists = fs.exists or path.exists |
Allow CoffeeScript to emit Node.js events. | helpers.extend CoffeeScript, new EventEmitter
printLine = (line) -> process.stdout.write line + '\n'
printWarn = (line) -> process.stderr.write line + '\n'
hidden = (file) -> /^\.|~$/.test file |
The help banner that is printed when | BANNER = '''
Usage: coffee [options] path/to/script.coffee -- [args]
If called without options, `coffee` will run your script.
''' |
The list of all the valid option flags that | SWITCHES = [
['-b', '--bare', 'compile without a top-level function wrapper']
['-c', '--compile', 'compile to JavaScript and save as .js files']
['-e', '--eval', 'pass a string from the command line as input']
['-h', '--help', 'display this help message']
['-i', '--interactive', 'run an interactive CoffeeScript REPL']
['-j', '--join [FILE]', 'concatenate the source CoffeeScript before compiling']
['-l', '--lint', 'pipe the compiled JavaScript through JavaScript Lint']
['-n', '--nodes', 'print out the parse tree that the parser produces']
[ '--nodejs [ARGS]', 'pass options directly to the "node" binary']
['-o', '--output [DIR]', 'set the output directory for compiled JavaScript']
['-p', '--print', 'print out the compiled JavaScript']
['-r', '--require [FILE*]', 'require a library before executing your script']
['-s', '--stdio', 'listen for and compile scripts over stdio']
['-t', '--tokens', 'print out the tokens that the lexer/rewriter produce']
['-v', '--version', 'display the version number']
['-w', '--watch', 'watch scripts for changes and rerun commands']
] |
Top-level objects shared by all the functions. | opts = {}
sources = []
sourceCode = []
notSources = {}
watchers = {}
optionParser = null |
Run | exports.run = ->
parseOptions()
return forkNode() if opts.nodejs
return usage() if opts.help
return version() if opts.version
loadRequires() if opts.require
return require './repl' if opts.interactive
if opts.watch and !fs.watch
return printWarn "The --watch feature depends on Node v0.6.0+. You are running #{process.version}."
return compileStdio() if opts.stdio
return compileScript null, sources[0] if opts.eval
return require './repl' unless sources.length
literals = if opts.run then sources.splice 1 else []
process.argv = process.argv[0..1].concat literals
process.argv[0] = 'coffee'
process.execPath = require.main.filename
for source in sources
compilePath source, yes, path.normalize source |
Compile a path, which could be a script or a directory. If a directory is passed, recursively compile all '.coffee' extension source files in it and all subdirectories. | compilePath = (source, topLevel, base) ->
fs.stat source, (err, stats) ->
throw err if err and err.code isnt 'ENOENT'
if err?.code is 'ENOENT'
if topLevel and source[-7..] isnt '.coffee'
source = sources[sources.indexOf(source)] = "#{source}.coffee"
return compilePath source, topLevel, base
if topLevel
console.error "File not found: #{source}"
process.exit 1
return
if stats.isDirectory()
watchDir source, base if opts.watch
fs.readdir source, (err, files) ->
throw err if err and err.code isnt 'ENOENT'
return if err?.code is 'ENOENT'
index = sources.indexOf source
files = files.filter (file) -> not hidden file
sources[index..index] = (path.join source, file for file in files)
sourceCode[index..index] = files.map -> null
files.forEach (file) ->
compilePath (path.join source, file), no, base
else if topLevel or path.extname(source) is '.coffee'
watch source, base if opts.watch
fs.readFile source, (err, code) ->
throw err if err and err.code isnt 'ENOENT'
return if err?.code is 'ENOENT'
compileScript(source, code.toString(), base)
else
notSources[source] = yes
removeSource source, base |
Compile a single source script, containing the given code, according to the
requested options. If evaluating the script directly sets | compileScript = (file, input, base) ->
o = opts
options = compileOptions file
try
t = task = {file, input, options}
CoffeeScript.emit 'compile', task
if o.tokens then printTokens CoffeeScript.tokens t.input
else if o.nodes then printLine CoffeeScript.nodes(t.input).toString().trim()
else if o.run then CoffeeScript.run t.input, t.options
else if o.join and t.file isnt o.join
sourceCode[sources.indexOf(t.file)] = t.input
compileJoin()
else
t.output = CoffeeScript.compile t.input, t.options
CoffeeScript.emit 'success', task
if o.print then printLine t.output.trim()
else if o.compile then writeJs t.file, t.output, base
else if o.lint then lint t.file, t.output
catch err
CoffeeScript.emit 'failure', err, task
return if CoffeeScript.listeners('failure').length
return printLine err.message + '\x07' if o.watch
printWarn err instanceof Error and err.stack or "ERROR: #{err}"
process.exit 1 |
Attach the appropriate listeners to compile scripts incoming over stdin, and write them back to stdout. | compileStdio = ->
code = ''
stdin = process.openStdin()
stdin.on 'data', (buffer) ->
code += buffer.toString() if buffer
stdin.on 'end', ->
compileScript null, code |
If all of the source files are done being read, concatenate and compile them together. | joinTimeout = null
compileJoin = ->
return unless opts.join
unless sourceCode.some((code) -> code is null)
clearTimeout joinTimeout
joinTimeout = wait 100, ->
compileScript opts.join, sourceCode.join('\n'), opts.join |
Load files that are to-be-required before compilation occurs. | loadRequires = ->
realFilename = module.filename
module.filename = '.'
require req for req in opts.require
module.filename = realFilename |
Watch a source CoffeeScript file using | watch = (source, base) ->
prevStats = null
compileTimeout = null
watchErr = (e) ->
if e.code is 'ENOENT'
return if sources.indexOf(source) is -1
try
rewatch()
compile()
catch e
removeSource source, base, yes
compileJoin()
else throw e
compile = ->
clearTimeout compileTimeout
compileTimeout = wait 25, ->
fs.stat source, (err, stats) ->
return watchErr err if err
return rewatch() if prevStats and stats.size is prevStats.size and
stats.mtime.getTime() is prevStats.mtime.getTime()
prevStats = stats
fs.readFile source, (err, code) ->
return watchErr err if err
compileScript(source, code.toString(), base)
rewatch()
try
watcher = fs.watch source, compile
catch e
watchErr e
rewatch = ->
watcher?.close()
watcher = fs.watch source, compile |
Watch a directory of files for new additions. | watchDir = (source, base) ->
readdirTimeout = null
try
watcher = fs.watch source, ->
clearTimeout readdirTimeout
readdirTimeout = wait 25, ->
fs.readdir source, (err, files) ->
if err
throw err unless err.code is 'ENOENT'
watcher.close()
return unwatchDir source, base
for file in files when not hidden(file) and not notSources[file]
file = path.join source, file
continue if sources.some (s) -> s.indexOf(file) >= 0
sources.push file
sourceCode.push null
compilePath file, no, base
catch e
throw e unless e.code is 'ENOENT'
unwatchDir = (source, base) ->
prevSources = sources[..]
toRemove = (file for file in sources when file.indexOf(source) >= 0)
removeSource file, base, yes for file in toRemove
return unless sources.some (s, i) -> prevSources[i] isnt s
compileJoin() |
Remove a file from our source list, and source code cache. Optionally remove the compiled JS version as well. | removeSource = (source, base, removeJs) ->
index = sources.indexOf source
sources.splice index, 1
sourceCode.splice index, 1
if removeJs and not opts.join
jsPath = outputPath source, base
exists jsPath, (itExists) ->
if itExists
fs.unlink jsPath, (err) ->
throw err if err and err.code isnt 'ENOENT'
timeLog "removed #{source}" |
Get the corresponding output JavaScript path for a source file. | outputPath = (source, base) ->
filename = path.basename(source, path.extname(source)) + '.js'
srcDir = path.dirname source
baseDir = if base is '.' then srcDir else srcDir.substring base.length
dir = if opts.output then path.join opts.output, baseDir else srcDir
path.join dir, filename |
Write out a JavaScript source file with the compiled code. By default, files
are written out in | writeJs = (source, js, base) ->
jsPath = outputPath source, base
jsDir = path.dirname jsPath
compile = ->
js = ' ' if js.length <= 0
fs.writeFile jsPath, js, (err) ->
if err
printLine err.message
else if opts.compile and opts.watch
timeLog "compiled #{source}"
exists jsDir, (itExists) ->
if itExists then compile() else exec "mkdir -p #{jsDir}", compile |
Convenience for cleaner setTimeouts. | wait = (milliseconds, func) -> setTimeout func, milliseconds |
When watching scripts, it's useful to log changes with the timestamp. | timeLog = (message) ->
console.log "#{(new Date).toLocaleTimeString()} - #{message}" |
Pipe compiled JS through JSLint (requires a working | lint = (file, js) ->
printIt = (buffer) -> printLine file + ':\t' + buffer.toString().trim()
conf = __dirname + '/../../extras/jsl.conf'
jsl = spawn 'jsl', ['-nologo', '-stdin', '-conf', conf]
jsl.stdout.on 'data', printIt
jsl.stderr.on 'data', printIt
jsl.stdin.write js
jsl.stdin.end() |
Pretty-print a stream of tokens. | printTokens = (tokens) ->
strings = for token in tokens
[tag, value] = [token[0], token[1].toString().replace(/\n/, '\\n')]
"[#{tag} #{value}]"
printLine strings.join(' ') |
Use the OptionParser module to extract all options from
| parseOptions = ->
optionParser = new optparse.OptionParser SWITCHES, BANNER
o = opts = optionParser.parse process.argv[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
sourceCode[i] = null for source, i in sources
return |
The compile-time options to pass to the CoffeeScript compiler. | compileOptions = (filename) ->
{filename, bare: opts.bare, header: opts.compile} |
Start up a new Node.js instance with the arguments in | 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 | usage = ->
printLine (new optparse.OptionParser SWITCHES, BANNER).help() |
Print the | version = ->
printLine "CoffeeScript version #{CoffeeScript.VERSION}"
|
grammar.coffee | |
---|---|
The CoffeeScript parser is generated by Jison from this grammar file. Jison is a bottom-up parser generator, similar in style to Bison, implemented in JavaScript. It can recognize LALR(1), LR(0), SLR(1), and LR(1) type grammars. To create the Jison parser, we list the pattern to match on the left-hand side, and the action to take (usually the creation of syntax tree nodes) on the right. As the parser runs, it shifts tokens from our token stream, from left to right, and attempts to match the token sequence against the rules below. When a match can be made, it reduces into the nonterminal (the enclosing name at the top), and we proceed from there. If you run the | |
The only dependency is on the Jison.Parser. | {Parser} = require 'jison' |
Jison DSL | |
Since we're going to be wrapped in a function by Jison in any case, if our action immediately returns a value, we can optimize by removing the function wrapper and just returning the value directly. | unwrap = /^function\s*\(\)\s*\{\s*return\s*([\s\S]*);\s*\}/ |
Our handy DSL for Jison grammar generation, thanks to Tim Caswell. For every rule in the grammar, we pass the pattern-defining string, the action to run, and extra options, optionally. If no action is specified, we simply pass the value of the previous nonterminal. | o = (patternString, action, options) ->
patternString = patternString.replace /\s{2,}/g, ' '
return [patternString, '$$ = $1;', options] unless action
action = if match = unwrap.exec action then match[1] else "(#{action}())"
action = action.replace /\bnew /g, '$&yy.'
action = action.replace /\b(?:Block\.wrap|extend)\b/g, 'yy.$&'
[patternString, "$$ = #{action};", options] |
Grammatical Rules | |
In all of the rules that follow, you'll see the name of the nonterminal as the key to a list of alternative matches. With each match's action, the dollar-sign variables are provided by Jison as references to the value of their numeric position, so in this rule:
| grammar = |
The Root is the top-level node in the syntax tree. Since we parse bottom-up, all parsing must end here. | Root: [
o '', -> new Block
o 'Body'
o 'Block TERMINATOR'
] |
Any list of statements and expressions, separated by line breaks or semicolons. | Body: [
o 'Line', -> Block.wrap [$1]
o 'Body TERMINATOR Line', -> $1.push $3
o 'Body TERMINATOR'
] |
Block and statements, which make up a line in a body. | Line: [
o 'Expression'
o 'Statement'
] |
Pure statements which cannot be expressions. | Statement: [
o 'Return'
o 'Comment'
o 'STATEMENT', -> new Literal $1
] |
All the different types of expressions in our language. The basic unit of CoffeeScript is the Expression -- everything that can be an expression is one. Blocks serve as the building blocks of many other rules, making them somewhat circular. | Expression: [
o 'Value'
o 'Invocation'
o 'Code'
o 'Operation'
o 'Assign'
o 'If'
o 'Try'
o 'While'
o 'For'
o 'Switch'
o 'Class'
o 'Throw'
] |
An indented block of expressions. Note that the Rewriter will convert some postfix forms into blocks for us, by adjusting the token stream. | Block: [
o 'INDENT OUTDENT', -> new Block
o 'INDENT Body OUTDENT', -> $2
] |
A literal identifier, a variable name or property. | Identifier: [
o 'IDENTIFIER', -> new Literal $1
] |
Alphanumerics are separated from the other Literal matchers because they can also serve as keys in object literals. | AlphaNumeric: [
o 'NUMBER', -> new Literal $1
o 'STRING', -> new Literal $1
] |
All of our immediate values. Generally these can be passed straight through and printed to JavaScript. | Literal: [
o 'AlphaNumeric'
o 'JS', -> new Literal $1
o 'REGEX', -> new Literal $1
o 'DEBUGGER', -> new Literal $1
o 'UNDEFINED', -> new Undefined
o 'NULL', -> new Null
o 'BOOL', -> new Bool $1
] |
Assignment of a variable, property, or index to a value. | Assign: [
o 'Assignable = Expression', -> new Assign $1, $3
o 'Assignable = TERMINATOR Expression', -> new Assign $1, $4
o 'Assignable = INDENT Expression OUTDENT', -> new Assign $1, $4
] |
Assignment when it happens within an object literal. The difference from the ordinary Assign is that these allow numbers and strings as keys. | AssignObj: [
o 'ObjAssignable', -> new Value $1
o 'ObjAssignable : Expression', -> new Assign new Value($1), $3, 'object'
o 'ObjAssignable :
INDENT Expression OUTDENT', -> new Assign new Value($1), $4, 'object'
o 'Comment'
]
ObjAssignable: [
o 'Identifier'
o 'AlphaNumeric'
o 'ThisProperty'
] |
A return statement from a function body. | Return: [
o 'RETURN Expression', -> new Return $2
o 'RETURN', -> new Return
] |
A block comment. | Comment: [
o 'HERECOMMENT', -> new Comment $1
] |
The Code node is the function literal. It's defined by an indented block of Block preceded by a function arrow, with an optional parameter list. | Code: [
o 'PARAM_START ParamList PARAM_END FuncGlyph Block', -> new Code $2, $5, $4
o 'FuncGlyph Block', -> new Code [], $2, $1
] |
CoffeeScript has two different symbols for functions. | FuncGlyph: [
o '->', -> 'func'
o '=>', -> 'boundfunc'
] |
An optional, trailing comma. | OptComma: [
o ''
o ','
] |
The list of parameters that a function accepts can be of any length. | ParamList: [
o '', -> []
o 'Param', -> [$1]
o 'ParamList , Param', -> $1.concat $3
o 'ParamList OptComma TERMINATOR Param', -> $1.concat $4
o 'ParamList OptComma INDENT ParamList OptComma OUTDENT', -> $1.concat $4
] |
A single parameter in a function definition can be ordinary, or a splat that hoovers up the remaining arguments. | Param: [
o 'ParamVar', -> new Param $1
o 'ParamVar ...', -> new Param $1, null, on
o 'ParamVar = Expression', -> new Param $1, $3
] |
Function Parameters | ParamVar: [
o 'Identifier'
o 'ThisProperty'
o 'Array'
o 'Object'
] |
A splat that occurs outside of a parameter list. | Splat: [
o 'Expression ...', -> new Splat $1
] |
Variables and properties that can be assigned to. | SimpleAssignable: [
o 'Identifier', -> new Value $1
o 'Value Accessor', -> $1.add $2
o 'Invocation Accessor', -> new Value $1, [].concat $2
o 'ThisProperty'
] |
Everything that can be assigned to. | Assignable: [
o 'SimpleAssignable'
o 'Array', -> new Value $1
o 'Object', -> new Value $1
] |
The types of things that can be treated as values -- assigned to, invoked as functions, indexed into, named as a class, etc. | Value: [
o 'Assignable'
o 'Literal', -> new Value $1
o 'Parenthetical', -> new Value $1
o 'Range', -> new Value $1
o 'This'
] |
The general group of accessors into an object, by property, by prototype or by array index or slice. | Accessor: [
o '. Identifier', -> new Access $2
o '?. Identifier', -> new Access $2, 'soak'
o ':: Identifier', -> [(new Access new Literal 'prototype'), new Access $2]
o '::', -> new Access new Literal 'prototype'
o 'Index'
] |
Indexing into an object or array using bracket notation. | Index: [
o 'INDEX_START IndexValue INDEX_END', -> $2
o 'INDEX_SOAK Index', -> extend $2, soak : yes
]
IndexValue: [
o 'Expression', -> new Index $1
o 'Slice', -> new Slice $1
] |
In CoffeeScript, an object literal is simply a list of assignments. | Object: [
o '{ AssignList OptComma }', -> new Obj $2, $1.generated
] |
Assignment of properties within an object literal can be separated by comma, as in JavaScript, or simply by newline. | AssignList: [
o '', -> []
o 'AssignObj', -> [$1]
o 'AssignList , AssignObj', -> $1.concat $3
o 'AssignList OptComma TERMINATOR AssignObj', -> $1.concat $4
o 'AssignList OptComma INDENT AssignList OptComma OUTDENT', -> $1.concat $4
] |
Class definitions have optional bodies of prototype property assignments, and optional references to the superclass. | Class: [
o 'CLASS', -> new Class
o 'CLASS Block', -> new Class null, null, $2
o 'CLASS EXTENDS Expression', -> new Class null, $3
o 'CLASS EXTENDS Expression Block', -> new Class null, $3, $4
o 'CLASS SimpleAssignable', -> new Class $2
o 'CLASS SimpleAssignable Block', -> new Class $2, null, $3
o 'CLASS SimpleAssignable EXTENDS Expression', -> new Class $2, $4
o 'CLASS SimpleAssignable EXTENDS Expression Block', -> new Class $2, $4, $5
] |
Ordinary function invocation, or a chained series of calls. | Invocation: [
o 'Value OptFuncExist Arguments', -> new Call $1, $3, $2
o 'Invocation OptFuncExist Arguments', -> new Call $1, $3, $2
o 'SUPER', -> new Call 'super', [new Splat new Literal 'arguments']
o 'SUPER Arguments', -> new Call 'super', $2
] |
An optional existence check on a function. | OptFuncExist: [
o '', -> no
o 'FUNC_EXIST', -> yes
] |
The list of arguments to a function call. | Arguments: [
o 'CALL_START CALL_END', -> []
o 'CALL_START ArgList OptComma CALL_END', -> $2
] |
A reference to the this current object. | This: [
o 'THIS', -> new Value new Literal 'this'
o '@', -> new Value new Literal 'this'
] |
A reference to a property on this. | ThisProperty: [
o '@ Identifier', -> new Value new Literal('this'), [new Access($2)], 'this'
] |
The array literal. | Array: [
o '[ ]', -> new Arr []
o '[ ArgList OptComma ]', -> new Arr $2
] |
Inclusive and exclusive range dots. | RangeDots: [
o '..', -> 'inclusive'
o '...', -> 'exclusive'
] |
The CoffeeScript range literal. | Range: [
o '[ Expression RangeDots Expression ]', -> new Range $2, $4, $3
] |
Array slice literals. | Slice: [
o 'Expression RangeDots Expression', -> new Range $1, $3, $2
o 'Expression RangeDots', -> new Range $1, null, $2
o 'RangeDots Expression', -> new Range null, $2, $1
o 'RangeDots', -> new Range null, null, $1
] |
The ArgList is both the list of objects passed into a function call, as well as the contents of an array literal (i.e. comma-separated expressions). Newlines work as well. | ArgList: [
o 'Arg', -> [$1]
o 'ArgList , Arg', -> $1.concat $3
o 'ArgList OptComma TERMINATOR Arg', -> $1.concat $4
o 'INDENT ArgList OptComma OUTDENT', -> $2
o 'ArgList OptComma INDENT ArgList OptComma OUTDENT', -> $1.concat $4
] |
Valid arguments are Blocks or Splats. | Arg: [
o 'Expression'
o 'Splat'
] |
Just simple, comma-separated, required arguments (no fancy syntax). We need this to be separate from the ArgList for use in Switch blocks, where having the newlines wouldn't make sense. | SimpleArgs: [
o 'Expression'
o 'SimpleArgs , Expression', -> [].concat $1, $3
] |
The variants of try/catch/finally exception handling blocks. | Try: [
o 'TRY Block', -> new Try $2
o 'TRY Block Catch', -> new Try $2, $3[0], $3[1]
o 'TRY Block FINALLY Block', -> new Try $2, null, null, $4
o 'TRY Block Catch FINALLY Block', -> new Try $2, $3[0], $3[1], $5
] |
A catch clause names its error and runs a block of code. | Catch: [
o 'CATCH Identifier Block', -> [$2, $3]
] |
Throw an exception object. | Throw: [
o 'THROW Expression', -> new Throw $2
] |
Parenthetical expressions. Note that the Parenthetical is a Value, not an Expression, so if you need to use an expression in a place where only values are accepted, wrapping it in parentheses will always do the trick. | Parenthetical: [
o '( Body )', -> new Parens $2
o '( INDENT Body OUTDENT )', -> new Parens $3
] |
The condition portion of a while loop. | WhileSource: [
o 'WHILE Expression', -> new While $2
o 'WHILE Expression WHEN Expression', -> new While $2, guard: $4
o 'UNTIL Expression', -> new While $2, invert: true
o 'UNTIL Expression WHEN Expression', -> new While $2, invert: true, guard: $4
] |
The while loop can either be normal, with a block of expressions to execute, or postfix, with a single expression. There is no do..while. | While: [
o 'WhileSource Block', -> $1.addBody $2
o 'Statement WhileSource', -> $2.addBody Block.wrap [$1]
o 'Expression WhileSource', -> $2.addBody Block.wrap [$1]
o 'Loop', -> $1
]
Loop: [
o 'LOOP Block', -> new While(new Literal 'true').addBody $2
o 'LOOP Expression', -> new While(new Literal 'true').addBody Block.wrap [$2]
] |
Array, object, and range comprehensions, at the most generic level. Comprehensions can either be normal, with a block of expressions to execute, or postfix, with a single expression. | For: [
o 'Statement ForBody', -> new For $1, $2
o 'Expression ForBody', -> new For $1, $2
o 'ForBody Block', -> new For $2, $1
]
ForBody: [
o 'FOR Range', -> source: new Value($2)
o 'ForStart ForSource', -> $2.own = $1.own; $2.name = $1[0]; $2.index = $1[1]; $2
]
ForStart: [
o 'FOR ForVariables', -> $2
o 'FOR OWN ForVariables', -> $3.own = yes; $3
] |
An array of all accepted values for a variable inside the loop. This enables support for pattern matching. | ForValue: [
o 'Identifier'
o 'ThisProperty'
o 'Array', -> new Value $1
o 'Object', -> new Value $1
] |
An array or range comprehension has variables for the current element and (optional) reference to the current index. Or, key, value, in the case of object comprehensions. | ForVariables: [
o 'ForValue', -> [$1]
o 'ForValue , ForValue', -> [$1, $3]
] |
The source of a comprehension is an array or object with an optional guard clause. If it's an array comprehension, you can also choose to step through in fixed-size increments. | ForSource: [
o 'FORIN Expression', -> source: $2
o 'FOROF Expression', -> source: $2, object: yes
o 'FORIN Expression WHEN Expression', -> source: $2, guard: $4
o 'FOROF Expression WHEN Expression', -> source: $2, guard: $4, object: yes
o 'FORIN Expression BY Expression', -> source: $2, step: $4
o 'FORIN Expression WHEN Expression BY Expression', -> source: $2, guard: $4, step: $6
o 'FORIN Expression BY Expression WHEN Expression', -> source: $2, step: $4, guard: $6
]
Switch: [
o 'SWITCH Expression INDENT Whens OUTDENT', -> new Switch $2, $4
o 'SWITCH Expression INDENT Whens ELSE Block OUTDENT', -> new Switch $2, $4, $6
o 'SWITCH INDENT Whens OUTDENT', -> new Switch null, $3
o 'SWITCH INDENT Whens ELSE Block OUTDENT', -> new Switch null, $3, $5
]
Whens: [
o 'When'
o 'Whens When', -> $1.concat $2
] |
An individual When clause, with action. | When: [
o 'LEADING_WHEN SimpleArgs Block', -> [[$2, $3]]
o 'LEADING_WHEN SimpleArgs Block TERMINATOR', -> [[$2, $3]]
] |
The most basic form of if is a condition and an action. The following if-related rules are broken up along these lines in order to avoid ambiguity. | IfBlock: [
o 'IF Expression Block', -> new If $2, $3, type: $1
o 'IfBlock ELSE IF Expression Block', -> $1.addElse new If $4, $5, type: $3
] |
The full complement of if expressions, including postfix one-liner if and unless. | If: [
o 'IfBlock'
o 'IfBlock ELSE Block', -> $1.addElse $3
o 'Statement POST_IF Expression', -> new If $3, Block.wrap([$1]), type: $2, statement: true
o 'Expression POST_IF Expression', -> new If $3, Block.wrap([$1]), type: $2, statement: true
] |
Arithmetic and logical operators, working on one or more operands. Here they are grouped by order of precedence. The actual precedence rules are defined at the bottom of the page. It would be shorter if we could combine most of these rules into a single generic Operand OpSymbol Operand -type rule, but in order to make the precedence binding possible, separate rules are necessary. | Operation: [
o 'UNARY Expression', -> new Op $1 , $2
o '- Expression', (-> new Op '-', $2), prec: 'UNARY'
o '+ Expression', (-> new Op '+', $2), prec: 'UNARY'
o '-- SimpleAssignable', -> new Op '--', $2
o '++ SimpleAssignable', -> new Op '++', $2
o 'SimpleAssignable --', -> new Op '--', $1, null, true
o 'SimpleAssignable ++', -> new Op '++', $1, null, true |
o 'Expression ?', -> new Existence $1
o 'Expression + Expression', -> new Op '+' , $1, $3
o 'Expression - Expression', -> new Op '-' , $1, $3
o 'Expression MATH Expression', -> new Op $2, $1, $3
o 'Expression SHIFT Expression', -> new Op $2, $1, $3
o 'Expression COMPARE Expression', -> new Op $2, $1, $3
o 'Expression LOGIC Expression', -> new Op $2, $1, $3
o 'Expression RELATION Expression', ->
if $2.charAt(0) is '!'
new Op($2[1..], $1, $3).invert()
else
new Op $2, $1, $3
o 'SimpleAssignable COMPOUND_ASSIGN
Expression', -> new Assign $1, $3, $2
o 'SimpleAssignable COMPOUND_ASSIGN
INDENT Expression OUTDENT', -> new Assign $1, $4, $2
o 'SimpleAssignable EXTENDS Expression', -> new Extends $1, $3
] | |
Precedence | |
Operators at the top of this list have higher precedence than the ones lower
down. Following these rules is what makes
And not: | operators = [
['left', '.', '?.', '::']
['left', 'CALL_START', 'CALL_END']
['nonassoc', '++', '--']
['left', '?']
['right', 'UNARY']
['left', 'MATH']
['left', '+', '-']
['left', 'SHIFT']
['left', 'RELATION']
['left', 'COMPARE']
['left', 'LOGIC']
['nonassoc', 'INDENT', 'OUTDENT']
['right', '=', ':', 'COMPOUND_ASSIGN', 'RETURN', 'THROW', 'EXTENDS']
['right', 'FORIN', 'FOROF', 'BY', 'WHEN']
['right', 'IF', 'ELSE', 'FOR', 'WHILE', 'UNTIL', 'LOOP', 'SUPER', 'CLASS']
['right', 'POST_IF']
] |
Wrapping Up | |
Finally, now that we have our grammar and our operators, we can create our Jison.Parser. We do this by processing all of our rules, recording all terminals (every symbol which does not appear as the name of a rule above) as "tokens". | tokens = []
for name, alternatives of grammar
grammar[name] = for alt in alternatives
for token in alt[0].split ' '
tokens.push token unless grammar[token]
alt[1] = "return #{alt[1]}" if name is 'Root'
alt |
Initialize the Parser with our list of terminal tokens, our grammar rules, and the name of the root. Reverse the operators because Jison orders precedence from low to high, and we have it high to low (as in Yacc). | exports.parser = new Parser
tokens : tokens.join ' '
bnf : grammar
operators : operators.reverse()
startSymbol : 'Root'
|
helpers.coffee | |
---|---|
This file contains the common helper functions that we'd like to share among the Lexer, Rewriter, and the Nodes. Merge objects, flatten arrays, count characters, that sort of thing. | |
Peek at the beginning of a given string to see if it matches a sequence. | exports.starts = (string, literal, start) ->
literal is string.substr start, literal.length |
Peek at the end of a given string to see if it matches a sequence. | exports.ends = (string, literal, back) ->
len = literal.length
literal is string.substr string.length - len - (back or 0), len |
Trim out all falsy values from an array. | exports.compact = (array) ->
item for item in array when item |
Count the number of occurrences of a string in a string. | exports.count = (string, substr) ->
num = pos = 0
return 1/0 unless substr.length
num++ while pos = 1 + string.indexOf substr, pos
num |
Merge objects, returning a fresh copy with attributes from both sides.
Used every time | exports.merge = (options, overrides) ->
extend (extend {}, options), overrides |
Extend a source object with the properties of another object (shallow copy). | extend = exports.extend = (object, properties) ->
for key, val of properties
object[key] = val
object |
Return a flattened version of an array.
Handy for getting a list of | exports.flatten = flatten = (array) ->
flattened = []
for element in array
if element instanceof Array
flattened = flattened.concat flatten element
else
flattened.push element
flattened |
Delete a key from an object, returning the value. Useful when a node is looking for a particular method in an options hash. | exports.del = (obj, key) ->
val = obj[key]
delete obj[key]
val |
Gets the last item of an array(-like) object. | exports.last = (array, back) -> array[array.length - (back or 0) - 1] |
Typical Array::some | exports.some = Array::some ? (fn) ->
return true for e in this when fn e
false
|
index.coffee | |
---|---|
Loader for CoffeeScript as a Node.js library. | exports[key] = val for key, val of require './coffee-script'
|
lexer.coffee | |
---|---|
The CoffeeScript Lexer. Uses a series of token-matching regexes to attempt matches against the beginning of the source code. When a match is found, a token is produced, we consume the match, and start again. Tokens are in the form:
Which is a format that can be fed directly into Jison. | {Rewriter, INVERSES} = require './rewriter' |
Import the helpers we need. | {count, starts, compact, last} = require './helpers' |
The Lexer Class | |
The Lexer class reads a stream of CoffeeScript and divvies it up into tagged tokens. Some potential ambiguity in the grammar has been avoided by pushing some extra smarts into the Lexer. | exports.Lexer = class Lexer |
tokenize is the Lexer's main method. Scan by attempting to match tokens one at a time, using a regular expression anchored at the start of the remaining code, or a custom recursive token-matching method (for interpolations). When the next token has been recorded, we move forward within the code past the token, and begin again. Each tokenizing method is responsible for returning the number of characters it has consumed. Before returning the token stream, run it through the Rewriter unless explicitly asked not to. | tokenize: (code, opts = {}) ->
code = "\n#{code}" if WHITESPACE.test code
code = code.replace(/\r/g, '').replace TRAILING_SPACES, ''
@code = code # The remainder of the source code.
@line = opts.line or 0 # The current line.
@indent = 0 # The current indentation level.
@indebt = 0 # The over-indentation at the current level.
@outdebt = 0 # The under-outdentation at the current level.
@indents = [] # The stack of all current indentation levels.
@ends = [] # The stack for pairing up tokens.
@tokens = [] # Stream of parsed tokens in the form `['TYPE', value, line]`. |
At every position, run through this list of attempted matches,
short-circuiting if any of them succeed. Their order determines precedence:
| i = 0
while @chunk = code[i..]
i += @identifierToken() or
@commentToken() or
@whitespaceToken() or
@lineToken() or
@heredocToken() or
@stringToken() or
@numberToken() or
@regexToken() or
@jsToken() or
@literalToken()
@closeIndentation()
@error "missing #{tag}" if tag = @ends.pop()
return @tokens if opts.rewrite is off
(new Rewriter).rewrite @tokens |
Tokenizers | |
Matches identifying literals: variables, keywords, method names, etc.
Check to ensure that JavaScript reserved words aren't being used as
identifiers. Because CoffeeScript reserves a handful of keywords that are
allowed in JavaScript, we're careful not to tag them as keywords when
referenced as property names here, so you can still do | identifierToken: ->
return 0 unless match = IDENTIFIER.exec @chunk
[input, id, colon] = match
if id is 'own' and @tag() is 'FOR'
@token 'OWN', id
return id.length
forcedIdentifier = colon or
(prev = last @tokens) and (prev[0] in ['.', '?.', '::'] or
not prev.spaced and prev[0] is '@')
tag = 'IDENTIFIER'
if not forcedIdentifier and (id in JS_KEYWORDS or id in COFFEE_KEYWORDS)
tag = id.toUpperCase()
if tag is 'WHEN' and @tag() in LINE_BREAK
tag = 'LEADING_WHEN'
else if tag is 'FOR'
@seenFor = yes
else if tag is 'UNLESS'
tag = 'IF'
else if tag in UNARY
tag = 'UNARY'
else if tag in RELATION
if tag isnt 'INSTANCEOF' and @seenFor
tag = 'FOR' + tag
@seenFor = no
else
tag = 'RELATION'
if @value() is '!'
@tokens.pop()
id = '!' + id
if id in JS_FORBIDDEN
if forcedIdentifier
tag = 'IDENTIFIER'
id = new String id
id.reserved = yes
else if id in RESERVED
@error "reserved word \"#{id}\""
unless forcedIdentifier
id = COFFEE_ALIAS_MAP[id] if id in COFFEE_ALIASES
tag = switch id
when '!' then 'UNARY'
when '==', '!=' then 'COMPARE'
when '&&', '||' then 'LOGIC'
when 'true', 'false' then 'BOOL'
when 'break', 'continue' then 'STATEMENT'
else tag
@token tag, id
@token ':', ':' if colon
input.length |
Matches numbers, including decimals, hex, and exponential notation. Be careful not to interfere with ranges-in-progress. | numberToken: ->
return 0 unless match = NUMBER.exec @chunk
number = match[0]
if /^0[BOX]/.test number
@error "radix prefix '#{number}' must be lowercase"
else if /E/.test(number) and not /^0x/.test number
@error "exponential notation '#{number}' must be indicated with a lowercase 'e'"
else if /^0\d*[89]/.test number
@error "decimal literal '#{number}' must not be prefixed with '0'"
else if /^0\d+/.test number
@error "octal literal '#{number}' must be prefixed with '0o'"
lexedLength = number.length
if octalLiteral = /^0o([0-7]+)/.exec number
number = '0x' + (parseInt octalLiteral[1], 8).toString 16
if binaryLiteral = /^0b([01]+)/.exec number
number = '0x' + (parseInt binaryLiteral[1], 2).toString 16
@token 'NUMBER', number
lexedLength |
Matches strings, including multi-line strings. Ensures that quotation marks are balanced within the string's contents, and within nested interpolations. | stringToken: ->
switch @chunk.charAt 0
when "'"
return 0 unless match = SIMPLESTR.exec @chunk
@token 'STRING', (string = match[0]).replace MULTILINER, '\\\n'
when '"'
return 0 unless string = @balancedString @chunk, '"'
if 0 < string.indexOf '#{', 1
@interpolateString string[1...-1]
else
@token 'STRING', @escapeLines string
else
return 0
if octalEsc = /^(?:\\.|[^\\])*\\(?:0[0-7]|[1-7])/.test string
@error "octal escape sequences #{string} are not allowed"
@line += count string, '\n'
string.length |
Matches heredocs, adjusting indentation to the correct level, as heredocs preserve whitespace, but ignore indentation to the left. | heredocToken: ->
return 0 unless match = HEREDOC.exec @chunk
heredoc = match[0]
quote = heredoc.charAt 0
doc = @sanitizeHeredoc match[2], quote: quote, indent: null
if quote is '"' and 0 <= doc.indexOf '#{'
@interpolateString doc, heredoc: yes
else
@token 'STRING', @makeString doc, quote, yes
@line += count heredoc, '\n'
heredoc.length |
Matches and consumes comments. | commentToken: ->
return 0 unless match = @chunk.match COMMENT
[comment, here] = match
if here
@token 'HERECOMMENT', @sanitizeHeredoc here,
herecomment: true, indent: Array(@indent + 1).join(' ')
@line += count comment, '\n'
comment.length |
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]
@line += count script, '\n'
script.length |
Matches regular expression literals. Lexing regular expressions is difficult to distinguish from division, so we borrow some basic heuristics from JavaScript and Ruby. | regexToken: ->
return 0 if @chunk.charAt(0) isnt '/'
if match = HEREGEX.exec @chunk
length = @heregexToken match
@line += count match[0], '\n'
return length
prev = last @tokens
return 0 if prev and (prev[0] in (if prev.spaced then NOT_REGEX else NOT_SPACED_REGEX))
return 0 unless match = REGEX.exec @chunk
[match, regex, flags] = match
if regex[..1] is '/*' then @error 'regular expressions cannot begin with `*`'
if regex is '//' then regex = '/(?:)/'
@token 'REGEX', "#{regex}#{flags}"
match.length |
Matches multiline extended regular expressions. | heregexToken: (match) ->
[heregex, body, flags] = match
if 0 > body.indexOf '#{'
re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/')
if re.match /^\*/ then @error 'regular expressions cannot begin with `*`'
@token 'REGEX', "/#{ re or '(?:)' }/#{flags}"
return heregex.length
@token 'IDENTIFIER', 'RegExp'
@tokens.push ['CALL_START', '(']
tokens = []
for [tag, value] in @interpolateString(body, regex: yes)
if tag is 'TOKENS'
tokens.push value...
else
continue unless value = value.replace HEREGEX_OMIT, ''
value = value.replace /\\/g, '\\\\'
tokens.push ['STRING', @makeString(value, '"', yes)]
tokens.push ['+', '+']
tokens.pop()
@tokens.push ['STRING', '""'], ['+', '+'] unless tokens[0]?[0] is 'STRING'
@tokens.push tokens...
@tokens.push [',', ','], ['STRING', '"' + flags + '"'] if flags
@token ')', ')'
heregex.length |
Matches newlines, indents, and outdents, and determines which is which. If we can detect that the current line is continued onto the the next line, then the newline is suppressed:
Keeps track of the level of indentation, because a single outdent token can close multiple indents, so we need to know how far in we happen to be. | lineToken: ->
return 0 unless match = MULTI_DENT.exec @chunk
indent = match[0]
@line += count indent, '\n'
@seenFor = no
size = indent.length - 1 - indent.lastIndexOf '\n'
noNewlines = @unfinished()
if size - @indebt is @indent
if noNewlines then @suppressNewlines() else @newlineToken()
return indent.length
if size > @indent
if noNewlines
@indebt = size - @indent
@suppressNewlines()
return indent.length
diff = size - @indent + @outdebt
@token 'INDENT', diff
@indents.push diff
@ends.push 'OUTDENT'
@outdebt = @indebt = 0
else
@indebt = 0
@outdentToken @indent - size, noNewlines
@indent = size
indent.length |
Record an outdent token or multiple tokens, if we happen to be moving back inwards past several recorded indents. | outdentToken: (moveOut, noNewlines) ->
while moveOut > 0
len = @indents.length - 1
if @indents[len] is undefined
moveOut = 0
else if @indents[len] is @outdebt
moveOut -= @outdebt
@outdebt = 0
else if @indents[len] < @outdebt
@outdebt -= @indents[len]
moveOut -= @indents[len]
else
dent = @indents.pop() - @outdebt
moveOut -= dent
@outdebt = 0
@pair 'OUTDENT'
@token 'OUTDENT', dent
@outdebt -= moveOut if dent
@tokens.pop() while @value() is ';'
@token 'TERMINATOR', '\n' unless @tag() is 'TERMINATOR' or noNewlines
this |
Matches and consumes non-meaningful whitespace. Tag the previous token as being "spaced", because there are some cases where it makes a difference. | whitespaceToken: ->
return 0 unless (match = WHITESPACE.exec @chunk) or
(nline = @chunk.charAt(0) is '\n')
prev = last @tokens
prev[if match then 'spaced' else 'newLine'] = true if prev
if match then match[0].length else 0 |
Generate a newline token. Consecutive newlines get merged together. | newlineToken: ->
@tokens.pop() while @value() is ';'
@token 'TERMINATOR', '\n' unless @tag() is 'TERMINATOR'
this |
Use a | suppressNewlines: ->
@tokens.pop() if @value() is '\\'
this |
We treat all other single characters as a token. E.g.: | literalToken: ->
if match = OPERATOR.exec @chunk
[value] = match
@tagParameters() if CODE.test value
else
value = @chunk.charAt 0
tag = value
prev = last @tokens
if value is '=' and prev
if not prev[1].reserved and prev[1] in JS_FORBIDDEN
@error "reserved word \"#{@value()}\" can't be assigned"
if prev[1] in ['||', '&&']
prev[0] = 'COMPOUND_ASSIGN'
prev[1] += '='
return value.length
if value is ';'
@seenFor = no
tag = 'TERMINATOR'
else if value in MATH then tag = 'MATH'
else if value in COMPARE then tag = 'COMPARE'
else if value in COMPOUND_ASSIGN then tag = 'COMPOUND_ASSIGN'
else if value in UNARY then tag = 'UNARY'
else if value in SHIFT then tag = 'SHIFT'
else if value in LOGIC or value is '?' and prev?.spaced then tag = 'LOGIC'
else if prev and not prev.spaced
if value is '(' and prev[0] in CALLABLE
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
tag = 'CALL_START'
else if value is '[' and prev[0] in INDEXABLE
tag = 'INDEX_START'
switch prev[0]
when '?' then prev[0] = 'INDEX_SOAK'
switch value
when '(', '{', '[' then @ends.push INVERSES[value]
when ')', '}', ']' then @pair value
@token tag, value
value.length |
Token Manipulators | |
Sanitize a heredoc or herecomment by erasing all external indentation on the left-hand side. | sanitizeHeredoc: (doc, options) ->
{indent, herecomment} = options
if herecomment
if HEREDOC_ILLEGAL.test doc
@error "block comment cannot contain \"*/\", starting"
return doc if doc.indexOf('\n') <= 0
else
while match = HEREDOC_INDENT.exec doc
attempt = match[1]
indent = attempt if indent is null or 0 < attempt.length < indent.length
doc = doc.replace /// \n #{indent} ///g, '\n' if indent
doc = doc.replace /^\n/, '' unless herecomment
doc |
A source of ambiguity in our grammar used to be parameter lists in function definitions versus argument lists in function calls. Walk backwards, tagging parameters specially in order to make things easier for the parser. | tagParameters: ->
return this if @tag() isnt ')'
stack = []
{tokens} = this
i = tokens.length
tokens[--i][0] = 'PARAM_END'
while tok = tokens[--i]
switch tok[0]
when ')'
stack.push tok
when '(', 'CALL_START'
if stack.length then stack.pop()
else if tok[0] is '('
tok[0] = 'PARAM_START'
return this
else return this
this |
Close up all remaining open blocks at the end of the file. | closeIndentation: ->
@outdentToken @indent |
Matches a balanced group such as a single or double-quoted string. Pass in 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, end) ->
continueCount = 0
stack = [end]
for i in [1...str.length]
if continueCount
--continueCount
continue
switch letter = str.charAt i
when '\\'
++continueCount
continue
when end
stack.pop()
unless stack.length
return str[0..i]
end = stack[stack.length - 1]
continue
if end is '}' and letter in ['"', "'"]
stack.push end = letter
else if end is '}' and letter is '/' and match = (HEREGEX.exec(str[i..]) or REGEX.exec(str[i..]))
continueCount += match[0].length - 1
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
@error "missing #{ stack.pop() }, starting" |
Expand variables and expressions inside double-quoted strings using Ruby-like notation for substitution of arbitrary expressions.
If it encounters an interpolation, this method will recursively create a new Lexer, tokenize the interpolated contents, and merge them into the token stream. | interpolateString: (str, options = {}) ->
{heredoc, regex} = options
tokens = []
pi = 0
i = -1
while letter = str.charAt i += 1
if letter is '\\'
i += 1
continue
unless letter is '#' and str.charAt(i+1) is '{' and
(expr = @balancedString str[i + 1..], '}')
continue
tokens.push ['NEOSTRING', str[pi...i]] if pi < i
inner = expr[1...-1]
if inner.length
nested = new Lexer().tokenize inner, line: @line, rewrite: off
nested.pop()
nested.shift() if nested[0]?[0] is 'TERMINATOR'
if len = nested.length
if len > 1
nested.unshift ['(', '(', @line]
nested.push [')', ')', @line]
tokens.push ['TOKENS', nested]
i += expr.length
pi = i + 1
tokens.push ['NEOSTRING', str[pi..]] if i > pi < str.length
return tokens if regex
return @token 'STRING', '""' unless tokens.length
tokens.unshift ['', ''] unless tokens[0][0] is 'NEOSTRING'
@token '(', '(' if interpolated = tokens.length > 1
for [tag, value], i in tokens
@token '+', '+' if i
if tag is 'TOKENS'
@tokens.push value...
else
@token 'STRING', @makeString value, '"', heredoc
@token ')', ')' if interpolated
tokens |
Pairs up a closing token, ensuring that all listed pairs of tokens are correctly balanced throughout the course of the token stream. | pair: (tag) ->
unless tag is wanted = last @ends
@error "unmatched #{tag}" unless 'OUTDENT' is wanted |
Auto-close INDENT to support syntax like this: | @indent -= size = last @indents
@outdentToken size, true
return @pair tag
@ends.pop() |
Helpers | |
Add a token to the results, taking note of the line number. | token: (tag, value) ->
@tokens.push [tag, value, @line] |
Peek at a tag in the current token stream. | tag: (index, tag) ->
(tok = last @tokens, index) and if tag then tok[0] = tag else tok[0] |
Peek at a value in the current token stream. | value: (index, val) ->
(tok = last @tokens, index) and if val then tok[1] = val else tok[1] |
Are we in the midst of an unfinished expression? | unfinished: ->
LINE_CONTINUER.test(@chunk) or
@tag() in ['\\', '.', '?.', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION'
'COMPARE', 'LOGIC', 'THROW', 'EXTENDS'] |
Converts newlines for string literals. | escapeLines: (str, heredoc) ->
str.replace MULTILINER, if heredoc then '\\n' else '' |
Constructs a string token by escaping quotes and newlines. | makeString: (body, quote, heredoc) ->
return quote + quote unless body
body = body.replace /\\([\s\S])/g, (match, contents) ->
if contents in ['\n', quote] then contents else match
body = body.replace /// #{quote} ///g, '\\$&'
quote + @escapeLines(body, heredoc) + quote |
Throws a syntax error on the current | error: (message) ->
throw SyntaxError "#{message} on line #{ @line + 1}" |
Constants | |
Keywords that CoffeeScript shares in common with JavaScript. | JS_KEYWORDS = [
'true', 'false', 'null', 'this'
'new', 'delete', 'typeof', 'in', 'instanceof'
'return', 'throw', 'break', 'continue', 'debugger'
'if', 'else', 'switch', 'for', 'while', 'do', 'try', 'catch', 'finally'
'class', 'extends', 'super'
] |
CoffeeScript-only keywords. | COFFEE_KEYWORDS = ['undefined', 'then', 'unless', 'until', 'loop', 'of', 'by', 'when']
COFFEE_ALIAS_MAP =
and : '&&'
or : '||'
is : '=='
isnt : '!='
not : '!'
yes : 'true'
no : 'false'
on : 'true'
off : 'false'
COFFEE_ALIASES = (key for key of COFFEE_ALIAS_MAP)
COFFEE_KEYWORDS = COFFEE_KEYWORDS.concat COFFEE_ALIASES |
The list of keywords that are reserved by JavaScript, but not used, or are used by CoffeeScript internally. We throw an error when these are encountered, to avoid having a JavaScript error at runtime. | RESERVED = [
'case', 'default', 'function', 'var', 'void', 'with', 'const', 'let', 'enum'
'export', 'import', 'native', '__hasProp', '__extends', '__slice', '__bind'
'__indexOf', 'implements', 'interface', 'package', 'private', 'protected'
'public', 'static', 'yield'
]
STRICT_PROSCRIBED = ['arguments', 'eval'] |
The superset of both JavaScript keywords and reserved words, none of which may be used as identifiers or properties. | JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED).concat(STRICT_PROSCRIBED)
exports.RESERVED = RESERVED.concat(JS_KEYWORDS).concat(COFFEE_KEYWORDS).concat(STRICT_PROSCRIBED)
exports.STRICT_PROSCRIBED = STRICT_PROSCRIBED |
Token matching regexes. | IDENTIFIER = /// ^
( [$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]* )
( [^\n\S]* : (?!:) )? # Is this a property name?
///
NUMBER = ///
^ 0b[01]+ | # binary
^ 0o[0-7]+ | # octal
^ 0x[\da-f]+ | # hex
^ \d*\.?\d+ (?:e[+-]?\d+)? # decimal
///i
HEREDOC = /// ^ ("""|''') ([\s\S]*?) (?:\n[^\n\S]*)? \1 ///
OPERATOR = /// ^ (
?: [-=]> # function
| [-+*/%<>&|^!?=]= # compound assign / compare
| >>>=? # zero-fill right shift
| ([-+:])\1 # doubles
| ([&|<>])\2=? # logic / shift
| \?\. # soak access
| \.{2,3} # range or splat
) ///
WHITESPACE = /^[^\n\S]+/
COMMENT = /^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)?$)|^(?:\s*#(?!##[^#]).*)+/
CODE = /^[-=]>/
MULTI_DENT = /^(?:\n[^\n\S]*)+/
SIMPLESTR = /^'[^\\']*(?:\\.[^\\']*)*'/
JSTOKEN = /^`[^\\`]*(?:\\.[^\\`]*)*`/ |
Regex-matching-regexes. | REGEX = /// ^
(/ (?! [\s=] ) # disallow leading whitespace or equals signs
[^ [ / \n \\ ]* # every other thing
(?:
(?: \\[\s\S] # anything escaped
| \[ # character class
[^ \] \n \\ ]*
(?: \\[\s\S] [^ \] \n \\ ]* )*
]
) [^ [ / \n \\ ]*
)*
/) ([imgy]{0,4}) (?!\w)
///
HEREGEX = /// ^ /{3} ([\s\S]+?) /{3} ([imgy]{0,4}) (?!\w) ///
HEREGEX_OMIT = /\s+(?:#.*)?/g |
Token cleaning regexes. | MULTILINER = /\n/g
HEREDOC_INDENT = /\n+([^\n\S]*)/g
HEREDOC_ILLEGAL = /\*\//
LINE_CONTINUER = /// ^ \s* (?: , | \??\.(?![.\d]) | :: ) ///
TRAILING_SPACES = /\s+$/ |
Compound assignment tokens. | COMPOUND_ASSIGN = [
'-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|='
] |
Unary tokens. | UNARY = ['!', '~', 'NEW', 'TYPEOF', 'DELETE', 'DO'] |
Logical tokens. | LOGIC = ['&&', '||', '&', '|', '^'] |
Bit-shifting tokens. | SHIFT = ['<<', '>>', '>>>'] |
Comparison tokens. | COMPARE = ['==', '!=', '<', '>', '<=', '>='] |
Mathematical tokens. | MATH = ['*', '/', '%'] |
Relational tokens that are negatable with | RELATION = ['IN', 'OF', 'INSTANCEOF'] |
Boolean tokens. | BOOL = ['TRUE', 'FALSE'] |
Tokens which a regular expression will never immediately follow, but which a division operator might. See: http://www.mozilla.org/js/language/js20-2002-04/rationale/syntax.html#regular-expressions Our list is shorter, due to sans-parentheses method calls. | NOT_REGEX = ['NUMBER', 'REGEX', 'BOOL', 'NULL', 'UNDEFINED', '++', '--', ']'] |
If the previous token is not spaced, there are more preceding tokens that force a division parse: | NOT_SPACED_REGEX = NOT_REGEX.concat ')', '}', 'THIS', 'IDENTIFIER', 'STRING' |
Tokens which could legitimately be invoked or indexed. An opening parentheses or bracket following these tokens will be recorded as the start of a function invocation or indexing operation. | CALLABLE = ['IDENTIFIER', 'STRING', 'REGEX', ')', ']', '}', '?', '::', '@', 'THIS', 'SUPER']
INDEXABLE = CALLABLE.concat 'NUMBER', 'BOOL', 'NULL', 'UNDEFINED' |
Tokens that, when immediately preceding a | LINE_BREAK = ['INDENT', 'OUTDENT', 'TERMINATOR']
|
nodes.coffee | |
---|---|
| {Scope} = require './scope'
{RESERVED, STRICT_PROSCRIBED} = require './lexer' |
Import the helpers we plan to use. | {compact, flatten, extend, merge, del, starts, ends, last, some} = require './helpers'
exports.extend = extend # for parser |
Constant functions for nodes that don't need customization. | YES = -> yes
NO = -> no
THIS = -> this
NEGATE = -> @negated = not @negated; this |
Base | |
The Base is the abstract base class for all nodes in the syntax tree.
Each subclass implements the | exports.Base = class Base |
Common logic for determining whether to wrap this node in a closure before compiling it, or to compile directly. We need to wrap if this node is a statement, and it's not a pureStatement, and we're not at the top level of a block (which would be unnecessary), and we haven't already been asked to return the result (because statements know how to return results). | compile: (o, lvl) ->
o = extend {}, o
o.level = lvl if lvl
node = @unfoldSoak(o) or this
node.tab = o.indent
if o.level is LEVEL_TOP or not node.isStatement(o)
node.compileNode o
else
node.compileClosure o |
Statements converted into expressions via closure-wrapping share a scope object with their parent closure, to preserve the expected lexical scope. | compileClosure: (o) ->
if @jumps()
throw SyntaxError 'cannot use a pure statement in an expression.'
o.sharedScope = yes
Closure.wrap(this).compileNode o |
If the code generation wishes to use the result of a complex expression in multiple places, ensure that the expression is only ever evaluated once, by assigning it to a temporary variable. Pass a level to precompile. | cache: (o, level, reused) ->
unless @isComplex()
ref = if level then @compile o, level else this
[ref, ref]
else
ref = new Literal reused or o.scope.freeVariable 'ref'
sub = new Assign ref, this
if level then [sub.compile(o, level), ref.value] else [sub, ref] |
Compile to a source/variable pair suitable for looping. | compileLoopReference: (o, name) ->
src = tmp = @compile o, LEVEL_LIST
unless -Infinity < +src < Infinity or IDENTIFIER.test(src) and o.scope.check(src, yes)
src = "#{ tmp = o.scope.freeVariable name } = #{src}"
[src, tmp] |
Construct a node that returns the current node's result. Note that this is overridden for smarter behavior for many statement nodes (e.g. If, For)... | makeReturn: (res) ->
me = @unwrapAll()
if res
new Call new Literal("#{res}.push"), [me]
else
new Return me |
Does this node, or any of its children, contain a node of a certain kind?
Recursively traverses down the children of the nodes, yielding to a block
and returning true when the block finds a match. | contains: (pred) ->
contains = no
@traverseChildren no, (node) ->
if pred node
contains = yes
return no
contains |
Is this node of a certain type, or does it contain the type? | containsType: (type) ->
this instanceof type or @contains (node) -> node instanceof type |
Pull out the last non-comment node of a node list. | lastNonComment: (list) ->
i = list.length
return list[i] while i-- when list[i] not instanceof Comment
null |
| toString: (idt = '', name = @constructor.name) ->
tree = '\n' + idt + name
tree += '?' if @soak
@eachChild (node) -> tree += node.toString idt + TAB
tree |
Passes each child to a function, breaking when the function returns | eachChild: (func) ->
return this unless @children
for attr in @children when @[attr]
for child in flatten [@[attr]]
return this if func(child) is false
this
traverseChildren: (crossScope, func) ->
@eachChild (child) ->
return false if func(child) is false
child.traverseChildren crossScope, func
invert: ->
new Op '!', this
unwrapAll: ->
node = this
continue until node is node = node.unwrap()
node |
Default implementations of the common node properties and methods. Nodes will override these with custom logic, if needed. | children: []
isStatement : NO
jumps : NO
isComplex : YES
isChainable : NO
isAssignable : NO
unwrap : THIS
unfoldSoak : NO |
Is this node used to assign a certain variable? | assigns: NO |
Block | |
The block is the list of expressions that forms the body of an
indented block of code -- the implementation of a function, a clause in an
| exports.Block = class Block extends Base
constructor: (nodes) ->
@expressions = compact flatten nodes or []
children: ['expressions'] |
Tack an expression on to the end of this expression list. | push: (node) ->
@expressions.push node
this |
Remove and return the last expression of this expression list. | pop: ->
@expressions.pop() |
Add an expression at the beginning of this expression list. | unshift: (node) ->
@expressions.unshift node
this |
If this Block consists of just a single node, unwrap it by pulling it back out. | unwrap: ->
if @expressions.length is 1 then @expressions[0] else this |
Is this an empty block of code? | isEmpty: ->
not @expressions.length
isStatement: (o) ->
for exp in @expressions when exp.isStatement o
return yes
no
jumps: (o) ->
for exp in @expressions
return exp if exp.jumps o |
A Block node does not return its entire body, rather it ensures that the final expression is returned. | makeReturn: (res) ->
len = @expressions.length
while len--
expr = @expressions[len]
if expr not instanceof Comment
@expressions[len] = expr.makeReturn res
@expressions.splice(len, 1) if expr instanceof Return and not expr.expression
break
this |
A Block is the only node that can serve as the root. | compile: (o = {}, level) ->
if o.scope then super o, level else @compileRoot o |
Compile all expressions within the Block body. If we need to return the result, and it's an expression, simply return it. If it's a statement, ask the statement to do so. | compileNode: (o) ->
@tab = o.indent
top = o.level is LEVEL_TOP
codes = []
for node in @expressions
node = node.unwrapAll()
node = (node.unfoldSoak(o) or node)
if node instanceof Block |
This is a nested block. We don't do anything special here like enclose it in a new scope; we just compile the statements in this block along with our own | codes.push node.compileNode o
else if top
node.front = true
code = node.compile o
unless node.isStatement o
code = "#{@tab}#{code};"
code = "#{code}\n" if node instanceof Literal
codes.push code
else
codes.push node.compile o, LEVEL_LIST
if top
if @spaced
return "\n#{codes.join '\n\n'}\n"
else
return codes.join '\n'
code = codes.join(', ') or 'void 0'
if codes.length > 1 and o.level >= LEVEL_LIST then "(#{code})" else code |
If we happen to be the top-level Block, wrap everything in a safety closure, unless requested not to. It would be better not to generate them in the first place, but for now, clean up obvious double-parentheses. | compileRoot: (o) ->
o.indent = if o.bare then '' else TAB
o.scope = new Scope null, this, null
o.level = LEVEL_TOP
@spaced = yes
prelude = ""
unless o.bare
preludeExps = for exp, i in @expressions
break unless exp.unwrap() instanceof Comment
exp
rest = @expressions[preludeExps.length...]
@expressions = preludeExps
prelude = "#{@compileNode merge(o, indent: '')}\n" if preludeExps.length
@expressions = rest
code = @compileWithDeclarations o
return code if o.bare
"#{prelude}(function() {\n#{code}\n}).call(this);\n" |
Compile the expressions body for the contents of a function, with declarations of all inner variables pushed up to the top. | compileWithDeclarations: (o) ->
code = post = ''
for exp, i in @expressions
exp = exp.unwrap()
break unless exp instanceof Comment or exp instanceof Literal
o = merge(o, level: LEVEL_TOP)
if i
rest = @expressions.splice i, 9e9
[spaced, @spaced] = [@spaced, no]
[code , @spaced] = [(@compileNode o), spaced]
@expressions = rest
post = @compileNode o
{scope} = o
if scope.expressions is this
declars = o.scope.hasDeclarations()
assigns = scope.hasAssignments
if declars or assigns
code += '\n' if i
code += "#{@tab}var "
if declars
code += scope.declaredVariables().join ', '
if assigns
code += ",\n#{@tab + TAB}" if declars
code += scope.assignedVariables().join ",\n#{@tab + TAB}"
code += ';\n'
code + post |
Wrap up the given nodes as a Block, unless it already happens to be one. | @wrap: (nodes) ->
return nodes[0] if nodes.length is 1 and nodes[0] instanceof Block
new Block nodes |
Literal | |
Literals are static values that can be passed through directly into
JavaScript without translation, such as: strings, numbers,
| exports.Literal = class Literal extends Base
constructor: (@value) ->
makeReturn: ->
if @isStatement() then this else super
isAssignable: ->
IDENTIFIER.test @value
isStatement: ->
@value in ['break', 'continue', 'debugger']
isComplex: NO
assigns: (name) ->
name is @value
jumps: (o) ->
return this if @value is 'break' and not (o?.loop or o?.block)
return this if @value is 'continue' and not o?.loop
compileNode: (o) ->
code = if @value is 'this'
if o.scope.method?.bound then o.scope.method.context else @value
else if @value.reserved
"\"#{@value}\""
else
@value
if @isStatement() then "#{@tab}#{code};" else code
toString: ->
' "' + @value + '"'
class exports.Undefined extends Base
isAssignable: NO
isComplex: NO
compileNode: (o) ->
if o.level >= LEVEL_ACCESS then '(void 0)' else 'void 0'
class exports.Null extends Base
isAssignable: NO
isComplex: NO
compileNode: -> "null"
class exports.Bool extends Base
isAssignable: NO
isComplex: NO
compileNode: -> @val
constructor: (@val) -> |
Return | |
A | exports.Return = class Return extends Base
constructor: (expr) ->
@expression = expr if expr and not expr.unwrap().isUndefined
children: ['expression']
isStatement: YES
makeReturn: THIS
jumps: THIS
compile: (o, level) ->
expr = @expression?.makeReturn()
if expr and expr not instanceof Return then expr.compile o, level else super o, level
compileNode: (o) ->
@tab + "return#{[" #{@expression.compile o, LEVEL_PAREN}" if @expression]};" |
Value | |
A value, variable or literal or parenthesized, indexed or dotted into, or vanilla. | exports.Value = class Value extends Base
constructor: (base, props, tag) ->
return base if not props and base instanceof Value
@base = base
@properties = props or []
@[tag] = true if tag
return this
children: ['base', 'properties'] |
Add a property (or properties ) | add: (props) ->
@properties = @properties.concat props
this
hasProperties: ->
!!@properties.length |
Some boolean checks for the benefit of other nodes. | isArray : -> not @properties.length and @base instanceof Arr
isComplex : -> @hasProperties() or @base.isComplex()
isAssignable : -> @hasProperties() or @base.isAssignable()
isSimpleNumber : -> @base instanceof Literal and SIMPLENUM.test @base.value
isString : -> @base instanceof Literal and IS_STRING.test @base.value
isAtomic : ->
for node in @properties.concat @base
return no if node.soak or node instanceof Call
yes
isStatement : (o) -> not @properties.length and @base.isStatement o
assigns : (name) -> not @properties.length and @base.assigns name
jumps : (o) -> not @properties.length and @base.jumps o
isObject: (onlyGenerated) ->
return no if @properties.length
(@base instanceof Obj) and (not onlyGenerated or @base.generated)
isSplice: ->
last(@properties) instanceof Slice |
The value can be unwrapped as its inner node, if there are no attached properties. | unwrap: ->
if @properties.length then this else @base |
A reference has base part ( | cacheReference: (o) ->
name = last @properties
if @properties.length < 2 and not @base.isComplex() and not name?.isComplex()
return [this, this] # `a` `a.b`
base = new Value @base, @properties[...-1]
if base.isComplex() # `a().b`
bref = new Literal o.scope.freeVariable 'base'
base = new Value new Parens new Assign bref, base
return [base, bref] unless name # `a()`
if name.isComplex() # `a[b()]`
nref = new Literal o.scope.freeVariable 'name'
name = new Index new Assign nref, name.index
nref = new Index nref
[base.add(name), new Value(bref or base.base, [nref or name])] |
We compile a value to JavaScript by compiling and joining each property.
Things get much more interesting if the chain of properties has soak
operators | compileNode: (o) ->
@base.front = @front
props = @properties
code = @base.compile o, if props.length then LEVEL_ACCESS else null
code = "#{code}." if (@base instanceof Parens or props.length) and SIMPLENUM.test code
code += prop.compile o for prop in props
code |
Unfold a soak into an | unfoldSoak: (o) ->
return @unfoldedSoak if @unfoldedSoak?
result = do =>
if ifn = @base.unfoldSoak o
Array::push.apply ifn.body.properties, @properties
return ifn
for prop, i in @properties when prop.soak
prop.soak = off
fst = new Value @base, @properties[...i]
snd = new Value @base, @properties[i..]
if fst.isComplex()
ref = new Literal o.scope.freeVariable 'ref'
fst = new Parens new Assign ref, fst
snd.base = ref
return new If new Existence(fst), snd, soak: on
null
@unfoldedSoak = result or no |
Comment | |
CoffeeScript passes through block comments as JavaScript block comments at the same position. | exports.Comment = class Comment extends Base
constructor: (@comment) ->
isStatement: YES
makeReturn: THIS
compileNode: (o, level) ->
code = '/*' + multident(@comment, @tab) + "\n#{@tab}*/\n"
code = o.indent + code if (level or o.level) is LEVEL_TOP
code |
Call | |
Node for a function invocation. Takes care of converting | exports.Call = class Call extends Base
constructor: (variable, @args = [], @soak) ->
@isNew = false
@isSuper = variable is 'super'
@variable = if @isSuper then null else variable
children: ['variable', 'args'] |
Tag this invocation as creating a new instance. | newInstance: ->
base = @variable?.base or @variable
if base instanceof Call and not base.isNew
base.newInstance()
else
@isNew = true
this |
Grab the reference to the superclass's implementation of the current method. | superReference: (o) ->
method = o.scope.namedMethod()
throw SyntaxError 'cannot call super outside of a function.' unless method
{name} = method
throw SyntaxError 'cannot call super on an anonymous function.' unless name?
if method.klass
accesses = [new Access(new Literal '__super__')]
accesses.push new Access new Literal 'constructor' if method.static
accesses.push new Access new Literal name
(new Value (new Literal method.klass), accesses).compile o
else
"#{name}.__super__.constructor" |
The appropriate | superThis : (o) ->
method = o.scope.method
(method and not method.klass and method.context) or "this" |
Soaked chained invocations unfold into if/else ternary structures. | unfoldSoak: (o) ->
if @soak
if @variable
return ifn if ifn = unfoldSoak o, this, 'variable'
[left, rite] = new Value(@variable).cacheReference o
else
left = new Literal @superReference o
rite = new Value left
rite = new Call rite, @args
rite.isNew = @isNew
left = new Literal "typeof #{ left.compile o } === \"function\""
return new If left, new Value(rite), soak: yes
call = this
list = []
loop
if call.variable instanceof Call
list.push call
call = call.variable
continue
break unless call.variable instanceof Value
list.push call
break unless (call = call.variable.base) instanceof Call
for call in list.reverse()
if ifn
if call.variable instanceof Call
call.variable = ifn
else
call.variable.base = ifn
ifn = unfoldSoak o, call, 'variable'
ifn |
Walk through the objects in the arguments, moving over simple values.
This allows syntax like | filterImplicitObjects: (list) ->
nodes = []
for node in list
unless node.isObject?() and node.base.generated
nodes.push node
continue
obj = null
for prop in node.base.properties
if prop instanceof Assign or prop instanceof Comment
nodes.push obj = new Obj properties = [], true if not obj
properties.push prop
else
nodes.push prop
obj = null
nodes |
Compile a vanilla function call. | compileNode: (o) ->
@variable?.front = @front
if code = Splat.compileSplattedArray o, @args, true
return @compileSplat o, code
args = @filterImplicitObjects @args
args = (arg.compile o, LEVEL_LIST for arg in args).join ', '
if @isSuper
@superReference(o) + ".call(#{@superThis(o)}#{ args and ', ' + args })"
else
(if @isNew then 'new ' else '') + @variable.compile(o, LEVEL_ACCESS) + "(#{args})" |
| compileSuper: (args, o) ->
"#{@superReference(o)}.call(#{@superThis(o)}#{ if args.length then ', ' else '' }#{args})" |
If you call a function with a splat, it's converted into a JavaScript
| compileSplat: (o, splatArgs) ->
return "#{ @superReference o }.apply(#{@superThis(o)}, #{splatArgs})" if @isSuper
if @isNew
idt = @tab + TAB
return """
(function(func, args, ctor) {
#{idt}ctor.prototype = func.prototype;
#{idt}var child = new ctor, result = func.apply(child, args);
#{idt}return Object(result) === result ? result : child;
#{@tab}})(#{ @variable.compile o, LEVEL_LIST }, #{splatArgs}, function(){})
"""
base = new Value @variable
if (name = base.properties.pop()) and base.isComplex()
ref = o.scope.freeVariable 'ref'
fun = "(#{ref} = #{ base.compile o, LEVEL_LIST })#{ name.compile o }"
else
fun = base.compile o, LEVEL_ACCESS
fun = "(#{fun})" if SIMPLENUM.test fun
if name
ref = fun
fun += name.compile o
else
ref = 'null'
"#{fun}.apply(#{ref}, #{splatArgs})" |
Extends | |
Node to extend an object's prototype with an ancestor object.
After | exports.Extends = class Extends extends Base
constructor: (@child, @parent) ->
children: ['child', 'parent'] |
Hooks one constructor into another's prototype chain. | compile: (o) ->
new Call(new Value(new Literal utility 'extends'), [@child, @parent]).compile o |
Access | |
A | exports.Access = class Access extends Base
constructor: (@name, tag) ->
@name.asKey = yes
@soak = tag is 'soak'
children: ['name']
compile: (o) ->
name = @name.compile o
if IDENTIFIER.test name then ".#{name}" else "[#{name}]"
isComplex: NO |
Index | |
A | exports.Index = class Index extends Base
constructor: (@index) ->
children: ['index']
compile: (o) ->
"[#{ @index.compile o, LEVEL_PAREN }]"
isComplex: ->
@index.isComplex() |
Range | |
A range literal. Ranges can be used to extract portions (slices) of arrays, to specify a range for comprehensions, or as a value, to be expanded into the corresponding array of integers at runtime. | exports.Range = class Range extends Base
children: ['from', 'to']
constructor: (@from, @to, tag) ->
@exclusive = tag is 'exclusive'
@equals = if @exclusive then '' else '=' |
Compiles the range's source variables -- where it starts and where it ends. But only if they need to be cached to avoid double evaluation. | compileVariables: (o) ->
o = merge o, top: true
[@fromC, @fromVar] = @from.cache o, LEVEL_LIST
[@toC, @toVar] = @to.cache o, LEVEL_LIST
[@step, @stepVar] = step.cache o, LEVEL_LIST if step = del o, 'step'
[@fromNum, @toNum] = [@fromVar.match(SIMPLENUM), @toVar.match(SIMPLENUM)]
@stepNum = @stepVar.match(SIMPLENUM) if @stepVar |
When compiled normally, the range returns the contents of the for loop needed to iterate over the values in the range. Used by comprehensions. | compileNode: (o) ->
@compileVariables o unless @fromVar
return @compileArray(o) unless o.index |
Set up endpoints. | known = @fromNum and @toNum
idx = del o, 'index'
idxName = del o, 'name'
namedIndex = idxName and idxName isnt idx
varPart = "#{idx} = #{@fromC}"
varPart += ", #{@toC}" if @toC isnt @toVar
varPart += ", #{@step}" if @step isnt @stepVar
[lt, gt] = ["#{idx} <#{@equals}", "#{idx} >#{@equals}"] |
Generate the condition. | condPart = if @stepNum
if +@stepNum > 0 then "#{lt} #{@toVar}" else "#{gt} #{@toVar}"
else if known
[from, to] = [+@fromNum, +@toNum]
if from <= to then "#{lt} #{to}" else "#{gt} #{to}"
else
cond = "#{@fromVar} <= #{@toVar}"
"#{cond} ? #{lt} #{@toVar} : #{gt} #{@toVar}" |
Generate the step. | stepPart = if @stepVar
"#{idx} += #{@stepVar}"
else if known
if namedIndex
if from <= to then "++#{idx}" else "--#{idx}"
else
if from <= to then "#{idx}++" else "#{idx}--"
else
if namedIndex
"#{cond} ? ++#{idx} : --#{idx}"
else
"#{cond} ? #{idx}++ : #{idx}--"
varPart = "#{idxName} = #{varPart}" if namedIndex
stepPart = "#{idxName} = #{stepPart}" if namedIndex |
The final loop body. | "#{varPart}; #{condPart}; #{stepPart}" |
When used as a value, expand the range into the equivalent array. | compileArray: (o) ->
if @fromNum and @toNum and Math.abs(@fromNum - @toNum) <= 20
range = [+@fromNum..+@toNum]
range.pop() if @exclusive
return "[#{ range.join(', ') }]"
idt = @tab + TAB
i = o.scope.freeVariable 'i'
result = o.scope.freeVariable 'results'
pre = "\n#{idt}#{result} = [];"
if @fromNum and @toNum
o.index = i
body = @compileNode o
else
vars = "#{i} = #{@fromC}" + if @toC isnt @toVar then ", #{@toC}" else ''
cond = "#{@fromVar} <= #{@toVar}"
body = "var #{vars}; #{cond} ? #{i} <#{@equals} #{@toVar} : #{i} >#{@equals} #{@toVar}; #{cond} ? #{i}++ : #{i}--"
post = "{ #{result}.push(#{i}); }\n#{idt}return #{result};\n#{o.indent}"
hasArgs = (node) -> node?.contains (n) -> n instanceof Literal and n.value is 'arguments' and not n.asKey
args = ', arguments' if hasArgs(@from) or hasArgs(@to)
"(function() {#{pre}\n#{idt}for (#{body})#{post}}).apply(this#{args ? ''})" |
Slice | |
An array slice literal. Unlike JavaScript's | exports.Slice = class Slice extends Base
children: ['range']
constructor: (@range) ->
super() |
We have to be careful when trying to slice through the end of the array,
| compileNode: (o) ->
{to, from} = @range
fromStr = from and from.compile(o, LEVEL_PAREN) or '0'
compiled = to and to.compile o, LEVEL_PAREN
if to and not (not @range.exclusive and +compiled is -1)
toStr = ', ' + if @range.exclusive
compiled
else if SIMPLENUM.test compiled
"#{+compiled + 1}"
else
compiled = to.compile o, LEVEL_ACCESS
"+#{compiled} + 1 || 9e9"
".slice(#{ fromStr }#{ toStr or '' })" |
Obj | |
An object literal, nothing fancy. | exports.Obj = class Obj extends Base
constructor: (props, @generated = false) ->
@objects = @properties = props or []
children: ['properties']
compileNode: (o) ->
props = @properties
return (if @front then '({})' else '{}') unless props.length
if @generated
for node in props when node instanceof Value
throw new Error 'cannot have an implicit value in an implicit object'
idt = o.indent += TAB
lastNoncom = @lastNonComment @properties
props = for prop, i in props
join = if i is props.length - 1
''
else if prop is lastNoncom or prop instanceof Comment
'\n'
else
',\n'
indent = if prop instanceof Comment then '' else idt
if prop instanceof Value and prop.this
prop = new Assign prop.properties[0].name, prop, 'object'
if prop not instanceof Comment
if prop not instanceof Assign
prop = new Assign prop, prop, 'object'
(prop.variable.base or prop.variable).asKey = yes
indent + prop.compile(o, LEVEL_TOP) + join
props = props.join ''
obj = "{#{ props and '\n' + props + '\n' + @tab }}"
if @front then "(#{obj})" else obj
assigns: (name) ->
for prop in @properties when prop.assigns name then return yes
no |
Arr | |
An array literal. | exports.Arr = class Arr extends Base
constructor: (objs) ->
@objects = objs or []
children: ['objects']
filterImplicitObjects: Call::filterImplicitObjects
compileNode: (o) ->
return '[]' unless @objects.length
o.indent += TAB
objs = @filterImplicitObjects @objects
return code if code = Splat.compileSplattedArray o, objs
code = (obj.compile o, LEVEL_LIST for obj in objs).join ', '
if code.indexOf('\n') >= 0
"[\n#{o.indent}#{code}\n#{@tab}]"
else
"[#{code}]"
assigns: (name) ->
for obj in @objects when obj.assigns name then return yes
no |
Class | |
The CoffeeScript class definition. Initialize a Class with its name, an optional superclass, and a list of prototype property assignments. | exports.Class = class Class extends Base
constructor: (@variable, @parent, @body = new Block) ->
@boundFuncs = []
@body.classBody = yes
children: ['variable', 'parent', 'body'] |
Figure out the appropriate name for the constructor function of this class. | determineName: ->
return null unless @variable
decl = if tail = last @variable.properties
tail instanceof Access and tail.name.value
else
@variable.base.value
if decl in STRICT_PROSCRIBED
throw SyntaxError "variable name may not be #{decl}"
decl and= IDENTIFIER.test(decl) and decl |
For all | setContext: (name) ->
@body.traverseChildren false, (node) ->
return false if node.classBody
if node instanceof Literal and node.value is 'this'
node.value = name
else if node instanceof Code
node.klass = name
node.context = name if node.bound |
Ensure that all functions bound to the instance are proxied in the constructor. | addBoundFunctions: (o) ->
if @boundFuncs.length
for bvar in @boundFuncs
lhs = (new Value (new Literal "this"), [new Access bvar]).compile o
@ctor.body.unshift new Literal "#{lhs} = #{utility 'bind'}(#{lhs}, this)" |
Merge the properties from a top-level object as prototypal properties on the class. | addProperties: (node, name, o) ->
props = node.base.properties[..]
exprs = while assign = props.shift()
if assign instanceof Assign
base = assign.variable.base
delete assign.context
func = assign.value
if base.value is 'constructor'
if @ctor
throw new Error 'cannot define more than one constructor in a class'
if func.bound
throw new Error 'cannot define a constructor as a bound function'
if func instanceof Code
assign = @ctor = func
else
@externalCtor = o.scope.freeVariable 'class'
assign = new Assign new Literal(@externalCtor), func
else
if assign.variable.this
func.static = yes
if func.bound
func.context = name
else
assign.variable = new Value(new Literal(name), [(new Access new Literal 'prototype'), new Access base ])
if func instanceof Code and func.bound
@boundFuncs.push base
func.bound = no
assign
compact exprs |
Walk the body of the class, looking for prototype properties to be converted. | walkBody: (name, o) ->
@traverseChildren false, (child) =>
return false if child instanceof Class
if child instanceof Block
for node, i in exps = child.expressions
if node instanceof Value and node.isObject(true)
exps[i] = @addProperties node, name, o
child.expressions = exps = flatten exps |
| hoistDirectivePrologue: ->
index = 0
{expressions} = @body
++index while (node = expressions[index]) and node instanceof Comment or
node instanceof Value and node.isString()
@directives = expressions.splice 0, index |
Make sure that a constructor is defined for the class, and properly configured. | ensureConstructor: (name) ->
if not @ctor
@ctor = new Code
@ctor.body.push new Literal "#{name}.__super__.constructor.apply(this, arguments)" if @parent
@ctor.body.push new Literal "#{@externalCtor}.apply(this, arguments)" if @externalCtor
@ctor.body.makeReturn()
@body.expressions.unshift @ctor
@ctor.ctor = @ctor.name = name
@ctor.klass = null
@ctor.noReturn = yes |
Instead of generating the JavaScript string directly, we build up the equivalent syntax tree and compile that, in pieces. You can see the constructor, property assignments, and inheritance getting built out below. | compileNode: (o) ->
decl = @determineName()
name = decl or '_Class'
name = "_#{name}" if name.reserved
lname = new Literal name
@hoistDirectivePrologue()
@setContext name
@walkBody name, o
@ensureConstructor name
@body.spaced = yes
@body.expressions.unshift @ctor unless @ctor instanceof Code
@body.expressions.push lname
@body.expressions.unshift @directives...
@addBoundFunctions o
call = Closure.wrap @body
if @parent
@superClass = new Literal o.scope.freeVariable 'super', no
@body.expressions.unshift new Extends lname, @superClass
call.args.push @parent
params = call.variable.params or call.variable.base.params
params.push new Param @superClass
klass = new Parens call, yes
klass = new Assign @variable, klass if @variable
klass.compile o |
Assign | |
The Assign is used to assign a local variable to value, or to set the property of an object -- including within object literals. | exports.Assign = class Assign extends Base
constructor: (@variable, @value, @context, options) ->
@param = options and options.param
@subpattern = options and options.subpattern
forbidden = (name = @variable.unwrapAll().value) in STRICT_PROSCRIBED
if forbidden and @context isnt 'object'
throw SyntaxError "variable name may not be \"#{name}\""
children: ['variable', 'value']
isStatement: (o) ->
o?.level is LEVEL_TOP and @context? and "?" in @context
assigns: (name) ->
@[if @context is 'object' then 'value' else 'variable'].assigns name
unfoldSoak: (o) ->
unfoldSoak o, this, 'variable' |
Compile an assignment, delegating to | compileNode: (o) ->
if isValue = @variable instanceof Value
return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
return @compileSplice o if @variable.isSplice()
return @compileConditional o if @context in ['||=', '&&=', '?=']
name = @variable.compile o, LEVEL_LIST
unless @context
unless (varBase = @variable.unwrapAll()).isAssignable()
throw SyntaxError "\"#{ @variable.compile o }\" cannot be assigned."
unless varBase.hasProperties?()
if @param
o.scope.add name, 'var'
else
o.scope.find name
if @value instanceof Code and match = METHOD_DEF.exec name
@value.klass = match[1] if match[1]
@value.name = match[2] ? match[3] ? match[4] ? match[5]
val = @value.compile o, LEVEL_LIST
return "#{name}: #{val}" if @context is 'object'
val = name + " #{ @context or '=' } " + val
if o.level <= LEVEL_LIST then val else "(#{val})" |
Brief implementation of recursive pattern matching, when assigning array or object literals to a value. Peeks at their properties to assign inner names. See the ECMAScript Harmony Wiki for details. | compilePatternMatch: (o) ->
top = o.level is LEVEL_TOP
{value} = this
{objects} = @variable.base
unless olen = objects.length
code = value.compile o
return if o.level >= LEVEL_OP then "(#{code})" else code
isObject = @variable.isObject()
if top and olen is 1 and (obj = objects[0]) not instanceof Splat |
Unroll simplest cases: | if obj instanceof Assign
{variable: {base: idx}, value: obj} = obj
else
if obj.base instanceof Parens
[obj, idx] = new Value(obj.unwrapAll()).cacheReference o
else
idx = if isObject
if obj.this then obj.properties[0].name else obj
else
new Literal 0
acc = IDENTIFIER.test idx.unwrap().value or 0
value = new Value value
value.properties.push new (if acc then Access else Index) idx
if obj.unwrap().value in RESERVED
throw new SyntaxError "assignment to a reserved word: #{obj.compile o} = #{value.compile o}"
return new Assign(obj, value, null, param: @param).compile o, LEVEL_TOP
vvar = value.compile o, LEVEL_LIST
assigns = []
splat = false
if not IDENTIFIER.test(vvar) or @variable.assigns(vvar)
assigns.push "#{ ref = o.scope.freeVariable 'ref' } = #{vvar}"
vvar = ref
for obj, i in objects |
A regular array pattern-match. | idx = i
if isObject
if obj instanceof Assign |
A regular object pattern-match. | {variable: {base: idx}, value: obj} = obj
else |
A shorthand | if obj.base instanceof Parens
[obj, idx] = new Value(obj.unwrapAll()).cacheReference o
else
idx = if obj.this then obj.properties[0].name else obj
if not splat and obj instanceof Splat
name = obj.name.unwrap().value
obj = obj.unwrap()
val = "#{olen} <= #{vvar}.length ? #{ utility 'slice' }.call(#{vvar}, #{i}"
if rest = olen - i - 1
ivar = o.scope.freeVariable 'i'
val += ", #{ivar} = #{vvar}.length - #{rest}) : (#{ivar} = #{i}, [])"
else
val += ") : []"
val = new Literal val
splat = "#{ivar}++"
else
name = obj.unwrap().value
if obj instanceof Splat
obj = obj.name.compile o
throw new SyntaxError \
"multiple splats are disallowed in an assignment: #{obj}..."
if typeof idx is 'number'
idx = new Literal splat or idx
acc = no
else
acc = isObject and IDENTIFIER.test idx.unwrap().value or 0
val = new Value new Literal(vvar), [new (if acc then Access else Index) idx]
if name? and name in RESERVED
throw new SyntaxError "assignment to a reserved word: #{obj.compile o} = #{val.compile o}"
assigns.push new Assign(obj, val, null, param: @param, subpattern: yes).compile o, LEVEL_LIST
assigns.push vvar unless top or @subpattern
code = assigns.join ', '
if o.level < LEVEL_LIST then code else "(#{code})" |
When compiling a conditional assignment, take care to ensure that the operands are only evaluated once, even though we have to reference them more than once. | compileConditional: (o) ->
[left, right] = @variable.cacheReference o |
Disallow conditional assignment of undefined variables. | if not left.properties.length and left.base instanceof Literal and
left.base.value != "this" and not o.scope.check left.base.value
throw new Error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been defined."
if "?" in @context then o.isExistentialEquals = true
new Op(@context[...-1], left, new Assign(right, @value, '=') ).compile o |
Compile the assignment from an array splice literal, using JavaScript's
| compileSplice: (o) ->
{range: {from, to, exclusive}} = @variable.properties.pop()
name = @variable.compile o
[fromDecl, fromRef] = from?.cache(o, LEVEL_OP) or ['0', '0']
if to
if from?.isSimpleNumber() and to.isSimpleNumber()
to = +to.compile(o) - +fromRef
to += 1 unless exclusive
else
to = to.compile(o, LEVEL_ACCESS) + ' - ' + fromRef
to += ' + 1' unless exclusive
else
to = "9e9"
[valDef, valRef] = @value.cache o, LEVEL_LIST
code = "[].splice.apply(#{name}, [#{fromDecl}, #{to}].concat(#{valDef})), #{valRef}"
if o.level > LEVEL_TOP then "(#{code})" else code |
Code | |
A function definition. This is the only node that creates a new Scope. When for the purposes of walking the contents of a function body, the Code has no children -- they're within the inner scope. | exports.Code = class Code extends Base
constructor: (params, body, tag) ->
@params = params or []
@body = body or new Block
@bound = tag is 'boundfunc'
@context = '_this' if @bound
children: ['params', 'body']
isStatement: -> !!@ctor
jumps: NO |
Compilation creates a new scope unless explicitly asked to share with the
outer scope. Handles splat parameters in the parameter list by peeking at
the JavaScript | compileNode: (o) ->
o.scope = new Scope o.scope, @body, this
o.scope.shared = del(o, 'sharedScope')
o.indent += TAB
delete o.bare
delete o.isExistentialEquals
params = []
exprs = []
for name in @paramNames() # this step must be performed before the others
unless o.scope.check name then o.scope.parameter name
for param in @params when param.splat
for {name: p} in @params
if p.this then p = p.properties[0].name
if p.value then o.scope.add p.value, 'var', yes
splats = new Assign new Value(new Arr(p.asReference o for p in @params)),
new Value new Literal 'arguments'
break
for param in @params
if param.isComplex()
val = ref = param.asReference o
val = new Op '?', ref, param.value if param.value
exprs.push new Assign new Value(param.name), val, '=', param: yes
else
ref = param
if param.value
lit = new Literal ref.name.value + ' == null'
val = new Assign new Value(param.name), param.value, '='
exprs.push new If lit, val
params.push ref unless splats
wasEmpty = @body.isEmpty()
exprs.unshift splats if splats
@body.expressions.unshift exprs... if exprs.length
o.scope.parameter params[i] = p.compile o for p, i in params
uniqs = []
for name in @paramNames()
throw SyntaxError "multiple parameters named '#{name}'" if name in uniqs
uniqs.push name
@body.makeReturn() unless wasEmpty or @noReturn
if @bound
if o.scope.parent.method?.bound
@bound = @context = o.scope.parent.method.context
else if not @static
o.scope.parent.assign '_this', 'this'
idt = o.indent
code = 'function'
code += ' ' + @name if @ctor
code += '(' + params.join(', ') + ') {'
code += "\n#{ @body.compileWithDeclarations o }\n#{@tab}" unless @body.isEmpty()
code += '}'
return @tab + code if @ctor
if @front or (o.level >= LEVEL_ACCESS) then "(#{code})" else code |
A list of parameter names, excluding those generated by the compiler. | paramNames: ->
names = []
names.push param.names()... for param in @params
names |
Short-circuit | traverseChildren: (crossScope, func) ->
super(crossScope, func) if crossScope |
Param | |
A parameter in a function definition. Beyond a typical Javascript parameter, these parameters can also attach themselves to the context of the function, as well as be a splat, gathering up a group of parameters into an array. | exports.Param = class Param extends Base
constructor: (@name, @value, @splat) ->
if (name = @name.unwrapAll().value) in STRICT_PROSCRIBED
throw SyntaxError "parameter name \"#{name}\" is not allowed"
children: ['name', 'value']
compile: (o) ->
@name.compile o, LEVEL_LIST
asReference: (o) ->
return @reference if @reference
node = @name
if node.this
node = node.properties[0].name
if node.value.reserved
node = new Literal o.scope.freeVariable node.value
else if node.isComplex()
node = new Literal o.scope.freeVariable 'arg'
node = new Value node
node = new Splat node if @splat
@reference = node
isComplex: ->
@name.isComplex() |
Finds the name or names of a | names: (name = @name)->
atParam = (obj) ->
{value} = obj.properties[0].name
return if value.reserved then [] else [value] |
| return [name.value] if name instanceof Literal |
| return atParam(name) if name instanceof Value
names = []
for obj in name.objects |
| if obj instanceof Assign
names.push obj.value.unwrap().value |
| else if obj instanceof Splat
names.push obj.name.unwrap().value
else if obj instanceof Value |
| if obj.isArray() or obj.isObject()
names.push @names(obj.base)... |
| else if obj.this
names.push atParam(obj)... |
| else names.push obj.base.value
else
throw SyntaxError "illegal parameter #{obj.compile()}"
names |
Splat | |
A splat, either as a parameter to a function, an argument to a call, or as part of a destructuring assignment. | exports.Splat = class Splat extends Base
children: ['name']
isAssignable: YES
constructor: (name) ->
@name = if name.compile then name else new Literal name
assigns: (name) ->
@name.assigns name
compile: (o) ->
if @index? then @compileParam o else @name.compile o
unwrap: -> @name |
Utility function that converts an arbitrary number of elements, mixed with splats, to a proper array. | @compileSplattedArray: (o, list, apply) ->
index = -1
continue while (node = list[++index]) and node not instanceof Splat
return '' if index >= list.length
if list.length is 1
code = list[0].compile o, LEVEL_LIST
return code if apply
return "#{ utility 'slice' }.call(#{code})"
args = list[index..]
for node, i in args
code = node.compile o, LEVEL_LIST
args[i] = if node instanceof Splat
then "#{ utility 'slice' }.call(#{code})"
else "[#{code}]"
return args[0] + ".concat(#{ args[1..].join ', ' })" if index is 0
base = (node.compile o, LEVEL_LIST for node in list[...index])
"[#{ base.join ', ' }].concat(#{ args.join ', ' })" |
While | |
A while loop, the only sort of low-level loop exposed by CoffeeScript. From it, all other loops can be manufactured. Useful in cases where you need more flexibility or more speed than a comprehension can provide. | exports.While = class While extends Base
constructor: (condition, options) ->
@condition = if options?.invert then condition.invert() else condition
@guard = options?.guard
children: ['condition', 'guard', 'body']
isStatement: YES
makeReturn: (res) ->
if res
super
else
@returns = not @jumps loop: yes
this
addBody: (@body) ->
this
jumps: ->
{expressions} = @body
return no unless expressions.length
for node in expressions
return node if node.jumps loop: yes
no |
The main difference from a JavaScript while is that the CoffeeScript while can be used as a part of a larger expression -- while loops may return an array containing the computed result of each iteration. | compileNode: (o) ->
o.indent += TAB
set = ''
{body} = this
if body.isEmpty()
body = ''
else
if @returns
body.makeReturn rvar = o.scope.freeVariable 'results'
set = "#{@tab}#{rvar} = [];\n"
if @guard
if body.expressions.length > 1
body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
else
body = Block.wrap [new If @guard, body] if @guard
body = "\n#{ body.compile o, LEVEL_TOP }\n#{@tab}"
code = set + @tab + "while (#{ @condition.compile o, LEVEL_PAREN }) {#{body}}"
if @returns
code += "\n#{@tab}return #{rvar};"
code |
Op | |
Simple Arithmetic and logical operations. Performs some conversion from CoffeeScript operations into their JavaScript equivalents. | exports.Op = class Op extends Base
constructor: (op, first, second, flip ) ->
return new In first, second if op is 'in'
if op is 'do'
return @generateDo first
if op is 'new'
return first.newInstance() if first instanceof Call and not first.do and not first.isNew
first = new Parens first if first instanceof Code and first.bound or first.do
@operator = CONVERSIONS[op] or op
@first = first
@second = second
@flip = !!flip
return this |
The map of conversions from CoffeeScript to JavaScript symbols. | CONVERSIONS =
'==': '==='
'!=': '!=='
'of': 'in' |
The map of invertible operators. | INVERSIONS =
'!==': '==='
'===': '!=='
children: ['first', 'second']
isSimpleNumber: NO
isUnary: ->
not @second
isComplex: ->
not (@isUnary() and (@operator in ['+', '-'])) or @first.isComplex() |
Am I capable of Python-style comparison chaining? | isChainable: ->
@operator in ['<', '>', '>=', '<=', '===', '!==']
invert: ->
if @isChainable() and @first.isChainable()
allInvertable = yes
curr = this
while curr and curr.operator
allInvertable and= (curr.operator of INVERSIONS)
curr = curr.first
return new Parens(this).invert() unless allInvertable
curr = this
while curr and curr.operator
curr.invert = !curr.invert
curr.operator = INVERSIONS[curr.operator]
curr = curr.first
this
else if op = INVERSIONS[@operator]
@operator = op
if @first.unwrap() instanceof Op
@first.invert()
this
else if @second
new Parens(this).invert()
else if @operator is '!' and (fst = @first.unwrap()) instanceof Op and
fst.operator in ['!', 'in', 'instanceof']
fst
else
new Op '!', this
unfoldSoak: (o) ->
@operator in ['++', '--', 'delete'] and unfoldSoak o, this, 'first'
generateDo: (exp) ->
passedParams = []
func = if exp instanceof Assign and (ref = exp.value.unwrap()) instanceof Code
ref
else
exp
for param in func.params or []
if param.value
passedParams.push param.value
delete param.value
else
passedParams.push param
call = new Call exp, passedParams
call.do = yes
call
compileNode: (o) ->
isChain = @isChainable() and @first.isChainable() |
In chains, there's no need to wrap bare obj literals in parens, as the chained expression is wrapped. | @first.front = @front unless isChain
if @operator is 'delete' and o.scope.check(@first.unwrapAll().value)
throw SyntaxError 'delete operand may not be argument or var'
if @operator in ['--', '++'] and @first.unwrapAll().value in STRICT_PROSCRIBED
throw SyntaxError 'prefix increment/decrement may not have eval or arguments operand'
return @compileUnary o if @isUnary()
return @compileChain o if isChain
return @compileExistence o if @operator is '?'
code = @first.compile(o, LEVEL_OP) + ' ' + @operator + ' ' +
@second.compile(o, LEVEL_OP)
if o.level <= LEVEL_OP then code else "(#{code})" |
Mimic Python's chained comparisons when multiple comparison operators are used sequentially. For example: | compileChain: (o) ->
[@first.second, shared] = @first.second.cache o
fst = @first.compile o, LEVEL_OP
code = "#{fst} #{if @invert then '&&' else '||'} #{ shared.compile o } #{@operator} #{ @second.compile o, LEVEL_OP }"
"(#{code})"
compileExistence: (o) ->
if @first.isComplex()
ref = new Literal o.scope.freeVariable 'ref'
fst = new Parens new Assign ref, @first
else
fst = @first
ref = fst
new If(new Existence(fst), ref, type: 'if').addElse(@second).compile o |
Compile a unary Op. | compileUnary: (o) ->
if o.level >= LEVEL_ACCESS
return (new Parens this).compile o
parts = [op = @operator]
plusMinus = op in ['+', '-']
parts.push ' ' if op in ['new', 'typeof', 'delete'] or
plusMinus and @first instanceof Op and @first.operator is op
if (plusMinus && @first instanceof Op) or (op is 'new' and @first.isStatement o)
@first = new Parens @first
parts.push @first.compile o, LEVEL_OP
parts.reverse() if @flip
parts.join ''
toString: (idt) ->
super idt, @constructor.name + ' ' + @operator |
In | exports.In = class In extends Base
constructor: (@object, @array) ->
children: ['object', 'array']
invert: NEGATE
compileNode: (o) ->
if @array instanceof Value and @array.isArray()
for obj in @array.base.objects when obj instanceof Splat
hasSplat = yes
break |
| return @compileOrTest o unless hasSplat
@compileLoopTest o
compileOrTest: (o) ->
return "#{!!@negated}" if @array.base.objects.length is 0
[sub, ref] = @object.cache o, LEVEL_OP
[cmp, cnj] = if @negated then [' !== ', ' && '] else [' === ', ' || ']
tests = for item, i in @array.base.objects
(if i then ref else sub) + cmp + item.compile o, LEVEL_ACCESS
tests = tests.join cnj
if o.level < LEVEL_OP then tests else "(#{tests})"
compileLoopTest: (o) ->
[sub, ref] = @object.cache o, LEVEL_LIST
code = utility('indexOf') + ".call(#{ @array.compile o, LEVEL_LIST }, #{ref}) " +
if @negated then '< 0' else '>= 0'
return code if sub is ref
code = sub + ', ' + code
if o.level < LEVEL_LIST then code else "(#{code})"
toString: (idt) ->
super idt, @constructor.name + if @negated then '!' else '' |
Try | |
A classic try/catch/finally block. | exports.Try = class Try extends Base
constructor: (@attempt, @error, @recovery, @ensure) ->
children: ['attempt', 'recovery', 'ensure']
isStatement: YES
jumps: (o) -> @attempt.jumps(o) or @recovery?.jumps(o)
makeReturn: (res) ->
@attempt = @attempt .makeReturn res if @attempt
@recovery = @recovery.makeReturn res if @recovery
this |
Compilation is more or less as you would expect -- the finally clause is optional, the catch is not. | compileNode: (o) ->
o.indent += TAB
errorPart = if @error then " (#{ @error.compile o }) " else ' '
tryPart = @attempt.compile o, LEVEL_TOP
catchPart = if @recovery
if @error.value in STRICT_PROSCRIBED
throw SyntaxError "catch variable may not be \"#{@error.value}\""
o.scope.add @error.value, 'param' unless o.scope.check @error.value
" catch#{errorPart}{\n#{ @recovery.compile o, LEVEL_TOP }\n#{@tab}}"
else unless @ensure or @recovery
' catch (_error) {}'
ensurePart = if @ensure then " finally {\n#{ @ensure.compile o, LEVEL_TOP }\n#{@tab}}" else ''
"""#{@tab}try {
#{tryPart}
#{@tab}}#{ catchPart or '' }#{ensurePart}""" |
Throw | |
Simple node to throw an exception. | exports.Throw = class Throw extends Base
constructor: (@expression) ->
children: ['expression']
isStatement: YES
jumps: NO |
A Throw is already a return, of sorts... | makeReturn: THIS
compileNode: (o) ->
@tab + "throw #{ @expression.compile o };" |
Existence | |
Checks a variable for existence -- not null and not undefined. This is
similar to | exports.Existence = class Existence extends Base
constructor: (@expression) ->
children: ['expression']
invert: NEGATE
compileNode: (o) ->
@expression.front = @front
code = @expression.compile o, LEVEL_OP
if IDENTIFIER.test(code) and not o.scope.check code
[cmp, cnj] = if @negated then ['===', '||'] else ['!==', '&&']
code = "typeof #{code} #{cmp} \"undefined\" #{cnj} #{code} #{cmp} null"
else |
do not use strict equality here; it will break existing code | code = "#{code} #{if @negated then '==' else '!='} null"
if o.level <= LEVEL_COND then code else "(#{code})" |
Parens | |
An extra set of parentheses, specified explicitly in the source. At one time we tried to clean up the results by detecting and removing redundant parentheses, but no longer -- you can put in as many as you please. Parentheses are a good way to force any statement to become an expression. | exports.Parens = class Parens extends Base
constructor: (@body) ->
children: ['body']
unwrap : -> @body
isComplex : -> @body.isComplex()
compileNode: (o) ->
expr = @body.unwrap()
if expr instanceof Value and expr.isAtomic()
expr.front = @front
return expr.compile o
code = expr.compile o, LEVEL_PAREN
bare = o.level < LEVEL_OP and (expr instanceof Op or expr instanceof Call or
(expr instanceof For and expr.returns))
if bare then code else "(#{code})" |
For | |
CoffeeScript's replacement for the for loop is our array and object comprehensions, that compile into for loops here. They also act as an expression, able to return the result of each filtered iteration. Unlike Python array comprehensions, they can be multi-line, and you can pass the current index of the loop as a second parameter. Unlike Ruby blocks, you can map and filter in a single pass. | exports.For = class For extends While
constructor: (body, source) ->
{@source, @guard, @step, @name, @index} = source
@body = Block.wrap [body]
@own = !!source.own
@object = !!source.object
[@name, @index] = [@index, @name] if @object
throw SyntaxError 'index cannot be a pattern matching expression' if @index instanceof Value
@range = @source instanceof Value and @source.base instanceof Range and not @source.properties.length
@pattern = @name instanceof Value
throw SyntaxError 'indexes do not apply to range loops' if @range and @index
throw SyntaxError 'cannot pattern match over range loops' if @range and @pattern
@returns = false
children: ['body', 'source', 'guard', 'step'] |
Welcome to the hairiest method in all of CoffeeScript. Handles the inner loop, filtering, stepping, and result saving for array, object, and range comprehensions. Some of the generated code can be shared in common, and some cannot. | compileNode: (o) ->
body = Block.wrap [@body]
lastJumps = last(body.expressions)?.jumps()
@returns = no if lastJumps and lastJumps instanceof Return
source = if @range then @source.base else @source
scope = o.scope
name = @name and @name.compile o, LEVEL_LIST
index = @index and @index.compile o, LEVEL_LIST
scope.find(name) if name and not @pattern
scope.find(index) if index
rvar = scope.freeVariable 'results' if @returns
ivar = (@object and index) or scope.freeVariable 'i'
kvar = (@range and name) or index or ivar
kvarAssign = if kvar isnt ivar then "#{kvar} = " else "" |
the | stepvar = scope.freeVariable "step" if @step and not @range
name = ivar if @pattern
varPart = ''
guardPart = ''
defPart = ''
idt1 = @tab + TAB
if @range
forPart = source.compile merge(o, {index: ivar, name, @step})
else
svar = @source.compile o, LEVEL_LIST
if (name or @own) and not IDENTIFIER.test svar
defPart = "#{@tab}#{ref = scope.freeVariable 'ref'} = #{svar};\n"
svar = ref
if name and not @pattern
namePart = "#{name} = #{svar}[#{kvar}]"
unless @object
lvar = scope.freeVariable 'len'
forVarPart = "#{kvarAssign}#{ivar} = 0, #{lvar} = #{svar}.length"
forVarPart += ", #{stepvar} = #{@step.compile o, LEVEL_OP}" if @step
stepPart = "#{kvarAssign}#{if @step then "#{ivar} += #{stepvar}" else (if kvar isnt ivar then "++#{ivar}" else "#{ivar}++")}"
forPart = "#{forVarPart}; #{ivar} < #{lvar}; #{stepPart}"
if @returns
resultPart = "#{@tab}#{rvar} = [];\n"
returnResult = "\n#{@tab}return #{rvar};"
body.makeReturn rvar
if @guard
if body.expressions.length > 1
body.expressions.unshift new If (new Parens @guard).invert(), new Literal "continue"
else
body = Block.wrap [new If @guard, body] if @guard
if @pattern
body.expressions.unshift new Assign @name, new Literal "#{svar}[#{kvar}]"
defPart += @pluckDirectCall o, body
varPart = "\n#{idt1}#{namePart};" if namePart
if @object
forPart = "#{kvar} in #{svar}"
guardPart = "\n#{idt1}if (!#{utility 'hasProp'}.call(#{svar}, #{kvar})) continue;" if @own
body = body.compile merge(o, indent: idt1), LEVEL_TOP
body = '\n' + body + '\n' if body
"""
#{defPart}#{resultPart or ''}#{@tab}for (#{forPart}) {#{guardPart}#{varPart}#{body}#{@tab}}#{returnResult or ''}
"""
pluckDirectCall: (o, body) ->
defs = ''
for expr, idx in body.expressions
expr = expr.unwrapAll()
continue unless expr instanceof Call
val = expr.variable.unwrapAll()
continue unless (val instanceof Code) or
(val instanceof Value and
val.base?.unwrapAll() instanceof Code and
val.properties.length is 1 and
val.properties[0].name?.value in ['call', 'apply'])
fn = val.base?.unwrapAll() or val
ref = new Literal o.scope.freeVariable 'fn'
base = new Value ref
if val.base
[val.base, base] = [base, val]
body.expressions[idx] = new Call base, expr.args
defs += @tab + new Assign(ref, fn).compile(o, LEVEL_TOP) + ';\n'
defs |
Switch | |
A JavaScript switch statement. Converts into a returnable expression on-demand. | exports.Switch = class Switch extends Base
constructor: (@subject, @cases, @otherwise) ->
children: ['subject', 'cases', 'otherwise']
isStatement: YES
jumps: (o = {block: yes}) ->
for [conds, block] in @cases
return block if block.jumps o
@otherwise?.jumps o
makeReturn: (res) ->
pair[1].makeReturn res for pair in @cases
@otherwise or= new Block [new Literal 'void 0'] if res
@otherwise?.makeReturn res
this
compileNode: (o) ->
idt1 = o.indent + TAB
idt2 = o.indent = idt1 + TAB
code = @tab + "switch (#{ @subject?.compile(o, LEVEL_PAREN) or false }) {\n"
for [conditions, block], i in @cases
for cond in flatten [conditions]
cond = cond.invert() unless @subject
code += idt1 + "case #{ cond.compile o, LEVEL_PAREN }:\n"
code += body + '\n' if body = block.compile o, LEVEL_TOP
break if i is @cases.length - 1 and not @otherwise
expr = @lastNonComment block.expressions
continue if expr instanceof Return or (expr instanceof Literal and expr.jumps() and expr.value isnt 'debugger')
code += idt2 + 'break;\n'
code += idt1 + "default:\n#{ @otherwise.compile o, LEVEL_TOP }\n" if @otherwise and @otherwise.expressions.length
code + @tab + '}' |
If | |
If/else statements. Acts as an expression by pushing down requested returns to the last line of each clause. Single-expression Ifs are compiled into conditional operators if possible, because ternaries are already proper expressions, and don't need conversion. | exports.If = class If extends Base
constructor: (condition, @body, options = {}) ->
@condition = if options.type is 'unless' then condition.invert() else condition
@elseBody = null
@isChain = false
{@soak} = options
children: ['condition', 'body', 'elseBody']
bodyNode: -> @body?.unwrap()
elseBodyNode: -> @elseBody?.unwrap() |
Rewrite a chain of Ifs to add a default case as the final else. | addElse: (elseBody) ->
if @isChain
@elseBodyNode().addElse elseBody
else
@isChain = elseBody instanceof If
@elseBody = @ensureBlock elseBody
this |
The If only compiles into a statement if either of its bodies needs to be a statement. Otherwise a conditional operator is safe. | isStatement: (o) ->
o?.level is LEVEL_TOP or
@bodyNode().isStatement(o) or @elseBodyNode()?.isStatement(o)
jumps: (o) -> @body.jumps(o) or @elseBody?.jumps(o)
compileNode: (o) ->
if @isStatement o then @compileStatement o else @compileExpression o
makeReturn: (res) ->
@elseBody or= new Block [new Literal 'void 0'] if res
@body and= new Block [@body.makeReturn res]
@elseBody and= new Block [@elseBody.makeReturn res]
this
ensureBlock: (node) ->
if node instanceof Block then node else new Block [node] |
Compile the | compileStatement: (o) ->
child = del o, 'chainChild'
exeq = del o, 'isExistentialEquals'
if exeq
return new If(@condition.invert(), @elseBodyNode(), type: 'if').compile o
cond = @condition.compile o, LEVEL_PAREN
o.indent += TAB
body = @ensureBlock(@body)
ifPart = "if (#{cond}) {\n#{body.compile(o)}\n#{@tab}}"
ifPart = @tab + ifPart unless child
return ifPart unless @elseBody
ifPart + ' else ' + if @isChain
o.indent = @tab
o.chainChild = yes
@elseBody.unwrap().compile o, LEVEL_TOP
else
"{\n#{ @elseBody.compile o, LEVEL_TOP }\n#{@tab}}" |
Compile the | compileExpression: (o) ->
cond = @condition.compile o, LEVEL_COND
body = @bodyNode().compile o, LEVEL_LIST
alt = if @elseBodyNode() then @elseBodyNode().compile(o, LEVEL_LIST) else 'void 0'
code = "#{cond} ? #{body} : #{alt}"
if o.level >= LEVEL_COND then "(#{code})" else code
unfoldSoak: ->
@soak and this |
Faux-NodesFaux-nodes are never created by the grammar, but are used during code generation to generate other combinations of nodes. | |
Closure | |
A faux-node used to wrap an expressions body in a closure. | Closure = |
Wrap the expressions body, unless it contains a pure statement,
in which case, no dice. If the body mentions | wrap: (expressions, statement, noReturn) ->
return expressions if expressions.jumps()
func = new Code [], Block.wrap [expressions]
args = []
if (mentionsArgs = expressions.contains @literalArgs) or expressions.contains @literalThis
meth = new Literal if mentionsArgs then 'apply' else 'call'
args = [new Literal 'this']
args.push new Literal 'arguments' if mentionsArgs
func = new Value func, [new Access meth]
func.noReturn = noReturn
call = new Call func, args
if statement then Block.wrap [call] else call
literalArgs: (node) ->
node instanceof Literal and node.value is 'arguments' and not node.asKey
literalThis: (node) ->
(node instanceof Literal and node.value is 'this' and not node.asKey) or
(node instanceof Code and node.bound) or
(node instanceof Call and node.isSuper) |
Unfold a node's child if soak, then tuck the node under created | unfoldSoak = (o, parent, name) ->
return unless ifn = parent[name].unfoldSoak o
parent[name] = ifn.body
ifn.body = new Value parent
ifn |
Constants | UTILITIES = |
Correctly set up a prototype chain for inheritance, including a reference
to the superclass for | extends: -> """
function(child, parent) { for (var key in parent) { if (#{utility 'hasProp'}.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }
""" |
Create a function bound to the current value of "this". | bind: -> '''
function(fn, me){ return function(){ return fn.apply(me, arguments); }; }
''' |
Discover if an item is in an array. | indexOf: -> """
[].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }
""" |
Shortcuts to speed up the lookup time for native functions. | hasProp: -> '{}.hasOwnProperty'
slice : -> '[].slice' |
Levels indicate a node's position in the AST. Useful for knowing if parens are necessary or superfluous. | LEVEL_TOP = 1 # ...;
LEVEL_PAREN = 2 # (...)
LEVEL_LIST = 3 # [...]
LEVEL_COND = 4 # ... ? x : y
LEVEL_OP = 5 # !...
LEVEL_ACCESS = 6 # ...[0] |
Tabs are two spaces for pretty printing. | TAB = ' '
IDENTIFIER_STR = "[$A-Za-z_\\x7f-\\uffff][$\\w\\x7f-\\uffff]*"
IDENTIFIER = /// ^ #{IDENTIFIER_STR} $ ///
SIMPLENUM = /^[+-]?\d+$/
METHOD_DEF = ///
^
(?:
(#{IDENTIFIER_STR})
\.prototype
(?:
\.(#{IDENTIFIER_STR})
| \[("(?:[^\\"\r\n]|\\.)*"|'(?:[^\\'\r\n]|\\.)*')\]
| \[(0x[\da-fA-F]+ | \d*\.?\d+ (?:[eE][+-]?\d+)?)\]
)
)
|
(#{IDENTIFIER_STR})
$
/// |
Is a literal value a string? | IS_STRING = /^['"]/ |
Utility Functions | |
Helper for ensuring that utility functions are assigned at the top level. | utility = (name) ->
ref = "__#{name}"
Scope.root.assign ref, UTILITIES[name]()
ref
multident = (code, tab) ->
code = code.replace /\n/g, '$&' + tab
code.replace /\s+$/, ''
|
optparse.coffee | |
---|---|
A simple OptionParser class to parse option flags from the command-line. Use it like so:
The first non-option is considered to be the start of the file (and file option) list, and all subsequent arguments are left unparsed. | exports.OptionParser = class OptionParser |
Initialize with a list of valid options, in the form:
Along with an an optional banner for the usage help. | constructor: (rules, @banner) ->
@rules = buildRules rules |
Parse the list of arguments, populating an | parse: (args) ->
options = arguments: []
skippingArgument = no
originalArgs = args
args = normalizeArguments args
for arg, i in args
if skippingArgument
skippingArgument = no
continue
if arg is '--'
pos = originalArgs.indexOf '--'
options.arguments = options.arguments.concat originalArgs[(pos + 1)..]
break
isOption = !!(arg.match(LONG_FLAG) or arg.match(SHORT_FLAG)) |
the CS option parser is a little odd; options after the first non-option argument are treated as non-option arguments themselves | seenNonOptionArg = options.arguments.length > 0
unless seenNonOptionArg
matchedRule = no
for rule in @rules
if rule.shortFlag is arg or rule.longFlag is arg
value = true
if rule.hasArgument
skippingArgument = yes
value = args[i + 1]
options[rule.name] = if rule.isList then (options[rule.name] or []).concat value else value
matchedRule = yes
break
throw new Error "unrecognized option: #{arg}" if isOption and not matchedRule
if seenNonOptionArg or not isOption
options.arguments.push arg
options |
Return the help text for this OptionParser, listing and describing all
of the valid options, for | help: ->
lines = []
lines.unshift "#{@banner}\n" if @banner
for rule in @rules
spaces = 15 - rule.longFlag.length
spaces = if spaces > 0 then Array(spaces + 1).join(' ') else ''
letPart = if rule.shortFlag then rule.shortFlag + ', ' else ' '
lines.push ' ' + letPart + rule.longFlag + spaces + rule.description
"\n#{ lines.join('\n') }\n" |
Helpers | |
Regex matchers for option flags. | LONG_FLAG = /^(--\w[\w\-]*)/
SHORT_FLAG = /^(-\w)$/
MULTI_FLAG = /^-(\w{2,})/
OPTIONAL = /\[(\w+(\*?))\]/ |
Build and return the list of option rules. If the optional short-flag is
unspecified, leave it out by padding with | buildRules = (rules) ->
for tuple in rules
tuple.unshift null if tuple.length < 3
buildRule tuple... |
Build a rule from a | buildRule = (shortFlag, longFlag, description, options = {}) ->
match = longFlag.match(OPTIONAL)
longFlag = longFlag.match(LONG_FLAG)[1]
{
name: longFlag.substr 2
shortFlag: shortFlag
longFlag: longFlag
description: description
hasArgument: !!(match and match[1])
isList: !!(match and match[2])
} |
Normalize arguments by expanding merged flags into multiple flags. This allows
you to have | normalizeArguments = (args) ->
args = args[..]
result = []
for arg in args
if match = arg.match MULTI_FLAG
result.push '-' + l for l in match[1].split ''
else
result.push arg
result
|
repl.coffee | |
---|---|
A very simple Read-Eval-Print-Loop. Compiles one line at a time to JavaScript and evaluates it. Good for simple tests, or poking around the Node.js API. Using it looks like this: | |
Start by opening up | stdin = process.openStdin()
stdout = process.stdout |
Require the coffee-script module to get access to the compiler. | CoffeeScript = require './coffee-script'
readline = require 'readline'
{inspect} = require 'util'
{Script} = require 'vm'
Module = require 'module' |
REPL Setup | |
Config | REPL_PROMPT = 'coffee> '
REPL_PROMPT_MULTILINE = '------> '
REPL_PROMPT_CONTINUATION = '......> '
enableColours = no
unless process.platform is 'win32'
enableColours = not process.env.NODE_DISABLE_COLORS |
Log an error. | error = (err) ->
stdout.write (err.stack or err.toString()) + '\n' |
Autocompletion | |
Regexes to match complete-able bits of text. | ACCESSOR = /\s*([\w\.]+)(?:\.(\w*))$/
SIMPLEVAR = /(\w+)$/i |
Returns a list of completions, and the completed text. | autocomplete = (text) ->
completeAttribute(text) or completeVariable(text) or [[], text] |
Attempt to autocomplete a chained dotted attribute: | completeAttribute = (text) ->
if match = text.match ACCESSOR
[all, obj, prefix] = match
try obj = Script.runInThisContext obj
catch e
return
return unless obj?
obj = Object obj
candidates = Object.getOwnPropertyNames obj
while obj = Object.getPrototypeOf obj
for key in Object.getOwnPropertyNames obj when key not in candidates
candidates.push key
completions = getCompletions prefix, candidates
[completions, prefix] |
Attempt to autocomplete an in-scope free variable: | completeVariable = (text) ->
free = text.match(SIMPLEVAR)?[1]
free = "" if text is ""
if free?
vars = Script.runInThisContext 'Object.getOwnPropertyNames(Object(this))'
keywords = (r for r in CoffeeScript.RESERVED when r[..1] isnt '__')
candidates = vars
for key in keywords when key not in candidates
candidates.push key
completions = getCompletions free, candidates
[completions, free] |
Return elements of candidates for which | getCompletions = (prefix, candidates) ->
el for el in candidates when 0 is el.indexOf prefix |
Make sure that uncaught exceptions don't kill the REPL. | process.on 'uncaughtException', error |
The current backlog of multi-line code. | backlog = '' |
The main REPL function. run is called every time a line of code is entered. Attempt to evaluate the command. If there's an exception, print it out instead of exiting. | run = (buffer) -> |
remove single-line comments | buffer = buffer.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3" |
remove trailing newlines | buffer = buffer.replace /[\r\n]+$/, ""
if multilineMode
backlog += "#{buffer}\n"
repl.setPrompt REPL_PROMPT_CONTINUATION
repl.prompt()
return
if !buffer.toString().trim() and !backlog
repl.prompt()
return
code = backlog += buffer
if code[code.length - 1] is '\\'
backlog = "#{backlog[...-1]}\n"
repl.setPrompt REPL_PROMPT_CONTINUATION
repl.prompt()
return
repl.setPrompt REPL_PROMPT
backlog = ''
try
_ = global._
returnValue = CoffeeScript.eval "_=(#{code}\n)", {
filename: 'repl'
modulename: 'repl'
}
if returnValue is undefined
global._ = _
repl.output.write "#{inspect returnValue, no, 2, enableColours}\n"
catch err
error err
repl.prompt()
if stdin.readable and stdin.isRaw |
handle piped input | pipedInput = ''
repl =
prompt: -> stdout.write @_prompt
setPrompt: (p) -> @_prompt = p
input: stdin
output: stdout
on: ->
stdin.on 'data', (chunk) ->
pipedInput += chunk
return unless /\n/.test pipedInput
lines = pipedInput.split "\n"
pipedInput = lines[lines.length - 1]
for line in lines[...-1] when line
stdout.write "#{line}\n"
run line
return
stdin.on 'end', ->
for line in pipedInput.trim().split "\n" when line
stdout.write "#{line}\n"
run line
stdout.write '\n'
process.exit 0
else |
Create the REPL by listening to stdin. | if readline.createInterface.length < 3
repl = readline.createInterface stdin, autocomplete
stdin.on 'data', (buffer) -> repl.write buffer
else
repl = readline.createInterface stdin, stdout, autocomplete
multilineMode = off |
Handle multi-line mode switch | repl.input.on 'keypress', (char, key) -> |
test for Ctrl-v | return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
cursorPos = repl.cursor
repl.output.cursorTo 0
repl.output.clearLine 1
multilineMode = not multilineMode
repl._line() if not multilineMode and backlog
backlog = ''
repl.setPrompt (newPrompt = if multilineMode then REPL_PROMPT_MULTILINE else REPL_PROMPT)
repl.prompt()
repl.output.cursorTo newPrompt.length + (repl.cursor = cursorPos) |
Handle Ctrl-d press at end of last line in multiline mode | repl.input.on 'keypress', (char, key) ->
return unless multilineMode and repl.line |
test for Ctrl-d | return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'd'
multilineMode = off
repl._line()
repl.on 'attemptClose', ->
if multilineMode
multilineMode = off
repl.output.cursorTo 0
repl.output.clearLine 1
repl._onLine repl.line
return
if backlog or repl.line
backlog = ''
repl.historyIndex = -1
repl.setPrompt REPL_PROMPT
repl.output.write '\n(^C again to quit)'
repl._line (repl.line = '')
else
repl.close()
repl.on 'close', ->
repl.output.write '\n'
repl.input.destroy()
repl.on 'line', run
repl.setPrompt REPL_PROMPT
repl.prompt()
|
rewriter.coffee | |
---|---|
The CoffeeScript language has a good deal of optional syntax, implicit syntax, and shorthand syntax. This can greatly complicate a grammar and bloat the resulting parse table. Instead of making the parser handle it all, we take a series of passes over the token stream, using this Rewriter to convert shorthand into the unambiguous long form, add implicit indentation and parentheses, and generally clean things up. | |
The Rewriter class is used by the Lexer, directly against its internal array of tokens. | class exports.Rewriter |
Helpful snippet for debugging: console.log (t[0] + '/' + t[1] for t in @tokens).join ' ' | |
Rewrite the token stream in multiple passes, one logical filter at a time. This could certainly be changed into a single pass through the stream, with a big ol' efficient switch, but it's much nicer to work with like this. The order of these passes matters -- indentation must be corrected before implicit parentheses can be wrapped around blocks of code. | rewrite: (@tokens) ->
@removeLeadingNewlines()
@removeMidExpressionNewlines()
@closeOpenCalls()
@closeOpenIndexes()
@addImplicitIndentation()
@tagPostfixConditionals()
@addImplicitBraces()
@addImplicitParentheses()
@tokens |
Rewrite the token stream, looking one token ahead and behind. Allow the return value of the block to tell us how many tokens to move forwards (or backwards) in the stream, to make sure we don't miss anything as tokens are inserted and removed, and the stream changes length under our feet. | scanTokens: (block) ->
{tokens} = this
i = 0
i += block.call this, token, i, tokens while token = tokens[i]
true
detectEnd: (i, condition, action) ->
{tokens} = this
levels = 0
while token = tokens[i]
return action.call this, token, i if levels is 0 and condition.call this, token, i
return action.call this, token, i - 1 if not token or levels < 0
if token[0] in EXPRESSION_START
levels += 1
else if token[0] in EXPRESSION_END
levels -= 1
i += 1
i - 1 |
Leading newlines would introduce an ambiguity in the grammar, so we dispatch them here. | removeLeadingNewlines: ->
break for [tag], i in @tokens when tag isnt 'TERMINATOR'
@tokens.splice 0, i if i |
Some blocks occur in the middle of expressions -- when we're expecting this, remove their trailing newlines. | removeMidExpressionNewlines: ->
@scanTokens (token, i, tokens) ->
return 1 unless token[0] is 'TERMINATOR' and @tag(i + 1) in EXPRESSION_CLOSE
tokens.splice i, 1
0 |
The lexer has tagged the opening parenthesis of a method call. Match it with its paired close. We have the mis-nested outdent case included here for calls that close on the same line, just before their outdent. | closeOpenCalls: ->
condition = (token, i) ->
token[0] in [')', 'CALL_END'] or
token[0] is 'OUTDENT' and @tag(i - 1) is ')'
action = (token, i) ->
@tokens[if token[0] is 'OUTDENT' then i - 1 else i][0] = 'CALL_END'
@scanTokens (token, i) ->
@detectEnd i + 1, condition, action if token[0] is 'CALL_START'
1 |
The lexer has tagged the opening parenthesis of an indexing operation call. Match it with its paired close. | closeOpenIndexes: ->
condition = (token, i) ->
token[0] in [']', 'INDEX_END']
action = (token, i) ->
token[0] = 'INDEX_END'
@scanTokens (token, i) ->
@detectEnd i + 1, condition, action if token[0] is 'INDEX_START'
1 |
Object literals may be written with implicit braces, for simple cases. Insert the missing braces here, so that the parser doesn't have to. | addImplicitBraces: ->
stack = []
start = null
startsLine = null
sameLine = yes
startIndent = 0
startIndex = 0
condition = (token, i) ->
[one, two, three] = @tokens[i + 1 .. i + 3]
return no if 'HERECOMMENT' is one?[0]
[tag] = token
sameLine = no if tag in LINEBREAKS
return (
(tag in ['TERMINATOR', 'OUTDENT'] or
(tag in IMPLICIT_END and sameLine and not (i - startIndex is 1))) and
((!startsLine and @tag(i - 1) isnt ',') or
not (two?[0] is ':' or one?[0] is '@' and three?[0] is ':'))) or
(tag is ',' and one and
one[0] not in ['IDENTIFIER', 'NUMBER', 'STRING', '@', 'TERMINATOR', 'OUTDENT']
)
action = (token, i) ->
tok = @generate '}', '}', token[2]
@tokens.splice i, 0, tok
@scanTokens (token, i, tokens) ->
if (tag = token[0]) in EXPRESSION_START
stack.push [(if tag is 'INDENT' and @tag(i - 1) is '{' then '{' else tag), i]
return 1
if tag in EXPRESSION_END
start = stack.pop()
return 1
return 1 unless tag is ':' and
((ago = @tag i - 2) is ':' or stack[stack.length - 1]?[0] isnt '{')
sameLine = yes
startIndex = i + 1
stack.push ['{']
idx = if ago is '@' then i - 2 else i - 1
idx -= 2 while @tag(idx - 2) is 'HERECOMMENT'
prevTag = @tag(idx - 1)
startsLine = not prevTag or (prevTag in LINEBREAKS)
value = new String('{')
value.generated = yes
tok = @generate '{', value, token[2]
tokens.splice idx, 0, tok
@detectEnd i + 2, condition, action
2 |
Methods may be optionally called without parentheses, for simple cases. Insert the implicit parentheses here, so that the parser doesn't have to deal with them. | addImplicitParentheses: ->
noCall = seenSingle = seenControl = no
condition = (token, i) ->
[tag] = token
return yes if not seenSingle and token.fromThen
seenSingle = yes if tag in ['IF', 'ELSE', 'CATCH', '->', '=>', 'CLASS']
seenControl = yes if tag in ['IF', 'ELSE', 'SWITCH', 'TRY', '=']
return yes if tag in ['.', '?.', '::'] and @tag(i - 1) is 'OUTDENT'
not token.generated and @tag(i - 1) isnt ',' and (tag in IMPLICIT_END or
(tag is 'INDENT' and not seenControl)) and
(tag isnt 'INDENT' or
(@tag(i - 2) not in ['CLASS', 'EXTENDS'] and @tag(i - 1) not in IMPLICIT_BLOCK and
not ((post = @tokens[i + 1]) and post.generated and post[0] is '{')))
action = (token, i) ->
@tokens.splice i, 0, @generate 'CALL_END', ')', token[2]
@scanTokens (token, i, tokens) ->
tag = token[0]
noCall = yes if tag in ['CLASS', 'IF', 'FOR', 'WHILE']
[prev, current, next] = tokens[i - 1 .. i + 1]
callObject = not noCall and tag is 'INDENT' and
next and next.generated and next[0] is '{' and
prev and prev[0] in IMPLICIT_FUNC
seenSingle = no
seenControl = no
noCall = no if tag in LINEBREAKS
token.call = yes if prev and not prev.spaced and tag is '?'
return 1 if token.fromThen
return 1 unless callObject or
prev?.spaced and (prev.call or prev[0] in IMPLICIT_FUNC) and
(tag in IMPLICIT_CALL or not (token.spaced or token.newLine) and tag in IMPLICIT_UNSPACED_CALL)
tokens.splice i, 0, @generate 'CALL_START', '(', token[2]
@detectEnd i + 1, condition, action
prev[0] = 'FUNC_EXIST' if prev[0] is '?'
2 |
Because our grammar is LALR(1), it can't handle some single-line expressions that lack ending delimiters. The Rewriter adds the implicit blocks, so it doesn't need to. ')' can close a single-line block, but we need to make sure it's balanced. | addImplicitIndentation: ->
starter = indent = outdent = null
condition = (token, i) ->
token[1] isnt ';' and token[0] in SINGLE_CLOSERS and
not (token[0] is 'ELSE' and starter not in ['IF', 'THEN'])
action = (token, i) ->
@tokens.splice (if @tag(i - 1) is ',' then i - 1 else i), 0, outdent
@scanTokens (token, i, tokens) ->
[tag] = token
if tag is 'TERMINATOR' and @tag(i + 1) is 'THEN'
tokens.splice i, 1
return 0
if tag is 'ELSE' and @tag(i - 1) isnt 'OUTDENT'
tokens.splice i, 0, @indentation(token)...
return 2
if tag is 'CATCH' and @tag(i + 2) in ['OUTDENT', 'TERMINATOR', 'FINALLY']
tokens.splice i + 2, 0, @indentation(token)...
return 4
if tag in SINGLE_LINERS and @tag(i + 1) isnt 'INDENT' and
not (tag is 'ELSE' and @tag(i + 1) is 'IF')
starter = tag
[indent, outdent] = @indentation token, yes
indent.fromThen = true if starter is 'THEN'
tokens.splice i + 1, 0, indent
@detectEnd i + 2, condition, action
tokens.splice i, 1 if tag is 'THEN'
return 1
return 1 |
Tag postfix conditionals as such, so that we can parse them with a different precedence. | tagPostfixConditionals: ->
original = null
condition = (token, i) ->
token[0] in ['TERMINATOR', 'INDENT']
action = (token, i) ->
if token[0] isnt 'INDENT' or (token.generated and not token.fromThen)
original[0] = 'POST_' + original[0]
@scanTokens (token, i) ->
return 1 unless token[0] is 'IF'
original = token
@detectEnd i + 1, condition, action
1 |
Generate the indentation tokens, based on another token on the same line. | indentation: (token, implicit = no) ->
indent = ['INDENT', 2, token[2]]
outdent = ['OUTDENT', 2, token[2]]
indent.generated = outdent.generated = yes if implicit
[indent, outdent] |
Create a generated token: one that exists due to a use of implicit syntax. | generate: (tag, value, line) ->
tok = [tag, value, line]
tok.generated = yes
tok |
Look up a tag by token index. | tag: (i) -> @tokens[i]?[0] |
Constants | |
List of the token pairs that must be balanced. | BALANCED_PAIRS = [
['(', ')']
['[', ']']
['{', '}']
['INDENT', 'OUTDENT'],
['CALL_START', 'CALL_END']
['PARAM_START', 'PARAM_END']
['INDEX_START', 'INDEX_END']
] |
The inverse mappings of | exports.INVERSES = INVERSES = {} |
The tokens that signal the start/end of a balanced pair. | EXPRESSION_START = []
EXPRESSION_END = []
for [left, rite] in BALANCED_PAIRS
EXPRESSION_START.push INVERSES[rite] = left
EXPRESSION_END .push INVERSES[left] = rite |
Tokens that indicate the close of a clause of an expression. | EXPRESSION_CLOSE = ['CATCH', 'WHEN', 'ELSE', 'FINALLY'].concat EXPRESSION_END |
Tokens that, if followed by an | IMPLICIT_FUNC = ['IDENTIFIER', 'SUPER', ')', 'CALL_END', ']', 'INDEX_END', '@', 'THIS'] |
If preceded by an | IMPLICIT_CALL = [
'IDENTIFIER', 'NUMBER', 'STRING', 'JS', 'REGEX', 'NEW', 'PARAM_START', 'CLASS'
'IF', 'TRY', 'SWITCH', 'THIS', 'BOOL', 'NULL', 'UNDEFINED', 'UNARY', 'SUPER'
'@', '->', '=>', '[', '(', '{', '--', '++'
]
IMPLICIT_UNSPACED_CALL = ['+', '-'] |
Tokens indicating that the implicit call must enclose a block of expressions. | IMPLICIT_BLOCK = ['->', '=>', '{', '[', ','] |
Tokens that always mark the end of an implicit call for single-liners. | IMPLICIT_END = ['POST_IF', 'FOR', 'WHILE', 'UNTIL', 'WHEN', 'BY', 'LOOP', 'TERMINATOR'] |
Single-line flavors of block expressions that have unclosed endings. The grammar can't disambiguate them, so we insert the implicit indentation. | SINGLE_LINERS = ['ELSE', '->', '=>', 'TRY', 'FINALLY', 'THEN']
SINGLE_CLOSERS = ['TERMINATOR', 'CATCH', 'FINALLY', 'ELSE', 'OUTDENT', 'LEADING_WHEN'] |
Tokens that end a line. | LINEBREAKS = ['TERMINATOR', 'INDENT', 'OUTDENT']
|
scope.coffee | |
---|---|
The Scope class regulates lexical scoping within CoffeeScript. As you
generate code, you create a tree of scopes in the same shape as the nested
function bodies. Each scope knows about the variables declared within it,
and has a reference to its parent enclosing scope. In this way, we know which
variables are new and need to be declared with | |
Import the helpers we plan to use. | {extend, last} = require './helpers'
exports.Scope = class Scope |
The top-level Scope object. | @root: null |
Initialize a scope with its parent, for lookups up the chain, as well as a reference to the Block node it belongs to, which is where it should declare its variables, and a reference to the function that it wraps. | constructor: (@parent, @expressions, @method) ->
@variables = [{name: 'arguments', type: 'arguments'}]
@positions = {}
Scope.root = this unless @parent |
Adds a new variable or overrides an existing one. | add: (name, type, immediate) ->
return @parent.add name, type, immediate if @shared and not immediate
if Object::hasOwnProperty.call @positions, name
@variables[@positions[name]].type = type
else
@positions[name] = @variables.push({name, type}) - 1 |
When | namedMethod: ->
return @method if @method.name or !@parent
@parent.namedMethod() |
Look up a variable name in lexical scope, and declare it if it does not already exist. | find: (name) ->
return yes if @check name
@add name, 'var'
no |
Reserve a variable name as originating from a function parameter for this
scope. No | parameter: (name) ->
return if @shared and @parent.check name, yes
@add name, 'param' |
Just check to see if a variable has already been declared, without reserving, walks up to the root scope. | check: (name) ->
!!(@type(name) or @parent?.check(name)) |
Generate a temporary variable name at the given index. | temporary: (name, index) ->
if name.length > 1
'_' + name + if index > 1 then index - 1 else ''
else
'_' + (index + parseInt name, 36).toString(36).replace /\d/g, 'a' |
Gets the type of a variable. | type: (name) ->
return v.type for v in @variables when v.name is name
null |
If we need to store an intermediate result, find an available name for a
compiler-generated variable. | freeVariable: (name, reserve=true) ->
index = 0
index++ while @check((temp = @temporary name, index))
@add temp, 'var', yes if reserve
temp |
Ensure that an assignment is made at the top of this scope (or at the top-level scope, if requested). | assign: (name, value) ->
@add name, {value, assigned: yes}, yes
@hasAssignments = yes |
Does this scope have any declared variables? | hasDeclarations: ->
!!@declaredVariables().length |
Return the list of variables first declared in this scope. | declaredVariables: ->
realVars = []
tempVars = []
for v in @variables when v.type is 'var'
(if v.name.charAt(0) is '_' then tempVars else realVars).push v.name
realVars.sort().concat tempVars.sort() |
Return the list of assignments that are supposed to be made at the top of this scope. | assignedVariables: ->
"#{v.name} = #{v.type.value}" for v in @variables when v.type.assigned
|
underscore.coffee | |
---|---|
Underscore.coffee (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. Underscore is freely distributable under the terms of the MIT license. Portions of Underscore are inspired by or borrowed from Prototype.js, Oliver Steele's Functional, and John Resig's Micro-Templating. For all details and documentation: http://documentcloud.github.com/underscore/ | |
Baseline setup | |
Establish the root object, | root = this |
Save the previous value of the | previousUnderscore = root._ |
Establish the object that gets thrown to break out of a loop iteration.
| breaker = if typeof(StopIteration) is 'undefined' then '__break__' else StopIteration |
Helper function to escape RegExp contents, because JS doesn't have one. | escapeRegExp = (string) -> string.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1') |
Save bytes in the minified (but not gzipped) version: | ArrayProto = Array.prototype
ObjProto = Object.prototype |
Create quick reference variables for speed access to core prototypes. | slice = ArrayProto.slice
unshift = ArrayProto.unshift
toString = ObjProto.toString
hasOwnProperty = ObjProto.hasOwnProperty
propertyIsEnumerable = ObjProto.propertyIsEnumerable |
All ECMA5 native implementations we hope to use are declared here. | nativeForEach = ArrayProto.forEach
nativeMap = ArrayProto.map
nativeReduce = ArrayProto.reduce
nativeReduceRight = ArrayProto.reduceRight
nativeFilter = ArrayProto.filter
nativeEvery = ArrayProto.every
nativeSome = ArrayProto.some
nativeIndexOf = ArrayProto.indexOf
nativeLastIndexOf = ArrayProto.lastIndexOf
nativeIsArray = Array.isArray
nativeKeys = Object.keys |
Create a safe reference to the Underscore object for use below. | _ = (obj) -> new wrapper(obj) |
Export the Underscore object for CommonJS. | if typeof(exports) != 'undefined' then exports._ = _ |
Export Underscore to global scope. | root._ = _ |
Current version. | _.VERSION = '1.1.0' |
Collection Functions | |
The cornerstone, an each implementation. Handles objects implementing forEach, arrays, and raw objects. | _.each = (obj, iterator, context) ->
try
if nativeForEach and obj.forEach is nativeForEach
obj.forEach iterator, context
else if _.isNumber obj.length
iterator.call context, obj[i], i, obj for i in [0...obj.length]
else
iterator.call context, val, key, obj for own key, val of obj
catch e
throw e if e isnt breaker
obj |
Return the results of applying the iterator to each element. Use JavaScript 1.6's version of map, if possible. | _.map = (obj, iterator, context) ->
return obj.map(iterator, context) if nativeMap and obj.map is nativeMap
results = []
_.each obj, (value, index, list) ->
results.push iterator.call context, value, index, list
results |
Reduce builds up a single result from a list of values. Also known as inject, or foldl. Uses JavaScript 1.8's version of reduce, if possible. | _.reduce = (obj, iterator, memo, context) ->
if nativeReduce and obj.reduce is nativeReduce
iterator = _.bind iterator, context if context
return obj.reduce iterator, memo
_.each obj, (value, index, list) ->
memo = iterator.call context, memo, value, index, list
memo |
The right-associative version of reduce, also known as foldr. Uses JavaScript 1.8's version of reduceRight, if available. | _.reduceRight = (obj, iterator, memo, context) ->
if nativeReduceRight and obj.reduceRight is nativeReduceRight
iterator = _.bind iterator, context if context
return obj.reduceRight iterator, memo
reversed = _.clone(_.toArray(obj)).reverse()
_.reduce reversed, iterator, memo, context |
Return the first value which passes a truth test. | _.detect = (obj, iterator, context) ->
result = null
_.each obj, (value, index, list) ->
if iterator.call context, value, index, list
result = value
_.breakLoop()
result |
Return all the elements that pass a truth test. Use JavaScript 1.6's filter, if it exists. | _.filter = (obj, iterator, context) ->
return obj.filter iterator, context if nativeFilter and obj.filter is nativeFilter
results = []
_.each obj, (value, index, list) ->
results.push value if iterator.call context, value, index, list
results |
Return all the elements for which a truth test fails. | _.reject = (obj, iterator, context) ->
results = []
_.each obj, (value, index, list) ->
results.push value if not iterator.call context, value, index, list
results |
Determine whether all of the elements match a truth test. Delegate to JavaScript 1.6's every, if it is present. | _.every = (obj, iterator, context) ->
iterator ||= _.identity
return obj.every iterator, context if nativeEvery and obj.every is nativeEvery
result = true
_.each obj, (value, index, list) ->
_.breakLoop() unless (result = result and iterator.call(context, value, index, list))
result |
Determine if at least one element in the object matches a truth test. Use JavaScript 1.6's some, if it exists. | _.some = (obj, iterator, context) ->
iterator ||= _.identity
return obj.some iterator, context if nativeSome and obj.some is nativeSome
result = false
_.each obj, (value, index, list) ->
_.breakLoop() if (result = iterator.call(context, value, index, list))
result |
Determine if a given value is included in the array or object,
based on | _.include = (obj, target) ->
return _.indexOf(obj, target) isnt -1 if nativeIndexOf and obj.indexOf is nativeIndexOf
return true for own key, val of obj when val is target
false |
Invoke a method with arguments on every item in a collection. | _.invoke = (obj, method) ->
args = _.rest arguments, 2
(if method then val[method] else val).apply(val, args) for val in obj |
Convenience version of a common use case of map: fetching a property. | _.pluck = (obj, key) ->
_.map(obj, (val) -> val[key]) |
Return the maximum item or (item-based computation). | _.max = (obj, iterator, context) ->
return Math.max.apply(Math, obj) if not iterator and _.isArray(obj)
result = computed: -Infinity
_.each obj, (value, index, list) ->
computed = if iterator then iterator.call(context, value, index, list) else value
computed >= result.computed and (result = {value: value, computed: computed})
result.value |
Return the minimum element (or element-based computation). | _.min = (obj, iterator, context) ->
return Math.min.apply(Math, obj) if not iterator and _.isArray(obj)
result = computed: Infinity
_.each obj, (value, index, list) ->
computed = if iterator then iterator.call(context, value, index, list) else value
computed < result.computed and (result = {value: value, computed: computed})
result.value |
Sort the object's values by a criterion produced by an iterator. | _.sortBy = (obj, iterator, context) ->
_.pluck(((_.map obj, (value, index, list) ->
{value: value, criteria: iterator.call(context, value, index, list)}
).sort((left, right) ->
a = left.criteria; b = right.criteria
if a < b then -1 else if a > b then 1 else 0
)), 'value') |
Use a comparator function to figure out at what index an object should be inserted so as to maintain order. Uses binary search. | _.sortedIndex = (array, obj, iterator) ->
iterator ||= _.identity
low = 0
high = array.length
while low < high
mid = (low + high) >> 1
if iterator(array[mid]) < iterator(obj) then low = mid + 1 else high = mid
low |
Convert anything iterable into a real, live array. | _.toArray = (iterable) ->
return [] if (!iterable)
return iterable.toArray() if (iterable.toArray)
return iterable if (_.isArray(iterable))
return slice.call(iterable) if (_.isArguments(iterable))
_.values(iterable) |
Return the number of elements in an object. | _.size = (obj) -> _.toArray(obj).length |
Array Functions | |
Get the first element of an array. Passing | _.first = (array, n, guard) ->
if n and not guard then slice.call(array, 0, n) else array[0] |
Returns everything but the first entry of the array. Aliased as tail.
Especially useful on the arguments object. Passing an | _.rest = (array, index, guard) ->
slice.call(array, if _.isUndefined(index) or guard then 1 else index) |
Get the last element of an array. | _.last = (array) -> array[array.length - 1] |
Trim out all falsy values from an array. | _.compact = (array) -> item for item in array when item |
Return a completely flattened version of an array. | _.flatten = (array) ->
_.reduce array, (memo, value) ->
return memo.concat(_.flatten(value)) if _.isArray value
memo.push value
memo
, [] |
Return a version of the array that does not contain the specified value(s). | _.without = (array) ->
values = _.rest arguments
val for val in _.toArray(array) when not _.include values, val |
Produce a duplicate-free version of the array. If the array has already been sorted, you have the option of using a faster algorithm. | _.uniq = (array, isSorted) ->
memo = []
for el, i in _.toArray array
memo.push el if i is 0 || (if isSorted is true then _.last(memo) isnt el else not _.include(memo, el))
memo |
Produce an array that contains every item shared between all the passed-in arrays. | _.intersect = (array) ->
rest = _.rest arguments
_.select _.uniq(array), (item) ->
_.all rest, (other) ->
_.indexOf(other, item) >= 0 |
Zip together multiple lists into a single array -- elements that share an index go together. | _.zip = ->
length = _.max _.pluck arguments, 'length'
results = new Array length
for i in [0...length]
results[i] = _.pluck arguments, String i
results |
If the browser doesn't supply us with indexOf (I'm looking at you, MSIE), we need this function. Return the position of the first occurrence of an item in an array, or -1 if the item is not included in the array. | _.indexOf = (array, item) ->
return array.indexOf item if nativeIndexOf and array.indexOf is nativeIndexOf
i = 0; l = array.length
while l - i
if array[i] is item then return i else i++
-1 |
Provide JavaScript 1.6's lastIndexOf, delegating to the native function, if possible. | _.lastIndexOf = (array, item) ->
return array.lastIndexOf(item) if nativeLastIndexOf and array.lastIndexOf is nativeLastIndexOf
i = array.length
while i
if array[i] is item then return i else i--
-1 |
Generate an integer Array containing an arithmetic progression. A port of the native Python range function. | _.range = (start, stop, step) ->
a = arguments
solo = a.length <= 1
i = start = if solo then 0 else a[0]
stop = if solo then a[0] else a[1]
step = a[2] or 1
len = Math.ceil((stop - start) / step)
return [] if len <= 0
range = new Array len
idx = 0
loop
return range if (if step > 0 then i - stop else stop - i) >= 0
range[idx] = i
idx++
i+= step |
Function Functions | |
Create a function bound to a given object (assigning | _.bind = (func, obj) ->
args = _.rest arguments, 2
-> func.apply obj or root, args.concat arguments |
Bind all of an object's methods to that object. Useful for ensuring that all callbacks defined on an object belong to it. | _.bindAll = (obj) ->
funcs = if arguments.length > 1 then _.rest(arguments) else _.functions(obj)
_.each funcs, (f) -> obj[f] = _.bind obj[f], obj
obj |
Delays a function for the given number of milliseconds, and then calls it with the arguments supplied. | _.delay = (func, wait) ->
args = _.rest arguments, 2
setTimeout((-> func.apply(func, args)), wait) |
Memoize an expensive function by storing its results. | _.memoize = (func, hasher) ->
memo = {}
hasher or= _.identity
->
key = hasher.apply this, arguments
return memo[key] if key of memo
memo[key] = func.apply this, arguments |
Defers a function, scheduling it to run after the current call stack has cleared. | _.defer = (func) ->
_.delay.apply _, [func, 1].concat _.rest arguments |
Returns the first function passed as an argument to the second, allowing you to adjust arguments, run code before and after, and conditionally execute the original function. | _.wrap = (func, wrapper) ->
-> wrapper.apply wrapper, [func].concat arguments |
Returns a function that is the composition of a list of functions, each consuming the return value of the function that follows. | _.compose = ->
funcs = arguments
->
args = arguments
for i in [funcs.length - 1..0] by -1
args = [funcs[i].apply(this, args)]
args[0] |
Object Functions | |
Retrieve the names of an object's properties. | _.keys = nativeKeys or (obj) ->
return _.range 0, obj.length if _.isArray(obj)
key for key, val of obj |
Retrieve the values of an object's properties. | _.values = (obj) ->
_.map obj, _.identity |
Return a sorted list of the function names available in Underscore. | _.functions = (obj) ->
_.filter(_.keys(obj), (key) -> _.isFunction(obj[key])).sort() |
Extend a given object with all of the properties in a source object. | _.extend = (obj) ->
for source in _.rest(arguments)
obj[key] = val for key, val of source
obj |
Create a (shallow-cloned) duplicate of an object. | _.clone = (obj) ->
return obj.slice 0 if _.isArray obj
_.extend {}, obj |
Invokes interceptor with the obj, and then returns obj. The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. | _.tap = (obj, interceptor) ->
interceptor obj
obj |
Perform a deep comparison to check if two objects are equal. | _.isEqual = (a, b) -> |
Check object identity. | return true if a is b |
Different types? | atype = typeof(a); btype = typeof(b)
return false if atype isnt btype |
Basic equality test (watch out for coercions). | return true if `a == b` |
One is falsy and the other truthy. | return false if (!a and b) or (a and !b) |
One of them implements an | return a.isEqual(b) if a.isEqual |
Check dates' integer values. | return a.getTime() is b.getTime() if _.isDate(a) and _.isDate(b) |
Both are NaN? | return false if _.isNaN(a) and _.isNaN(b) |
Compare regular expressions. | if _.isRegExp(a) and _.isRegExp(b)
return a.source is b.source and
a.global is b.global and
a.ignoreCase is b.ignoreCase and
a.multiline is b.multiline |
If a is not an object by this point, we can't handle it. | return false if atype isnt 'object' |
Check for different array lengths before comparing contents. | return false if a.length and (a.length isnt b.length) |
Nothing else worked, deep compare the contents. | aKeys = _.keys(a); bKeys = _.keys(b) |
Different object sizes? | return false if aKeys.length isnt bKeys.length |
Recursive comparison of contents. | return false for key, val of a when !(key of b) or !_.isEqual(val, b[key])
true |
Is a given array or object empty? | _.isEmpty = (obj) ->
return obj.length is 0 if _.isArray(obj) or _.isString(obj)
return false for own key of obj
true |
Is a given value a DOM element? | _.isElement = (obj) -> obj and obj.nodeType is 1 |
Is a given value an array? | _.isArray = nativeIsArray or (obj) -> !!(obj and obj.concat and obj.unshift and not obj.callee) |
Is a given variable an arguments object? | _.isArguments = (obj) -> obj and obj.callee |
Is the given value a function? | _.isFunction = (obj) -> !!(obj and obj.constructor and obj.call and obj.apply) |
Is the given value a string? | _.isString = (obj) -> !!(obj is '' or (obj and obj.charCodeAt and obj.substr)) |
Is a given value a number? | _.isNumber = (obj) -> (obj is +obj) or toString.call(obj) is '[object Number]' |
Is a given value a boolean? | _.isBoolean = (obj) -> obj is true or obj is false |
Is a given value a Date? | _.isDate = (obj) -> !!(obj and obj.getTimezoneOffset and obj.setUTCFullYear) |
Is the given value a regular expression? | _.isRegExp = (obj) -> !!(obj and obj.exec and (obj.ignoreCase or obj.ignoreCase is false)) |
Is the given value NaN -- this one is interesting. | _.isNaN = (obj) -> _.isNumber(obj) and window.isNaN(obj) |
Is a given value equal to null? | _.isNull = (obj) -> obj is null |
Is a given variable undefined? | _.isUndefined = (obj) -> typeof obj is 'undefined' |
Utility Functions | |
Run Underscore.js in noConflict mode, returning the | _.noConflict = ->
root._ = previousUnderscore
this |
Keep the identity function around for default iterators. | _.identity = (value) -> value |
Run a function | _.times = (n, iterator, context) ->
iterator.call context, i for i in [0...n] |
Break out of the middle of an iteration. | _.breakLoop = -> throw breaker |
Add your own custom functions to the Underscore object, ensuring that they're correctly added to the OOP wrapper as well. | _.mixin = (obj) ->
for name in _.functions(obj)
addToWrapper name, _[name] = obj[name] |
Generate a unique integer id (unique within the entire client session). Useful for temporary DOM ids. | idCounter = 0
_.uniqueId = (prefix) ->
(prefix or '') + idCounter++ |
By default, Underscore uses ERB-style template delimiters, change the following template settings to use alternative delimiters. | _.templateSettings = {
start: '<%'
end: '%>'
interpolate: /<%=(.+?)%>/g
} |
JavaScript templating a-la ERB, pilfered from John Resig's Secrets of the JavaScript Ninja, page 83. Single-quote fix from Rick Strahl. With alterations for arbitrary delimiters, and to preserve whitespace. | _.template = (str, data) ->
c = _.templateSettings
endMatch = new RegExp("'(?=[^"+c.end.substr(0, 1)+"]*"+escapeRegExp(c.end)+")","g")
fn = new Function 'obj',
'var p=[],print=function(){p.push.apply(p,arguments);};' +
'with(obj||{}){p.push(\'' +
str.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
.replace(endMatch,"✄")
.split("'").join("\\'")
.split("✄").join("'")
.replace(c.interpolate, "',$1,'")
.split(c.start).join("');")
.split(c.end).join("p.push('") +
"');}return p.join('');"
if data then fn(data) else fn |
Aliases | _.forEach = _.each
_.foldl = _.inject = _.reduce
_.foldr = _.reduceRight
_.select = _.filter
_.all = _.every
_.any = _.some
_.contains = _.include
_.head = _.first
_.tail = _.rest
_.methods = _.functions |
Setup the OOP Wrapper | |
If Underscore is called as a function, it returns a wrapped object that can be used OO-style. This wrapper holds altered versions of all the underscore functions. Wrapped objects may be chained. | wrapper = (obj) ->
this._wrapped = obj
this |
Helper function to continue chaining intermediate results. | result = (obj, chain) ->
if chain then _(obj).chain() else obj |
A method to easily add functions to the OOP wrapper. | addToWrapper = (name, func) ->
wrapper.prototype[name] = ->
args = _.toArray arguments
unshift.call args, this._wrapped
result func.apply(_, args), this._chain |
Add all ofthe Underscore functions to the wrapper object. | _.mixin _ |
Add all mutator Array functions to the wrapper. | _.each ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], (name) ->
method = Array.prototype[name]
wrapper.prototype[name] = ->
method.apply(this._wrapped, arguments)
result(this._wrapped, this._chain) |
Add all accessor Array functions to the wrapper. | _.each ['concat', 'join', 'slice'], (name) ->
method = Array.prototype[name]
wrapper.prototype[name] = ->
result(method.apply(this._wrapped, arguments), this._chain) |
Start chaining a wrapped Underscore object. | wrapper::chain = ->
this._chain = true
this |
Extracts the result from a wrapped and chained object. | wrapper::value = -> this._wrapped
|