UNPKG

5.5 kBJavaScriptView Raw
1// @ts-nocheck
2
3'use strict';
4
5const isOnlyWhitespace = require('../../utils/isOnlyWhitespace');
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 = 'no-eol-whitespace';
13
14const messages = ruleMessages(ruleName, {
15 rejected: 'Unexpected whitespace at end of line',
16});
17
18const whitespacesToReject = new Set([' ', '\t']);
19
20function fixString(str) {
21 return str.replace(/[ \t]+$/, '');
22}
23
24function findErrorStartIndex(
25 lastEOLIndex,
26 string,
27 { ignoreEmptyLines, isRootFirst } = {
28 ignoreEmptyLines: false,
29 isRootFirst: false,
30 },
31) {
32 const eolWhitespaceIndex = lastEOLIndex - 1;
33
34 // If the character before newline is not whitespace, ignore
35 if (!whitespacesToReject.has(string[eolWhitespaceIndex])) {
36 return -1;
37 }
38
39 if (ignoreEmptyLines) {
40 // If there is only whitespace between the previous newline and
41 // this newline, ignore
42 const beforeNewlineIndex = string.lastIndexOf('\n', eolWhitespaceIndex);
43
44 if (beforeNewlineIndex >= 0 || isRootFirst) {
45 const line = string.substring(beforeNewlineIndex, eolWhitespaceIndex);
46
47 if (isOnlyWhitespace(line)) {
48 return -1;
49 }
50 }
51 }
52
53 return eolWhitespaceIndex;
54}
55
56function rule(on, options, context) {
57 return (root, result) => {
58 const validOptions = validateOptions(
59 result,
60 ruleName,
61 {
62 actual: on,
63 },
64 {
65 optional: true,
66 actual: options,
67 possible: {
68 ignore: ['empty-lines'],
69 },
70 },
71 );
72
73 if (!validOptions) {
74 return;
75 }
76
77 const ignoreEmptyLines = optionsMatches(options, 'ignore', 'empty-lines');
78
79 if (context.fix) {
80 fix(root);
81 }
82
83 const rootString = context.fix ? root.toString() : root.source.input.css;
84 const reportFromIndex = (index) => {
85 report({
86 message: messages.rejected,
87 node: root,
88 index,
89 result,
90 ruleName,
91 });
92 };
93
94 eachEolWhitespace(rootString, reportFromIndex, true);
95
96 const errorIndex = findErrorStartIndex(rootString.length, rootString, {
97 ignoreEmptyLines,
98 isRootFirst: true,
99 });
100
101 if (errorIndex > -1) {
102 reportFromIndex(errorIndex);
103 }
104
105 /**
106 * Iterate each whitespace at the end of each line of the given string.
107 * @param {string} string the source code string
108 * @param {Function} callback callback the whitespace index at the end of each line.
109 * @param {boolean} isRootFirst set `true` if the given string is the first token of the root.
110 * @returns {void}
111 */
112 function eachEolWhitespace(string, callback, isRootFirst) {
113 styleSearch(
114 {
115 source: string,
116 target: ['\n', '\r'],
117 comments: 'check',
118 },
119 (match) => {
120 const errorIndex = findErrorStartIndex(match.startIndex, string, {
121 ignoreEmptyLines,
122 isRootFirst,
123 });
124
125 if (errorIndex > -1) {
126 callback(errorIndex);
127 }
128 },
129 );
130 }
131
132 function fix(root) {
133 let isRootFirst = true;
134
135 root.walk((node) => {
136 fixText(
137 node.raws.before,
138 (fixed) => {
139 node.raws.before = fixed;
140 },
141 isRootFirst,
142 );
143 isRootFirst = false;
144
145 // AtRule
146 fixText(node.raws.afterName, (fixed) => {
147 node.raws.afterName = fixed;
148 });
149
150 if (node.raws.params) {
151 fixText(node.raws.params.raw, (fixed) => {
152 node.raws.params.raw = fixed;
153 });
154 } else {
155 fixText(node.params, (fixed) => {
156 node.params = fixed;
157 });
158 }
159
160 // Rule
161 if (node.raws.selector) {
162 fixText(node.raws.selector.raw, (fixed) => {
163 node.raws.selector.raw = fixed;
164 });
165 } else {
166 fixText(node.selector, (fixed) => {
167 node.selector = fixed;
168 });
169 }
170
171 // AtRule or Rule or Decl
172 fixText(node.raws.between, (fixed) => {
173 node.raws.between = fixed;
174 });
175
176 // Decl
177 if (node.raws.value) {
178 fixText(node.raws.value.raw, (fixed) => {
179 node.raws.value.raw = fixed;
180 });
181 } else {
182 fixText(node.value, (fixed) => {
183 node.value = fixed;
184 });
185 }
186
187 // Comment
188 fixText(node.raws.left, (fixed) => {
189 node.raws.left = fixed;
190 });
191
192 if (node.raws.inline) {
193 node.raws.right = fixString(node.raws.right);
194 } else {
195 fixText(node.raws.right, (fixed) => {
196 node.raws.right = fixed;
197 });
198 }
199
200 fixText(node.text, (fixed) => {
201 node.text = fixed;
202 });
203
204 fixText(node.raws.after, (fixed) => {
205 node.raws.after = fixed;
206 });
207 });
208
209 fixText(
210 root.raws.after,
211 (fixed) => {
212 root.raws.after = fixed;
213 },
214 isRootFirst,
215 );
216
217 if (typeof root.raws.after === 'string') {
218 const lastEOL = Math.max(
219 root.raws.after.lastIndexOf('\n'),
220 root.raws.after.lastIndexOf('\r'),
221 );
222
223 if (lastEOL !== root.raws.after.length - 1) {
224 root.raws.after =
225 root.raws.after.slice(0, lastEOL + 1) + fixString(root.raws.after.slice(lastEOL + 1));
226 }
227 }
228 }
229
230 function fixText(value, fix, isRootFirst) {
231 if (!value) {
232 return;
233 }
234
235 let fixed = '';
236 let lastIndex = 0;
237
238 eachEolWhitespace(
239 value,
240 (index) => {
241 const newlineIndex = index + 1;
242
243 fixed += fixString(value.slice(lastIndex, newlineIndex));
244 lastIndex = newlineIndex;
245 },
246 isRootFirst,
247 );
248
249 if (lastIndex) {
250 fixed += value.slice(lastIndex);
251 fix(fixed);
252 }
253 }
254 };
255}
256
257rule.ruleName = ruleName;
258rule.messages = messages;
259module.exports = rule;