UNPKG

4.9 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const addEmptyLineBefore = require('../../utils/addEmptyLineBefore');
5const getPreviousNonSharedLineCommentNode = require('../../utils/getPreviousNonSharedLineCommentNode');
6const hasEmptyLine = require('../../utils/hasEmptyLine');
7const isAfterComment = require('../../utils/isAfterComment');
8const isBlocklessAtRuleAfterBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterBlocklessAtRule');
9const isBlocklessAtRuleAfterSameNameBlocklessAtRule = require('../../utils/isBlocklessAtRuleAfterSameNameBlocklessAtRule');
10const isFirstNested = require('../../utils/isFirstNested');
11const isFirstNodeOfRoot = require('../../utils/isFirstNodeOfRoot');
12const isStandardSyntaxAtRule = require('../../utils/isStandardSyntaxAtRule');
13const optionsMatches = require('../../utils/optionsMatches');
14const removeEmptyLinesBefore = require('../../utils/removeEmptyLinesBefore');
15const report = require('../../utils/report');
16const ruleMessages = require('../../utils/ruleMessages');
17const validateOptions = require('../../utils/validateOptions');
18
19const ruleName = 'at-rule-empty-line-before';
20
21const messages = ruleMessages(ruleName, {
22 expected: 'Expected empty line before at-rule',
23 rejected: 'Unexpected empty line before at-rule',
24});
25
26function rule(expectation, options, context) {
27 return (root, result) => {
28 const validOptions = validateOptions(
29 result,
30 ruleName,
31 {
32 actual: expectation,
33 possible: ['always', 'never'],
34 },
35 {
36 actual: options,
37 possible: {
38 except: [
39 'after-same-name',
40 'inside-block',
41 'blockless-after-same-name-blockless',
42 'blockless-after-blockless',
43 'first-nested',
44 ],
45 ignore: [
46 'after-comment',
47 'first-nested',
48 'inside-block',
49 'blockless-after-same-name-blockless',
50 'blockless-after-blockless',
51 ],
52 ignoreAtRules: [_.isString],
53 },
54 optional: true,
55 },
56 );
57
58 if (!validOptions) {
59 return;
60 }
61
62 root.walkAtRules((atRule) => {
63 const isNested = atRule.parent.type !== 'root';
64
65 // Ignore the first node
66 if (isFirstNodeOfRoot(atRule)) {
67 return;
68 }
69
70 if (!isStandardSyntaxAtRule(atRule)) {
71 return;
72 }
73
74 // Return early if at-rule is to be ignored
75 if (optionsMatches(options, 'ignoreAtRules', atRule.name)) {
76 return;
77 }
78
79 // Optionally ignore the expectation if the node is blockless
80 if (
81 optionsMatches(options, 'ignore', 'blockless-after-blockless') &&
82 isBlocklessAtRuleAfterBlocklessAtRule(atRule)
83 ) {
84 return;
85 }
86
87 // Optionally ignore the node if it is the first nested
88 if (optionsMatches(options, 'ignore', 'first-nested') && isFirstNested(atRule)) {
89 return;
90 }
91
92 // Optionally ignore the expectation if the node is blockless
93 // and following another blockless at-rule with the same name
94 if (
95 optionsMatches(options, 'ignore', 'blockless-after-same-name-blockless') &&
96 isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule)
97 ) {
98 return;
99 }
100
101 // Optionally ignore the expectation if the node is inside a block
102 if (optionsMatches(options, 'ignore', 'inside-block') && isNested) {
103 return;
104 }
105
106 // Optionally ignore the expectation if a comment precedes this node
107 if (optionsMatches(options, 'ignore', 'after-comment') && isAfterComment(atRule)) {
108 return;
109 }
110
111 const hasEmptyLineBefore = hasEmptyLine(atRule.raws.before);
112 let expectEmptyLineBefore = expectation === 'always';
113
114 // Optionally reverse the expectation if any exceptions apply
115 if (
116 (optionsMatches(options, 'except', 'after-same-name') &&
117 isAtRuleAfterSameNameAtRule(atRule)) ||
118 (optionsMatches(options, 'except', 'inside-block') && isNested) ||
119 (optionsMatches(options, 'except', 'first-nested') && isFirstNested(atRule)) ||
120 (optionsMatches(options, 'except', 'blockless-after-blockless') &&
121 isBlocklessAtRuleAfterBlocklessAtRule(atRule)) ||
122 (optionsMatches(options, 'except', 'blockless-after-same-name-blockless') &&
123 isBlocklessAtRuleAfterSameNameBlocklessAtRule(atRule))
124 ) {
125 expectEmptyLineBefore = !expectEmptyLineBefore;
126 }
127
128 // Return if the expectation is met
129 if (expectEmptyLineBefore === hasEmptyLineBefore) {
130 return;
131 }
132
133 // Fix
134 if (context.fix) {
135 if (expectEmptyLineBefore) {
136 addEmptyLineBefore(atRule, context.newline);
137 } else {
138 removeEmptyLinesBefore(atRule, context.newline);
139 }
140
141 return;
142 }
143
144 const message = expectEmptyLineBefore ? messages.expected : messages.rejected;
145
146 report({ message, node: atRule, result, ruleName });
147 });
148 };
149}
150
151function isAtRuleAfterSameNameAtRule(atRule) {
152 const previousNode = getPreviousNonSharedLineCommentNode(atRule);
153
154 return previousNode && previousNode.type === 'atrule' && previousNode.name === atRule.name;
155}
156
157rule.ruleName = ruleName;
158rule.messages = messages;
159module.exports = rule;