UNPKG

9.67 kBJavaScriptView Raw
1'use strict';
2
3const configurationError = require('./configurationError');
4const isSingleLineString = require('./isSingleLineString');
5const isWhitespace = require('./isWhitespace');
6
7/**
8 * @typedef {object} Messages
9 * @property {function} [expectedBefore]
10 * @property {function} [rejectedBefore]
11 * @property {function} [expectedAfter]
12 * @property {function} [rejectedAfter]
13 * @property {function} [expectedBeforeSingleLine]
14 * @property {function} [rejectedBeforeSingleLine]
15 * @property {function} [expectedBeforeMultiLine]
16 * @property {function} [rejectedBeforeMultiLine]
17 * @property {function} [expectedAfterSingleLine]
18 * @property {function} [rejectedAfterSingleLine]
19 * @property {function} [expectedAfterMultiLine]
20 * @property {function} [rejectedAfterMultiLine]
21 */
22
23/**
24 * @typedef {object} WhitespaceCheckerArgs
25 * @property {string} source - The source string
26 * @property {number} index - The index of the character to check before
27 * @property {function} err - If a violation is found, this callback
28 * will be invoked with the relevant warning message.
29 * Typically this callback will report() the violation.
30 * @property {function} errTarget - If a violation is found, this string
31 * will be sent to the relevant warning message.
32 * @property {string} [lineCheckStr] - Single- and multi-line checkers
33 * will use this string to determine whether they should proceed,
34 * i.e. if this string is one line only, single-line checkers will check,
35 * multi-line checkers will ignore.
36 * If none is passed, they will use `source`.
37 * @property {boolean} [onlyOneChar=false] - Only check *one* character before.
38 * By default, "always-*" checks will look for the `targetWhitespace` one
39 * before and then ensure there is no whitespace two before. This option
40 * bypasses that second check.
41 * @property {boolean} [allowIndentation=false] - Allow arbitrary indentation
42 * between the `targetWhitespace` (almost definitely a newline) and the `index`.
43 * With this option, the checker will see if a newline *begins* the whitespace before
44 * the `index`.
45 */
46
47/**
48 * @callback WhitespaceChecker
49 * @param {WhitespaceCheckerArgs} args
50 */
51
52/**
53 * Create a whitespaceChecker, which exposes the following functions:
54 * - `before()`
55 * - `beforeAllowingIndentation()`
56 * - `after()`
57 * - `afterOneOnly()`
58 *
59 * @param {"space" | "newline"} targetWhitespace - This is a keyword instead
60 * of the actual character (e.g. " ") in order to accommodate
61 * different styles of newline ("\n" vs "\r\n")
62 * @param { "always" | "never" | "always-single-line" | "always-multi-line" | "never-single-line" | "never-multi-line" } expectation
63 * @param {Messages} messages - An object of message functions;
64 * calling `before*()` or `after*()` and the `expectation` that is passed
65 * determines which message functions are required
66 *
67 * @returns {object} The checker, with its exposed checking functions
68 */
69module.exports = function (targetWhitespace, expectation, messages) {
70 // Keep track of active arguments in order to avoid passing
71 // too much stuff around, making signatures long and confusing.
72 // This variable gets reset anytime a checking function is called.
73 /**
74 * @type {{
75 source?: any,
76 index?: any,
77 err: any,
78 errTarget: any,
79 onlyOneChar: any,
80 allowIndentation?: any,
81 }}
82 */
83 let activeArgs;
84
85 /**
86 * Check for whitespace *before* a character.
87 * @type {WhitespaceChecker}
88 */
89 function before({
90 source,
91 index,
92 err,
93 errTarget,
94 lineCheckStr,
95 onlyOneChar = false,
96 allowIndentation = false,
97 }) {
98 activeArgs = {
99 source,
100 index,
101 err,
102 errTarget,
103 onlyOneChar,
104 allowIndentation,
105 };
106
107 switch (expectation) {
108 case 'always':
109 expectBefore();
110 break;
111 case 'never':
112 rejectBefore();
113 break;
114 case 'always-single-line':
115 if (!isSingleLineString(lineCheckStr || source)) {
116 return;
117 }
118
119 expectBefore(messages.expectedBeforeSingleLine);
120 break;
121 case 'never-single-line':
122 if (!isSingleLineString(lineCheckStr || source)) {
123 return;
124 }
125
126 rejectBefore(messages.rejectedBeforeSingleLine);
127 break;
128 case 'always-multi-line':
129 if (isSingleLineString(lineCheckStr || source)) {
130 return;
131 }
132
133 expectBefore(messages.expectedBeforeMultiLine);
134 break;
135 case 'never-multi-line':
136 if (isSingleLineString(lineCheckStr || source)) {
137 return;
138 }
139
140 rejectBefore(messages.rejectedBeforeMultiLine);
141 break;
142 default:
143 throw configurationError(`Unknown expectation "${expectation}"`);
144 }
145 }
146
147 /**
148 * Check for whitespace *after* a character.
149 * @type {WhitespaceChecker}
150 */
151 function after({ source, index, err, errTarget, lineCheckStr, onlyOneChar = false }) {
152 activeArgs = { source, index, err, errTarget, onlyOneChar };
153
154 switch (expectation) {
155 case 'always':
156 expectAfter();
157 break;
158 case 'never':
159 rejectAfter();
160 break;
161 case 'always-single-line':
162 if (!isSingleLineString(lineCheckStr || source)) {
163 return;
164 }
165
166 expectAfter(messages.expectedAfterSingleLine);
167 break;
168 case 'never-single-line':
169 if (!isSingleLineString(lineCheckStr || source)) {
170 return;
171 }
172
173 rejectAfter(messages.rejectedAfterSingleLine);
174 break;
175 case 'always-multi-line':
176 if (isSingleLineString(lineCheckStr || source)) {
177 return;
178 }
179
180 expectAfter(messages.expectedAfterMultiLine);
181 break;
182 case 'never-multi-line':
183 if (isSingleLineString(lineCheckStr || source)) {
184 return;
185 }
186
187 rejectAfter(messages.rejectedAfterMultiLine);
188 break;
189 default:
190 throw configurationError(`Unknown expectation "${expectation}"`);
191 }
192 }
193
194 /**
195 * @param {WhitespaceCheckerArgs} obj
196 */
197 function beforeAllowingIndentation(obj) {
198 before({ ...obj, allowIndentation: true });
199 }
200
201 /**
202 * @param {Function} [messageFunc]
203 */
204 function expectBefore(messageFunc = messages.expectedBefore) {
205 if (activeArgs.allowIndentation) {
206 expectBeforeAllowingIndentation(messageFunc);
207
208 return;
209 }
210
211 const _activeArgs = activeArgs;
212 const source = _activeArgs.source;
213 const index = _activeArgs.index;
214
215 const oneCharBefore = source[index - 1];
216 const twoCharsBefore = source[index - 2];
217
218 if (!isValue(oneCharBefore)) {
219 return;
220 }
221
222 if (targetWhitespace === 'space' && oneCharBefore === ' ') {
223 if (activeArgs.onlyOneChar || !isWhitespace(twoCharsBefore)) {
224 return;
225 }
226 }
227
228 const msgFunc = assertFunction(messageFunc);
229
230 activeArgs.err(msgFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
231 }
232
233 /**
234 * @param {Function} [messageFunc]
235 */
236 function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) {
237 const _activeArgs2 = activeArgs;
238 const source = _activeArgs2.source;
239 const index = _activeArgs2.index;
240 const err = _activeArgs2.err;
241
242 const expectedChar = (function () {
243 if (targetWhitespace === 'newline') {
244 return '\n';
245 }
246 })();
247 let i = index - 1;
248
249 while (source[i] !== expectedChar) {
250 if (source[i] === '\t' || source[i] === ' ') {
251 i--;
252 continue;
253 }
254
255 const msgFunc = assertFunction(messageFunc);
256
257 err(msgFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
258
259 return;
260 }
261 }
262
263 /**
264 * @param {Function} [messageFunc]
265 */
266 function rejectBefore(messageFunc = messages.rejectedBefore) {
267 const _activeArgs3 = activeArgs;
268 const source = _activeArgs3.source;
269 const index = _activeArgs3.index;
270
271 const oneCharBefore = source[index - 1];
272
273 if (isValue(oneCharBefore) && isWhitespace(oneCharBefore)) {
274 const msgFunc = assertFunction(messageFunc);
275
276 activeArgs.err(msgFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
277 }
278 }
279
280 /**
281 * @param {WhitespaceCheckerArgs} obj
282 */
283 function afterOneOnly(obj) {
284 after({ ...obj, onlyOneChar: true });
285 }
286
287 /**
288 * @param {Function} [messageFunc]
289 */
290 function expectAfter(messageFunc = messages.expectedAfter) {
291 const _activeArgs4 = activeArgs;
292 const source = _activeArgs4.source;
293 const index = _activeArgs4.index;
294
295 const oneCharAfter = source[index + 1];
296 const twoCharsAfter = source[index + 2];
297
298 if (!isValue(oneCharAfter)) {
299 return;
300 }
301
302 if (targetWhitespace === 'newline') {
303 // If index is followed by a Windows CR-LF ...
304 if (oneCharAfter === '\r' && twoCharsAfter === '\n') {
305 if (activeArgs.onlyOneChar || !isWhitespace(source[index + 3])) {
306 return;
307 }
308 }
309
310 // If index is followed by a Unix LF ...
311 if (oneCharAfter === '\n') {
312 if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
313 return;
314 }
315 }
316 }
317
318 if (targetWhitespace === 'space' && oneCharAfter === ' ') {
319 if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
320 return;
321 }
322 }
323
324 const msgFunc = assertFunction(messageFunc);
325
326 activeArgs.err(msgFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
327 }
328
329 /**
330 * @param {Function} [messageFunc]
331 */
332 function rejectAfter(messageFunc = messages.rejectedAfter) {
333 const _activeArgs5 = activeArgs;
334 const source = _activeArgs5.source;
335 const index = _activeArgs5.index;
336
337 const oneCharAfter = source[index + 1];
338
339 if (isValue(oneCharAfter) && isWhitespace(oneCharAfter)) {
340 const msgFunc = assertFunction(messageFunc);
341
342 activeArgs.err(msgFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
343 }
344 }
345
346 return {
347 before,
348 beforeAllowingIndentation,
349 after,
350 afterOneOnly,
351 };
352};
353
354/**
355 * @param {any} x
356 */
357function isValue(x) {
358 return x !== undefined && x !== null;
359}
360
361/**
362 * @param {unknown} x
363 */
364function assertFunction(x) {
365 if (typeof x === 'function') {
366 return x;
367 }
368
369 throw new Error(`\`${x}\` must be a function`);
370}