UNPKG

5.64 kBJavaScriptView Raw
1'use strict';
2
3const optionsMatches = require('../../utils/optionsMatches');
4const report = require('../../utils/report');
5const ruleMessages = require('../../utils/ruleMessages');
6const styleSearch = require('style-search');
7const validateOptions = require('../../utils/validateOptions');
8const { isNumber } = require('../../utils/validateTypes');
9
10const ruleName = 'max-empty-lines';
11
12const messages = ruleMessages(ruleName, {
13 expected: (max) => `Expected no more than ${max} empty ${max === 1 ? 'line' : 'lines'}`,
14});
15
16/** @type {import('stylelint').Rule} */
17const rule = (primary, secondaryOptions, context) => {
18 let emptyLines = 0;
19 let lastIndex = -1;
20
21 return (root, result) => {
22 const validOptions = validateOptions(
23 result,
24 ruleName,
25 {
26 actual: primary,
27 possible: isNumber,
28 },
29 {
30 actual: secondaryOptions,
31 possible: {
32 ignore: ['comments'],
33 },
34 optional: true,
35 },
36 );
37
38 if (!validOptions) {
39 return;
40 }
41
42 const ignoreComments = optionsMatches(secondaryOptions, 'ignore', 'comments');
43 const getChars = replaceEmptyLines.bind(null, primary);
44
45 /**
46 * 1. walk nodes & replace enterchar
47 * 2. deal with special case.
48 */
49 if (context.fix) {
50 root.walk((node) => {
51 if (node.type === 'comment' && !ignoreComments) {
52 node.raws.left = getChars(node.raws.left);
53 node.raws.right = getChars(node.raws.right);
54 }
55
56 if (node.raws.before) {
57 node.raws.before = getChars(node.raws.before);
58 }
59 });
60
61 // first node
62 const firstNodeRawsBefore = root.first && root.first.raws.before;
63 // root raws
64 const rootRawsAfter = root.raws.after;
65
66 // not document node
67 // @ts-expect-error -- TS2339: Property 'document' does not exist on type 'Root'.
68 if ((root.document && root.document.constructor.name) !== 'Document') {
69 if (firstNodeRawsBefore) {
70 root.first.raws.before = getChars(firstNodeRawsBefore, true);
71 }
72
73 if (rootRawsAfter) {
74 // when max setted 0, should be treated as 1 in this situation.
75 root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter, true);
76 }
77 } else if (rootRawsAfter) {
78 // `css in js` or `html`
79 root.raws.after = replaceEmptyLines(primary === 0 ? 1 : primary, rootRawsAfter);
80 }
81
82 return;
83 }
84
85 emptyLines = 0;
86 lastIndex = -1;
87 const rootString = root.toString();
88
89 styleSearch(
90 {
91 source: rootString,
92 target: /\r\n/.test(rootString) ? '\r\n' : '\n',
93 comments: ignoreComments ? 'skip' : 'check',
94 },
95 (match) => {
96 checkMatch(rootString, match.startIndex, match.endIndex, root);
97 },
98 );
99
100 /**
101 * @param {string} source
102 * @param {number} matchStartIndex
103 * @param {number} matchEndIndex
104 * @param {import('postcss').Root} node
105 */
106 function checkMatch(source, matchStartIndex, matchEndIndex, node) {
107 const eof = matchEndIndex === source.length;
108 let problem = false;
109
110 // Additional check for beginning of file
111 if (!matchStartIndex || lastIndex === matchStartIndex) {
112 emptyLines++;
113 } else {
114 emptyLines = 0;
115 }
116
117 lastIndex = matchEndIndex;
118
119 if (emptyLines > primary) problem = true;
120
121 if (!eof && !problem) return;
122
123 if (problem) {
124 report({
125 message: messages.expected(primary),
126 node,
127 index: matchStartIndex,
128 result,
129 ruleName,
130 });
131 }
132
133 // Additional check for end of file
134 if (eof && primary) {
135 emptyLines++;
136
137 if (emptyLines > primary && isEofNode(result.root, node)) {
138 report({
139 message: messages.expected(primary),
140 node,
141 index: matchEndIndex,
142 result,
143 ruleName,
144 });
145 }
146 }
147 }
148
149 /**
150 * @param {number} maxLines
151 * @param {unknown} str
152 * @param {boolean?} isSpecialCase
153 */
154 function replaceEmptyLines(maxLines, str, isSpecialCase = false) {
155 const repeatTimes = isSpecialCase ? maxLines : maxLines + 1;
156
157 if (repeatTimes === 0 || typeof str !== 'string') {
158 return '';
159 }
160
161 const emptyLFLines = '\n'.repeat(repeatTimes);
162 const emptyCRLFLines = '\r\n'.repeat(repeatTimes);
163
164 return /(?:\r\n)+/.test(str)
165 ? str.replace(/(\r\n)+/g, ($1) => {
166 if ($1.length / 2 > repeatTimes) {
167 return emptyCRLFLines;
168 }
169
170 return $1;
171 })
172 : str.replace(/(\n)+/g, ($1) => {
173 if ($1.length > repeatTimes) {
174 return emptyLFLines;
175 }
176
177 return $1;
178 });
179 }
180 };
181};
182
183/**
184 * Checks whether the given node is the last node of file.
185 * @param {import('stylelint').PostcssResult['root']} document - the document node with `postcss-html` and `postcss-jsx`.
186 * @param {import('postcss').Root} root - the root node of css
187 */
188function isEofNode(document, root) {
189 if (!document || document.constructor.name !== 'Document' || !('type' in document)) {
190 return true;
191 }
192
193 // In the `postcss-html` and `postcss-jsx` syntax, checks that there is text after the given node.
194 let after;
195
196 // @ts-expect-error -- TS2367: This condition will always return 'false' since the types 'Root' and 'ChildNode | undefined' have no overlap.
197 if (root === document.last) {
198 after = document.raws && document.raws.afterEnd;
199 } else {
200 // @ts-expect-error -- TS2345: Argument of type 'Root' is not assignable to parameter of type 'number | ChildNode'.
201 const rootIndex = document.index(root);
202
203 const nextNode = document.nodes[rootIndex + 1];
204
205 // @ts-expect-error -- TS2339: Property 'beforeStart' does not exist on type 'AtRuleRaws | RuleRaws | DeclarationRaws | CommentRaws'.
206 after = nextNode && nextNode.raws && nextNode.raws.beforeStart;
207 }
208
209 return !String(after).trim();
210}
211
212rule.ruleName = ruleName;
213rule.messages = messages;
214module.exports = rule;