UNPKG

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