js2coffee
Version:
JavaScript to CoffeeScript compiler
427 lines (366 loc) • 9.67 kB
text/coffeescript
###
# delimit() : delimit(list, joiner)
# Intersperses `joiner` into `list`. Used for things like adding indentations.
#
# delimit( [a, b, c], X )
# => [a, X, b, X, c]
###
exports.delimit = (list, joiner) ->
newlist = []
for item, i in list
newlist.push(joiner) if i > 0
newlist.push(item)
newlist
###*
# commaDelimit() : commaDelimit(list)
# Turns an array of strings into a comma-separated list. Takes new lines into
# account.
#
# commaDelimit( [ 'a', 'b', 'c' ] )
# => 'a, b, c'
###
#
exports.commaDelimit = (list) ->
newlist = []
for item, i in list
if i > 0
if /^\n/.test(item.toString())
newlist.push(',')
else
newlist.push(', ')
newlist.push(item)
newlist
###*
# prependAll():
# Prepends every item in the `list` with a given `prefix\`.
#
# prependAll( [ 1, 2, 3 ], 'x' )
# => [ 'x', 1, 'x', 2, 'x', 3 ]
###
exports.prependAll = (list, prefix) ->
newlist = []
for item, i in list
newlist.push(prefix)
newlist.push(item)
newlist
###*
# buildError():
# Builds a syntax error message.
#
# e =
# description: "Unexpected indentifier"
# start: { line: 3, column: 1 }
# end: { line: 3, column: 5 }
# err = buildError(e, code, "index.js")
#
# Or esprima-like:
#
# e =
# description: "Unexpected indentifier"
# lineNumber: 3,
# column: 1
# err = buildError(e, code, "index.js")
#
# Output:
#
# err.message #=> "index.js:3:1: Unexpected indentifier\n..."
# err.start #=> { line: 3, column: 1 }
# err.end #=> { line: 3, column: 5 }
# err.description #=> "Unexpected identifier"
# err.sourcePreview
###
exports.buildError = (err, source, file = '') ->
if err.js2coffee then return err
{description} = err
line = err.start?.line ? err.lineNumber
column = err.start?.column ? (err.column && (err.column - 1)) ? 0
heading = "#{file}:#{line}:#{column}: #{description}"
# Build a source preview.
lines = source.split("\n")
min = Math.max(line-3, 0)
max = line-1
digits = max.toString().length
length = 1
length = Math.max(err.end.column - err.start.column, 1) if err.end
pad = (s) -> Array(1 + digits - s.toString().length).join(" ") + s
source = lines[min..max].map (line, i) -> "#{pad(1+i+min)} #{line}"
source.push Array(digits + 3).join(" ") + Array(column+1).join(" ") + Array(length+1).join("^")
source = source.join("\n")
message = heading + "\n\n" + source
_err = err
err = new Error(message)
err.filename = file
err.description = _err.description
err.start = { line, column }
err.end = _err.end
err.sourcePreview = source
err.js2coffee = true
err
###*
# space():
# Delimit using spaces. This also accounts for times where one of the
# statements begin with a new line, such as in the case of function
# expressions and object expressions.
#
# space [ 'a', '=', 'b' ]
# => [ 'a', ' ', '=', ' ', 'b' ]
# space [ 'a', '=', '\n b: 2' ]
# => [ 'a', ' ', '=', '\n b: 2' ]
###
exports.space = (list) ->
list.reduce ((newlist, item, i) ->
if i is 0
newlist.concat [ item ]
else if item.toString().substr(0, 1) is "\n"
newlist.concat [ item ]
else
newlist.concat [ ' ', item ]
), []
###*
# newline():
# Appends a new line to a given SourceNode (what `walk()` returns). If it
# already ends in a newline, it is left alone.
#
# newline(@walk(node.body))
# => [ node, "\n" ]
###
exports.newline = (srcnode) ->
if (/\n$/).test(srcnode.toString())
srcnode
else
[ srcnode, "\n" ]
###*
# inspect():
# For debugging.
###
exports.inspect = (node) ->
"~~~~\n" +
require('util').inspect(node, depth: 1000, colors: true) +
"\n~~~~"
###*
# replace() : replace(node, newNode)
# Fabricates a replacement node for `node` that maintains the same source
# location.
#
# node = { type: "FunctionExpression", range: [0,1], loc: { ... } }
# @replace(node, { type: "Identifier", name: "xxx" })
###
exports.replace = (node, newNode) ->
newNode.range = node.range
newNode.loc = node.loc
newNode
###*
# clone() : clone(object)
# Duplicates an object.
###
exports.clone = (obj) ->
JSON.parse JSON.stringify obj
###*
# quote() : quote(string)
# Quotes a string with single quotes.
#
# quote("hello")
# => "'hello'"
###
exports.quote = (str) ->
if typeof str is 'string'
# escape quotes
format_char_code = (char) ->
ret = char.charCodeAt(0).toString(16)
ret = ("0" for [0...(4 - ret.length)]).join('') + ret
"\\u" + ret
re = str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/\n/g, '\\n')
.replace(/[\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g,
format_char_code)
"'#{re}'"
else
JSON.stringify(str)
###*
# getPrecedence() : getPrecedence(node)
# Returns the precedence level. If a node's precedence level is greater that
# its parent, it has to be parenthesized.
#
# getPrecedence({ type: 'BinaryExpression', operator: '&' })
# => 8
###
exports.getPrecedence = (node) ->
type = node.type
isOper = (ops) ->
ops.indexOf(node.operator) > -1
binExpressions =
'**': 17 # coffee-only
'*': 14
'/': 14
'%': 14
'+': 13
'-': 13
'<<': 12
'>>': 12
'>>>': 12
'<': 11
'>': 11
'<=': 11
'>=': 11
'in': 11
'instanceof': 11
'==': 10
'===': 10
'!=': 10
'!==': 10
'&': 9
'^': 8
'|': 7
logExpressions =
'&&': 6
'||': 5
switch type
when 'Literal', 'Identifier'
99
when 'MemberExpression', 'CallExpression'
18
when 'NewExpression'
if node.arguments.length is 0 then 17 else 18
when 'UpdateExpression'
if node.prefix then 15 else 16
when 'UnaryExpression'
15
when 'BinaryExpression'
binExpressions[node.operator]
when 'LogicalExpression'
logExpressions[node.operator]
when 'ConditionalExpression'
4
when 'AssignmentExpression'
3
when 'SequenceExpression'
0
else
-1
###*
# lastStatement() : lastStatement(body)
###
exports.lastStatement = (body) ->
for i in [(body.length-1)..0]
node = body[i]
continue unless node
if ! exports.isComment(node)
return node
###*
# getReturnStatements():
# Returns the final return statements in a body.
###
exports.getReturnStatements = (body) ->
{getReturnStatements, lastStatement} = exports
# Find the last pertinent statement
if !body
return
else if body.length
node = lastStatement(body)
else
node = body
# See what it is, recurse as needed
if !node
[ ]
else if node.type is 'ReturnStatement'
[ node ]
else if node.type is 'BlockStatement'
getReturnStatements node.body
else if node.type is 'IfStatement' and node.consequent and node.alternate
cons = getReturnStatements(node.consequent)
alt = getReturnStatements(node.alternate)
if cons.length > 0 and alt.length > 0
cons.concat(alt)
else
[ ]
else
[ ]
###*
# joinLines() : joinLines(properties, indent)
# Joins multiple tokens as lines. Takes trailing newlines into
# account.
#
# joinLines(["a\n", "b", "c"], " ")
# => [ " ", "a\n", " ", "b", "\n", " ", "c" ]
###
exports.joinLines = (props, indent) ->
newlist = []
for item, i in props
newlist.push(indent)
newlist.push(item)
isLast = (i isnt props.length-1)
if !item.toString().match(/\n$/) and isLast
newlist.push "\n"
newlist
###*
# reservedWords
###
exports.reserved =
# taken from COFFEE_KEYWORDS (lexer.coffee)
# (also, don't check for 'undefined' because it's already explicitly
# accounted for elsewhere)
keywords: [
'then', 'unless', 'until', 'loop', 'of', 'by', 'when' ]
# taken from RESERVED (lexer.coffee)
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' ]
# taken from COFFEE_ALIAS_MAP (lexer.coffee)
aliases: [
'and', 'or', 'is', 'isnt', 'not', 'yes', 'no', 'on', 'off' ]
exports.reservedWords =
exports.reserved.keywords.concat \
exports.reserved.reserved.concat \
exports.reserved.aliases
###*
# Next until
###
exports.nextUntil = (body, node, fn) ->
idx = body.indexOf(node)
for i in [idx+1..body.length]
next = body[i]
return next if next?.type and fn(next)
###*
# Next until a non-comment node
###
exports.nextNonComment = (body, node) ->
exports.nextUntil(body, node, (n) ->
n.type isnt 'BlockComment' and
n.type isnt 'LineComment')
###
# isLoop() : isLoop(node)
# Checks if a loop is forever
###
exports.isLoop = (node) ->
not node.test? or exports.isTruthy(node.test)
###
# isTruthy() : isTruthy(node)
# Checks if a given node is truthy
###
exports.isTruthy = (node) ->
(node.type is 'Literal' and node.value)
###
# escapeJs()
###
exports.escapeJs = (node) ->
exports.replace node,
type: 'CoffeeEscapedExpression'
raw: require('escodegen').generate(node)
exports.nonComments = (body) ->
body.filter (n) ->
! exports.isComment(n)
exports.isComment = (node) ->
node.type is 'LineComment' or
node.type is 'BlockComment'
exports.toIndent = (ind) ->
if ind is 'tab' or ind is 't'
"\t"
else if typeof ind is 'string' and "#{+ind}" isnt ind
ind
else
Array(+ind + 1).join " "