redeyed.js 8.9 KB

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