UNPKG

3.23 kBJavaScriptView Raw
1'use strict';
2
3const addEmptyLineAfter = require('../../utils/addEmptyLineAfter');
4const blockString = require('../../utils/blockString');
5const hasBlock = require('../../utils/hasBlock');
6const hasEmptyBlock = require('../../utils/hasEmptyBlock');
7const hasEmptyLine = require('../../utils/hasEmptyLine');
8const isSingleLineString = require('../../utils/isSingleLineString');
9const optionsMatches = require('../../utils/optionsMatches');
10const removeEmptyLinesAfter = require('../../utils/removeEmptyLinesAfter');
11const report = require('../../utils/report');
12const ruleMessages = require('../../utils/ruleMessages');
13const validateOptions = require('../../utils/validateOptions');
14
15const ruleName = 'block-closing-brace-empty-line-before';
16
17const messages = ruleMessages(ruleName, {
18 expected: 'Expected empty line before closing brace',
19 rejected: 'Unexpected empty line before closing brace',
20});
21
22/** @type {import('stylelint').Rule} */
23const rule = (primary, secondaryOptions, context) => {
24 return (root, result) => {
25 const validOptions = validateOptions(
26 result,
27 ruleName,
28 {
29 actual: primary,
30 possible: ['always-multi-line', 'never'],
31 },
32 {
33 actual: secondaryOptions,
34 possible: {
35 except: ['after-closing-brace'],
36 },
37 optional: true,
38 },
39 );
40
41 if (!validOptions) {
42 return;
43 }
44
45 // Check both kinds of statements: rules and at-rules
46 root.walkRules(check);
47 root.walkAtRules(check);
48
49 /**
50 * @param {import('postcss').Rule | import('postcss').AtRule} statement
51 */
52 function check(statement) {
53 // Return early if blockless or has empty block
54 if (!hasBlock(statement) || hasEmptyBlock(statement)) {
55 return;
56 }
57
58 // Get whitespace after ""}", ignoring extra semicolon
59 const before = (statement.raws.after || '').replace(/;+/, '');
60
61 // Calculate index
62 const statementString = statement.toString();
63 let index = statementString.length - 1;
64
65 if (statementString[index - 1] === '\r') {
66 index -= 1;
67 }
68
69 // Set expectation
70 const expectEmptyLineBefore = (() => {
71 const childNodeTypes = statement.nodes.map((item) => item.type);
72
73 // Reverse the primary options if `after-closing-brace` is set
74 if (
75 optionsMatches(secondaryOptions, 'except', 'after-closing-brace') &&
76 statement.type === 'atrule' &&
77 !childNodeTypes.includes('decl')
78 ) {
79 return primary === 'never';
80 }
81
82 return primary === 'always-multi-line' && !isSingleLineString(blockString(statement));
83 })();
84
85 // Check for at least one empty line
86 const hasEmptyLineBefore = hasEmptyLine(before);
87
88 // Return if the expectation is met
89 if (expectEmptyLineBefore === hasEmptyLineBefore) {
90 return;
91 }
92
93 if (context.fix) {
94 const { newline } = context;
95
96 if (typeof newline !== 'string') return;
97
98 if (expectEmptyLineBefore) {
99 addEmptyLineAfter(statement, newline);
100 } else {
101 removeEmptyLinesAfter(statement, newline);
102 }
103
104 return;
105 }
106
107 const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
108
109 report({
110 message,
111 result,
112 ruleName,
113 node: statement,
114 index,
115 });
116 }
117 };
118};
119
120rule.ruleName = ruleName;
121rule.messages = messages;
122module.exports = rule;