whitespaceChecker.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. 'use strict';
  2. const configurationError = require('./configurationError');
  3. const isSingleLineString = require('./isSingleLineString');
  4. const isWhitespace = require('./isWhitespace');
  5. const { assertFunction, isNullish } = require('./validateTypes');
  6. /**
  7. * @typedef {(message: string) => string} MessageFunction
  8. */
  9. /**
  10. * @typedef {Object} Messages
  11. * @property {MessageFunction} [expectedBefore]
  12. * @property {MessageFunction} [rejectedBefore]
  13. * @property {MessageFunction} [expectedAfter]
  14. * @property {MessageFunction} [rejectedAfter]
  15. * @property {MessageFunction} [expectedBeforeSingleLine]
  16. * @property {MessageFunction} [rejectedBeforeSingleLine]
  17. * @property {MessageFunction} [expectedBeforeMultiLine]
  18. * @property {MessageFunction} [rejectedBeforeMultiLine]
  19. * @property {MessageFunction} [expectedAfterSingleLine]
  20. * @property {MessageFunction} [rejectedAfterSingleLine]
  21. * @property {MessageFunction} [expectedAfterMultiLine]
  22. * @property {MessageFunction} [rejectedAfterMultiLine]
  23. */
  24. /**
  25. * @typedef {Object} WhitespaceCheckerArgs
  26. * @property {string} source - The source string
  27. * @property {number} index - The index of the character to check before
  28. * @property {(message: string) => void} err - If a problem is found, this callback
  29. * will be invoked with the relevant warning message.
  30. * Typically this callback will report() the problem.
  31. * @property {string} [errTarget] - If a problem is found, this string
  32. * will be sent to the relevant warning message.
  33. * @property {string} [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. * @property {boolean} [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. * @property {boolean} [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. * @typedef {(args: WhitespaceCheckerArgs) => void} WhitespaceChecker
  49. */
  50. /**
  51. * @typedef {{
  52. * before: WhitespaceChecker,
  53. * beforeAllowingIndentation: WhitespaceChecker,
  54. * after: WhitespaceChecker,
  55. * afterOneOnly: WhitespaceChecker,
  56. * }} WhitespaceCheckers
  57. */
  58. /**
  59. * Create a whitespaceChecker, which exposes the following functions:
  60. * - `before()`
  61. * - `beforeAllowingIndentation()`
  62. * - `after()`
  63. * - `afterOneOnly()`
  64. *
  65. * @param {"space" | "newline"} targetWhitespace - This is a keyword instead
  66. * of the actual character (e.g. " ") in order to accommodate
  67. * different styles of newline ("\n" vs "\r\n")
  68. * @param {"always" | "never" | "always-single-line" | "always-multi-line" | "never-single-line" | "never-multi-line"} expectation
  69. * @param {Messages} messages - An object of message functions;
  70. * calling `before*()` or `after*()` and the `expectation` that is passed
  71. * determines which message functions are required
  72. *
  73. * @returns {WhitespaceCheckers} The checker, with its exposed checking functions
  74. */
  75. module.exports = function whitespaceChecker(targetWhitespace, expectation, messages) {
  76. // Keep track of active arguments in order to avoid passing
  77. // too much stuff around, making signatures long and confusing.
  78. // This variable gets reset anytime a checking function is called.
  79. /** @type {WhitespaceCheckerArgs} */
  80. let activeArgs;
  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. switch (expectation) {
  103. case 'always':
  104. expectBefore();
  105. break;
  106. case 'never':
  107. rejectBefore();
  108. break;
  109. case 'always-single-line':
  110. if (!isSingleLineString(lineCheckStr || source)) {
  111. return;
  112. }
  113. expectBefore(messages.expectedBeforeSingleLine);
  114. break;
  115. case 'never-single-line':
  116. if (!isSingleLineString(lineCheckStr || source)) {
  117. return;
  118. }
  119. rejectBefore(messages.rejectedBeforeSingleLine);
  120. break;
  121. case 'always-multi-line':
  122. if (isSingleLineString(lineCheckStr || source)) {
  123. return;
  124. }
  125. expectBefore(messages.expectedBeforeMultiLine);
  126. break;
  127. case 'never-multi-line':
  128. if (isSingleLineString(lineCheckStr || source)) {
  129. return;
  130. }
  131. rejectBefore(messages.rejectedBeforeMultiLine);
  132. break;
  133. default:
  134. throw configurationError(`Unknown expectation "${expectation}"`);
  135. }
  136. }
  137. /**
  138. * Check for whitespace *after* a character.
  139. * @type {WhitespaceChecker}
  140. */
  141. function after({ source, index, err, errTarget, lineCheckStr, onlyOneChar = false }) {
  142. activeArgs = { source, index, err, errTarget, onlyOneChar };
  143. switch (expectation) {
  144. case 'always':
  145. expectAfter();
  146. break;
  147. case 'never':
  148. rejectAfter();
  149. break;
  150. case 'always-single-line':
  151. if (!isSingleLineString(lineCheckStr || source)) {
  152. return;
  153. }
  154. expectAfter(messages.expectedAfterSingleLine);
  155. break;
  156. case 'never-single-line':
  157. if (!isSingleLineString(lineCheckStr || source)) {
  158. return;
  159. }
  160. rejectAfter(messages.rejectedAfterSingleLine);
  161. break;
  162. case 'always-multi-line':
  163. if (isSingleLineString(lineCheckStr || source)) {
  164. return;
  165. }
  166. expectAfter(messages.expectedAfterMultiLine);
  167. break;
  168. case 'never-multi-line':
  169. if (isSingleLineString(lineCheckStr || source)) {
  170. return;
  171. }
  172. rejectAfter(messages.rejectedAfterMultiLine);
  173. break;
  174. default:
  175. throw configurationError(`Unknown expectation "${expectation}"`);
  176. }
  177. }
  178. /**
  179. * @type {WhitespaceChecker}
  180. */
  181. function beforeAllowingIndentation(obj) {
  182. before({ ...obj, allowIndentation: true });
  183. }
  184. function expectBefore(messageFunc = messages.expectedBefore) {
  185. if (activeArgs.allowIndentation) {
  186. expectBeforeAllowingIndentation(messageFunc);
  187. return;
  188. }
  189. const _activeArgs = activeArgs;
  190. const source = _activeArgs.source;
  191. const index = _activeArgs.index;
  192. const oneCharBefore = source[index - 1];
  193. const twoCharsBefore = source[index - 2];
  194. if (isNullish(oneCharBefore)) {
  195. return;
  196. }
  197. if (
  198. targetWhitespace === 'space' &&
  199. oneCharBefore === ' ' &&
  200. (activeArgs.onlyOneChar || isNullish(twoCharsBefore) || !isWhitespace(twoCharsBefore))
  201. ) {
  202. return;
  203. }
  204. assertFunction(messageFunc);
  205. activeArgs.err(messageFunc(activeArgs.errTarget || source.charAt(index)));
  206. }
  207. function expectBeforeAllowingIndentation(messageFunc = messages.expectedBefore) {
  208. const _activeArgs2 = activeArgs;
  209. const source = _activeArgs2.source;
  210. const index = _activeArgs2.index;
  211. const err = _activeArgs2.err;
  212. const expectedChar = targetWhitespace === 'newline' ? '\n' : undefined;
  213. let i = index - 1;
  214. while (source[i] !== expectedChar) {
  215. if (source[i] === '\t' || source[i] === ' ') {
  216. i--;
  217. continue;
  218. }
  219. assertFunction(messageFunc);
  220. err(messageFunc(activeArgs.errTarget || source.charAt(index)));
  221. return;
  222. }
  223. }
  224. function rejectBefore(messageFunc = messages.rejectedBefore) {
  225. const _activeArgs3 = activeArgs;
  226. const source = _activeArgs3.source;
  227. const index = _activeArgs3.index;
  228. const oneCharBefore = source[index - 1];
  229. if (!isNullish(oneCharBefore) && isWhitespace(oneCharBefore)) {
  230. assertFunction(messageFunc);
  231. activeArgs.err(messageFunc(activeArgs.errTarget || source.charAt(index)));
  232. }
  233. }
  234. /**
  235. * @type {WhitespaceChecker}
  236. */
  237. function afterOneOnly(obj) {
  238. after({ ...obj, onlyOneChar: true });
  239. }
  240. function expectAfter(messageFunc = messages.expectedAfter) {
  241. const _activeArgs4 = activeArgs;
  242. const source = _activeArgs4.source;
  243. const index = _activeArgs4.index;
  244. const oneCharAfter = source[index + 1];
  245. const twoCharsAfter = source[index + 2];
  246. const threeCharsAfter = source[index + 3];
  247. if (isNullish(oneCharAfter)) {
  248. return;
  249. }
  250. if (targetWhitespace === 'newline') {
  251. // If index is followed by a Windows CR-LF ...
  252. if (
  253. oneCharAfter === '\r' &&
  254. twoCharsAfter === '\n' &&
  255. (activeArgs.onlyOneChar || isNullish(threeCharsAfter) || !isWhitespace(threeCharsAfter))
  256. ) {
  257. return;
  258. }
  259. // If index is followed by a Unix LF ...
  260. if (
  261. oneCharAfter === '\n' &&
  262. (activeArgs.onlyOneChar || isNullish(twoCharsAfter) || !isWhitespace(twoCharsAfter))
  263. ) {
  264. return;
  265. }
  266. }
  267. if (
  268. targetWhitespace === 'space' &&
  269. oneCharAfter === ' ' &&
  270. (activeArgs.onlyOneChar || isNullish(twoCharsAfter) || !isWhitespace(twoCharsAfter))
  271. ) {
  272. return;
  273. }
  274. assertFunction(messageFunc);
  275. activeArgs.err(messageFunc(activeArgs.errTarget || source.charAt(index)));
  276. }
  277. function rejectAfter(messageFunc = messages.rejectedAfter) {
  278. const _activeArgs5 = activeArgs;
  279. const source = _activeArgs5.source;
  280. const index = _activeArgs5.index;
  281. const oneCharAfter = source[index + 1];
  282. if (!isNullish(oneCharAfter) && isWhitespace(oneCharAfter)) {
  283. assertFunction(messageFunc);
  284. activeArgs.err(messageFunc(activeArgs.errTarget || source.charAt(index)));
  285. }
  286. }
  287. return {
  288. before,
  289. beforeAllowingIndentation,
  290. after,
  291. afterOneOnly,
  292. };
  293. };