You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
8.8 KiB

;(function() {
'use strict'
/* global define */
var esprima
var exportFn
var toString = Object.prototype.toString
if (typeof module === 'object' && typeof module.exports === 'object' && typeof require === 'function') {
// server side
esprima = require('esprima')
exportFn = function(redeyed) { module.exports = redeyed }
bootstrap(esprima, exportFn)
} else if (typeof define === 'function' && define.amd) {
// client side
// amd
define(['esprima'], function(esprima) {
return bootstrap(esprima)
})
} else if (typeof window === 'object') {
// no amd -> attach to window if it exists
// Note that this requires 'esprima' to be defined on the window, so that script has to be loaded first
window.redeyed = bootstrap(window.esprima)
}
function bootstrap(esprima, exportFn) {
function isFunction(obj) {
return toString.call(obj) === '[object Function]'
}
function isString(obj) {
return toString.call(obj) === '[object String]'
}
function isObject(obj) {
return toString.call(obj) === '[object Object]'
}
function surroundWith(before, after) {
return function(s) { return before + s + after }
}
function isNonCircular(key) {
return key !== '_parent'
}
function objectizeString(value) {
var vals = value.split(':')
if (vals.length === 0 || vals.length > 2) {
throw new Error(
'illegal string config: ' + value +
'\nShould be of format "before:after"'
)
}
if (vals.length === 1 || vals[1].length === 0) {
return vals.indexOf(':') < 0 ? { _before: vals[0] } : { _after: vals[0] }
} else {
return { _before: vals[0], _after: vals[1] }
}
}
function objectize(node) {
// Converts 'bef:aft' to { _before: bef, _after: aft }
// and resolves undefined before/after from parent or root
function resolve(value, key) {
// resolve before/after from root or parent if it isn't present on the current node
if (!value._parent) return undefined
// Immediate parent
if (value._parent._default && value._parent._default[key]) return value._parent._default[key]
// Root
var root = value._parent._parent
if (!root) return undefined
return root._default ? root._default[key] : undefined
}
function process(key) {
var value = node[key]
if (!value) return
if (isFunction(value)) return
// normalize all strings to objects
if (isString(value)) {
node[key] = value = objectizeString(value)
}
value._parent = node
if (isObject(value)) {
if (!value._before && !value._after) return objectize(value)
// resolve missing _before or _after from parent(s)
// in case we only have either one on this node
value._before = value._before || resolve(value, '_before')
value._after = value._after || resolve(value, '_after')
return
}
throw new Error('nodes need to be either {String}, {Object} or {Function}.' + value + ' is neither.')
}
// Process _default ones first so children can resolve missing before/after from them
if (node._default) process('_default')
Object.keys(node)
.filter(function(key) {
return isNonCircular(key)
&& node.hasOwnProperty(key)
&& key !== '_before'
&& key !== '_after'
&& key !== '_default'
})
.forEach(process)
}
function functionize(node) {
Object.keys(node)
.filter(function(key) {
return isNonCircular(key) && node.hasOwnProperty(key)
})
.forEach(function(key) {
var value = node[key]
if (isFunction(value)) return
if (isObject(value)) {
if (!value._before && !value._after) return functionize(value)
// at this point before/after were "inherited" from the parent or root
// (see objectize)
var before = value._before || ''
var after = value._after || ''
node[key] = surroundWith(before, after)
return node[key]
}
})
}
function normalize(root) {
objectize(root)
functionize(root)
}
function mergeTokensAndComments(tokens, comments) {
var all = {}
function addToAllByRangeStart(t) { all[ t.range[0] ] = t }
tokens.forEach(addToAllByRangeStart)
comments.forEach(addToAllByRangeStart)
// keys are sorted automatically
return Object.keys(all)
.map(function(k) { return all[k] })
}
function redeyed(code, config, opts) {
opts = opts || {}
var parser = opts.parser || esprima
var jsx = !!opts.jsx
// tokenizer doesn't support JSX at this point (esprima@4.0.0)
// therefore we need to generate the AST via the parser not only to
// avoid the tokenizer from erroring but also to get JSXIdentifier tokens
var buildAst = jsx || !!opts.buildAst
var hashbang = ''
var ast
var tokens
var comments
var lastSplitEnd = 0
var splits = []
var transformedCode
var all
var info
// Replace hashbang line with empty whitespaces to preserve token locations
if (code[0] === '#' && code[1] === '!') {
hashbang = code.substr(0, code.indexOf('\n') + 1)
code = Array.apply(0, Array(hashbang.length)).join(' ') + '\n' + code.substr(hashbang.length)
}
if (buildAst) {
ast = parser.parse(code, { tokens: true, comment: true, range: true, loc: true, tolerant: true, jsx: true })
tokens = ast.tokens
comments = ast.comments
} else {
tokens = []
comments = []
parser.tokenize(code, { range: true, loc: true, comment: true }, function(token) {
if (token.type === 'LineComment') {
token.type = 'Line'
comments.push(token)
} else if (token.type === 'BlockComment') {
token.type = 'Block'
comments.push(token)
} else {
// Optimistically upgrade 'static' to a keyword
if (token.type === 'Identifier' && token.value === 'static') token.type = 'Keyword'
tokens.push(token)
}
})
}
normalize(config)
function tokenIndex(tokens, tkn, start) {
var current
var rangeStart = tkn.range[0]
for (current = start; current < tokens.length; current++) {
if (tokens[current].range[0] === rangeStart) return current
}
throw new Error('Token %s not found at or after index: %d', tkn, start)
}
function process(surround) {
var result
var currentIndex
var nextIndex
var skip = 0
var splitEnd
result = surround(code.slice(start, end), info)
if (isObject(result)) {
splits.push(result.replacement)
currentIndex = info.tokenIndex
nextIndex = tokenIndex(info.tokens, result.skipPastToken, currentIndex)
skip = nextIndex - currentIndex
splitEnd = skip > 0 ? tokens[nextIndex - 1].range[1] : end
} else {
splits.push(result)
splitEnd = end
}
return { skip: skip, splitEnd: splitEnd }
}
function addSplit(start, end, surround, info) {
var result
var skip = 0
if (start >= end) return
if (surround) {
result = process(surround)
skip = result.skip
lastSplitEnd = result.splitEnd
} else {
splits.push(code.slice(start, end))
lastSplitEnd = end
}
return skip
}
all = mergeTokensAndComments(tokens, comments)
for (var tokenIdx = 0; tokenIdx < all.length; tokenIdx++) {
var token = all[tokenIdx]
var surroundForType = config[token.type]
var surround
var start
var end
// At least the type (e.g., 'Keyword') needs to be specified for the token to be surrounded
if (surroundForType) {
// root defaults are only taken into account while resolving before/after otherwise
// a root default would apply to everything, even if no type default was specified
surround = surroundForType
&& surroundForType.hasOwnProperty(token.value)
&& surroundForType[token.value]
&& isFunction(surroundForType[token.value])
? surroundForType[token.value]
: surroundForType._default
start = token.range[0]
end = token.range[1]
addSplit(lastSplitEnd, start)
info = { tokenIndex: tokenIdx, tokens: all, ast: ast, code: code }
tokenIdx += addSplit(start, end, surround, info)
}
}
if (lastSplitEnd < code.length) {
addSplit(lastSplitEnd, code.length)
}
if (!opts.nojoin) {
transformedCode = splits.join('')
if (hashbang.length > 0) {
transformedCode = hashbang + transformedCode.substr(hashbang.length)
}
}
return {
ast : ast
, tokens : tokens
, comments : comments
, splits : splits
, code : transformedCode
}
}
return exportFn ? exportFn(redeyed) : redeyed
}
})()