-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeneratesymbols.js
executable file
·307 lines (265 loc) · 10.5 KB
/
generatesymbols.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import { writeFileSync } from 'fs';
import { JSDOM } from 'jsdom';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import TurndownService from 'turndown';
const REGEX_SPLIT = /\w+\s+\w+/g;
const REGEX_OPTIONAL_PARAMS = /\[([^\]]+)\]/g;
const WIKI_HREF_REPLACE_REGEX = /href="\/wiki\/(.*?)"/g;
const BASE_URL_WIKI = 'https://wiki.multitheftauto.com';
const RATE_LIMIT_MS = 1500;
const DEPRECATED_URL = '/wiki/Category:Deprecated';
const turndownService = new TurndownService();
/** Simple mapping for the type and availability. */
const TYPE_AVAILABLE_MAP = {
'Shared function': {
'type': 'method',
'available': 'shared'
},
'Shared event': {
'type': 'event',
'available': 'shared'
},
'Client-side function': {
'type': 'method',
'available': 'client'
},
'Client-side event': {
'type': 'event',
'available': 'client'
},
'Server-side function': {
'type': 'method',
'available': 'server'
},
'Server-side event': {
'type': 'event',
'available': 'server'
}
}
/** These are all the categories from the Wiki pages. */
const WIKI_EXTRACT_URLS = {
'https://wiki.multitheftauto.com/wiki/Client_Scripting_Functions': 'client',
// 'https://wiki.multitheftauto.com/wiki/Client_Scripting_Events': 'client',
// 'https://wiki.multitheftauto.com/wiki/Server_Scripting_Functions': 'server',
// 'https://wiki.multitheftauto.com/wiki/Server_Scripting_Events': 'server',
// 'https://wiki.multitheftauto.com/wiki/Shared_Scripting_Functions: 'shared'
};
/** Our results which will be written into a file. */
let results = {};
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* This method extracts the required data from the URL article.
* @param {string} functionName Function name we are going to extract from the wiki.
* @return {Object} Returns an object that can be saved into JSON.
*/
export const getSymbolFromURL = async (functionName, desiredType) => {
const wikiBaseURL = "https://wiki.multitheftauto.com/wiki";
const url = `${wikiBaseURL}/${functionName}`;
const response = await fetch(url);
if (response.status != 200) {
return Promise.reject(response);
}
const data = await response?.text();
if (!data?.length) {
return Promise.reject(data);
}
const virtualDoc = new JSDOM(data);
const querySelectorElement = 'pre.prettyprint';
let element = virtualDoc.window.document.querySelector(querySelectorElement);
let description = '';
let deprecated = false;
const type = virtualDoc.window.document.querySelector("[name='headingclass']");
const mainContent = virtualDoc.window.document.querySelector('.mw-parser-output');
const categories = virtualDoc.window.document.querySelector('#mw-normal-catlinks');
Array.from(mainContent.querySelectorAll('p')).forEach(p => {
const h2 = mainContent.querySelector('h2');
if (h2 && p.compareDocumentPosition(h2) & 4) {
description = description + (p?.innerHTML ?? '');
}
});
if (description?.length) {
// Turn the links inside the description to use the wiki base URL
description = description.replace(
WIKI_HREF_REPLACE_REGEX,
`href="${BASE_URL_WIKI}/wiki/$1"`
);
// Turn it to Markdown so it is interpreted by VSCode correctly.
description = turndownService.turndown(description);
}
if (categories && categories.querySelector(`a[href='${DEPRECATED_URL}']`)) {
deprecated = true;
}
const desiredAvailability = TYPE_AVAILABLE_MAP[desiredType];
if (desiredAvailability && desiredType == 'shared') {
element = virtualDoc.window.document.querySelector(`.serverContent ${querySelectorElement}`);
}
return Promise.resolve({content: element.textContent?.trim(), type: type?.getAttribute("data-subcaption"), description, deprecated});
}
/**
* This method interprets the data from the Wiki and then sets it in an object which
* can be stringified.
* @param {string} result Original result, in plaintext format.
* @returns {Object} Returns an object.
*/
export const interpretData = (result, description, deprecated, config) => {
console.log(result);
const splittedWords = result.match(REGEX_SPLIT);
const optionalParams = result.match(REGEX_OPTIONAL_PARAMS); // At max it will only contain 1 item.
const interpreted = {};
let functionName = "";
splittedWords.forEach((param, idx) => {
// We first split using spaces.
const splittedData = param.split(" ");
// first iteration corresponds to the type and function name.
if (idx == 0) {
functionName = splittedData[1];
interpreted[functionName] = {
type: splittedData[0],
parameters: []
}
} else {
// The rest are parameter iterations.
let param = {
type: splittedData[0],
name: splittedData[1]
};
if (optionalParams?.length) {
// We get the default values until the next comma, using the parameter we found. This will
// include until the "=".
const defaultValues = optionalParams[0]?.match(new RegExp(`${splittedData[1]}[^,]*`, 'g')) ?? [];
if (defaultValues?.length) {
defaultValues.forEach((defaultValue) => {
if (defaultValue.includes(splittedData[1])) {
const splittedParam = defaultValue.split('=');
// Some articles for some reason lack the default value.
// Maybe report these to MTA devs?
if (splittedParam?.length === 1) {
console.warn(`[${functionName}]: Optional param likely missing default value from the Wiki. To check, and if it is really missing, report this to the devs?`);
} else {
console.log(splittedParam);
param = {
...param,
value: splittedParam[1].replace(/[\[\]\s]+/g, '')
}
}
}
});
}
}
// Append the data to the resulting object.
interpreted[functionName] = {
...interpreted[functionName],
parameters: [
...interpreted[functionName]?.parameters,
param
]
}
}
});
// Append the config to the resulting object.
interpreted[functionName] = {
...interpreted[functionName],
...config,
description,
deprecated
}
return interpreted;
}
/**
* This method appends the data into our results.
* @param {Object} result Result data that we have generated
*/
const appendToResults = (result) => {
const config = TYPE_AVAILABLE_MAP[result?.type] ?? {};
let interpreted = interpretData(result?.content, result?.description, result?.deprecated, config);
results = {
...results,
...interpreted,
};
}
/**
* This method just sleeps... yeah.
* @param {number} ms Miliseconds to sleep for.
* @returns {Promise} Returns a promise, but it's not used.
*/
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* This is the main method that generates the data into an output JSON file. Iterates over the categories
* and for each article, makes a request to get all the associated wiki information.
* @returns {Promise} Returns a resolved promise.
*/
const generateData = async (functionName = null, type = null) => {
const generateFromCategories = async () => {
const urls = Object.entries(WIKI_EXTRACT_URLS);
for (const [wikiUrl, desiredType] of urls) {
const response = await fetch(wikiUrl);
const data = await response?.text();
if (!data) {
console.log(`[${wikiUrl}]: no data found.`);
return Promise.reject('No data.');
}
const domData = new JSDOM(data);
const linkLists = domData.window.document.querySelectorAll("h2 + ul:not([class])");
const linkListCount = linkLists?.length;
let currentListLinkCount = 0;
for (const ulList of linkLists) {
currentListLinkCount++;
const links = ulList.querySelectorAll('a:not(:has(span))');
const linkCount = links?.length ?? 0;
let count = 0;
console.log(`[${wikiUrl}] Starting list ${currentListLinkCount}/${linkListCount}`);
for (const link of links) {
generate(link?.textContent, desiredType, wikiUrl);
count++;
console.log(`[${wikiUrl}] Done. Result appended to results array. Current: ${count}/${linkCount}`);
// To avoid rate limits.
await sleep(RATE_LIMIT_MS);
}
console.log(`[${wikiUrl}] List complete. Current: ${currentListLinkCount}/${linkListCount}`);
}
}
}
const generate = async (name, type, wikiUrl = null) => {
console.log(`[${wikiUrl ?? name}] Interpreting ${name}`);
const uninterpreted = await getSymbolFromURL(name, type);
appendToResults(uninterpreted);
}
if (functionName && type) {
await generateFromCategories();
} else {
await generate(functionName, type);
}
writeFile();
return Promise.resolve(results);
}
/**
* This method writes into the disk the results.
*/
const writeFile = (name = 'generated') => {
const pathName = `${__dirname}/src/symbols/${name}.json`;
console.log(`[GENERAL] Writing to ${pathName}`);
writeFileSync(pathName, JSON.stringify(results));
console.log(`[GENERAL] Done.`);
}
// Output the current data in results if interrupted.
process.on('SIGINT', () => {
console.log('[GENERAL] Interrupt signal received.');
writeFile();
process.exit(1);
});
process.on("uncaughtException", (error) => {
console.error('[GENERAL] Uncaught exception');
console.error(`[GENERAL] ${error}`);
console.error(error.stack);
writeFile('unfinished');
process.exit(1);
});
generateData('shutdown');
/**
* pages to review:
* https://wiki.multitheftauto.com/wiki/OutputChatBox
*
*/