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