7b00e5f45c40ce731ebb6e7be02f4e98e53ad900
[platform/framework/web/crosswalk-tizen.git] /
1 'use strict';
2
3 var debug = require('debug')('rocambole:indent');
4 var tk = require('rocambole-token');
5 var escapeRegExp = require('mout/string/escapeRegExp');
6 var repeat = require('mout/string/repeat');
7
8
9 // ---
10
11 var _opts = {
12   value: '  ',
13   CommentInsideEmptyBlock: 1
14 };
15
16 // ---
17
18
19 exports.setOptions = function(opts) {
20   _opts = opts;
21 };
22
23
24 exports.inBetween = indentInBetween;
25 function indentInBetween(startToken, endToken, level) {
26   level = level != null ? level : 1;
27
28   if (!level || (!startToken || !endToken) || startToken === endToken) {
29     debug(
30       '[inBetween] not going to indent. start: %s, end: %s, level: %s',
31       startToken && startToken.value,
32       endToken && endToken.value,
33       level
34     );
35     return;
36   }
37
38   var token = startToken && startToken.next;
39   var endsWithBraces = isClosingBrace(endToken);
40   while (token && token !== endToken) {
41     if (tk.isBr(token.prev)) {
42       // we ignore the last indent (if first token on the line is a ws or
43       // ident) just because in most cases we don't want to change the indent
44       // just before "}", ")" and "]" - this allow us to pass
45       // `node.body.startToken` and `node.body.endToken` as the range
46       if (token.next !== endToken || !endsWithBraces || !tk.isEmpty(token)) {
47         addLevel(token, level);
48       }
49     }
50     token = token.next;
51   }
52 }
53
54
55 function isClosingBrace(token) {
56   var val = token.value;
57   return val === ')' || val === '}' || val === ']';
58 }
59
60
61 exports.addLevel = addLevel;
62 function addLevel(token, level) {
63   if (!level) {
64     // zero is a noop
65     return;
66   }
67
68   token = findStartOfLine(token);
69
70   if (!token) {
71     // we never indent empty lines!
72     debug('[indent.addLevel] can\'t find start of line');
73     return;
74   }
75
76   var value = repeat(_opts.value, Math.abs(level));
77
78   if (tk.isIndent(token)) {
79     if (level > 0) {
80       // if it's already an Indent we just bump the value & level
81       token.value += value;
82       token.level += level;
83     } else {
84       if (token.level + level <= 0) {
85         tk.remove(token);
86       } else {
87         token.value = token.value.replace(value, '');
88         token.level += level;
89       }
90     }
91     if (token.next && token.next.type === 'BlockComment') {
92       updateBlockComment(token.next);
93     }
94     return;
95   }
96
97   if (level < 1) {
98     // we can't remove indent if previous token isn't an indent
99     debug(
100       '[addLevel] we can\'t decrement if line doesn\'t start with Indent. token: %s, level: %s',
101       token && token.value,
102       level
103     );
104     return;
105   }
106
107   if (tk.isWs(token)) {
108     // convert WhiteSpace token into Indent
109     token.type = 'Indent';
110     token.value = value;
111     token.level = level;
112     return;
113   }
114
115   // if regular token we add a new Indent before it
116   tk.before(token, {
117     type: 'Indent',
118     value: value,
119     level: level
120   });
121
122   if (token.type === 'BlockComment') {
123     updateBlockComment(token);
124   }
125 }
126
127 function findStartOfLine(token) {
128   if (tk.isBr(token) && tk.isBr(token.prev)) {
129     // empty lines are ignored
130     return null;
131   }
132   var prev = token.prev;
133   while (true) {
134     if (!prev || tk.isBr(prev)) {
135       return token;
136     }
137     token = prev;
138     prev = token.prev;
139   }
140 }
141
142
143 exports.sanitize = sanitize;
144 function sanitize(astOrNode) {
145   var token = astOrNode.startToken;
146   var end = astOrNode.endToken && astOrNode.endToken.next;
147   while (token && token !== end) {
148     var next = token.next;
149     if (isOriginalIndent(token)) {
150       tk.remove(token);
151     }
152     token = next;
153   }
154 }
155
156
157 function isOriginalIndent(token) {
158   // original indent don't have a "level" value
159   // we also need to remove any indent that happens after a token that
160   // isn't a line break (just in case these are added by mistake)
161   return (token.type === 'WhiteSpace' && (!token.prev || tk.isBr(token.prev)) && !tk.isBr(token.next)) ||
162     (token.type === 'Indent' && (token.level == null || !tk.isBr(token.prev)));
163 }
164
165
166 exports.updateBlockComment = updateBlockComment;
167 function updateBlockComment(comment) {
168   var orig = new RegExp('([\\n\\r]+)' + escapeRegExp(comment.originalIndent || ''), 'gm');
169   var update = comment.prev && comment.prev.type === 'Indent' ? comment.prev.value : '';
170   comment.raw = comment.raw.replace(orig, '$1' + update);
171   // override the originalIndent so multiple consecutive calls still work as
172   // expected
173   comment.originalIndent = update;
174 }
175
176
177 // comments are aligned based on the next line unless the line/block is
178 // followed by an empty line, in that case it will use the previous line as
179 // reference.
180 exports.alignComments = alignComments;
181 function alignComments(nodeOrAst) {
182   var first = nodeOrAst.startToken && nodeOrAst.startToken.prev;
183   var token = nodeOrAst.endToken;
184   while (token && token !== first) {
185     if (tk.isComment(token) && isFirstNonEmptyTokenOfLine(token)) {
186       var base = findReferenceIndent(token);
187       matchBaseIndent(token, base);
188
189       // if inside an empty block we add indent otherwise it looks weird
190       var change = _opts.CommentInsideEmptyBlock != null ?
191         _opts.CommentInsideEmptyBlock : 1;
192       if (change && isInsideEmptyBlock(token)) {
193         addLevel(token, change);
194       }
195
196       if (token.type === 'BlockComment') {
197         updateBlockComment(token);
198       }
199     }
200
201     token = token.prev;
202   }
203 }
204
205 function matchBaseIndent(token, base) {
206   if (!base) {
207     if (isIndentOrWs(token.prev)) {
208       tk.remove(token.prev);
209     }
210     return;
211   }
212
213   if (isIndentOrWs(token.prev)) {
214     // we reuse whitespace just because user might not have converted all
215     // the whitespaces into Indent tokens
216     token.prev.type = 'Indent';
217     token.prev.value = base.value;
218     token.prev.level = inferLevel(base, _opts.value);
219     return;
220   }
221
222   tk.before(token, {
223     type: 'Indent',
224     value: base.value,
225     level: inferLevel(base, _opts.value)
226   });
227 }
228
229 function isFirstNonEmptyTokenOfLine(token) {
230   if (!token.prev || tk.isBr(token.prev)) return true;
231   var prev = tk.findPrevNonEmpty(token);
232   return !prev ? true : tk.findInBetween(prev, token, tk.isBr);
233 }
234
235 function findReferenceIndent(start) {
236   var prevLine = findPrevReference(start);
237   var nextLine = findNextReference(start);
238   if (isAtBeginingOfBlock(start)) {
239     // this handles an edge case of comment just after "{" followed by an empty
240     // line (would use the previous line as reference by mistake)
241     while (nextLine && tk.isBr(nextLine)) {
242       nextLine = findNextReference(nextLine.prev);
243     }
244   }
245   // we favor nextLine unless it's empty
246   if (tk.isBr(nextLine) || !nextLine) {
247     return isIndentOrWs(prevLine) ? prevLine : null;
248   }
249   return isIndentOrWs(nextLine) ? nextLine : null;
250 }
251
252 function findPrevReference(start) {
253   var token = start.prev;
254   var changedLine = false;
255   while (token) {
256     // multiple consecutive comments should use the same reference (consider as
257     // a single block)
258     if (changedLine && tk.isBr(token) && !tk.isBr(token.next) && nextInLineNotComment(token)) {
259       return token.next;
260     }
261     if (tk.isBr(token)) {
262       changedLine = true;
263     }
264     token = token.prev;
265   }
266 }
267
268 function findNextReference(start) {
269   var token = start.next;
270   while (token) {
271     // multiple consecutive comments should use the same reference (consider as
272     // a single block)
273     if (tk.isBr(token) && nextInLineNotComment(token)) {
274       return token.next;
275     }
276     token = token.next;
277   }
278 }
279
280 function isIndentOrWs(token) {
281   return tk.isIndent(token) || tk.isWs(token);
282 }
283
284 function nextInLineNotComment(token) {
285   token = token.next;
286   while (token) {
287     if (tk.isBr(token)) {
288       return true;
289     }
290     if (!tk.isEmpty(token)) {
291       return !tk.isComment(token);
292     }
293     token = token.next;
294   }
295   return true;
296 }
297
298 function isAtBeginingOfBlock(token) {
299   var open = tk.findPrev(token, tk.isCode);
300   if (!open) return false;
301   var a = open.value;
302   return a === '(' || a === '[' || a === '{';
303 }
304
305 function isAtEndOfBlock(token) {
306   var close = tk.findNext(token, tk.isCode);
307   if (!close) return false;
308   var z = close.value;
309   return (z === ')' || z === ']' || z === '}');
310 }
311
312 function isInsideEmptyBlock(token) {
313   return isAtEndOfBlock(token) && isAtBeginingOfBlock(token);
314 }
315
316 exports.whiteSpaceToIndent = whiteSpaceToIndent;
317 function whiteSpaceToIndent(token, indentValue) {
318   if (tk.isWs(token) && (tk.isBr(token.prev) || !token.prev)) {
319     token.type = 'Indent';
320     // we can't add level if we don't know original indentValue
321     indentValue = indentValue || _opts.value;
322     if (indentValue) {
323       token.level = inferLevel(token, indentValue);
324     }
325   }
326 }
327
328 function inferLevel(token, indentValue) {
329   return Math.max(token.value.split(indentValue).length - 1, 0);
330 }