js2coffee
Version:
JavaScript to CoffeeScript compiler
691 lines (532 loc) • 16 kB
text/coffeescript
# The JavaScript to CoffeeScript compiler.
# Common usage:
#
#
# var src = "var square = function(n) { return n * n };"
#
# js2coffee = require('js2coffee');
# js2coffee.build(src);
# //=> "square = (n) -> n * n"
# ## Requires
#
# Js2coffee relies on Narcissus's parser. (Narcissus is Mozilla's JavaScript
# engine written in JavaScript).
if window?
narcissus = window.Narcissus
_ = window._
else
narcissus = require('./narcissus_packed')
_ = require('underscore')
tokens = narcissus.definitions.tokens
parser = narcissus.parser
Node = parser.Node
# ## Main entry point
# This is `require('js2coffee').build()`. It takes a JavaScript source
# string as an argument, and it returns the CoffeeScript version.
#
# 1. Ask Narcissus to break it down into Nodes (`parser.parse`). This
# returns a `Node` object of type `script`.
#
# 2. This node is now passed onto `build()`.
buildCoffee = (str) ->
scriptNode = parser.parse("#{str}\n")
trim build(scriptNode)
# ## Narcissus node helpers
# Some extensions to the Node class to make things easier later on.
# `left() / right()`
# These are aliases for the first and last child.
# Often helpful for things like binary operators.
Node.prototype.left = -> @children[0]
Node.prototype.right = -> @children[1]
# `unsupported()`
# Throws an unsupported error.
Node.prototype.unsupported = (msg) ->
throw new UnsupportedError("Unsupported: #{msg}", @)
# `typeName()`
# Returns the typename in lowercase. (eg, 'function')
Node.prototype.typeName = -> Types[@type]
# ## Main functions
# `build()`
# This finds the appropriate builder function for `node` based on it's type,
# the passes the node onto that function.
#
# For instance, for a `function` node, it calls `Builders.function(node)`.
# It defaults to `Builders.other` if it can't find a function for it.
build = (node, opts={}) ->
name = 'other'
name = node.typeName() if node != undefined and node.typeName
out = (Builders[name] or Builders.other).apply(node, [opts])
if node.parenthesized? then paren(out) else out
# `re()`
# Works like `build()`, except it explicitly states which function should
# handle it. (essentially, *re*routing it to another builder)
re = (type, str, args...) ->
Builders[type].apply str, args
# `body()`
# Works like `build()`, and is used for code blocks. It cleans up the returned
# code block by removing any extraneous spaces and such.
body = (item, opts={}) ->
str = build(item, opts)
str = blockTrim(str)
str = unshift(str)
if str.length > 0 then str else ""
# ## Types
# The `Types` global object contains a map of Narcissus type numbers to type
# names. It probably looks like:
#
# Types = { ...
# '42': 'script',
# '43': 'block',
# '44': 'label',
# '45': 'for_in',
# ...
# }
Types = (->
dict = {}
for i of tokens
dict[tokens[i]] = i.toLowerCase() if typeof tokens[i] == 'number'
dict
)()
# ## The builders
#
# Each of these functions are apply'd to a Node, and is expected to return
# a string representation of it CoffeeScript counterpart.
#
# These are invoked using `build()`.
Builders =
# `script`
# This is the main entry point.
'script': (opts={}) ->
c = new Code
len = @children.length
if len > 0
# *Omit returns if not needed.*
if opts.returnable?
@children[len-1].last = true
# *CoffeeScript does not need `break` statements on `switch` blocks.*
if opts.noBreak? and @children[len-1].typeName() == 'break'
delete @children[len-1]
# *Functions must always be declared first in a block.*
if @children?
_.each @children, (item) ->
c.add build(item) if item.typeName() == 'function'
_.each @children, (item) ->
c.add build(item) if item.typeName() != 'function'
c.toString()
# `property_identifier`
# A key in an object literal.
'property_identifier': ->
str = @value.toString()
# **Caveat:**
# *In object literals like `{ '#foo click': b }`, ensure that the key is
# quoted if need be.*
if str.match(/^([_\$a-z][a-z0-9_]*)$/i) or str.match(/^[0-9]+/i)
str
else
strEscape str
# `identifier`
# Any object identifier like a variable name.
'identifier': ->
unreserve @value.toString()
'number': ->
"#{@value}"
'id': ->
unreserve @
# `id_param`
# Function parameters. Belongs to `list`.
'id_param': ->
if @toString() in ['undefined']
"#{@}_"
else
re 'id', @
# `return`
# A return statement. Has `@value` of type `id`.
'return': ->
# **Caveat 1:**
# *Empty returns need to always be `return`, regardless if it's the last
# statement in the block or not.*
if not @value?
"return"
# **Caveat 2:**
# *If it's the last statement in the block, we can omit the 'return' keyword.*
else if @last?
build(@value)
else
"return #{build(@value)}"
# `;` (aka, statement)
# A single statement.
';': ->
# **Caveat:**
# Some statements can be blank as some people are silly enough to use `;;`
# sometimes. They should be ignored.
if not @expression?
""
# **Caveat 2:**
# *If the statement only has one function call (eg, `alert(2);`), the
# parentheses should be omitted (eg, `alert 2`).*
else if @expression.typeName() == 'call'
re('call_statement', @expression) + "\n"
else
build(@expression) + "\n"
# `new` + `new_with_args`
# For `new X` and `new X(y)` respctively.
'new': -> "new #{build @left()}"
'new_with_args': -> "new #{build @left()}(#{build @right()})"
# ### Unary operators
'unary_plus': -> "+#{build @left()}"
'unary_minus': -> "-#{build @left()}"
# ### Keywords
'this': -> 'this'
'null': -> 'null'
'true': -> 'true'
'false': -> 'false'
'void': -> 'undefined'
'break': -> "break\n"
'continue': -> "continue\n"
# ### Some simple operators
'!': -> "not #{build @left()}"
'~': -> "~#{build @left()}"
'typeof': -> "typeof #{build @left()}"
'index': -> "#{build @left()}[#{build @right()}]"
'throw': -> "throw #{build @exception}"
# ### Binary operators
# All of these are rerouted to the `binary_operator` builder.
'+': -> re('binary_operator', @, '+')
'-': -> re('binary_operator', @, '-')
'*': -> re('binary_operator', @, '*')
'/': -> re('binary_operator', @, '/')
'%': -> re('binary_operator', @, '%')
'>': -> re('binary_operator', @, '>')
'<': -> re('binary_operator', @, '<')
'&': -> re('binary_operator', @, '&')
'|': -> re('binary_operator', @, '|')
'^': -> re('binary_operator', @, '^')
'&&': -> re('binary_operator', @, 'and')
'||': -> re('binary_operator', @, 'or')
'in': -> re('binary_operator', @, 'in')
'==': -> re('binary_operator', @, '==')
'<<': -> re('binary_operator', @, '<<')
'<=': -> re('binary_operator', @, '<=')
'>>': -> re('binary_operator', @, '>>')
'>=': -> re('binary_operator', @, '>=')
'!=': -> re('binary_operator', @, '!=')
'===': -> re('binary_operator', @, 'is')
'!==': -> re('binary_operator', @, 'isnt')
'instanceof': -> re('binary_operator', @, 'instanceof')
'binary_operator': (sign) ->
"#{build @left()} #{sign} #{build @right()}"
# ### Increments and decrements
# For `a++` and `--b`.
'--': -> re('increment_decrement', @, '--')
'++': -> re('increment_decrement', @, '++')
'increment_decrement': (sign) ->
if @postfix
"#{build @left()}#{sign}"
else
"#{sign}#{build @left()}"
# `=` (aka, assignment)
# For `a = b` (but not `var a = b`: that's `var`).
'=': ->
sign = if @assignOp?
Types[@assignOp] + '='
else
'='
"#{build @left()} #{sign} #{build @right()}"
# `,` (aka, comma)
# For `a = 1, b = 2'
',': ->
list = _.map @children, (item) -> build(item) + "\n"
list.join('')
# `regexp`
# Regular expressions.
#
'regexp': ->
m = @value.toString().match(/^\/(.*)\/([a-z]?)/)
value = m[1]
flag = m[2]
# **Caveat:**
# *If it begins with `=` or a space, the CoffeeScript parser will choke if
# it's written as `/=/`. Hence, they are written as `new RegExp('=')`.*
begins_with = value[0]
if begins_with in [' ', '=']
if flag.length > 0
"RegExp(#{strEscape value}, \"#{flag}\")"
else
"RegExp(#{strEscape value})"
else
"/#{value}/#{flag}"
'string': ->
strEscape @value
# `call`
# A Function call.
# `@left` is an `id`, and `@right` is a `list`.
'call': ->
if @right().children.length == 0
"#{build @left()}()"
else
"#{build @left()}(#{build @right()})"
# `call_statement`
# A `call` that's on it's own line.
'call_statement': ->
left = build @left()
# **Caveat:**
# *When calling in this way: `function () { ... }()`,
# ensure that there are parenthesis around the anon function
# (eg, `(-> ...)()`).*
left = paren(left) if @left().typeName() == 'function'
if @right().children.length == 0
"#{left}()"
else
"#{left} #{build @right()}"
# `list`
# A parameter list.
'list': ->
list = _.map(@children, (item) -> build(item))
list.join(", ")
'delete': ->
ids = _.map(@children, (el) -> build(el))
ids = ids.join(', ')
"delete #{ids}\n"
# `.` (scope resolution?)
# For instances such as `object.value`.
'.': ->
# **Caveat:**
# *If called as `this.xxx`, it should use the at sign (`@xxx`).*
if @left().typeName() == 'this'
"@#{build @right()}"
else
"#{build @left()}.#{build @right()}"
'try': ->
c = new Code
c.add 'try'
c.scope body(@tryBlock)
_.each @catchClauses, (clause) ->
c.add build(clause)
if @finallyBlock?
c.add "finally"
c.scope body(@finallyBlock)
c
'catch': ->
c = new Code
if @varName?
c.add "catch #{@varName}"
else
c.add 'catch'
c.scope body(@block)
c
# `?` (ternary operator)
# For `a ? b : c`. Note that these will always be parenthesized, as (I
# believe) the order of operations in JS is different in CS.
'?': ->
"(if #{build @left()} then #{build @children[1]} else #{build @children[2]})"
'for': ->
c = new Code
if @setup?
c.add "#{build @setup}\n"
if @condition?
c.add "while #{build @condition}\n"
else
c.add "while true"
c.scope body(@body)
c.scope body(@update) if @update?
c
'for_in': ->
c = new Code
c.add "for #{build @iterator} of #{build @object}"
c.scope body(@body)
c
'while': ->
c = new Code
c.add "while #{build @condition}"
c.scope body(@body)
c
'do': ->
c = new Code
c.add "while true"
c.scope body(@body)
c.scope "break unless #{build @condition}" if @condition?
c
'if': ->
c = new Code
c.add "if #{build @condition}"
c.scope body(@thenPart)
if @elsePart?
if @elsePart.typeName() == 'if'
c.add "else #{build(@elsePart).toString()}"
else
c.add "else\n"
c.scope body(@elsePart)
c
'switch': ->
c = new Code
c.add "switch #{build @discriminant}\n"
_.each @cases, (item) ->
if item.value == 'default'
c.scope "else"
else
c.scope "when #{build item.caseLabel}\n"
c.scope body(item.statements, noBreak: true), 2
first = false
c
'array_init': ->
if @children.length == 0
"[]"
else
list = re('list', @)
"[ #{list} ]"
# `property_init`
# Belongs to `object_init`;
# left is a `identifier`, right can be anything.
'property_init': ->
"#{re 'property_identifier', @left()}: #{build @right()}"
# `object_init`
# An object initializer.
# Has many `property_init`.
'object_init': ->
if @children.length == 0
"{}"
else if @children.length == 1
build @children[0]
else
list = _.map @children, (item) -> build item
c = new Code
c.scope list.join("\n")
c
# `function`
# A function. Can be an anonymous function (`function () { .. }`), or a named
# function (`function name() { .. }`).
'function': ->
c = new Code
params = _.map(@params, (str) -> re('id_param', str))
if @name
c.add "#{@name} = "
if @params.length > 0
c.add "(#{params.join ', '}) ->"
else
c.add "->"
c.scope body(@body, returnable: true)
c
'var': ->
list = _.map @children, (item) ->
"#{item.value} = #{build(item.initializer)}" if item.initializer?
_.compact(list).join("\n") + "\n"
# ### Unsupported things
#
# Due to CoffeeScript limitations, the following things are not supported:
#
# * New getter/setter syntax (`x.prototype = { get name() { ... } };`)
# * Break labels (`my_label: ...`)
# * Constants
'other': -> @unsupported "#{@typeName()} is not supported yet"
'getter': -> @unsupported "getter syntax is not supported; use __defineGetter__"
'setter': -> @unsupported "setter syntax is not supported; use __defineSetter__"
'label': -> @unsupported "labels are not supported by CoffeeScript"
'const': -> @unsupported "consts are not supported by CoffeeScript"
Builders.block = Builders.script
# ## Unsupported Error exception
class UnsupportedError
constructor: (str, src) ->
@message = str
@cursor = src.start
@line = src.lineno
@source = src.tokenizer.source
toString: -> @message
# ## Code snippet helper
# A helper class to deal with building code.
class Code
constructor: ->
@code = ''
add: (str) ->
@code += str.toString()
@
scope: (str, level=1) ->
indent = strRepeat(" ", level)
@code = rtrim(@code) + "\n"
@code += indent + rtrim(str).replace(/\n/g, "\n#{indent}") + "\n"
@
toString: ->
@code
# ## String helpers
# These are functions that deal with strings.
# `paren()`
# Wraps a given string in parentheses.
# Examples:
#
# paren 'hi' => "(hi)"
# paren '(hi)' => "(hi)"
#
paren = (string) ->
str = string.toString()
if str.substr(0, 1) == '(' and str.substr(-1, 1) == ')'
str
else
"(#{str})"
# `strRepeat()`
# Repeats a string a certain number of times.
# Example:
#
# strRepeat('.', 3) => "..."
#
strRepeat = (str, times) ->
(str for i in [0...times]).join('')
# `trim()` *and friends*
# String trimming functions.
ltrim = (str) ->
"#{str}".replace(/^\s*/g, '')
rtrim = (str) ->
"#{str}".replace(/\s*$/g, '')
blockTrim = (str) ->
"#{str}".replace(/^\s*\n|\s*$/g, '')
trim = (str) ->
"#{str}".replace(/^\s*|\s*$/g, '')
# `unshift()`
# Removes any unneccesary indentation from a code block string.
unshift = (str) ->
str = "#{str}"
while true
m1 = str.match(/^/gm)
m2 = str.match(/^ /gm)
return str if !m1 or !m2 or m1.length != m2.length
str = str.replace(/^ /gm, '')
# `strEscape()`
# Escapes a string.
# Example:
#
# * `hello "there"` turns to `"hello \"there\""`
#
strEscape = (str) ->
JSON.stringify "#{str}"
# `p()`
# Debugging tool. Prints an object to the console.
# Not actually used, but here for convenience.
p = (str) ->
if typeof str == 'object'
delete str.tokenizer if str.tokenizer?
console.log str
else if str.constructor == String
console.log JSON.stringify(str)
else
console.log str
''
# `unreserve()`
# Picks the next best thing for a reserved keyword.
# Example:
#
# "in" => "in_"
# "hello" => "hello"
# "off" => "off"
#
unreserve = (str) ->
if "#{str}" in ['in', 'loop', 'off', 'on', 'when', 'not', 'until', '__bind', '__indexOf']
"#{str}_"
else
"#{str}"
# ## Exports
exports =
version: '0.0.4'
build: buildCoffee
UnsupportedError: UnsupportedError
if window?
window.Js2coffee = exports
if module?
module.exports = exports