UNPKG

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