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 |
|
19 |
|
20 |
|
21 |
|
22 | function rule(space, options = {}, context) {
|
23 | const isTab = space === 'tab';
|
24 | const indentChar = isTab ? '\t' : ' '.repeat(space);
|
25 | const warningWord = isTab ? 'tab' : 'space';
|
26 |
|
27 | return (root, result) => {
|
28 | const validOptions = validateOptions(
|
29 | result,
|
30 | ruleName,
|
31 | {
|
32 | actual: space,
|
33 | possible: [_.isNumber, 'tab'],
|
34 | },
|
35 | {
|
36 | actual: options,
|
37 | possible: {
|
38 | baseIndentLevel: [_.isNumber, 'auto'],
|
39 | except: ['block', 'value', 'param'],
|
40 | ignore: ['value', 'param', 'inside-parens'],
|
41 | indentInsideParens: ['twice', 'once-at-root-twice-in-block'],
|
42 | indentClosingBrace: [_.isBoolean],
|
43 | },
|
44 | optional: true,
|
45 | },
|
46 | );
|
47 |
|
48 | if (!validOptions) {
|
49 | return;
|
50 | }
|
51 |
|
52 |
|
53 | root.walk((node) => {
|
54 | if (node.type === 'root') {
|
55 |
|
56 | return;
|
57 | }
|
58 |
|
59 | const nodeLevel = indentationLevel(node);
|
60 |
|
61 |
|
62 | const before = (node.raws.before || '').replace(/[*_]$/, '');
|
63 | const after = node.raws.after || '';
|
64 | const parent = node.parent;
|
65 |
|
66 | const expectedOpeningBraceIndentation = indentChar.repeat(nodeLevel);
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 | const isFirstChild = parent.type === 'root' && parent.first === node;
|
74 | const lastIndexOfNewline = before.lastIndexOf('\n');
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | if (
|
81 | (lastIndexOfNewline !== -1 ||
|
82 | (isFirstChild && (!getDocument(parent) || parent.raws.beforeStart.endsWith('\n')))) &&
|
83 | before.slice(lastIndexOfNewline + 1) !== expectedOpeningBraceIndentation
|
84 | ) {
|
85 | if (context.fix) {
|
86 | if (isFirstChild && _.isString(node.raws.before)) {
|
87 | node.raws.before = node.raws.before.replace(
|
88 | /^[ \t]*(?=\S|$)/,
|
89 | expectedOpeningBraceIndentation,
|
90 | );
|
91 | }
|
92 |
|
93 | node.raws.before = fixIndentation(node.raws.before, expectedOpeningBraceIndentation);
|
94 | } else {
|
95 | report({
|
96 | message: messages.expected(legibleExpectation(nodeLevel)),
|
97 | node,
|
98 | result,
|
99 | ruleName,
|
100 | });
|
101 | }
|
102 | }
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | const closingBraceLevel = options.indentClosingBrace ? nodeLevel + 1 : nodeLevel;
|
109 | const expectedClosingBraceIndentation = indentChar.repeat(closingBraceLevel);
|
110 |
|
111 | if (
|
112 | hasBlock(node) &&
|
113 | after &&
|
114 | after.includes('\n') &&
|
115 | after.slice(after.lastIndexOf('\n') + 1) !== expectedClosingBraceIndentation
|
116 | ) {
|
117 | if (context.fix) {
|
118 | node.raws.after = fixIndentation(node.raws.after, expectedClosingBraceIndentation);
|
119 | } else {
|
120 | report({
|
121 | message: messages.expected(legibleExpectation(closingBraceLevel)),
|
122 | node,
|
123 | index: node.toString().length - 1,
|
124 | result,
|
125 | ruleName,
|
126 | });
|
127 | }
|
128 | }
|
129 |
|
130 |
|
131 | if (node.value) {
|
132 | checkValue(node, nodeLevel);
|
133 | }
|
134 |
|
135 |
|
136 | if (node.selector) {
|
137 | checkSelector(node, nodeLevel);
|
138 | }
|
139 |
|
140 |
|
141 | if (node.type === 'atrule') {
|
142 | checkAtRuleParams(node, nodeLevel);
|
143 | }
|
144 | });
|
145 |
|
146 | function indentationLevel(node, level = 0) {
|
147 | if (node.parent.type === 'root') {
|
148 | return level + getRootBaseIndentLevel(node.parent, options.baseIndentLevel, space);
|
149 | }
|
150 |
|
151 | let calculatedLevel;
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | calculatedLevel = indentationLevel(node.parent, level + 1);
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | if (
|
162 | optionsMatches(options, 'except', 'block') &&
|
163 | (node.type === 'rule' || node.type === 'atrule') &&
|
164 | hasBlock(node)
|
165 | ) {
|
166 | calculatedLevel--;
|
167 | }
|
168 |
|
169 | return calculatedLevel;
|
170 | }
|
171 |
|
172 | function checkValue(decl, declLevel) {
|
173 | if (!decl.value.includes('\n')) {
|
174 | return;
|
175 | }
|
176 |
|
177 | if (optionsMatches(options, 'ignore', 'value')) {
|
178 | return;
|
179 | }
|
180 |
|
181 | const declString = decl.toString();
|
182 | const valueLevel = optionsMatches(options, 'except', 'value') ? declLevel : declLevel + 1;
|
183 |
|
184 | checkMultilineBit(declString, valueLevel, decl);
|
185 | }
|
186 |
|
187 | function checkSelector(rule, ruleLevel) {
|
188 | const selector = rule.selector;
|
189 |
|
190 |
|
191 | if (rule.params) {
|
192 | ruleLevel += 1;
|
193 | }
|
194 |
|
195 | checkMultilineBit(selector, ruleLevel, rule);
|
196 | }
|
197 |
|
198 | function checkAtRuleParams(atRule, ruleLevel) {
|
199 | if (optionsMatches(options, 'ignore', 'param')) {
|
200 | return;
|
201 | }
|
202 |
|
203 |
|
204 |
|
205 | const paramLevel =
|
206 | optionsMatches(options, 'except', 'param') ||
|
207 | atRule.name === 'nest' ||
|
208 | atRule.name === 'at-root'
|
209 | ? ruleLevel
|
210 | : ruleLevel + 1;
|
211 |
|
212 | checkMultilineBit(beforeBlockString(atRule).trim(), paramLevel, atRule);
|
213 | }
|
214 |
|
215 | function checkMultilineBit(source, newlineIndentLevel, node) {
|
216 | if (!source.includes('\n')) {
|
217 | return;
|
218 | }
|
219 |
|
220 |
|
221 | const fixPositions = [];
|
222 |
|
223 |
|
224 |
|
225 | let parentheticalDepth = 0;
|
226 |
|
227 | styleSearch(
|
228 | {
|
229 | source,
|
230 | target: '\n',
|
231 | outsideParens: optionsMatches(options, 'ignore', 'inside-parens'),
|
232 | },
|
233 | (match, matchCount) => {
|
234 | const precedesClosingParenthesis = /^[ \t]*\)/.test(source.slice(match.startIndex + 1));
|
235 |
|
236 | if (
|
237 | optionsMatches(options, 'ignore', 'inside-parens') &&
|
238 | (precedesClosingParenthesis || match.insideParens)
|
239 | ) {
|
240 | return;
|
241 | }
|
242 |
|
243 | let expectedIndentLevel = newlineIndentLevel;
|
244 |
|
245 |
|
246 | if (!optionsMatches(options, 'ignore', 'inside-parens') && match.insideParens) {
|
247 |
|
248 | if (matchCount === 1) parentheticalDepth -= 1;
|
249 |
|
250 |
|
251 | let newlineIndex = match.startIndex;
|
252 |
|
253 | if (source[match.startIndex - 1] === '\r') {
|
254 | newlineIndex--;
|
255 | }
|
256 |
|
257 | const followsOpeningParenthesis = /\([ \t]*$/.test(source.slice(0, newlineIndex));
|
258 |
|
259 | if (followsOpeningParenthesis) {
|
260 | parentheticalDepth += 1;
|
261 | }
|
262 |
|
263 | const followsOpeningBrace = /\{[ \t]*$/.test(source.slice(0, newlineIndex));
|
264 |
|
265 | if (followsOpeningBrace) {
|
266 | parentheticalDepth += 1;
|
267 | }
|
268 |
|
269 | const startingClosingBrace = /^[ \t]*}/.test(source.slice(match.startIndex + 1));
|
270 |
|
271 | if (startingClosingBrace) {
|
272 | parentheticalDepth -= 1;
|
273 | }
|
274 |
|
275 | expectedIndentLevel += parentheticalDepth;
|
276 |
|
277 |
|
278 |
|
279 | if (precedesClosingParenthesis) {
|
280 | parentheticalDepth -= 1;
|
281 | }
|
282 |
|
283 | switch (options.indentInsideParens) {
|
284 | case 'twice':
|
285 | if (!precedesClosingParenthesis || options.indentClosingBrace) {
|
286 | expectedIndentLevel += 1;
|
287 | }
|
288 |
|
289 | break;
|
290 | case 'once-at-root-twice-in-block':
|
291 | if (node.parent === node.root()) {
|
292 | if (precedesClosingParenthesis && !options.indentClosingBrace) {
|
293 | expectedIndentLevel -= 1;
|
294 | }
|
295 |
|
296 | break;
|
297 | }
|
298 |
|
299 | if (!precedesClosingParenthesis || options.indentClosingBrace) {
|
300 | expectedIndentLevel += 1;
|
301 | }
|
302 |
|
303 | break;
|
304 | default:
|
305 | if (precedesClosingParenthesis && !options.indentClosingBrace) {
|
306 | expectedIndentLevel -= 1;
|
307 | }
|
308 | }
|
309 | }
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | const afterNewlineSpaceMatches = /^([ \t]*)\S/.exec(source.slice(match.startIndex + 1));
|
315 |
|
316 | if (!afterNewlineSpaceMatches) {
|
317 | return;
|
318 | }
|
319 |
|
320 | const afterNewlineSpace = afterNewlineSpaceMatches[1];
|
321 | const expectedIndentation = indentChar.repeat(
|
322 | expectedIndentLevel > 0 ? expectedIndentLevel : 0,
|
323 | );
|
324 |
|
325 | if (afterNewlineSpace !== expectedIndentation) {
|
326 | if (context.fix) {
|
327 |
|
328 | fixPositions.unshift({
|
329 | expectedIndentation,
|
330 | currentIndentation: afterNewlineSpace,
|
331 | startIndex: match.startIndex,
|
332 | });
|
333 | } else {
|
334 | report({
|
335 | message: messages.expected(legibleExpectation(expectedIndentLevel)),
|
336 | node,
|
337 | index: match.startIndex + afterNewlineSpace.length + 1,
|
338 | result,
|
339 | ruleName,
|
340 | });
|
341 | }
|
342 | }
|
343 | },
|
344 | );
|
345 |
|
346 | if (fixPositions.length) {
|
347 | if (node.type === 'rule') {
|
348 | fixPositions.forEach((fixPosition) => {
|
349 | node.selector = replaceIndentation(
|
350 | node.selector,
|
351 | fixPosition.currentIndentation,
|
352 | fixPosition.expectedIndentation,
|
353 | fixPosition.startIndex,
|
354 | );
|
355 | });
|
356 | }
|
357 |
|
358 | if (node.type === 'decl') {
|
359 | const declProp = node.prop;
|
360 | const declBetween = node.raws.between;
|
361 |
|
362 | fixPositions.forEach((fixPosition) => {
|
363 | if (fixPosition.startIndex < declProp.length + declBetween.length) {
|
364 | node.raws.between = replaceIndentation(
|
365 | declBetween,
|
366 | fixPosition.currentIndentation,
|
367 | fixPosition.expectedIndentation,
|
368 | fixPosition.startIndex - declProp.length,
|
369 | );
|
370 | } else {
|
371 | node.value = replaceIndentation(
|
372 | node.value,
|
373 | fixPosition.currentIndentation,
|
374 | fixPosition.expectedIndentation,
|
375 | fixPosition.startIndex - declProp.length - declBetween.length,
|
376 | );
|
377 | }
|
378 | });
|
379 | }
|
380 |
|
381 | if (node.type === 'atrule') {
|
382 | const atRuleName = node.name;
|
383 | const atRuleAfterName = node.raws.afterName;
|
384 | const atRuleParams = node.params;
|
385 |
|
386 | fixPositions.forEach((fixPosition) => {
|
387 |
|
388 | if (fixPosition.startIndex < 1 + atRuleName.length + atRuleAfterName.length) {
|
389 | node.raws.afterName = replaceIndentation(
|
390 | atRuleAfterName,
|
391 | fixPosition.currentIndentation,
|
392 | fixPosition.expectedIndentation,
|
393 | fixPosition.startIndex - atRuleName.length - 1,
|
394 | );
|
395 | } else {
|
396 | node.params = replaceIndentation(
|
397 | atRuleParams,
|
398 | fixPosition.currentIndentation,
|
399 | fixPosition.expectedIndentation,
|
400 | fixPosition.startIndex - atRuleName.length - atRuleAfterName.length - 1,
|
401 | );
|
402 | }
|
403 | });
|
404 | }
|
405 | }
|
406 | }
|
407 | };
|
408 |
|
409 | function legibleExpectation(level) {
|
410 | const count = isTab ? level : level * space;
|
411 | const quantifiedWarningWord = count === 1 ? warningWord : `${warningWord}s`;
|
412 |
|
413 | return `${count} ${quantifiedWarningWord}`;
|
414 | }
|
415 | }
|
416 |
|
417 | function getRootBaseIndentLevel(root, baseIndentLevel, space) {
|
418 | const document = getDocument(root);
|
419 |
|
420 | if (!document) {
|
421 | return 0;
|
422 | }
|
423 |
|
424 | let indentLevel = root.source.baseIndentLevel;
|
425 |
|
426 | if (!Number.isSafeInteger(indentLevel)) {
|
427 | indentLevel = inferRootIndentLevel(root, baseIndentLevel, () =>
|
428 | inferDocIndentSize(document, space),
|
429 | );
|
430 | root.source.baseIndentLevel = indentLevel;
|
431 | }
|
432 |
|
433 | return indentLevel;
|
434 | }
|
435 |
|
436 | function getDocument(node) {
|
437 | const document = node.document;
|
438 |
|
439 | if (document) {
|
440 | return document;
|
441 | }
|
442 |
|
443 | const root = node.root();
|
444 |
|
445 | return root && root.document;
|
446 | }
|
447 |
|
448 | function inferDocIndentSize(document, space) {
|
449 | let indentSize = document.source.indentSize;
|
450 |
|
451 | if (Number.isSafeInteger(indentSize)) {
|
452 | return indentSize;
|
453 | }
|
454 |
|
455 | const source = document.source.input.css;
|
456 | const indents = source.match(/^ *(?=\S)/gm);
|
457 |
|
458 | if (indents) {
|
459 | const scores = {};
|
460 | let lastIndentSize = 0;
|
461 | let lastLeadingSpacesLength = 0;
|
462 | const vote = (leadingSpacesLength) => {
|
463 | if (leadingSpacesLength) {
|
464 | lastIndentSize = Math.abs(leadingSpacesLength - lastLeadingSpacesLength) || lastIndentSize;
|
465 |
|
466 | if (lastIndentSize > 1) {
|
467 | if (scores[lastIndentSize]) {
|
468 | scores[lastIndentSize]++;
|
469 | } else {
|
470 | scores[lastIndentSize] = 1;
|
471 | }
|
472 | }
|
473 | } else {
|
474 | lastIndentSize = 0;
|
475 | }
|
476 |
|
477 | lastLeadingSpacesLength = leadingSpacesLength;
|
478 | };
|
479 |
|
480 | indents.forEach((leadingSpaces) => {
|
481 | vote(leadingSpaces.length);
|
482 | });
|
483 |
|
484 | let bestScore = 0;
|
485 |
|
486 | for (const indentSizeDate in scores) {
|
487 | if (Object.prototype.hasOwnProperty.call(scores, indentSizeDate)) {
|
488 | const score = scores[indentSizeDate];
|
489 |
|
490 | if (score > bestScore) {
|
491 | bestScore = score;
|
492 | indentSize = indentSizeDate;
|
493 | }
|
494 | }
|
495 | }
|
496 | }
|
497 |
|
498 | indentSize = Number(indentSize) || (indents && indents[0].length) || Number(space) || 2;
|
499 | document.source.indentSize = indentSize;
|
500 |
|
501 | return indentSize;
|
502 | }
|
503 |
|
504 | function inferRootIndentLevel(root, baseIndentLevel, indentSize) {
|
505 | function getIndentLevel(indent) {
|
506 | let tabCount = indent.match(/\t/g);
|
507 |
|
508 | tabCount = tabCount ? tabCount.length : 0;
|
509 | let spaceCount = indent.match(/ /g);
|
510 |
|
511 | spaceCount = spaceCount ? Math.round(spaceCount.length / indentSize()) : 0;
|
512 |
|
513 | return tabCount + spaceCount;
|
514 | }
|
515 |
|
516 | if (!Number.isSafeInteger(baseIndentLevel)) {
|
517 | let source = root.source.input.css;
|
518 |
|
519 | source = source.replace(/^[^\r\n]+/, (firstLine) => {
|
520 | if (/(?:^|\n)([ \t]*)$/.test(root.raws.beforeStart)) {
|
521 | return RegExp.$1 + firstLine;
|
522 | }
|
523 |
|
524 | return '';
|
525 | });
|
526 |
|
527 | const indents = source.match(/^[ \t]*(?=\S)/gm);
|
528 |
|
529 | if (indents) {
|
530 | return Math.min(...indents.map(getIndentLevel));
|
531 | }
|
532 |
|
533 | baseIndentLevel = 1;
|
534 | }
|
535 |
|
536 | const indents = [];
|
537 | const foundIndents = /(?:^|\n)([ \t]*)\S[^\r\n]*(?:\r?\n\s*)*$/m.exec(root.raws.beforeStart);
|
538 |
|
539 |
|
540 | if (foundIndents) {
|
541 | let shortest = Number.MAX_SAFE_INTEGER;
|
542 | let i = 0;
|
543 |
|
544 | while (++i < foundIndents.length) {
|
545 | const current = getIndentLevel(foundIndents[i]);
|
546 |
|
547 | if (current < shortest) {
|
548 | shortest = current;
|
549 |
|
550 | if (shortest === 0) {
|
551 | break;
|
552 | }
|
553 | }
|
554 | }
|
555 |
|
556 | if (shortest !== Number.MAX_SAFE_INTEGER) {
|
557 | indents.push(new Array(shortest).fill(' ').join(''));
|
558 | }
|
559 | }
|
560 |
|
561 | const after = root.raws.after;
|
562 |
|
563 | if (after) {
|
564 | let afterEnd;
|
565 |
|
566 | if (after.endsWith('\n')) {
|
567 | const document = root.document;
|
568 |
|
569 | if (document) {
|
570 | const nextRoot = document.nodes[document.nodes.indexOf(root) + 1];
|
571 |
|
572 | if (nextRoot) {
|
573 | afterEnd = nextRoot.raws.beforeStart;
|
574 | } else {
|
575 | afterEnd = document.raws.afterEnd;
|
576 | }
|
577 | } else {
|
578 |
|
579 | const parent = root.parent;
|
580 |
|
581 | const nextRoot = parent.nodes[parent.nodes.indexOf(root) + 1];
|
582 |
|
583 | if (nextRoot) {
|
584 | afterEnd = nextRoot.raws.beforeStart;
|
585 | } else {
|
586 | afterEnd = root.raws.afterEnd;
|
587 | }
|
588 | }
|
589 | } else {
|
590 | afterEnd = after;
|
591 | }
|
592 |
|
593 | indents.push(afterEnd.match(/^[ \t]*/)[0]);
|
594 | }
|
595 |
|
596 | if (indents.length) {
|
597 | return Math.max(...indents.map(getIndentLevel)) + baseIndentLevel;
|
598 | }
|
599 |
|
600 | return baseIndentLevel;
|
601 | }
|
602 |
|
603 | function fixIndentation(str, whitespace) {
|
604 | if (!_.isString(str)) {
|
605 | return str;
|
606 | }
|
607 |
|
608 | return str.replace(/\n[ \t]*(?=\S|$)/g, `\n${whitespace}`);
|
609 | }
|
610 |
|
611 | function replaceIndentation(input, searchString, replaceString, startIndex) {
|
612 | const offset = startIndex + 1;
|
613 | const stringStart = input.slice(0, offset);
|
614 | const stringEnd = input.slice(offset + searchString.length);
|
615 |
|
616 | return stringStart + replaceString + stringEnd;
|
617 | }
|
618 |
|
619 | rule.ruleName = ruleName;
|
620 | rule.messages = messages;
|
621 | module.exports = rule;
|