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');
13 CommentInsideEmptyBlock: 1
19 exports.setOptions = function(opts) {
24 exports.inBetween = indentInBetween;
25 function indentInBetween(startToken, endToken, level) {
26 level = level != null ? level : 1;
28 if (!level || (!startToken || !endToken) || startToken === endToken) {
30 '[inBetween] not going to indent. start: %s, end: %s, level: %s',
31 startToken && startToken.value,
32 endToken && endToken.value,
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);
55 function isClosingBrace(token) {
56 var val = token.value;
57 return val === ')' || val === '}' || val === ']';
61 exports.addLevel = addLevel;
62 function addLevel(token, level) {
68 token = findStartOfLine(token);
71 // we never indent empty lines!
72 debug('[indent.addLevel] can\'t find start of line');
76 var value = repeat(_opts.value, Math.abs(level));
78 if (tk.isIndent(token)) {
80 // if it's already an Indent we just bump the value & level
84 if (token.level + level <= 0) {
87 token.value = token.value.replace(value, '');
91 if (token.next && token.next.type === 'BlockComment') {
92 updateBlockComment(token.next);
98 // we can't remove indent if previous token isn't an indent
100 '[addLevel] we can\'t decrement if line doesn\'t start with Indent. token: %s, level: %s',
101 token && token.value,
107 if (tk.isWs(token)) {
108 // convert WhiteSpace token into Indent
109 token.type = 'Indent';
115 // if regular token we add a new Indent before it
122 if (token.type === 'BlockComment') {
123 updateBlockComment(token);
127 function findStartOfLine(token) {
128 if (tk.isBr(token) && tk.isBr(token.prev)) {
129 // empty lines are ignored
132 var prev = token.prev;
134 if (!prev || tk.isBr(prev)) {
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)) {
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)));
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
173 comment.originalIndent = update;
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
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);
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);
196 if (token.type === 'BlockComment') {
197 updateBlockComment(token);
205 function matchBaseIndent(token, base) {
207 if (isIndentOrWs(token.prev)) {
208 tk.remove(token.prev);
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);
225 level: inferLevel(base, _opts.value)
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);
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);
245 // we favor nextLine unless it's empty
246 if (tk.isBr(nextLine) || !nextLine) {
247 return isIndentOrWs(prevLine) ? prevLine : null;
249 return isIndentOrWs(nextLine) ? nextLine : null;
252 function findPrevReference(start) {
253 var token = start.prev;
254 var changedLine = false;
256 // multiple consecutive comments should use the same reference (consider as
258 if (changedLine && tk.isBr(token) && !tk.isBr(token.next) && nextInLineNotComment(token)) {
261 if (tk.isBr(token)) {
268 function findNextReference(start) {
269 var token = start.next;
271 // multiple consecutive comments should use the same reference (consider as
273 if (tk.isBr(token) && nextInLineNotComment(token)) {
280 function isIndentOrWs(token) {
281 return tk.isIndent(token) || tk.isWs(token);
284 function nextInLineNotComment(token) {
287 if (tk.isBr(token)) {
290 if (!tk.isEmpty(token)) {
291 return !tk.isComment(token);
298 function isAtBeginingOfBlock(token) {
299 var open = tk.findPrev(token, tk.isCode);
300 if (!open) return false;
302 return a === '(' || a === '[' || a === '{';
305 function isAtEndOfBlock(token) {
306 var close = tk.findNext(token, tk.isCode);
307 if (!close) return false;
309 return (z === ')' || z === ']' || z === '}');
312 function isInsideEmptyBlock(token) {
313 return isAtEndOfBlock(token) && isAtBeginingOfBlock(token);
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;
323 token.level = inferLevel(token, indentValue);
328 function inferLevel(token, indentValue) {
329 return Math.max(token.value.split(indentValue).length - 1, 0);