.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / stylelint / lib / rules / indentation / index.js
1 "use strict";
2
3 const _ = require("lodash");
4 const beforeBlockString = require("../../utils/beforeBlockString");
5 const hasBlock = require("../../utils/hasBlock");
6 const optionsMatches = require("../../utils/optionsMatches");
7 const report = require("../../utils/report");
8 const ruleMessages = require("../../utils/ruleMessages");
9 const styleSearch = require("style-search");
10 const validateOptions = require("../../utils/validateOptions");
11
12 const ruleName = "indentation";
13 const messages = ruleMessages(ruleName, {
14   expected: x => `Expected indentation of ${x}`
15 });
16
17 /**
18  * @param {number|"tab"} space - Number of whitespaces to expect, or else
19  *   keyword "tab" for single `\t`
20  * @param {object} [options]
21  */
22 const rule = function(space, options, context) {
23   options = options || {};
24
25   const isTab = space === "tab";
26   const indentChar = isTab ? "\t" : _.repeat(" ", space);
27   const warningWord = isTab ? "tab" : "space";
28
29   return (root, result) => {
30     const validOptions = validateOptions(
31       result,
32       ruleName,
33       {
34         actual: space,
35         possible: [_.isNumber, "tab"]
36       },
37       {
38         actual: options,
39         possible: {
40           except: ["block", "value", "param"],
41           ignore: ["value", "param", "inside-parens"],
42           indentInsideParens: ["twice", "once-at-root-twice-in-block"],
43           indentClosingBrace: [_.isBoolean]
44         },
45         optional: true
46       }
47     );
48     if (!validOptions) {
49       return;
50     }
51
52     // Cycle through all nodes using walk.
53     root.walk(node => {
54       const nodeLevel = indentationLevel(node);
55       const expectedWhitespace = _.repeat(indentChar, nodeLevel);
56
57       let before = node.raws.before || "";
58       const after = node.raws.after || "";
59
60       // Only inspect the spaces before the node
61       // if this is the first node in root
62       // or there is a newline in the `before` string.
63       // (If there is no newline before a node,
64       // there is no "indentation" to check.)
65       const inspectBefore =
66         node.root().first === node || before.indexOf("\n") !== -1;
67
68       // Cut out any * and _ hacks from `before`
69       if (
70         before[before.length - 1] === "*" ||
71         before[before.length - 1] === "_"
72       ) {
73         before = before.slice(0, before.length - 1);
74       }
75
76       // Inspect whitespace in the `before` string that is
77       // *after* the *last* newline character,
78       // because anything besides that is not indentation for this node:
79       // it is some other kind of separation, checked by some separate rule
80       if (
81         inspectBefore &&
82         before.slice(before.lastIndexOf("\n") + 1) !== expectedWhitespace
83       ) {
84         if (context.fix) {
85           node.raws.before = fixIndentation(
86             node.raws.before,
87             expectedWhitespace
88           );
89         } else {
90           report({
91             message: messages.expected(legibleExpectation(nodeLevel)),
92             node,
93             result,
94             ruleName
95           });
96         }
97       }
98
99       // Only blocks have the `after` string to check.
100       // Only inspect `after` strings that start with a newline;
101       // otherwise there's no indentation involved.
102       // And check `indentClosingBrace` to see if it should be indented an extra level.
103       const closingBraceLevel = options.indentClosingBrace
104         ? nodeLevel + 1
105         : nodeLevel;
106       const expectedClosingBraceIndentation = _.repeat(
107         indentChar,
108         closingBraceLevel
109       );
110       if (
111         hasBlock(node) &&
112         after &&
113         after.indexOf("\n") !== -1 &&
114         after.slice(after.lastIndexOf("\n") + 1) !==
115           expectedClosingBraceIndentation
116       ) {
117         if (context.fix) {
118           node.raws.after = fixIndentation(
119             node.raws.after,
120             expectedClosingBraceIndentation
121           );
122         } else {
123           report({
124             message: messages.expected(legibleExpectation(closingBraceLevel)),
125             node,
126             index: node.toString().length - 1,
127             result,
128             ruleName
129           });
130         }
131       }
132
133       // If this is a declaration, check the value
134       if (node.value) {
135         checkValue(node, nodeLevel);
136       }
137
138       // If this is a rule, check the selector
139       if (node.selector) {
140         checkSelector(node, nodeLevel);
141       }
142
143       // If this is an at rule, check the params
144       if (node.type === "atrule") {
145         checkAtRuleParams(node, nodeLevel);
146       }
147     });
148
149     function indentationLevel(node, level) {
150       level = level || 0;
151
152       if (node.parent.type === "root") {
153         return level + getRootBaseIndentLevel(node.parent);
154       }
155
156       let calculatedLevel;
157
158       // Indentation level equals the ancestor nodes
159       // separating this node from root; so recursively
160       // run this operation
161       calculatedLevel = indentationLevel(node.parent, level + 1);
162
163       // If options.except includes "block",
164       // blocks are taken down one from their calculated level
165       // (all blocks are the same level as their parents)
166       if (
167         optionsMatches(options, "except", "block") &&
168         (node.type === "rule" || node.type === "atrule") &&
169         hasBlock(node)
170       ) {
171         calculatedLevel--;
172       }
173
174       return calculatedLevel;
175     }
176
177     function checkValue(decl, declLevel) {
178       if (decl.value.indexOf("\n") === -1) {
179         return;
180       }
181       if (optionsMatches(options, "ignore", "value")) {
182         return;
183       }
184
185       const declString = decl.toString();
186       const valueLevel = optionsMatches(options, "except", "value")
187         ? declLevel
188         : declLevel + 1;
189
190       checkMultilineBit(declString, valueLevel, decl);
191     }
192
193     function checkSelector(rule, ruleLevel) {
194       const selector = rule.selector;
195
196       // Less mixins have params, and they should be indented extra
197       if (rule.params) {
198         ruleLevel += 1;
199       }
200
201       checkMultilineBit(selector, ruleLevel, rule);
202     }
203
204     function checkAtRuleParams(atRule, ruleLevel) {
205       if (optionsMatches(options, "ignore", "param")) {
206         return;
207       }
208
209       // @nest rules should be treated like regular rules, not expected
210       // to have their params (selectors) indented
211       const paramLevel =
212         optionsMatches(options, "except", "param") || atRule.name === "nest"
213           ? ruleLevel
214           : ruleLevel + 1;
215
216       checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
217     }
218
219     function checkMultilineBit(source, newlineIndentLevel, node) {
220       if (source.indexOf("\n") === -1) {
221         return;
222       }
223
224       // Data for current node fixing
225       const fixPositions = [];
226
227       // `outsideParens` because function arguments and also non-standard parenthesized stuff like
228       // Sass maps are ignored to allow for arbitrary indentation
229       let parentheticalDepth = 0;
230
231       styleSearch(
232         {
233           source,
234           target: "\n",
235           outsideParens: optionsMatches(options, "ignore", "inside-parens")
236         },
237         (match, matchCount) => {
238           const precedesClosingParenthesis = /^[ \t]*\)/.test(
239             source.slice(match.startIndex + 1)
240           );
241
242           if (
243             optionsMatches(options, "ignore", "inside-parens") &&
244             (precedesClosingParenthesis || match.insideParens)
245           ) {
246             return;
247           }
248
249           let expectedIndentLevel = newlineIndentLevel;
250
251           // Modififications for parenthetical content
252           if (
253             !optionsMatches(options, "ignore", "inside-parens") &&
254             match.insideParens
255           ) {
256             // If the first match in is within parentheses, reduce the parenthesis penalty
257             if (matchCount === 1) parentheticalDepth -= 1;
258
259             // Account for windows line endings
260             let newlineIndex = match.startIndex;
261             if (source[match.startIndex - 1] === "\r") {
262               newlineIndex--;
263             }
264
265             const followsOpeningParenthesis = /\([ \t]*$/.test(
266               source.slice(0, newlineIndex)
267             );
268             if (followsOpeningParenthesis) {
269               parentheticalDepth += 1;
270             }
271
272             const followsOpeningBrace = /\{[ \t]*$/.test(
273               source.slice(0, newlineIndex)
274             );
275             if (followsOpeningBrace) {
276               parentheticalDepth += 1;
277             }
278
279             const startingClosingBrace = /^[ \t]*}/.test(
280               source.slice(match.startIndex + 1)
281             );
282             if (startingClosingBrace) {
283               parentheticalDepth -= 1;
284             }
285
286             expectedIndentLevel += parentheticalDepth;
287
288             // Past this point, adjustments to parentheticalDepth affect next line
289
290             if (precedesClosingParenthesis) {
291               parentheticalDepth -= 1;
292             }
293
294             switch (options.indentInsideParens) {
295               case "twice":
296                 if (!precedesClosingParenthesis || options.indentClosingBrace) {
297                   expectedIndentLevel += 1;
298                 }
299                 break;
300               case "once-at-root-twice-in-block":
301                 if (node.parent === node.root()) {
302                   if (
303                     precedesClosingParenthesis &&
304                     !options.indentClosingBrace
305                   ) {
306                     expectedIndentLevel -= 1;
307                   }
308                   break;
309                 }
310                 if (!precedesClosingParenthesis || options.indentClosingBrace) {
311                   expectedIndentLevel += 1;
312                 }
313                 break;
314               default:
315                 if (precedesClosingParenthesis && !options.indentClosingBrace) {
316                   expectedIndentLevel -= 1;
317                 }
318             }
319           }
320
321           // Starting at the index after the newline, we want to
322           // check that the whitespace characters (excluding newlines) before the first
323           // non-whitespace character equal the expected indentation
324           const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(
325             source.slice(match.startIndex + 1)
326           );
327
328           if (!afterNewlineSpaceMatches) {
329             return;
330           }
331
332           const afterNewlineSpace = afterNewlineSpaceMatches[1];
333           const expectedIndentation = _.repeat(indentChar, expectedIndentLevel);
334
335           if (afterNewlineSpace !== expectedIndentation) {
336             if (context.fix) {
337               // Adding fixes position in reverse order, because if we change indent in the beginning of the string it will break all following fixes for that string
338               fixPositions.unshift({
339                 expectedIndentation,
340                 currentIndentation: afterNewlineSpace,
341                 startIndex: match.startIndex
342               });
343             } else {
344               report({
345                 message: messages.expected(
346                   legibleExpectation(expectedIndentLevel)
347                 ),
348                 node,
349                 index: match.startIndex + afterNewlineSpace.length + 1,
350                 result,
351                 ruleName
352               });
353             }
354           }
355         }
356       );
357
358       if (fixPositions.length) {
359         if (node.type === "rule") {
360           fixPositions.forEach(function(fixPosition) {
361             node.selector = replaceIndentation(
362               node.selector,
363               fixPosition.currentIndentation,
364               fixPosition.expectedIndentation,
365               fixPosition.startIndex
366             );
367           });
368         }
369
370         if (node.type === "decl") {
371           const declProp = node.prop;
372           const declBetween = node.raws.between;
373
374           fixPositions.forEach(function(fixPosition) {
375             if (fixPosition.startIndex < declProp.length + declBetween.length) {
376               node.raws.between = replaceIndentation(
377                 declBetween,
378                 fixPosition.currentIndentation,
379                 fixPosition.expectedIndentation,
380                 fixPosition.startIndex - declProp.length
381               );
382             } else {
383               node.value = replaceIndentation(
384                 node.value,
385                 fixPosition.currentIndentation,
386                 fixPosition.expectedIndentation,
387                 fixPosition.startIndex - declProp.length - declBetween.length
388               );
389             }
390           });
391         }
392
393         if (node.type === "atrule") {
394           const atRuleName = node.name;
395           const atRuleAfterName = node.raws.afterName;
396           const atRuleParams = node.params;
397
398           fixPositions.forEach(function(fixPosition) {
399             // 1 — it's a @ length
400             if (
401               fixPosition.startIndex <
402               1 + atRuleName.length + atRuleAfterName.length
403             ) {
404               node.raws.afterName = replaceIndentation(
405                 atRuleAfterName,
406                 fixPosition.currentIndentation,
407                 fixPosition.expectedIndentation,
408                 fixPosition.startIndex - atRuleName.length - 1
409               );
410             } else {
411               node.params = replaceIndentation(
412                 atRuleParams,
413                 fixPosition.currentIndentation,
414                 fixPosition.expectedIndentation,
415                 fixPosition.startIndex -
416                   atRuleName.length -
417                   atRuleAfterName.length -
418                   1
419               );
420             }
421           });
422         }
423       }
424     }
425
426     function getRootBaseIndentLevel(node) {
427       if (node === root) {
428         return 0;
429       }
430       if (isNaN(node.source.baseIndentLevel)) {
431         node.source.baseIndentLevel = getBaseIndentLevel(node, root, space);
432       }
433       return node.source.baseIndentLevel;
434     }
435   };
436
437   function legibleExpectation(level) {
438     const count = isTab ? level : level * space;
439     const quantifiedWarningWord = count === 1 ? warningWord : warningWord + "s";
440
441     return `${count} ${quantifiedWarningWord}`;
442   }
443 };
444
445 function getBaseIndentLength(indents) {
446   return Math.min.apply(Math, indents.map(indent => indent.length));
447 }
448
449 function getSoftIndentLevel(softIndentCount, space) {
450   return Math.round(softIndentCount / (parseInt(space) || 2));
451 }
452
453 function getBaseIndentLevel(node, root, space) {
454   const code = node.source.input.css;
455   if (/^\S+/m.test(code)) {
456     return 0;
457   }
458
459   const softIndents = code.match(/^ +(?=\S+)/gm);
460   const hardIndents = code.match(/^\t+(?=\S+)/gm);
461   let softIndentCount = softIndents ? softIndents.length : 0;
462   let hardIndentCount = hardIndents ? hardIndents.length : 0;
463
464   if (softIndentCount > hardIndentCount) {
465     return getSoftIndentLevel(getBaseIndentLength(softIndents), space);
466   } else if (hardIndentCount) {
467     return getBaseIndentLength(hardIndents);
468   }
469
470   let afterEnd = root.nodes[root.nodes.indexOf(node) + 1];
471   if (afterEnd) {
472     afterEnd = afterEnd.raws.beforeStart;
473   } else {
474     afterEnd = root.raws.afterEnd;
475   }
476   hardIndentCount = 0;
477   softIndentCount = afterEnd.match(/^\s*/)[0].replace(/\t/g, () => {
478     hardIndentCount++;
479     return "";
480   }).length;
481
482   return hardIndentCount + getSoftIndentLevel(softIndentCount, space);
483 }
484
485 function fixIndentation(str, whitespace) {
486   if (!_.isString(str)) {
487     return str;
488   }
489
490   const strLength = str.length;
491
492   if (!strLength) {
493     return str;
494   }
495
496   let stringEnd = str[strLength - 1];
497
498   if (stringEnd !== "*" && stringEnd !== "_") {
499     stringEnd = "";
500   }
501
502   const stringStart = str.slice(0, str.lastIndexOf("\n") + 1);
503
504   return stringStart + whitespace + stringEnd;
505 }
506
507 function replaceIndentation(input, searchString, replaceString, startIndex) {
508   const offset = startIndex + 1;
509   const stringStart = input.slice(0, offset);
510   const stringEnd = input.slice(offset + searchString.length);
511
512   return stringStart + replaceString + stringEnd;
513 }
514
515 rule.ruleName = ruleName;
516 rule.messages = messages;
517 module.exports = rule;