UNPKG

9.29 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 activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
229 }
230
231 /**
232 * @param {Function} [messageFunc]
233 */
234 function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) {
235 const _activeArgs2 = activeArgs;
236 const source = _activeArgs2.source;
237 const index = _activeArgs2.index;
238 const err = _activeArgs2.err;
239
240 const expectedChar = (function () {
241 if (targetWhitespace === 'newline') {
242 return '\n';
243 }
244 })();
245 let i = index - 1;
246
247 while (source[i] !== expectedChar) {
248 if (source[i] === '\t' || source[i] === ' ') {
249 i--;
250 continue;
251 }
252
253 err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
254
255 return;
256 }
257 }
258
259 /**
260 * @param {Function} [messageFunc]
261 */
262 function rejectBefore(messageFunc = messages.rejectedBefore) {
263 const _activeArgs3 = activeArgs;
264 const source = _activeArgs3.source;
265 const index = _activeArgs3.index;
266
267 const oneCharBefore = source[index - 1];
268
269 if (isValue(oneCharBefore) && isWhitespace(oneCharBefore)) {
270 activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
271 }
272 }
273
274 /**
275 * @param {WhitespaceCheckerArgs} obj
276 */
277 function afterOneOnly(obj) {
278 after({ ...obj, onlyOneChar: true });
279 }
280
281 /**
282 * @param {Function} [messageFunc]
283 */
284 function expectAfter(messageFunc = messages.expectedAfter) {
285 const _activeArgs4 = activeArgs;
286 const source = _activeArgs4.source;
287 const index = _activeArgs4.index;
288
289 const oneCharAfter = source[index + 1];
290 const twoCharsAfter = source[index + 2];
291
292 if (!isValue(oneCharAfter)) {
293 return;
294 }
295
296 if (targetWhitespace === 'newline') {
297 // If index is followed by a Windows CR-LF ...
298 if (oneCharAfter === '\r' && twoCharsAfter === '\n') {
299 if (activeArgs.onlyOneChar || !isWhitespace(source[index + 3])) {
300 return;
301 }
302 }
303
304 // If index is followed by a Unix LF ...
305 if (oneCharAfter === '\n') {
306 if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
307 return;
308 }
309 }
310 }
311
312 if (targetWhitespace === 'space' && oneCharAfter === ' ') {
313 if (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) {
314 return;
315 }
316 }
317
318 activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
319 }
320
321 /**
322 * @param {Function} [messageFunc]
323 */
324 function rejectAfter(messageFunc = messages.rejectedAfter) {
325 const _activeArgs5 = activeArgs;
326 const source = _activeArgs5.source;
327 const index = _activeArgs5.index;
328
329 const oneCharAfter = source[index + 1];
330
331 if (isValue(oneCharAfter) && isWhitespace(oneCharAfter)) {
332 activeArgs.err(messageFunc(activeArgs.errTarget ? activeArgs.errTarget : source[index]));
333 }
334 }
335
336 return {
337 before,
338 beforeAllowingIndentation,
339 after,
340 afterOneOnly,
341 };
342};
343
344/**
345 * @param {any} x
346 */
347function isValue(x) {
348 return x !== undefined && x !== null;
349}