redeyed.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. ;(function() {
  2. 'use strict'
  3. /* global define */
  4. var esprima
  5. var exportFn
  6. var toString = Object.prototype.toString
  7. if (typeof module === 'object' && typeof module.exports === 'object' && typeof require === 'function') {
  8. // server side
  9. esprima = require('esprima')
  10. exportFn = function(redeyed) { module.exports = redeyed }
  11. bootstrap(esprima, exportFn)
  12. } else if (typeof define === 'function' && define.amd) {
  13. // client side
  14. // amd
  15. define(['esprima'], function(esprima) {
  16. return bootstrap(esprima)
  17. })
  18. } else if (typeof window === 'object') {
  19. // no amd -> attach to window if it exists
  20. // Note that this requires 'esprima' to be defined on the window, so that script has to be loaded first
  21. window.redeyed = bootstrap(window.esprima)
  22. }
  23. function bootstrap(esprima, exportFn) {
  24. function isFunction(obj) {
  25. return toString.call(obj) === '[object Function]'
  26. }
  27. function isString(obj) {
  28. return toString.call(obj) === '[object String]'
  29. }
  30. function isObject(obj) {
  31. return toString.call(obj) === '[object Object]'
  32. }
  33. function surroundWith(before, after) {
  34. return function(s) { return before + s + after }
  35. }
  36. function isNonCircular(key) {
  37. return key !== '_parent'
  38. }
  39. function objectizeString(value) {
  40. var vals = value.split(':')
  41. if (vals.length === 0 || vals.length > 2) {
  42. throw new Error(
  43. 'illegal string config: ' + value +
  44. '\nShould be of format "before:after"'
  45. )
  46. }
  47. if (vals.length === 1 || vals[1].length === 0) {
  48. return vals.indexOf(':') < 0 ? { _before: vals[0] } : { _after: vals[0] }
  49. } else {
  50. return { _before: vals[0], _after: vals[1] }
  51. }
  52. }
  53. function objectize(node) {
  54. // Converts 'bef:aft' to { _before: bef, _after: aft }
  55. // and resolves undefined before/after from parent or root
  56. function resolve(value, key) {
  57. // resolve before/after from root or parent if it isn't present on the current node
  58. if (!value._parent) return undefined
  59. // Immediate parent
  60. if (value._parent._default && value._parent._default[key]) return value._parent._default[key]
  61. // Root
  62. var root = value._parent._parent
  63. if (!root) return undefined
  64. return root._default ? root._default[key] : undefined
  65. }
  66. function process(key) {
  67. var value = node[key]
  68. if (!value) return
  69. if (isFunction(value)) return
  70. // normalize all strings to objects
  71. if (isString(value)) {
  72. node[key] = value = objectizeString(value)
  73. }
  74. value._parent = node
  75. if (isObject(value)) {
  76. if (!value._before && !value._after) return objectize(value)
  77. // resolve missing _before or _after from parent(s)
  78. // in case we only have either one on this node
  79. value._before = value._before || resolve(value, '_before')
  80. value._after = value._after || resolve(value, '_after')
  81. return
  82. }
  83. throw new Error('nodes need to be either {String}, {Object} or {Function}.' + value + ' is neither.')
  84. }
  85. // Process _default ones first so children can resolve missing before/after from them
  86. if (node._default) process('_default')
  87. Object.keys(node)
  88. .filter(function(key) {
  89. return isNonCircular(key)
  90. && node.hasOwnProperty(key)
  91. && key !== '_before'
  92. && key !== '_after'
  93. && key !== '_default'
  94. })
  95. .forEach(process)
  96. }
  97. function functionize(node) {
  98. Object.keys(node)
  99. .filter(function(key) {
  100. return isNonCircular(key) && node.hasOwnProperty(key)
  101. })
  102. .forEach(function(key) {
  103. var value = node[key]
  104. if (isFunction(value)) return
  105. if (isObject(value)) {
  106. if (!value._before && !value._after) return functionize(value)
  107. // at this point before/after were "inherited" from the parent or root
  108. // (see objectize)
  109. var before = value._before || ''
  110. var after = value._after || ''
  111. node[key] = surroundWith(before, after)
  112. return node[key]
  113. }
  114. })
  115. }
  116. function normalize(root) {
  117. objectize(root)
  118. functionize(root)
  119. }
  120. function mergeTokensAndComments(tokens, comments) {
  121. var all = {}
  122. function addToAllByRangeStart(t) { all[ t.range[0] ] = t }
  123. tokens.forEach(addToAllByRangeStart)
  124. comments.forEach(addToAllByRangeStart)
  125. // keys are sorted automatically
  126. return Object.keys(all)
  127. .map(function(k) { return all[k] })
  128. }
  129. function redeyed(code, config, opts) {
  130. opts = opts || {}
  131. var parser = opts.parser || esprima
  132. var jsx = !!opts.jsx
  133. // tokenizer doesn't support JSX at this point (esprima@4.0.0)
  134. // therefore we need to generate the AST via the parser not only to
  135. // avoid the tokenizer from erroring but also to get JSXIdentifier tokens
  136. var buildAst = jsx || !!opts.buildAst
  137. var hashbang = ''
  138. var ast
  139. var tokens
  140. var comments
  141. var lastSplitEnd = 0
  142. var splits = []
  143. var transformedCode
  144. var all
  145. var info
  146. // Replace hashbang line with empty whitespaces to preserve token locations
  147. if (code[0] === '#' && code[1] === '!') {
  148. hashbang = code.substr(0, code.indexOf('\n') + 1)
  149. code = Array.apply(0, Array(hashbang.length)).join(' ') + '\n' + code.substr(hashbang.length)
  150. }
  151. if (buildAst) {
  152. ast = parser.parse(code, { tokens: true, comment: true, range: true, loc: true, tolerant: true, jsx: true })
  153. tokens = ast.tokens
  154. comments = ast.comments
  155. } else {
  156. tokens = []
  157. comments = []
  158. parser.tokenize(code, { range: true, loc: true, comment: true }, function(token) {
  159. if (token.type === 'LineComment') {
  160. token.type = 'Line'
  161. comments.push(token)
  162. } else if (token.type === 'BlockComment') {
  163. token.type = 'Block'
  164. comments.push(token)
  165. } else {
  166. // Optimistically upgrade 'static' to a keyword
  167. if (token.type === 'Identifier' && token.value === 'static') token.type = 'Keyword'
  168. tokens.push(token)
  169. }
  170. })
  171. }
  172. normalize(config)
  173. function tokenIndex(tokens, tkn, start) {
  174. var current
  175. var rangeStart = tkn.range[0]
  176. for (current = start; current < tokens.length; current++) {
  177. if (tokens[current].range[0] === rangeStart) return current
  178. }
  179. throw new Error('Token %s not found at or after index: %d', tkn, start)
  180. }
  181. function process(surround) {
  182. var result
  183. var currentIndex
  184. var nextIndex
  185. var skip = 0
  186. var splitEnd
  187. result = surround(code.slice(start, end), info)
  188. if (isObject(result)) {
  189. splits.push(result.replacement)
  190. currentIndex = info.tokenIndex
  191. nextIndex = tokenIndex(info.tokens, result.skipPastToken, currentIndex)
  192. skip = nextIndex - currentIndex
  193. splitEnd = skip > 0 ? tokens[nextIndex - 1].range[1] : end
  194. } else {
  195. splits.push(result)
  196. splitEnd = end
  197. }
  198. return { skip: skip, splitEnd: splitEnd }
  199. }
  200. function addSplit(start, end, surround, info) {
  201. var result
  202. var skip = 0
  203. if (start >= end) return
  204. if (surround) {
  205. result = process(surround)
  206. skip = result.skip
  207. lastSplitEnd = result.splitEnd
  208. } else {
  209. splits.push(code.slice(start, end))
  210. lastSplitEnd = end
  211. }
  212. return skip
  213. }
  214. all = mergeTokensAndComments(tokens, comments)
  215. for (var tokenIdx = 0; tokenIdx < all.length; tokenIdx++) {
  216. var token = all[tokenIdx]
  217. var surroundForType = config[token.type]
  218. var surround
  219. var start
  220. var end
  221. // At least the type (e.g., 'Keyword') needs to be specified for the token to be surrounded
  222. if (surroundForType) {
  223. // root defaults are only taken into account while resolving before/after otherwise
  224. // a root default would apply to everything, even if no type default was specified
  225. surround = surroundForType
  226. && surroundForType.hasOwnProperty(token.value)
  227. && surroundForType[token.value]
  228. && isFunction(surroundForType[token.value])
  229. ? surroundForType[token.value]
  230. : surroundForType._default
  231. start = token.range[0]
  232. end = token.range[1]
  233. addSplit(lastSplitEnd, start)
  234. info = { tokenIndex: tokenIdx, tokens: all, ast: ast, code: code }
  235. tokenIdx += addSplit(start, end, surround, info)
  236. }
  237. }
  238. if (lastSplitEnd < code.length) {
  239. addSplit(lastSplitEnd, code.length)
  240. }
  241. if (!opts.nojoin) {
  242. transformedCode = splits.join('')
  243. if (hashbang.length > 0) {
  244. transformedCode = hashbang + transformedCode.substr(hashbang.length)
  245. }
  246. }
  247. return {
  248. ast : ast
  249. , tokens : tokens
  250. , comments : comments
  251. , splits : splits
  252. , code : transformedCode
  253. }
  254. }
  255. return exportFn ? exportFn(redeyed) : redeyed
  256. }
  257. })()