-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathmodUtils.js
134 lines (127 loc) · 6.52 KB
/
modUtils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// @ts-check
import UglifyJS from 'uglify-js';
// https://stackoverflow.com/a/63838890
const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
export function minifyCode(/** @type {string} */ script) {
const output = UglifyJS.minify(script, {
compress: false,
mangle: false
});
if (output.error) throw output.error;
if (output.warnings) throw (output.warnings);
if (output.warnings) console.warn(output.warnings);
return output.code;
}
class ModUtils {
script = "";
/** @type {{[key: string]: string}} */
dictionary = {};
/** @type {Function[]} */
postMinifyHandlers = [];
constructor(/** @type {string} */ script) {
this.script = script;
// Bind methods
this.matchDictionaryExpression = this.matchDictionaryExpression.bind(this);
this.generateRegularExpression = this.generateRegularExpression.bind(this);
this.replace = this.replace.bind(this);
this.replaceOne = this.replaceOne.bind(this);
this.replaceRawCode = this.replaceRawCode.bind(this);
this.matchOne = this.matchOne.bind(this);
this.matchRawCode = this.matchRawCode.bind(this);
this.replaceCode = this.replaceCode.bind(this);
this.waitForMinification = this.waitForMinification.bind(this);
}
/** @param {RegExp} expression */
matchDictionaryExpression(expression) {
const result = this.matchOne(expression);
// @ts-ignore
for (let [key, value] of Object.entries(result.groups)) this.addToDictionary(key, value);
}
replace(/** @type {Parameters<typeof String.prototype.replace>} */ ...args) {
return this.script = this.script.replace(...args);
};
/** Expressions passed to this function must have the global flag set. */
matchOne(/** @type {RegExp} */ expression) {
const result = expression.exec(this.script);
if (result === null) throw new Error("no match for: " + expression.toString());
if (expression.exec(this.script) !== null) throw new Error("more than one match for: " + expression.toString());
return result;
};
addToDictionary(/** @type {string} */ key, /** @type {string} */ value) {
if (this.dictionary[key] !== undefined && this.dictionary[key] !== value)
throw new Error("name different from existing one:\n KEY: " + key + "\n VALUE: " + value + "\n Value in dictionary: " + this.dictionary[key]);
this.dictionary[key] = value;
};
/**
* @param {RegExp} expression
* @param {string} replaceValue
*/
replaceOne(expression, replaceValue) {
const result = this.matchOne(expression);
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
this.script = this.script.replace(expression, replaceValue);
return result;
};
// Return value example:
// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code
// and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
let localizerCount = 0;
let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
.replace(/\w+/g, match => {
// these would get stored as "___localizer1", "___localizer2", ...
if (match === "__L") match = "___localizer" + (++localizerCount);
return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
});
//console.log(replacementString);
let expressionMatchResult;
try { expressionMatchResult = this.replaceOne(expression, replacementString); }
catch (e) {
throw new Error("replaceRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
}
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
}
matchRawCode(/** @type {string} */ raw, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
const expressionMatchResult = this.matchOne(expression);
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
}
generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) {
const groups = {};
let groupNumberCounter = 1;
let localizerCount = 0;
let raw = escapeRegExp(code).replaceAll("__L\\(\\)", "___localizer\\)")
// when there is a parameter, add a comma to separate it from the added number
.replaceAll("__L\\(", "___localizer,");
raw = raw.replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
// if a substitution string for the "word" is specified in the nameMappings, use it
if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
// if the "word" is a number or is one of these specific words, ingore it
if (/^\d/.test(word) || ["return", "this", "var", "function", "new", "Math", "WebSocket"].includes(word)) return word;
// for easy localizer function matching
else if (word === "___localizer") {
groups[word + (++localizerCount)] = groupNumberCounter++;
return "\\b(L\\(\\d+)"; // would match "L(123", "L(50" and etc. when using "__L("
}
else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group
else {
groups[word] = groupNumberCounter++;
return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
}
});
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
return { expression, groups };
}
replaceCode(code, replacement, options) {
return this.replaceRawCode(minifyCode(code), replacement);
}
waitForMinification(/** @type {Function} */ handler) {
this.postMinifyHandlers.push(handler);
}
executePostMinifyHandlers() {
this.postMinifyHandlers.forEach(handler => handler());
}
escapeRegExp = escapeRegExp
}
export default ModUtils;