| 'use strict'; |
| |
| const configurationError = require('./configurationError'); |
| const isSingleLineString = require('./isSingleLineString'); |
| const isWhitespace = require('./isWhitespace'); |
| |
| /** |
| * @typedef {(message: string) => string} MessageFunction |
| */ |
| |
| /** |
| * @typedef {Object} Messages |
| * @property {MessageFunction} [expectedBefore] |
| * @property {MessageFunction} [rejectedBefore] |
| * @property {MessageFunction} [expectedAfter] |
| * @property {MessageFunction} [rejectedAfter] |
| * @property {MessageFunction} [expectedBeforeSingleLine] |
| * @property {MessageFunction} [rejectedBeforeSingleLine] |
| * @property {MessageFunction} [expectedBeforeMultiLine] |
| * @property {MessageFunction} [rejectedBeforeMultiLine] |
| * @property {MessageFunction} [expectedAfterSingleLine] |
| * @property {MessageFunction} [rejectedAfterSingleLine] |
| * @property {MessageFunction} [expectedAfterMultiLine] |
| * @property {MessageFunction} [rejectedAfterMultiLine] |
| */ |
| |
| /** |
| * @typedef {Object} WhitespaceCheckerArgs |
| * @property {string} source - The source string |
| * @property {number} index - The index of the character to check before |
| * @property {(message: string) => void} err - If a problem is found, this callback |
| * will be invoked with the relevant warning message. |
| * Typically this callback will report() the problem. |
| * @property {string} [errTarget] - If a problem is found, this string |
| * will be sent to the relevant warning message. |
| * @property {string} [lineCheckStr] - Single- and multi-line checkers |
| * will use this string to determine whether they should proceed, |
| * i.e. if this string is one line only, single-line checkers will check, |
| * multi-line checkers will ignore. |
| * If none is passed, they will use `source`. |
| * @property {boolean} [onlyOneChar=false] - Only check *one* character before. |
| * By default, "always-*" checks will look for the `targetWhitespace` one |
| * before and then ensure there is no whitespace two before. This option |
| * bypasses that second check. |
| * @property {boolean} [allowIndentation=false] - Allow arbitrary indentation |
| * between the `targetWhitespace` (almost definitely a newline) and the `index`. |
| * With this option, the checker will see if a newline *begins* the whitespace before |
| * the `index`. |
| */ |
| |
| /** |
| * @typedef {(args: WhitespaceCheckerArgs) => void} WhitespaceChecker |
| */ |
| |
| /** |
| * @typedef {{ |
| * before: WhitespaceChecker, |
| * beforeAllowingIndentation: WhitespaceChecker, |
| * after: WhitespaceChecker, |
| * afterOneOnly: WhitespaceChecker, |
| * }} WhitespaceCheckers |
| */ |
| |
| /** |
| * Create a whitespaceChecker, which exposes the following functions: |
| * - `before()` |
| * - `beforeAllowingIndentation()` |
| * - `after()` |
| * - `afterOneOnly()` |
| * |
| * @param {"space" | "newline"} targetWhitespace - This is a keyword instead |
| * of the actual character (e.g. " ") in order to accommodate |
| * different styles of newline ("\n" vs "\r\n") |
| * @param {"always" | "never" | "always-single-line" | "always-multi-line" | "never-single-line" | "never-multi-line"} expectation |
| * @param {Messages} messages - An object of message functions; |
| * calling `before*()` or `after*()` and the `expectation` that is passed |
| * determines which message functions are required |
| * |
| * @returns {WhitespaceCheckers} The checker, with its exposed checking functions |
| */ |
| module.exports = function whitespaceChecker(targetWhitespace, expectation, messages) { |
| // Keep track of active arguments in order to avoid passing |
| // too much stuff around, making signatures long and confusing. |
| // This variable gets reset anytime a checking function is called. |
| /** @type {WhitespaceCheckerArgs} */ |
| let activeArgs; |
| |
| /** |
| * Check for whitespace *before* a character. |
| * @type {WhitespaceChecker} |
| */ |
| function before({ |
| source, |
| index, |
| err, |
| errTarget, |
| lineCheckStr, |
| onlyOneChar = false, |
| allowIndentation = false, |
| }) { |
| activeArgs = { |
| source, |
| index, |
| err, |
| errTarget, |
| onlyOneChar, |
| allowIndentation, |
| }; |
| |
| switch (expectation) { |
| case 'always': |
| expectBefore(); |
| break; |
| case 'never': |
| rejectBefore(); |
| break; |
| case 'always-single-line': |
| if (!isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| expectBefore(messages.expectedBeforeSingleLine); |
| break; |
| case 'never-single-line': |
| if (!isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| rejectBefore(messages.rejectedBeforeSingleLine); |
| break; |
| case 'always-multi-line': |
| if (isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| expectBefore(messages.expectedBeforeMultiLine); |
| break; |
| case 'never-multi-line': |
| if (isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| rejectBefore(messages.rejectedBeforeMultiLine); |
| break; |
| default: |
| throw configurationError(`Unknown expectation "${expectation}"`); |
| } |
| } |
| |
| /** |
| * Check for whitespace *after* a character. |
| * @type {WhitespaceChecker} |
| */ |
| function after({ source, index, err, errTarget, lineCheckStr, onlyOneChar = false }) { |
| activeArgs = { source, index, err, errTarget, onlyOneChar }; |
| |
| switch (expectation) { |
| case 'always': |
| expectAfter(); |
| break; |
| case 'never': |
| rejectAfter(); |
| break; |
| case 'always-single-line': |
| if (!isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| expectAfter(messages.expectedAfterSingleLine); |
| break; |
| case 'never-single-line': |
| if (!isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| rejectAfter(messages.rejectedAfterSingleLine); |
| break; |
| case 'always-multi-line': |
| if (isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| expectAfter(messages.expectedAfterMultiLine); |
| break; |
| case 'never-multi-line': |
| if (isSingleLineString(lineCheckStr || source)) { |
| return; |
| } |
| |
| rejectAfter(messages.rejectedAfterMultiLine); |
| break; |
| default: |
| throw configurationError(`Unknown expectation "${expectation}"`); |
| } |
| } |
| |
| /** |
| * @type {WhitespaceChecker} |
| */ |
| function beforeAllowingIndentation(obj) { |
| before({ ...obj, allowIndentation: true }); |
| } |
| |
| function expectBefore(messageFunc = messages.expectedBefore) { |
| if (activeArgs.allowIndentation) { |
| expectBeforeAllowingIndentation(messageFunc); |
| |
| return; |
| } |
| |
| const _activeArgs = activeArgs; |
| const source = _activeArgs.source; |
| const index = _activeArgs.index; |
| |
| const oneCharBefore = source[index - 1]; |
| const twoCharsBefore = source[index - 2]; |
| |
| if (!isValue(oneCharBefore)) { |
| return; |
| } |
| |
| if ( |
| targetWhitespace === 'space' && |
| oneCharBefore === ' ' && |
| (activeArgs.onlyOneChar || !isWhitespace(twoCharsBefore)) |
| ) { |
| return; |
| } |
| |
| assertFunction(messageFunc); |
| activeArgs.err(messageFunc(activeArgs.errTarget || source[index])); |
| } |
| |
| function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) { |
| const _activeArgs2 = activeArgs; |
| const source = _activeArgs2.source; |
| const index = _activeArgs2.index; |
| const err = _activeArgs2.err; |
| |
| const expectedChar = (function () { |
| if (targetWhitespace === 'newline') { |
| return '\n'; |
| } |
| })(); |
| let i = index - 1; |
| |
| while (source[i] !== expectedChar) { |
| if (source[i] === '\t' || source[i] === ' ') { |
| i--; |
| continue; |
| } |
| |
| assertFunction(messageFunc); |
| err(messageFunc(activeArgs.errTarget || source[index])); |
| |
| return; |
| } |
| } |
| |
| function rejectBefore(messageFunc = messages.rejectedBefore) { |
| const _activeArgs3 = activeArgs; |
| const source = _activeArgs3.source; |
| const index = _activeArgs3.index; |
| |
| const oneCharBefore = source[index - 1]; |
| |
| if (isValue(oneCharBefore) && isWhitespace(oneCharBefore)) { |
| assertFunction(messageFunc); |
| activeArgs.err(messageFunc(activeArgs.errTarget || source[index])); |
| } |
| } |
| |
| /** |
| * @type {WhitespaceChecker} |
| */ |
| function afterOneOnly(obj) { |
| after({ ...obj, onlyOneChar: true }); |
| } |
| |
| function expectAfter(messageFunc = messages.expectedAfter) { |
| const _activeArgs4 = activeArgs; |
| const source = _activeArgs4.source; |
| const index = _activeArgs4.index; |
| |
| const oneCharAfter = source[index + 1]; |
| const twoCharsAfter = source[index + 2]; |
| |
| if (!isValue(oneCharAfter)) { |
| return; |
| } |
| |
| if (targetWhitespace === 'newline') { |
| // If index is followed by a Windows CR-LF ... |
| if ( |
| oneCharAfter === '\r' && |
| twoCharsAfter === '\n' && |
| (activeArgs.onlyOneChar || !isWhitespace(source[index + 3])) |
| ) { |
| return; |
| } |
| |
| // If index is followed by a Unix LF ... |
| if (oneCharAfter === '\n' && (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter))) { |
| return; |
| } |
| } |
| |
| if ( |
| targetWhitespace === 'space' && |
| oneCharAfter === ' ' && |
| (activeArgs.onlyOneChar || !isWhitespace(twoCharsAfter)) |
| ) { |
| return; |
| } |
| |
| assertFunction(messageFunc); |
| activeArgs.err(messageFunc(activeArgs.errTarget || source[index])); |
| } |
| |
| function rejectAfter(messageFunc = messages.rejectedAfter) { |
| const _activeArgs5 = activeArgs; |
| const source = _activeArgs5.source; |
| const index = _activeArgs5.index; |
| |
| const oneCharAfter = source[index + 1]; |
| |
| if (isValue(oneCharAfter) && isWhitespace(oneCharAfter)) { |
| assertFunction(messageFunc); |
| activeArgs.err(messageFunc(activeArgs.errTarget || source[index])); |
| } |
| } |
| |
| return { |
| before, |
| beforeAllowingIndentation, |
| after, |
| afterOneOnly, |
| }; |
| }; |
| |
| /** |
| * @param {unknown} x |
| */ |
| function isValue(x) { |
| return x !== undefined && x !== null; |
| } |
| |
| /** |
| * @param {unknown} x |
| * @returns {asserts x is Function} |
| */ |
| function assertFunction(x) { |
| if (typeof x !== 'function') { |
| throw new TypeError(`\`${x}\` must be a function`); |
| } |
| } |