matcher.js 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
  2. export const escapeStringRegexp = (text) => {
  3. if (typeof text !== 'string') {
  4. throw new TypeError('Expected a string');
  5. }
  6. // Escape characters with special meaning either inside or outside character sets.
  7. // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
  8. return text?.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
  9. };
  10. const regexpCache = new Map();
  11. const sanitizeArray = (input, inputName) => {
  12. if (!Array.isArray(input)) {
  13. switch (typeof input) {
  14. case 'string':
  15. input = [input];
  16. break;
  17. case 'undefined':
  18. input = [];
  19. break;
  20. default:
  21. throw new TypeError(`Expected '${inputName}' to be a string or an array, but got a type of '${typeof input}'`);
  22. }
  23. }
  24. return input.filter((string) => {
  25. if (typeof string !== 'string') {
  26. if (typeof string === 'undefined') {
  27. return false;
  28. }
  29. throw new TypeError(`Expected '${inputName}' to be an array of strings, but found a type of '${typeof string}' in the array`);
  30. }
  31. return true;
  32. });
  33. };
  34. const makeRegexp = (pattern, options = {}) => {
  35. options = {
  36. caseSensitive: false,
  37. ...options,
  38. };
  39. const cacheKey = pattern + JSON.stringify(options);
  40. if (regexpCache.has(cacheKey)) {
  41. return regexpCache.get(cacheKey);
  42. }
  43. const negated = pattern[0] === '!';
  44. if (negated) {
  45. pattern = pattern.slice(1);
  46. }
  47. pattern = escapeStringRegexp(pattern).replace(/\\\*/g, '[\\s\\S]*');
  48. const regexp = new RegExp(`^${pattern}$`, options.caseSensitive ? '' : 'i');
  49. regexp.negated = negated;
  50. regexpCache.set(cacheKey, regexp);
  51. return regexp;
  52. };
  53. const baseMatcher = (inputs, patterns, options = {}, firstMatchOnly = false) => {
  54. inputs = sanitizeArray(inputs, 'inputs');
  55. patterns = sanitizeArray(patterns, 'patterns');
  56. if (patterns.length === 0) {
  57. return [];
  58. }
  59. patterns = patterns.map((pattern) => makeRegexp(pattern, options));
  60. const { allPatterns } = options || {};
  61. const result = [];
  62. for (const input of inputs) {
  63. // String is included only if it matches at least one non-negated pattern supplied.
  64. // Note: the `allPatterns` option requires every non-negated pattern to be matched once.
  65. // Matching a negated pattern excludes the string.
  66. let matches;
  67. const didFit = [...patterns].fill(false);
  68. for (const [index, pattern] of patterns.entries()) {
  69. if (pattern.test(input)) {
  70. didFit[index] = true;
  71. matches = !pattern.negated;
  72. if (!matches) {
  73. break;
  74. }
  75. }
  76. }
  77. if (!(matches === false ||
  78. (matches === undefined &&
  79. patterns.some((pattern) => !pattern.negated)) ||
  80. (allPatterns &&
  81. didFit.some((yes, index) => !yes && !patterns[index].negated)))) {
  82. result.push(input);
  83. if (firstMatchOnly) {
  84. break;
  85. }
  86. }
  87. }
  88. return result;
  89. };
  90. export const matcher = (inputs, patterns, options = {}) => {
  91. return baseMatcher(inputs, patterns, options, false);
  92. };
  93. export const isMatch = (inputs, patterns, options = {}) => {
  94. return baseMatcher(inputs, patterns, options, true).length > 0;
  95. };