js2coffee.coffee | |
---|---|
The JavaScript to CoffeeScript compiler. Common usage: | |
RequiresJs2coffee 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 pointThis is
| buildCoffee = (str) ->
scriptNode = parser.parse("#{str}\n")
trim build(scriptNode) |
Narcissus node helpersSome extensions to the Node class to make things easier later on. | |
| Node.prototype.left = -> @children[0]
Node.prototype.right = -> @children[1] |
| Node.prototype.unsupported = (msg) ->
throw new UnsupportedError("Unsupported: #{msg}", @) |
| Node.prototype.typeName = -> Types[@type] |
Main functions | |
For instance, for a | 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 = (type, str, args...) ->
Builders[type].apply str, args |
| body = (item, opts={}) ->
str = build(item, opts)
str = blockTrim(str)
str = unshift(str)
if str.length > 0 then str else "" |
TypesThe | Types = (->
dict = {}
for i of tokens
dict[tokens[i]] = i.toLowerCase() if typeof tokens[i] == 'number'
dict
)() |
The buildersEach 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 | Builders = |
| '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 | 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': ->
str = @value.toString() |
Caveat:
In object literals like | if str.match(/^([_\$a-z][_\$a-z0-9]*)$/i) or str.match(/^[0-9]+$/i)
str
else
strEscape str |
| 'identifier': ->
unreserve @value.toString()
'number': ->
"#{@value}"
'id': ->
unreserve @ |
| 'id_param': ->
if @toString() in ['undefined']
"#{@}_"
else
re 'id', @ |
| 'return': -> |
Caveat 1:
Empty returns need to always be | 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)}" |
| ';': -> |
Caveat:
Some statements can be blank as some people are silly enough to use | if not @expression?
"" |
Caveat 2:
If the statement only has one function call (eg, | else if @expression.typeName() == 'call'
re('call_statement', @expression) + "\n"
else if @expression.typeName() == 'object_init'
paren(re('object_init', @expression, brackets: true)) + "\n"
else
build(@expression) + "\n" |
| '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'
'debugger': -> "debugger\n"
'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 operatorsAll of these are rerouted to the | '+': -> 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 decrementsFor | '--': -> re('increment_decrement', @, '--')
'++': -> re('increment_decrement', @, '++')
'increment_decrement': (sign) ->
if @postfix
"#{build @left()}#{sign}"
else
"#{sign}#{build @left()}" |
| '=': ->
sign = if @assignOp?
Types[@assignOp] + '='
else
'='
"#{build @left()} #{sign} #{build @right()}" |
| ',': ->
list = _.map @children, (item) -> build(item) + "\n"
list.join('') |
| 'regexp': ->
m = @value.toString().match(/^\/(.*)\/([a-z]?)/)
value = m[1]
flag = m[2] |
Caveat:
If it begins with | 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': ->
if @right().children.length == 0
"#{build @left()}()"
else
"#{build @left()}(#{build @right()})" |
| 'call_statement': ->
left = build @left() |
Caveat:
When calling in this way: | left = paren(left) if @left().typeName() == 'function'
if @right().children.length == 0
"#{left}()"
else
"#{left} #{build @right()}" |
| 'list': ->
list = _.map(@children, (item) -> build(item))
list.join(", ")
'delete': ->
ids = _.map(@children, (el) -> build(el))
ids = ids.join(', ')
"delete #{ids}\n" |
| '.': -> |
Caveat:
If called as | |
Caveat:
If called as | isThis = (@left().typeName() == 'this')
isPrototype = (@right().typeName() == 'identifier' and @right().value == 'prototype')
if isThis and isPrototype
"@::"
else if isThis
"@#{build @right()}"
else if isPrototype
"#{build @left()}::"
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 |
| '?': ->
"(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': ->
"#{re 'property_identifier', @left()}: #{build @right()}" |
| 'object_init': (options={}) ->
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 = "{#{c}}" if options.brackets?
c |
| 'function': ->
c = new Code
params = _.map @params, (str) ->
if str.constructor == String
re('id_param', str)
else
build 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 thingsDue to CoffeeScript limitations, the following things are not supported:
| '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 helperA 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 helpersThese are functions that deal with strings. | |
| paren = (string) ->
str = string.toString()
if str.substr(0, 1) == '(' and str.substr(-1, 1) == ')'
str
else
"(#{str})" |
| strRepeat = (str, times) ->
(str for i in [0...times]).join('') |
| 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 = (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 = (str) ->
JSON.stringify "#{str}" |
| 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 = (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
|