compileBooleanMatcher.js 5.7 KB

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