5 var esprima = require('esprima');
11 // we expose the flags so other tools can tweak the values (#8)
12 exports.BYPASS_RECURSION = {
24 // esprima@2.1 introduces a "handler" property on TryStatement, so we would
25 // loop the same node twice (see jquery/esprima/issues/1031 and #264)`
28 // IMPORTANT! "value" can't be bypassed since it is used by object
44 exports.parseFn = esprima.parse;
45 exports.parseContext = esprima;
46 // we need range/tokens/comment info to build the tokens linked list!
47 exports.parseOptions = {
53 // parse string and return an augmented AST
54 exports.parse = function parse(source, opts){
56 _addLocInfo = Boolean(opts.loc);
57 source = source.toString();
59 Object.keys(exports.parseOptions).forEach(function(key) {
61 opts[key] = exports.parseOptions[key];
65 var ast = exports.parseFn.call(exports.parseContext, source, opts);
67 // we augment just root node since program is "empty"
68 // can't check `ast.body.length` because it might contain just comments
69 if (!ast.tokens.length && !ast.comments.length) {
71 ast.startToken = ast.endToken = null;
72 ast.toString = _nodeProto.toString;
76 instrumentTokens(ast, source);
78 // update program range since it doesn't include white spaces and comments
79 // before/after the program body by default
80 var lastToken = ast.tokens[ast.tokens.length - 1];
81 ast.range[0] = ast.tokens[0].range[0];
82 ast.range[1] = lastToken.range[1];
84 ast.loc.start.line = 0;
85 ast.loc.start.column = 0;
86 ast.loc.end.line = lastToken.loc.end.line;
87 ast.loc.end.column = lastToken.loc.end.column;
90 var toString = _nodeProto.toString;
91 var instrumentNodes = function(node, parent, prev, next){
96 node.depth = parent? parent.depth + 1 : 0; // used later for moonwalk
98 node.toString = toString;
100 // we do not add nextToken and prevToken to avoid updating even more
101 // references during each remove/before/after you can grab the
102 // prev/next token by simply accesing the startToken.prev and
104 var prevToken = prev? prev.endToken : (parent? parent.startToken : null);
105 var nextToken = parent? parent.endToken : null;
106 node.startToken = prevToken? getNodeStartToken(prevToken, node.range) : ast.tokens[0];
107 node.endToken = nextToken? getNodeEndToken(nextToken, node.range) : ast.tokens[ast.tokens.length - 1];
109 recursiveWalk(ast, instrumentNodes);
117 // get the node string
118 _nodeProto.toString = function(){
120 var token = this.startToken;
121 if (!token) return str;
123 str += ('raw' in token)? token.raw : token.value;
125 } while (token && token !== this.endToken.next);
130 function getNodeStartToken(token, range){
131 var startRange = range[0];
133 if (token.range[0] >= startRange) {
140 function getNodeEndToken(token, range){
141 var endRange = range[1];
143 if (token.range[1] <= endRange) {
152 function getPrevToken(tokens, range){
154 startRange = range[0],
158 if (token.range[1] <= startRange) {
171 function instrumentTokens(ast, source){
173 var tokens = ast.tokens;
176 // --- inject comments into tokens list
177 var comments = ast.comments;
180 nComments = comments.length;
182 while (++q < nComments) {
183 comment = comments[q];
184 // we edit it in place since it is faster, will also affect
185 comment.raw = comment.type === 'Block'? '/*'+ comment.value +'*/' : '//'+ comment.value;
186 comment.type += 'Comment';
188 var prevToken = getPrevToken(tokens, comment.range);
189 var prevIndex = prevToken? tokens.indexOf(prevToken) : -1;
190 tokens.splice(prevIndex + 1, 0, comment);
194 // --- inject white spaces and line breaks
196 // we create a new array since it's simpler than using splice, it will
197 // also avoid mistakes
200 // insert white spaces before start of program
202 var firstToken = ast.tokens[0];
204 if ( firstToken.range[0] ) {
205 raw = source.substring(0, firstToken.range[0]);
206 result = result.concat( getWhiteSpaceTokens(raw, null) );
209 // insert white spaces between regular tokens
210 // faster than forEach and reduce lookups
212 nTokens = tokens.length,
215 while (++i < nTokens) {
218 if (prev.range[1] < token.range[0]) {
219 wsTokens = getWhiteSpaceTokens(source.substring(prev.range[1], token.range[0]), prev);
220 // faster than concat or push.apply
222 nWs = wsTokens.length;
224 result.push(wsTokens[k]);
232 // insert white spaces after end of program
233 var lastToken = ast.tokens[ast.tokens.length - 1];
234 if (lastToken.range[1] < source.length) {
235 wsTokens = getWhiteSpaceTokens(source.substring(lastToken.range[1], source.length), lastToken);
237 nWs = wsTokens.length;
239 result.push(wsTokens[k]);
243 // --- instrument tokens
245 // need to come afterwards since we add line breaks and comments
247 for (i = 0, n = result.length, token; i < n; i++) {
249 token.prev = i? result[i - 1] : undefined;
250 token.next = result[i + 1];
251 token.root = ast; // used internally
252 // original indent is very important for block comments since some
253 // transformations require manipulation of raw comment value
255 token.type === 'BlockComment' &&
256 token.prev && token.prev.type === 'WhiteSpace' &&
257 (!token.prev.prev || (token.prev.prev.type === 'LineBreak'))
259 token.originalIndent = token.prev.value;
267 function getWhiteSpaceTokens(raw, prev){
268 var whiteSpaces = getWhiteSpaces(raw);
270 var startRange = prev? prev.range[1] : 0;
271 // line starts at 1 !!!
272 var startLine, startColumn;
274 startLine = prev? prev.loc.end.line : 1;
275 startColumn = prev? prev.loc.end.column : 0;
279 for (var i = 0, n = whiteSpaces.length, value; i < n; i++){
280 value = whiteSpaces[i];
282 var wsToken = { value : value };
283 var isBr = '\r\n'.indexOf(value) >= 0;
284 wsToken.type = isBr? 'LineBreak' : 'WhiteSpace';
285 wsToken.range = [startRange, startRange + value.length];
294 line : startLine, // yes, br starts and end on same line
295 column : startColumn + value.length
300 // next token after a <br> always starts at zero and on next line
301 startLine = wsToken.loc.end.line + 1;
304 startLine = wsToken.loc.end.line;
305 startColumn = wsToken.loc.end.column;
309 startRange += value.length;
310 tokens.push(wsToken);
317 function getWhiteSpaces(source) {
319 var whiteSpaces = source.split('');
321 for (var value, i = 0, nSpaces = whiteSpaces.length; i < nSpaces; i++) {
322 value = whiteSpaces[i];
327 result.push(buf + value);
338 // might be multiple consecutive Mac breaks
349 // group multiple white spaces into same token
362 exports.walk = exports.recursive = recursiveWalk;
364 // heavily inspired by node-falafel
365 // walk nodes recursively starting from root
366 function recursiveWalk(node, fn, parent, prev, next){
367 // sparse arrays might have `null` elements, so we skip those for now
369 if ( !node || fn(node, parent, prev, next) === false ) {
370 return; // stop recursion
373 // faster than for in
374 var keys = Object.keys(node),
377 for (var i = 0, nKeys = keys.length; i < nKeys; i++) {
382 // only need to recurse real nodes and arrays
383 // ps: typeof null == 'object'
384 if (!child || typeof child !== 'object' || exports.BYPASS_RECURSION[key]) {
389 if (typeof child.type === 'string') { // faster than boolean coercion
390 recursiveWalk(child, fn, node);
391 } else if ( typeof child.length === 'number' ) { // faster than Array.isArray and boolean coercion
392 // faster than forEach
393 for (var k = 0, nChilds = child.length; k < nChilds; k++) {
394 recursiveWalk(child[k], fn, node, (k? child[k - 1] : undefined), child[k + 1] );
404 // walk AST starting from leaf nodes
405 exports.moonwalk = function moonwalk(ast, fn){
406 if (typeof ast === 'string') {
407 ast = exports.parse(ast);
410 // we create a separate array for each depth and than we flatten it to
411 // boost performance, way faster than doing an insertion sort
413 recursiveWalk(ast, function(node){
414 if (! swap[node.depth]) {
415 swap[node.depth] = [];
417 swap[node.depth].push(node);
421 var nDepths = swap.length, cur;
422 while (cur = swap[--nDepths]) {
423 for (var i = 0, n = cur.length; i < n; i++) {