| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | /* | 
					
						
							|  |  |  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
					
						
							|  |  |  | 	Author Tobias Koppers @sokra | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {string} str string | 
					
						
							|  |  |  |  * @returns {string} quoted meta | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-07-31 11:31:11 +08:00
										 |  |  | const quoteMeta = str => str.replace(/[-[\]\\/{}()*+?.^$|]/g, "\\$&"); | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {string} str string | 
					
						
							|  |  |  |  * @returns {string} string | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | const toSimpleString = str => { | 
					
						
							| 
									
										
										
										
											2024-07-31 11:11:11 +08:00
										 |  |  | 	if (`${Number(str)}` === str) { | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 		return str; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return JSON.stringify(str); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {Record<string|number, boolean>} map value map | 
					
						
							| 
									
										
										
										
											2022-02-08 20:29:56 +08:00
										 |  |  |  * @returns {boolean|(function(string): string)} true/false, when unconditionally true/false, or a template function to determine the value at runtime | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  |  */ | 
					
						
							|  |  |  | const compileBooleanMatcher = map => { | 
					
						
							|  |  |  | 	const positiveItems = Object.keys(map).filter(i => map[i]); | 
					
						
							|  |  |  | 	const negativeItems = Object.keys(map).filter(i => !map[i]); | 
					
						
							|  |  |  | 	if (positiveItems.length === 0) return false; | 
					
						
							|  |  |  | 	if (negativeItems.length === 0) return true; | 
					
						
							| 
									
										
										
										
											2020-10-23 20:52:23 +08:00
										 |  |  | 	return compileBooleanMatcherFromLists(positiveItems, negativeItems); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @param {string[]} positiveItems positive items | 
					
						
							|  |  |  |  * @param {string[]} negativeItems negative items | 
					
						
							|  |  |  |  * @returns {function(string): string} a template function to determine the value at runtime | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const compileBooleanMatcherFromLists = (positiveItems, negativeItems) => { | 
					
						
							| 
									
										
										
										
											2020-10-27 18:14:17 +08:00
										 |  |  | 	if (positiveItems.length === 0) return () => "false"; | 
					
						
							|  |  |  | 	if (negativeItems.length === 0) return () => "true"; | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	if (positiveItems.length === 1) | 
					
						
							|  |  |  | 		return value => `${toSimpleString(positiveItems[0])} == ${value}`; | 
					
						
							|  |  |  | 	if (negativeItems.length === 1) | 
					
						
							|  |  |  | 		return value => `${toSimpleString(negativeItems[0])} != ${value}`; | 
					
						
							|  |  |  | 	const positiveRegexp = itemsToRegexp(positiveItems); | 
					
						
							|  |  |  | 	const negativeRegexp = itemsToRegexp(negativeItems); | 
					
						
							|  |  |  | 	if (positiveRegexp.length <= negativeRegexp.length) { | 
					
						
							|  |  |  | 		return value => `/^${positiveRegexp}$/.test(${value})`; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-07-31 04:21:27 +08:00
										 |  |  | 	return value => `!/^${negativeRegexp}$/.test(${value})`; | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {Set<string>} itemsSet items set | 
					
						
							|  |  |  |  * @param {(str: string) => string | false} getKey get key function | 
					
						
							|  |  |  |  * @param {(str: Array<string>) => boolean} condition condition | 
					
						
							|  |  |  |  * @returns {Array<Array<string>>} list of common items | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | const popCommonItems = (itemsSet, getKey, condition) => { | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | 	/** @type {Map<string, Array<string>>} */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	const map = new Map(); | 
					
						
							|  |  |  | 	for (const item of itemsSet) { | 
					
						
							|  |  |  | 		const key = getKey(item); | 
					
						
							|  |  |  | 		if (key) { | 
					
						
							|  |  |  | 			let list = map.get(key); | 
					
						
							|  |  |  | 			if (list === undefined) { | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | 				/** @type {Array<string>} */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 				list = []; | 
					
						
							|  |  |  | 				map.set(key, list); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			list.push(item); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | 	/** @type {Array<Array<string>>} */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	const result = []; | 
					
						
							|  |  |  | 	for (const list of map.values()) { | 
					
						
							|  |  |  | 		if (condition(list)) { | 
					
						
							|  |  |  | 			for (const item of list) { | 
					
						
							|  |  |  | 				itemsSet.delete(item); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			result.push(list); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return result; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {Array<string>} items items | 
					
						
							|  |  |  |  * @returns {string} common prefix | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | const getCommonPrefix = items => { | 
					
						
							|  |  |  | 	let prefix = items[0]; | 
					
						
							|  |  |  | 	for (let i = 1; i < items.length; i++) { | 
					
						
							|  |  |  | 		const item = items[i]; | 
					
						
							|  |  |  | 		for (let p = 0; p < prefix.length; p++) { | 
					
						
							|  |  |  | 			if (item[p] !== prefix[p]) { | 
					
						
							|  |  |  | 				prefix = prefix.slice(0, p); | 
					
						
							|  |  |  | 				break; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return prefix; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {Array<string>} items items | 
					
						
							|  |  |  |  * @returns {string} common suffix | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | const getCommonSuffix = items => { | 
					
						
							|  |  |  | 	let suffix = items[0]; | 
					
						
							|  |  |  | 	for (let i = 1; i < items.length; i++) { | 
					
						
							|  |  |  | 		const item = items[i]; | 
					
						
							|  |  |  | 		for (let p = item.length - 1, s = suffix.length - 1; s >= 0; p--, s--) { | 
					
						
							|  |  |  | 			if (item[p] !== suffix[s]) { | 
					
						
							|  |  |  | 				suffix = suffix.slice(s + 1); | 
					
						
							|  |  |  | 				break; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return suffix; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @param {Array<string>} itemsArr array of items | 
					
						
							|  |  |  |  * @returns {string} regexp | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | const itemsToRegexp = itemsArr => { | 
					
						
							|  |  |  | 	if (itemsArr.length === 1) { | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 		return quoteMeta(itemsArr[0]); | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | 	/** @type {Array<string>} */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	const finishedItems = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// merge single char items: (a|b|c|d|ef) => ([abcd]|ef)
 | 
					
						
							|  |  |  | 	let countOfSingleCharItems = 0; | 
					
						
							|  |  |  | 	for (const item of itemsArr) { | 
					
						
							|  |  |  | 		if (item.length === 1) { | 
					
						
							|  |  |  | 			countOfSingleCharItems++; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	// special case for only single char items
 | 
					
						
							|  |  |  | 	if (countOfSingleCharItems === itemsArr.length) { | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 		return `[${quoteMeta(itemsArr.sort().join(""))}]`; | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 	const items = new Set(itemsArr.sort()); | 
					
						
							|  |  |  | 	if (countOfSingleCharItems > 2) { | 
					
						
							|  |  |  | 		let singleCharItems = ""; | 
					
						
							|  |  |  | 		for (const item of items) { | 
					
						
							|  |  |  | 			if (item.length === 1) { | 
					
						
							|  |  |  | 				singleCharItems += item; | 
					
						
							|  |  |  | 				items.delete(item); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 		finishedItems.push(`[${quoteMeta(singleCharItems)}]`); | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// special case for 2 items with common prefix/suffix
 | 
					
						
							|  |  |  | 	if (finishedItems.length === 0 && items.size === 2) { | 
					
						
							|  |  |  | 		const prefix = getCommonPrefix(itemsArr); | 
					
						
							| 
									
										
										
										
											2020-12-22 19:29:08 +08:00
										 |  |  | 		const suffix = getCommonSuffix( | 
					
						
							| 
									
										
										
										
											2020-12-22 21:24:32 +08:00
										 |  |  | 			itemsArr.map(item => item.slice(prefix.length)) | 
					
						
							| 
									
										
										
										
											2020-12-22 19:29:08 +08:00
										 |  |  | 		); | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 		if (prefix.length > 0 || suffix.length > 0) { | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 			return `${quoteMeta(prefix)}${itemsToRegexp( | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 				itemsArr.map(i => i.slice(prefix.length, -suffix.length || undefined)) | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 			)}${quoteMeta(suffix)}`;
 | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// special case for 2 items with common suffix
 | 
					
						
							|  |  |  | 	if (finishedItems.length === 0 && items.size === 2) { | 
					
						
							| 
									
										
										
										
											2023-05-01 04:51:59 +08:00
										 |  |  | 		/** @type {Iterator<string>} */ | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 		const it = items[Symbol.iterator](); | 
					
						
							|  |  |  | 		const a = it.next().value; | 
					
						
							|  |  |  | 		const b = it.next().value; | 
					
						
							|  |  |  | 		if (a.length > 0 && b.length > 0 && a.slice(-1) === b.slice(-1)) { | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 			return `${itemsToRegexp([a.slice(0, -1), b.slice(0, -1)])}${quoteMeta( | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 				a.slice(-1) | 
					
						
							|  |  |  | 			)}`;
 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// find common prefix: (a1|a2|a3|a4|b5) => (a(1|2|3|4)|b5)
 | 
					
						
							|  |  |  | 	const prefixed = popCommonItems( | 
					
						
							|  |  |  | 		items, | 
					
						
							|  |  |  | 		item => (item.length >= 1 ? item[0] : false), | 
					
						
							|  |  |  | 		list => { | 
					
						
							|  |  |  | 			if (list.length >= 3) return true; | 
					
						
							|  |  |  | 			if (list.length <= 1) return false; | 
					
						
							|  |  |  | 			return list[0][1] === list[1][1]; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | 	for (const prefixedItems of prefixed) { | 
					
						
							|  |  |  | 		const prefix = getCommonPrefix(prefixedItems); | 
					
						
							|  |  |  | 		finishedItems.push( | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 			`${quoteMeta(prefix)}${itemsToRegexp( | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 				prefixedItems.map(i => i.slice(prefix.length)) | 
					
						
							|  |  |  | 			)}`
 | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// find common suffix: (a1|b1|c1|d1|e2) => ((a|b|c|d)1|e2)
 | 
					
						
							|  |  |  | 	const suffixed = popCommonItems( | 
					
						
							|  |  |  | 		items, | 
					
						
							|  |  |  | 		item => (item.length >= 1 ? item.slice(-1) : false), | 
					
						
							|  |  |  | 		list => { | 
					
						
							|  |  |  | 			if (list.length >= 3) return true; | 
					
						
							|  |  |  | 			if (list.length <= 1) return false; | 
					
						
							|  |  |  | 			return list[0].slice(-2) === list[1].slice(-2); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | 	for (const suffixedItems of suffixed) { | 
					
						
							|  |  |  | 		const suffix = getCommonSuffix(suffixedItems); | 
					
						
							|  |  |  | 		finishedItems.push( | 
					
						
							|  |  |  | 			`${itemsToRegexp( | 
					
						
							|  |  |  | 				suffixedItems.map(i => i.slice(0, -suffix.length)) | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 			)}${quoteMeta(suffix)}`
 | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 		); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// TODO further optimize regexp, i. e.
 | 
					
						
							|  |  |  | 	// use ranges: (1|2|3|4|a) => [1-4a]
 | 
					
						
							| 
									
										
										
										
											2020-03-13 17:28:55 +08:00
										 |  |  | 	const conditional = finishedItems.concat(Array.from(items, quoteMeta)); | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | 	if (conditional.length === 1) return conditional[0]; | 
					
						
							|  |  |  | 	return `(${conditional.join("|")})`; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-23 20:52:23 +08:00
										 |  |  | compileBooleanMatcher.fromLists = compileBooleanMatcherFromLists; | 
					
						
							|  |  |  | compileBooleanMatcher.itemsToRegexp = itemsToRegexp; | 
					
						
							| 
									
										
										
										
											2019-05-22 19:07:10 +08:00
										 |  |  | module.exports = compileBooleanMatcher; |