compileBooleanMatcher.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. /**
  7. * @param {string} str string
  8. * @returns {string} quoted meta
  9. */
  10. const quoteMeta = str => str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&");
  11. /**
  12. * @param {string} str string
  13. * @returns {string} string
  14. */
  15. const toSimpleString = str => {
  16. if (`${Number(str)}` === str) {
  17. return str;
  18. }
  19. return JSON.stringify(str);
  20. };
  21. /**
  22. * @param {Record<string|number, boolean>} map value map
  23. * @returns {boolean|(function(string): string)} true/false, when unconditionally true/false, or a template function to determine the value at runtime
  24. */
  25. const compileBooleanMatcher = map => {
  26. const positiveItems = Object.keys(map).filter(i => map[i]);
  27. const negativeItems = Object.keys(map).filter(i => !map[i]);
  28. if (positiveItems.length === 0) return false;
  29. if (negativeItems.length === 0) return true;
  30. return compileBooleanMatcherFromLists(positiveItems, negativeItems);
  31. };
  32. /**
  33. * @param {string[]} positiveItems positive items
  34. * @param {string[]} negativeItems negative items
  35. * @returns {function(string): string} a template function to determine the value at runtime
  36. */
  37. const compileBooleanMatcherFromLists = (positiveItems, negativeItems) => {
  38. if (positiveItems.length === 0) return () => "false";
  39. if (negativeItems.length === 0) return () => "true";
  40. if (positiveItems.length === 1)
  41. return value => `${toSimpleString(positiveItems[0])} == ${value}`;
  42. if (negativeItems.length === 1)
  43. return value => `${toSimpleString(negativeItems[0])} != ${value}`;
  44. const positiveRegexp = itemsToRegexp(positiveItems);
  45. const negativeRegexp = itemsToRegexp(negativeItems);
  46. if (positiveRegexp.length <= negativeRegexp.length) {
  47. return value => `/^${positiveRegexp}$/.test(${value})`;
  48. }
  49. return value => `!/^${negativeRegexp}$/.test(${value})`;
  50. };
  51. /**
  52. * @param {Set<string>} itemsSet items set
  53. * @param {(str: string) => string | false} getKey get key function
  54. * @param {(str: Array<string>) => boolean} condition condition
  55. * @returns {Array<Array<string>>} list of common items
  56. */
  57. const popCommonItems = (itemsSet, getKey, condition) => {
  58. /** @type {Map<string, Array<string>>} */
  59. const map = new Map();
  60. for (const item of itemsSet) {
  61. const key = getKey(item);
  62. if (key) {
  63. let list = map.get(key);
  64. if (list === undefined) {
  65. /** @type {Array<string>} */
  66. list = [];
  67. map.set(key, list);
  68. }
  69. list.push(item);
  70. }
  71. }
  72. /** @type {Array<Array<string>>} */
  73. const result = [];
  74. for (const list of map.values()) {
  75. if (condition(list)) {
  76. for (const item of list) {
  77. itemsSet.delete(item);
  78. }
  79. result.push(list);
  80. }
  81. }
  82. return result;
  83. };
  84. /**
  85. * @param {Array<string>} items items
  86. * @returns {string} common prefix
  87. */
  88. const getCommonPrefix = items => {
  89. let prefix = items[0];
  90. for (let i = 1; i < items.length; i++) {
  91. const item = items[i];
  92. for (let p = 0; p < prefix.length; p++) {
  93. if (item[p] !== prefix[p]) {
  94. prefix = prefix.slice(0, p);
  95. break;
  96. }
  97. }
  98. }
  99. return prefix;
  100. };
  101. /**
  102. * @param {Array<string>} items items
  103. * @returns {string} common suffix
  104. */
  105. const getCommonSuffix = items => {
  106. let suffix = items[0];
  107. for (let i = 1; i < items.length; i++) {
  108. const item = items[i];
  109. for (let p = item.length - 1, s = suffix.length - 1; s >= 0; p--, s--) {
  110. if (item[p] !== suffix[s]) {
  111. suffix = suffix.slice(s + 1);
  112. break;
  113. }
  114. }
  115. }
  116. return suffix;
  117. };
  118. /**
  119. * @param {Array<string>} itemsArr array of items
  120. * @returns {string} regexp
  121. */
  122. const itemsToRegexp = itemsArr => {
  123. if (itemsArr.length === 1) {
  124. return quoteMeta(itemsArr[0]);
  125. }
  126. /** @type {Array<string>} */
  127. const finishedItems = [];
  128. // merge single char items: (a|b|c|d|ef) => ([abcd]|ef)
  129. let countOfSingleCharItems = 0;
  130. for (const item of itemsArr) {
  131. if (item.length === 1) {
  132. countOfSingleCharItems++;
  133. }
  134. }
  135. // special case for only single char items
  136. if (countOfSingleCharItems === itemsArr.length) {
  137. return `[${quoteMeta(itemsArr.sort().join(""))}]`;
  138. }
  139. const items = new Set(itemsArr.sort());
  140. if (countOfSingleCharItems > 2) {
  141. let singleCharItems = "";
  142. for (const item of items) {
  143. if (item.length === 1) {
  144. singleCharItems += item;
  145. items.delete(item);
  146. }
  147. }
  148. finishedItems.push(`[${quoteMeta(singleCharItems)}]`);
  149. }
  150. // special case for 2 items with common prefix/suffix
  151. if (finishedItems.length === 0 && items.size === 2) {
  152. const prefix = getCommonPrefix(itemsArr);
  153. const suffix = getCommonSuffix(
  154. itemsArr.map(item => item.slice(prefix.length))
  155. );
  156. if (prefix.length > 0 || suffix.length > 0) {
  157. return `${quoteMeta(prefix)}${itemsToRegexp(
  158. itemsArr.map(i => i.slice(prefix.length, -suffix.length || undefined))
  159. )}${quoteMeta(suffix)}`;
  160. }
  161. }
  162. // special case for 2 items with common suffix
  163. if (finishedItems.length === 0 && items.size === 2) {
  164. /** @type {Iterator<string>} */
  165. const it = items[Symbol.iterator]();
  166. const a = it.next().value;
  167. const b = it.next().value;
  168. if (a.length > 0 && b.length > 0 && a.slice(-1) === b.slice(-1)) {
  169. return `${itemsToRegexp([a.slice(0, -1), b.slice(0, -1)])}${quoteMeta(
  170. a.slice(-1)
  171. )}`;
  172. }
  173. }
  174. // find common prefix: (a1|a2|a3|a4|b5) => (a(1|2|3|4)|b5)
  175. const prefixed = popCommonItems(
  176. items,
  177. item => (item.length >= 1 ? item[0] : false),
  178. list => {
  179. if (list.length >= 3) return true;
  180. if (list.length <= 1) return false;
  181. return list[0][1] === list[1][1];
  182. }
  183. );
  184. for (const prefixedItems of prefixed) {
  185. const prefix = getCommonPrefix(prefixedItems);
  186. finishedItems.push(
  187. `${quoteMeta(prefix)}${itemsToRegexp(
  188. prefixedItems.map(i => i.slice(prefix.length))
  189. )}`
  190. );
  191. }
  192. // find common suffix: (a1|b1|c1|d1|e2) => ((a|b|c|d)1|e2)
  193. const suffixed = popCommonItems(
  194. items,
  195. item => (item.length >= 1 ? item.slice(-1) : false),
  196. list => {
  197. if (list.length >= 3) return true;
  198. if (list.length <= 1) return false;
  199. return list[0].slice(-2) === list[1].slice(-2);
  200. }
  201. );
  202. for (const suffixedItems of suffixed) {
  203. const suffix = getCommonSuffix(suffixedItems);
  204. finishedItems.push(
  205. `${itemsToRegexp(
  206. suffixedItems.map(i => i.slice(0, -suffix.length))
  207. )}${quoteMeta(suffix)}`
  208. );
  209. }
  210. // TODO further optimize regexp, i. e.
  211. // use ranges: (1|2|3|4|a) => [1-4a]
  212. const conditional = finishedItems.concat(Array.from(items, quoteMeta));
  213. if (conditional.length === 1) return conditional[0];
  214. return `(${conditional.join("|")})`;
  215. };
  216. compileBooleanMatcher.fromLists = compileBooleanMatcherFromLists;
  217. compileBooleanMatcher.itemsToRegexp = itemsToRegexp;
  218. module.exports = compileBooleanMatcher;