5 var esprima = require('esprima');
11 // we expose the flags so other tools can tweak the values (#8)
12 exports.BYPASS_RECURSION = {
24 // IMPORTANT! "value" can't be bypassed since it is used by object
41 // parse string and return an augmented AST
42 exports.parse = function parse(source, opts){
43 _addLocInfo = opts && opts.loc;
44 source = source.toString();
46 var ast = esprima.parse(source, {
53 // we augment just root node since program is "empty"
54 // can't check `ast.body.length` because it might contain just comments
55 if (!ast.tokens.length && !ast.comments.length) {
57 ast.startToken = ast.endToken = null;
58 ast.toString = _nodeProto.toString;
62 instrumentTokens(ast, source);
64 // update program range since it doesn't include white spaces and comments
65 // before/after the program body by default
66 var lastToken = ast.tokens[ast.tokens.length - 1];
67 ast.range[0] = ast.tokens[0].range[0];
68 ast.range[1] = lastToken.range[1];
70 ast.loc.start.line = 0;
71 ast.loc.start.column = 0;
72 ast.loc.end.line = lastToken.loc.end.line;
73 ast.loc.end.column = lastToken.loc.end.column;
76 var toString = _nodeProto.toString;
77 var instrumentNodes = function(node, parent, prev, next){
82 node.depth = parent? parent.depth + 1 : 0; // used later for moonwalk
84 node.toString = toString;
86 // we do not add nextToken and prevToken to avoid updating even more
87 // references during each remove/before/after you can grab the
88 // prev/next token by simply accesing the startToken.prev and
90 var prevToken = prev? prev.endToken : (parent? parent.startToken : null);
91 var nextToken = parent? parent.endToken : null;
92 node.startToken = prevToken? getNodeStartToken(prevToken, node.range) : ast.tokens[0];
93 node.endToken = nextToken? getNodeEndToken(nextToken, node.range) : ast.tokens[ast.tokens.length - 1];
95 recursiveWalk(ast, instrumentNodes);
103 // get the node string
104 _nodeProto.toString = function(){
106 var token = this.startToken;
107 if (!token) return str;
109 str += ('raw' in token)? token.raw : token.value;
111 } while (token && token !== this.endToken.next);
116 function getNodeStartToken(token, range){
117 var startRange = range[0];
119 if (token.range[0] >= startRange) {
126 function getNodeEndToken(token, range){
127 var endRange = range[1];
129 if (token.range[1] <= endRange) {
138 function getPrevToken(tokens, range){
140 startRange = range[0],
144 if (token.range[1] <= startRange) {
157 function instrumentTokens(ast, source){
159 var tokens = ast.tokens;
162 // --- inject comments into tokens list
163 var comments = ast.comments;
166 nComments = comments.length;
168 while (++q < nComments) {
169 comment = comments[q];
170 // we edit it in place since it is faster, will also affect
171 comment.raw = comment.type === 'Block'? '/*'+ comment.value +'*/' : '//'+ comment.value;
172 comment.type += 'Comment';
174 var prevToken = getPrevToken(tokens, comment.range);
175 var prevIndex = prevToken? tokens.indexOf(prevToken) : -1;
176 tokens.splice(prevIndex + 1, 0, comment);
180 // --- inject white spaces and line breaks
182 // we create a new array since it's simpler than using splice, it will
183 // also avoid mistakes
186 // insert white spaces before start of program
188 var firstToken = ast.tokens[0];
190 if ( firstToken.range[0] ) {
191 raw = source.substring(0, firstToken.range[0]);
192 result = result.concat( getWhiteSpaceTokens(raw, null) );
195 // insert white spaces between regular tokens
196 // faster than forEach and reduce lookups
198 nTokens = tokens.length,
201 while (++i < nTokens) {
204 if (prev.range[1] < token.range[0]) {
205 wsTokens = getWhiteSpaceTokens(source.substring(prev.range[1], token.range[0]), prev);
206 // faster than concat or push.apply
208 nWs = wsTokens.length;
210 result.push(wsTokens[k]);
218 // insert white spaces after end of program
219 var lastToken = ast.tokens[ast.tokens.length - 1];
220 if (lastToken.range[1] < source.length) {
221 wsTokens = getWhiteSpaceTokens(source.substring(lastToken.range[1], source.length), lastToken);
223 nWs = wsTokens.length;
225 result.push(wsTokens[k]);
229 // --- instrument tokens
231 // need to come afterwards since we add line breaks and comments
233 for (i = 0, n = result.length, token; i < n; i++) {
235 token.prev = i? result[i - 1] : undefined;
236 token.next = result[i + 1];
237 token.root = ast; // used internally
238 // original indent is very important for block comments since some
239 // transformations require manipulation of raw comment value
241 token.type === 'BlockComment' &&
242 token.prev && token.prev.type === 'WhiteSpace' &&
243 (!token.prev.prev || (token.prev.prev.type === 'LineBreak'))
245 token.originalIndent = token.prev.value;
253 function getWhiteSpaceTokens(raw, prev){
254 var whiteSpaces = getWhiteSpaces(raw);
256 var startRange = prev? prev.range[1] : 0;
257 // line starts at 1 !!!
258 var startLine, startColumn;
260 startLine = prev? prev.loc.end.line : 1;
261 startColumn = prev? prev.loc.end.column : 0;
265 for (var i = 0, n = whiteSpaces.length, value; i < n; i++){
266 value = whiteSpaces[i];
268 var wsToken = { value : value };
269 var isBr = '\r\n'.indexOf(value) >= 0;
270 wsToken.type = isBr? 'LineBreak' : 'WhiteSpace';
271 wsToken.range = [startRange, startRange + value.length];
280 line : startLine, // yes, br starts and end on same line
281 column : startColumn + value.length
286 // next token after a <br> always starts at zero and on next line
287 startLine = wsToken.loc.end.line + 1;
290 startLine = wsToken.loc.end.line;
291 startColumn = wsToken.loc.end.column;
295 startRange += value.length;
296 tokens.push(wsToken);
303 function getWhiteSpaces(source) {
305 var whiteSpaces = source.split('');
307 for (var value, i = 0, nSpaces = whiteSpaces.length; i < nSpaces; i++) {
308 value = whiteSpaces[i];
313 result.push(buf + value);
324 // might be multiple consecutive Mac breaks
335 // group multiple white spaces into same token
348 exports.recursive = recursiveWalk;
350 // heavily inspired by node-falafel
351 // walk nodes recursively starting from root
352 function recursiveWalk(node, fn, parent, prev, next){
353 // sparse arrays might have `null` elements, so we skip those for now
355 if ( !node || fn(node, parent, prev, next) === false ) {
356 return; // stop recursion
359 // faster than for in
360 var keys = Object.keys(node),
363 for (var i = 0, nKeys = keys.length; i < nKeys; i++) {
368 // only need to recurse real nodes and arrays
369 // ps: typeof null == 'object'
370 if (!child || typeof child !== 'object' || exports.BYPASS_RECURSION[key]) {
375 if (typeof child.type === 'string') { // faster than boolean coercion
376 recursiveWalk(child, fn, node);
377 } else if ( typeof child.length === 'number' ) { // faster than Array.isArray and boolean coercion
378 // faster than forEach
379 for (var k = 0, nChilds = child.length; k < nChilds; k++) {
380 recursiveWalk(child[k], fn, node, (k? child[k - 1] : undefined), child[k + 1] );
390 // walk AST starting from leaf nodes
391 exports.moonwalk = function moonwalk(ast, fn){
392 if (typeof ast === 'string') {
393 ast = exports.parse(ast);
396 // we create a separate array for each depth and than we flatten it to
397 // boost performance, way faster than doing an insertion sort
399 recursiveWalk(ast, function(node){
400 if (! swap[node.depth]) {
401 swap[node.depth] = [];
403 swap[node.depth].push(node);
407 var nDepths = swap.length, cur;
408 while (cur = swap[--nDepths]) {
409 for (var i = 0, n = cur.length; i < n; i++) {