UNPKG

6.26 kBJavaScriptView Raw
1'use strict';
2
3const declarationValueIndex = require('../../utils/declarationValueIndex');
4const getDeclarationValue = require('../../utils/getDeclarationValue');
5const isSingleLineString = require('../../utils/isSingleLineString');
6const isStandardSyntaxFunction = require('../../utils/isStandardSyntaxFunction');
7const report = require('../../utils/report');
8const ruleMessages = require('../../utils/ruleMessages');
9const setDeclarationValue = require('../../utils/setDeclarationValue');
10const validateOptions = require('../../utils/validateOptions');
11const valueParser = require('postcss-value-parser');
12
13const ruleName = 'function-parentheses-newline-inside';
14
15const messages = ruleMessages(ruleName, {
16 expectedOpening: 'Expected newline after "("',
17 expectedClosing: 'Expected newline before ")"',
18 expectedOpeningMultiLine: 'Expected newline after "(" in a multi-line function',
19 rejectedOpeningMultiLine: 'Unexpected whitespace after "(" in a multi-line function',
20 expectedClosingMultiLine: 'Expected newline before ")" in a multi-line function',
21 rejectedClosingMultiLine: 'Unexpected whitespace before ")" in a multi-line function',
22});
23
24/** @type {import('stylelint').Rule} */
25const rule = (primary, _secondaryOptions, context) => {
26 return (root, result) => {
27 const validOptions = validateOptions(result, ruleName, {
28 actual: primary,
29 possible: ['always', 'always-multi-line', 'never-multi-line'],
30 });
31
32 if (!validOptions) {
33 return;
34 }
35
36 root.walkDecls((decl) => {
37 if (!decl.value.includes('(')) {
38 return;
39 }
40
41 let hasFixed = false;
42 const declValue = getDeclarationValue(decl);
43 const parsedValue = valueParser(declValue);
44
45 parsedValue.walk((valueNode) => {
46 if (valueNode.type !== 'function') {
47 return;
48 }
49
50 if (!isStandardSyntaxFunction(valueNode)) {
51 return;
52 }
53
54 const functionString = valueParser.stringify(valueNode);
55 const isMultiLine = !isSingleLineString(functionString);
56 const containsNewline = (/** @type {string} */ str) => str.includes('\n');
57
58 // Check opening ...
59
60 const openingIndex = valueNode.sourceIndex + valueNode.value.length + 1;
61 const checkBefore = getCheckBefore(valueNode);
62
63 if (primary === 'always' && !containsNewline(checkBefore)) {
64 if (context.fix) {
65 hasFixed = true;
66 fixBeforeForAlways(valueNode, context.newline || '');
67 } else {
68 complain(messages.expectedOpening, openingIndex);
69 }
70 }
71
72 if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkBefore)) {
73 if (context.fix) {
74 hasFixed = true;
75 fixBeforeForAlways(valueNode, context.newline || '');
76 } else {
77 complain(messages.expectedOpeningMultiLine, openingIndex);
78 }
79 }
80
81 if (isMultiLine && primary === 'never-multi-line' && checkBefore !== '') {
82 if (context.fix) {
83 hasFixed = true;
84 fixBeforeForNever(valueNode);
85 } else {
86 complain(messages.rejectedOpeningMultiLine, openingIndex);
87 }
88 }
89
90 // Check closing ...
91
92 const closingIndex = valueNode.sourceIndex + functionString.length - 2;
93 const checkAfter = getCheckAfter(valueNode);
94
95 if (primary === 'always' && !containsNewline(checkAfter)) {
96 if (context.fix) {
97 hasFixed = true;
98 fixAfterForAlways(valueNode, context.newline || '');
99 } else {
100 complain(messages.expectedClosing, closingIndex);
101 }
102 }
103
104 if (isMultiLine && primary === 'always-multi-line' && !containsNewline(checkAfter)) {
105 if (context.fix) {
106 hasFixed = true;
107 fixAfterForAlways(valueNode, context.newline || '');
108 } else {
109 complain(messages.expectedClosingMultiLine, closingIndex);
110 }
111 }
112
113 if (isMultiLine && primary === 'never-multi-line' && checkAfter !== '') {
114 if (context.fix) {
115 hasFixed = true;
116 fixAfterForNever(valueNode);
117 } else {
118 complain(messages.rejectedClosingMultiLine, closingIndex);
119 }
120 }
121 });
122
123 if (hasFixed) {
124 setDeclarationValue(decl, parsedValue.toString());
125 }
126
127 /**
128 * @param {string} message
129 * @param {number} offset
130 */
131 function complain(message, offset) {
132 report({
133 ruleName,
134 result,
135 message,
136 node: decl,
137 index: declarationValueIndex(decl) + offset,
138 });
139 }
140 });
141 };
142};
143
144/** @typedef {import('postcss-value-parser').FunctionNode} FunctionNode */
145
146/**
147 * @param {FunctionNode} valueNode
148 */
149function getCheckBefore(valueNode) {
150 let before = valueNode.before;
151
152 for (const node of valueNode.nodes) {
153 if (node.type === 'comment') {
154 continue;
155 }
156
157 if (node.type === 'space') {
158 before += node.value;
159 continue;
160 }
161
162 break;
163 }
164
165 return before;
166}
167
168/**
169 * @param {FunctionNode} valueNode
170 */
171function getCheckAfter(valueNode) {
172 let after = '';
173
174 for (const node of [...valueNode.nodes].reverse()) {
175 if (node.type === 'comment') {
176 continue;
177 }
178
179 if (node.type === 'space') {
180 after = node.value + after;
181 continue;
182 }
183
184 break;
185 }
186
187 after += valueNode.after;
188
189 return after;
190}
191
192/**
193 * @param {FunctionNode} valueNode
194 * @param {string} newline
195 */
196function fixBeforeForAlways(valueNode, newline) {
197 let target;
198
199 for (const node of valueNode.nodes) {
200 if (node.type === 'comment') {
201 continue;
202 }
203
204 if (node.type === 'space') {
205 target = node;
206 continue;
207 }
208
209 break;
210 }
211
212 if (target) {
213 target.value = newline + target.value;
214 } else {
215 valueNode.before = newline + valueNode.before;
216 }
217}
218
219/**
220 * @param {FunctionNode} valueNode
221 */
222function fixBeforeForNever(valueNode) {
223 valueNode.before = '';
224
225 for (const node of valueNode.nodes) {
226 if (node.type === 'comment') {
227 continue;
228 }
229
230 if (node.type === 'space') {
231 node.value = '';
232 continue;
233 }
234
235 break;
236 }
237}
238
239/**
240 * @param {FunctionNode} valueNode
241 * @param {string} newline
242 */
243function fixAfterForAlways(valueNode, newline) {
244 valueNode.after = newline + valueNode.after;
245}
246
247/**
248 * @param {FunctionNode} valueNode
249 */
250function fixAfterForNever(valueNode) {
251 valueNode.after = '';
252
253 for (const node of [...valueNode.nodes].reverse()) {
254 if (node.type === 'comment') {
255 continue;
256 }
257
258 if (node.type === 'space') {
259 node.value = '';
260 continue;
261 }
262
263 break;
264 }
265}
266
267rule.ruleName = ruleName;
268rule.messages = messages;
269module.exports = rule;