424983c8301a7ff81e32827e48b2425d28c1a53e
[platform/framework/web/crosswalk-tizen.git] /
1 /*jshint node:true */
2 "use strict";
3
4
5 var esprima = require('esprima');
6
7
8
9 // ---
10
11 // we expose the flags so other tools can tweak the values (#8)
12 exports.BYPASS_RECURSION = {
13     root : true,
14     comments : true,
15     tokens : true,
16
17     loc : true,
18     range : true,
19
20     parent : true,
21     next : true,
22     prev : true,
23
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)`
26     handler : true,
27
28     // IMPORTANT! "value" can't be bypassed since it is used by object
29     // expression
30     type : true,
31     raw : true,
32
33     startToken : true,
34     endToken : true
35 };
36
37
38 // ---
39
40 var _addLocInfo;
41
42 // ---
43
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 = {
48     range: true,
49     tokens: true,
50     comment: true
51 };
52
53 // parse string and return an augmented AST
54 exports.parse = function parse(source, opts){
55     opts = opts || {};
56     _addLocInfo = Boolean(opts.loc);
57     source = source.toString();
58
59     Object.keys(exports.parseOptions).forEach(function(key) {
60         if (!(key in opts)) {
61             opts[key] = exports.parseOptions[key];
62         }
63     });
64
65     var ast = exports.parseFn.call(exports.parseContext, source, opts);
66
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) {
70         ast.depth = 0;
71         ast.startToken = ast.endToken = null;
72         ast.toString = _nodeProto.toString;
73         return ast;
74     }
75
76     instrumentTokens(ast, source);
77
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];
83     if (_addLocInfo) {
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;
88     }
89
90     var toString = _nodeProto.toString;
91     var instrumentNodes = function(node, parent, prev, next){
92
93         node.parent = parent;
94         node.prev = prev;
95         node.next = next;
96         node.depth = parent? parent.depth + 1 : 0; // used later for moonwalk
97
98         node.toString = toString;
99
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
103         // endToken.next
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];
108     };
109     recursiveWalk(ast, instrumentNodes);
110
111     return ast;
112 };
113
114
115 var _nodeProto = {};
116
117 // get the node string
118 _nodeProto.toString = function(){
119     var str = '';
120     var token = this.startToken;
121     if (!token) return str;
122     do {
123         str += ('raw' in token)? token.raw : token.value;
124         token = token.next;
125     } while (token && token !== this.endToken.next);
126     return str;
127 };
128
129
130 function getNodeStartToken(token, range){
131     var startRange = range[0];
132     while (token){
133         if (token.range[0] >= startRange) {
134             return token;
135         }
136         token = token.next;
137     }
138 }
139
140 function getNodeEndToken(token, range){
141     var endRange = range[1];
142     while (token){
143         if (token.range[1] <= endRange) {
144             return token;
145         }
146         token = token.prev;
147     }
148 }
149
150
151
152 function getPrevToken(tokens, range){
153     var result, token,
154         startRange = range[0],
155         n = tokens.length;
156     while (n--) {
157         token = tokens[n];
158         if (token.range[1] <= startRange) {
159             result = token;
160             break;
161         }
162     }
163     return result;
164 }
165
166
167
168
169
170
171 function instrumentTokens(ast, source){
172
173     var tokens = ast.tokens;
174
175
176     // --- inject comments into tokens list
177     var comments = ast.comments;
178     var comment,
179         q = -1,
180         nComments = comments.length;
181
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';
187
188         var prevToken = getPrevToken(tokens, comment.range);
189         var prevIndex = prevToken? tokens.indexOf(prevToken) : -1;
190         tokens.splice(prevIndex + 1, 0, comment);
191     }
192
193
194     // --- inject white spaces and line breaks
195
196     // we create a new array since it's simpler than using splice, it will
197     // also avoid mistakes
198     var result = [];
199
200     // insert white spaces before start of program
201     var wsTokens;
202     var firstToken = ast.tokens[0];
203     var raw;
204     if ( firstToken.range[0] ) {
205         raw = source.substring(0, firstToken.range[0]);
206         result = result.concat( getWhiteSpaceTokens(raw, null) );
207     }
208
209     // insert white spaces between regular tokens
210     // faster than forEach and reduce lookups
211     var i = -1,
212         nTokens = tokens.length,
213         token, prev;
214     var k, nWs;
215     while (++i < nTokens) {
216         token = tokens[i];
217         if (i) {
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
221                 k = -1;
222                 nWs = wsTokens.length;
223                 while (++k < nWs) {
224                     result.push(wsTokens[k]);
225                 }
226             }
227         }
228         result.push(token);
229         prev = token;
230     }
231
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);
236         k = -1;
237         nWs = wsTokens.length;
238         while (++k < nWs) {
239             result.push(wsTokens[k]);
240         }
241     }
242
243     // --- instrument tokens
244
245     // need to come afterwards since we add line breaks and comments
246     var n;
247     for (i = 0, n = result.length, token; i < n; i++) {
248         token = result[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
254         if (
255           token.type === 'BlockComment' &&
256           token.prev && token.prev.type === 'WhiteSpace' &&
257           (!token.prev.prev || (token.prev.prev.type === 'LineBreak'))
258         ) {
259           token.originalIndent = token.prev.value;
260         }
261     }
262
263     ast.tokens = result;
264 }
265
266
267 function getWhiteSpaceTokens(raw, prev){
268     var whiteSpaces = getWhiteSpaces(raw);
269
270     var startRange = prev? prev.range[1] : 0;
271     // line starts at 1 !!!
272     var startLine, startColumn;
273     if (_addLocInfo) {
274         startLine = prev? prev.loc.end.line : 1;
275         startColumn = prev? prev.loc.end.column : 0;
276     }
277
278     var tokens = [];
279     for (var i = 0, n = whiteSpaces.length, value; i < n; i++){
280         value = whiteSpaces[i];
281
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];
286
287         if (_addLocInfo) {
288             wsToken.loc = {
289                 start : {
290                     line : startLine,
291                     column : startColumn
292                 },
293                 end : {
294                     line : startLine, // yes, br starts and end on same line
295                     column : startColumn + value.length
296                 }
297             };
298
299             if (isBr) {
300                 // next token after a <br> always starts at zero and on next line
301                 startLine = wsToken.loc.end.line + 1;
302                 startColumn = 0;
303             } else {
304                 startLine = wsToken.loc.end.line;
305                 startColumn = wsToken.loc.end.column;
306             }
307         }
308
309         startRange += value.length;
310         tokens.push(wsToken);
311     }
312
313     return tokens;
314 }
315
316
317 function getWhiteSpaces(source) {
318     var result = [];
319     var whiteSpaces = source.split('');
320     var buf = '';
321     for (var value, i = 0, nSpaces = whiteSpaces.length; i < nSpaces; i++) {
322         value = whiteSpaces[i];
323         switch(value){
324             case '\n':
325                 if (buf === '\r') {
326                     // DOS line break
327                     result.push(buf + value);
328                 } else {
329                     if (buf) {
330                         result.push(buf);
331                     }
332                     // unix break
333                     result.push(value);
334                 }
335                 buf = '';
336                 break;
337             case '\r':
338                 // might be multiple consecutive Mac breaks
339                 if (buf) {
340                     result.push(buf);
341                 }
342                 buf = value;
343                 break;
344             default:
345                 if (buf === '\r') {
346                     result.push(buf);
347                     buf = value;
348                 } else {
349                     // group multiple white spaces into same token
350                     buf += value;
351                 }
352         }
353     }
354     if (buf) {
355         result.push(buf);
356     }
357     return result;
358 }
359
360
361
362 exports.walk = exports.recursive = recursiveWalk;
363
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
368     // see issue #15
369     if ( !node || fn(node, parent, prev, next) === false ) {
370         return; // stop recursion
371     }
372
373     // faster than for in
374     var keys = Object.keys(node),
375         child, key;
376
377     for (var i = 0, nKeys = keys.length; i < nKeys; i++) {
378
379         key = keys[i];
380         child = node[key];
381
382         // only need to recurse real nodes and arrays
383         // ps: typeof null == 'object'
384         if (!child || typeof child !== 'object' || exports.BYPASS_RECURSION[key]) {
385             continue;
386         }
387
388         // inception
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] );
395             }
396         }
397
398     }
399
400 }
401
402
403
404 // walk AST starting from leaf nodes
405 exports.moonwalk = function moonwalk(ast, fn){
406     if (typeof ast === 'string') {
407         ast = exports.parse(ast);
408     }
409
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
412     var swap = [];
413     recursiveWalk(ast, function(node){
414         if (! swap[node.depth]) {
415             swap[node.depth] = [];
416         }
417         swap[node.depth].push(node);
418     });
419
420     var nodes = [];
421     var nDepths = swap.length, cur;
422     while (cur = swap[--nDepths]) {
423         for (var i = 0, n = cur.length; i < n; i++) {
424             nodes.push(cur[i]);
425         }
426     }
427
428     nodes.forEach(fn);
429     return ast;
430 };
431