@mxfb/tools 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/TODO.md +1 -0
  2. package/agnostic/@publisher/index.d.ts +47 -0
  3. package/agnostic/@publisher/index.js +361 -0
  4. package/agnostic/elo/index.d.ts +23 -0
  5. package/agnostic/elo/index.js +28 -0
  6. package/agnostic/elo/index.test.js +67 -0
  7. package/agnostic/index.d.ts +5 -0
  8. package/agnostic/index.js +5 -0
  9. package/agnostic/poetry/generate-rhymes/index.d.ts +1 -0
  10. package/agnostic/poetry/generate-rhymes/index.js +125 -0
  11. package/agnostic/poetry/index.d.ts +1 -0
  12. package/agnostic/poetry/index.js +1 -0
  13. package/agnostic/scrapers/@meteociel/index.js +240 -186
  14. package/agnostic/scrapers/@meteociel/index.test.js +43 -0
  15. package/agnostic/scrapers/@meteociel/locations/index.js +9 -5
  16. package/agnostic/scrapers/@meteociel/types.js +1 -0
  17. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.d.ts +22 -0
  18. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.js +124 -0
  19. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.d.ts +1 -0
  20. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.js +107 -0
  21. package/agnostic/scrapers/@tumblr/blogs/index.d.ts +8 -0
  22. package/agnostic/scrapers/@tumblr/blogs/index.js +23 -0
  23. package/agnostic/scrapers/@tumblr/index.d.ts +73 -0
  24. package/agnostic/scrapers/@tumblr/index.js +180 -0
  25. package/agnostic/scrapers/@tumblr/index.test.d.ts +1 -0
  26. package/agnostic/scrapers/@tumblr/index.test.js +319 -0
  27. package/agnostic/scrapers/@tumblr/types.d.ts +238 -0
  28. package/agnostic/scrapers/@tumblr/types.js +38 -0
  29. package/agnostic/scrapers/@tumblr/utils.d.ts +12 -0
  30. package/agnostic/scrapers/@tumblr/utils.js +407 -0
  31. package/agnostic/scrapers/index.d.ts +2 -0
  32. package/agnostic/scrapers/index.js +2 -0
  33. package/agnostic/weather/index.d.ts +1 -0
  34. package/agnostic/weather/index.js +1 -0
  35. package/agnostic/weather/types.d.ts +2 -2
  36. package/agnostic/weather/types.js +8 -0
  37. package/components/index.d.ts +1 -0
  38. package/components/index.js +1 -0
  39. package/components/public-classnames.d.ts +5 -0
  40. package/components/public-classnames.js +5 -0
  41. package/components/utils/index.d.ts +1 -0
  42. package/components/utils/index.js +12 -0
  43. package/components/utils/types.d.ts +3 -0
  44. package/components/utils/types.js +1 -0
  45. package/index.d.ts +3 -0
  46. package/index.js +3 -0
  47. package/node/headless-browser/index.d.ts +37 -0
  48. package/node/headless-browser/index.js +126 -0
  49. package/node/headless-browser/types.d.ts +47 -0
  50. package/node/headless-browser/types.js +5 -0
  51. package/node/index.js +1 -0
  52. package/node/index.test.d.ts +1 -0
  53. package/node/index.test.js +6 -0
  54. package/node/scrapers/@tumblr/index.d.ts +61 -0
  55. package/node/scrapers/@tumblr/index.js +182 -0
  56. package/node/scrapers/@tumblr/index.test.d.ts +1 -0
  57. package/node/scrapers/@tumblr/index.test.js +185 -0
  58. package/node/scrapers/@tumblr/types.d.ts +91 -0
  59. package/node/scrapers/@tumblr/types.js +1 -0
  60. package/node/scrapers/@tumblr/utils.d.ts +23 -0
  61. package/node/scrapers/@tumblr/utils.js +328 -0
  62. package/node/scrapers/index.d.ts +1 -0
  63. package/node/scrapers/index.js +1 -0
  64. package/package.json +197 -31
  65. package/chunks/chunk-JQLUR5LO.js +0 -12
  66. package/components/Comp/index.js +0 -0
  67. /package/{components/Comp/index.d.ts → agnostic/elo/index.test.d.ts} +0 -0
package/TODO.md ADDED
@@ -0,0 +1 @@
1
+ # To do
@@ -0,0 +1,47 @@
1
+ import { Core, type EndpointName, type ContractOf, type BodyOf, type ParamsOf, type QueryOf, type FileFieldsOf, type ResponseOf, type ErrorResponseOf } from '@design-edito/publisher';
2
+ type Options<E extends EndpointName> = (BodyOf<E> extends null ? {} : {
3
+ body: BodyOf<E>;
4
+ }) & (QueryOf<E> extends null ? {} : {
5
+ query: QueryOf<E>;
6
+ }) & (ParamsOf<E> extends null ? {} : {
7
+ params: ParamsOf<E>;
8
+ }) & (FileFieldsOf<E> extends null ? {} : {
9
+ files: Partial<Record<NonNullable<FileFieldsOf<E>>, Array<File>>>;
10
+ });
11
+ type IsOptionsOptional<E extends EndpointName> = BodyOf<E> extends null ? (QueryOf<E> extends null ? (ParamsOf<E> extends null ? (FileFieldsOf<E> extends null ? true : false) : false) : false) : false;
12
+ type FormattedResponse<E extends EndpointName> = undefined | Blob | ResponseOf<E>;
13
+ type FetchParams<E extends EndpointName> = IsOptionsOptional<E> extends true ? [endpoint: E, options?: Options<E>, previousErrors?: Core.Errors.Codes[]] : [endpoint: E, options: Options<E>, previousErrors?: Core.Errors.Codes[]];
14
+ type OnRequestCallback = (request: {
15
+ url: string;
16
+ endpoint: EndpointName;
17
+ requestInit: RequestInit;
18
+ }) => void;
19
+ type OnResponseCallback = (response: FormattedResponse<EndpointName>, request: {
20
+ url: string;
21
+ endpoint: EndpointName;
22
+ requestInit: RequestInit;
23
+ }) => void;
24
+ type RequesterOptions = {
25
+ onRequest?: OnRequestCallback;
26
+ onResponse?: OnResponseCallback;
27
+ accessTokenLocalStorageKey?: string;
28
+ };
29
+ export default class Requester {
30
+ private baseUrl;
31
+ private onRequest;
32
+ private onResponse;
33
+ private storedAccessTokenKey;
34
+ private csrfToken;
35
+ private accessToken;
36
+ constructor(baseUrl: string, options?: RequesterOptions);
37
+ private setCsrfToken;
38
+ private setAccessToken;
39
+ private getServerResponse;
40
+ private csrfRefreshPromise;
41
+ private formatServerResponse;
42
+ private refreshCsrfToken;
43
+ private refreshAccessToken;
44
+ fetch<E extends EndpointName>(...args: FetchParams<E>): Promise<ContractOf<E>['type'] extends 'void' ? undefined | ErrorResponseOf<E> : ContractOf<E>['type'] extends 'file' ? Blob | ErrorResponseOf<E> : ResponseOf<E>>;
45
+ private contractsDetails;
46
+ }
47
+ export { Requester };
@@ -0,0 +1,361 @@
1
+ /* Linting is suspended on this file: 44 of its 74 findings come from the deferred
2
+ conditional response type and cannot be fixed without reworking it. See « Two files
3
+ opted out of linting » in README.md before removing this. */
4
+ /* eslint-disable
5
+ @typescript-eslint/array-type,
6
+ @typescript-eslint/class-methods-use-this,
7
+ @typescript-eslint/consistent-type-assertions,
8
+ @typescript-eslint/dot-notation,
9
+ @typescript-eslint/explicit-function-return-type,
10
+ @typescript-eslint/no-empty-object-type,
11
+ @typescript-eslint/no-explicit-any,
12
+ @typescript-eslint/no-unnecessary-condition,
13
+ @typescript-eslint/no-unnecessary-type-assertion,
14
+ @typescript-eslint/no-unsafe-argument,
15
+ @typescript-eslint/no-unsafe-assignment,
16
+ @typescript-eslint/no-unsafe-member-access,
17
+ @typescript-eslint/no-unsafe-return,
18
+ @typescript-eslint/no-unsafe-type-assertion,
19
+ @typescript-eslint/prefer-nullish-coalescing,
20
+ @typescript-eslint/prefer-readonly,
21
+ @typescript-eslint/return-await,
22
+ @typescript-eslint/strict-boolean-expressions,
23
+ object-shorthand,
24
+ preserve-caught-error,
25
+ require-unicode-regexp
26
+ */
27
+ import { minutes } from '@design-edito/tools/agnostic/time/duration/index.js';
28
+ import { unknownToString } from '@design-edito/tools/agnostic/errors/unknown-to-string/index.js';
29
+ import { Core, Endpoint } from '@design-edito/publisher';
30
+ const { Codes } = Core.Errors;
31
+ export default class Requester {
32
+ baseUrl;
33
+ onRequest;
34
+ onResponse;
35
+ storedAccessTokenKey;
36
+ csrfToken = { value: null, fetchedAt: null };
37
+ accessToken = { value: null, fetchedAt: null };
38
+ constructor(baseUrl, options) {
39
+ this.baseUrl = baseUrl;
40
+ this.onRequest = options?.onRequest;
41
+ this.onResponse = options?.onResponse;
42
+ this.fetch = this.fetch.bind(this);
43
+ this.storedAccessTokenKey = options?.accessTokenLocalStorageKey ?? 'access_token';
44
+ const storedToken = localStorage.getItem(`${this.storedAccessTokenKey}/value`);
45
+ const storedFetchedAt = localStorage.getItem(`${this.storedAccessTokenKey}/fetchedAt`);
46
+ if (storedToken) {
47
+ this.accessToken.value = storedToken;
48
+ this.accessToken.fetchedAt = storedFetchedAt ? parseInt(storedFetchedAt) : null;
49
+ }
50
+ }
51
+ setCsrfToken(token) {
52
+ // console.group('setCsrfToken')
53
+ // console.log('->', token)
54
+ this.csrfToken.value = token;
55
+ this.csrfToken.fetchedAt = token !== null ? Date.now() : null;
56
+ // console.log('<-', this.csrfToken)
57
+ // console.groupEnd()
58
+ }
59
+ setAccessToken(token) {
60
+ // console.group('setAccessToken')
61
+ // console.log('->', token)
62
+ this.accessToken.value = token;
63
+ const fetchedAt = Date.now();
64
+ this.accessToken.fetchedAt = token !== null ? fetchedAt : null;
65
+ if (token !== null) {
66
+ localStorage.setItem(`${this.storedAccessTokenKey}/value`, token);
67
+ localStorage.setItem(`${this.storedAccessTokenKey}/fetchedAt`, `${fetchedAt}`);
68
+ }
69
+ else {
70
+ localStorage.removeItem(`${this.storedAccessTokenKey}/value`);
71
+ localStorage.removeItem(`${this.storedAccessTokenKey}/fetchedAt`);
72
+ }
73
+ // console.log('<-', this.accessToken)
74
+ // console.groupEnd()
75
+ }
76
+ async getServerResponse(...args) {
77
+ // console.group('getServerResponse')
78
+ const [endpoint, options = {}] = args;
79
+ // console.log('->', { endpoint, options })
80
+ // Route
81
+ const route = Endpoint[endpoint];
82
+ // Method
83
+ const [rawMethod, ...rawPathColonSplitted] = route.split(':');
84
+ const method = rawMethod?.toUpperCase() ?? '';
85
+ if (method !== 'GET'
86
+ && method !== 'POST'
87
+ && method !== 'PUT'
88
+ && method !== 'DELETE')
89
+ throw new Error(`Invalid method: ${method}`);
90
+ // Path
91
+ const pathChunks = rawPathColonSplitted
92
+ .join(':')
93
+ .replace(/^\/+/, '')
94
+ .split('/');
95
+ const path = pathChunks.join('/');
96
+ // Payload
97
+ const { body: rawBody, query: rawQuery, params: rawParams, files: rawFiles } = options;
98
+ const { type, upload } = this.contractsDetails[endpoint];
99
+ const body = rawBody ?? undefined;
100
+ const query = rawQuery ?? undefined;
101
+ const params = (rawParams ?? undefined);
102
+ const files = rawFiles ?? {};
103
+ // Params
104
+ let pathString = path;
105
+ pathString = pathString.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
106
+ return params?.[key] !== undefined
107
+ ? params[key]
108
+ : `:${key}`;
109
+ });
110
+ // Query
111
+ let queryString = '';
112
+ Object
113
+ .entries(query ?? {})
114
+ .forEach(([key, value]) => {
115
+ const val = Array.isArray(value) ? value : [value];
116
+ val.forEach(v => {
117
+ queryString += `${key}=${v}&`;
118
+ });
119
+ });
120
+ queryString = queryString.slice(0, -1);
121
+ // URL
122
+ const url = new URL(pathString, this.baseUrl);
123
+ url.search = queryString;
124
+ // Form data
125
+ const formData = new FormData();
126
+ Object
127
+ .entries(files)
128
+ .forEach(([key, value]) => value?.forEach(file => formData.append(key, file)));
129
+ formData.append('_json', JSON.stringify(body ?? {}));
130
+ // Fetch
131
+ const headers = {};
132
+ if (this.accessToken.value !== null) {
133
+ headers['Authorization'] = `Bearer ${this.accessToken.value}`;
134
+ }
135
+ if (this.csrfToken.value !== null) {
136
+ headers['x-csrf-token'] = this.csrfToken.value;
137
+ }
138
+ if (!upload) {
139
+ headers['Content-Type'] = 'application/json';
140
+ }
141
+ if (type === 'file') {
142
+ headers['Accept'] = 'application/octet-stream';
143
+ }
144
+ else if (type === 'void') {
145
+ delete headers['Accept'];
146
+ }
147
+ else if (type === 'json') {
148
+ headers['Accept'] = 'application/json';
149
+ }
150
+ else if (type === 'list') {
151
+ headers['Accept'] = 'application/json';
152
+ }
153
+ else {
154
+ throw new Error(`Invalid type: ${type}`);
155
+ }
156
+ const reqUrl = url.toString();
157
+ const requestInit = {
158
+ method,
159
+ credentials: 'include',
160
+ body: method !== 'GET'
161
+ ? (upload ? formData : JSON.stringify(body ?? {}))
162
+ : undefined,
163
+ headers: headers
164
+ };
165
+ // console.log({ url, endpoint, headers })
166
+ const requestData = { url: reqUrl, endpoint, requestInit };
167
+ this.onRequest?.(requestData);
168
+ const res = await window.fetch(reqUrl, requestInit);
169
+ // Check returned access tokens
170
+ const rawAccessToken = res.headers.get('x-access-token');
171
+ // console.log({ respondedToken: rawAccessToken })
172
+ if (rawAccessToken !== null) {
173
+ const accessToken = rawAccessToken.split(/\s+/g).at(1) ?? null;
174
+ if (accessToken !== this.accessToken.value)
175
+ this.setAccessToken(accessToken);
176
+ }
177
+ const formattedResponse = await this.formatServerResponse(res, type);
178
+ this.onResponse?.(formattedResponse, requestData);
179
+ // console.log('<-', res)
180
+ // console.groupEnd()
181
+ return res;
182
+ }
183
+ csrfRefreshPromise = null;
184
+ async formatServerResponse(res, type) {
185
+ if (type === 'void')
186
+ return res.status === 204
187
+ ? undefined
188
+ : await res.clone().json();
189
+ else if (type === 'file') {
190
+ const contentType = res.headers.get('Content-Type');
191
+ if (contentType === 'application/json')
192
+ return await res.clone().json();
193
+ return await res.blob();
194
+ }
195
+ else if (type === 'json')
196
+ return await res.clone().json();
197
+ else if (type === 'list')
198
+ return await res.clone().json();
199
+ else
200
+ throw new Error(`Invalid endpoint type: ${type}`);
201
+ }
202
+ async refreshCsrfToken() {
203
+ // console.group('refreshCsrfToken')
204
+ if (this.csrfRefreshPromise !== null) {
205
+ // console.log('already pending')
206
+ // console.groupEnd()
207
+ return this.csrfRefreshPromise;
208
+ }
209
+ this.csrfRefreshPromise = (async () => {
210
+ const res = await this.getServerResponse('CSRF_GET_TOKEN');
211
+ const formattedRes = await this.formatServerResponse(res, 'json');
212
+ if (!res.ok || !formattedRes.success) {
213
+ this.setCsrfToken(null);
214
+ return false;
215
+ }
216
+ const newToken = formattedRes.payload.token;
217
+ this.setCsrfToken(newToken);
218
+ return true;
219
+ })().finally(() => {
220
+ this.csrfRefreshPromise = null;
221
+ });
222
+ // console.log(this.csrfRefreshPromise)
223
+ // console.groupEnd()
224
+ return this.csrfRefreshPromise;
225
+ }
226
+ async refreshAccessToken() {
227
+ // console.group('refreshAccessToken')
228
+ const res = await this.getServerResponse('AUTH_REFRESH_TOKEN');
229
+ if (!res.ok) {
230
+ this.setAccessToken(null);
231
+ // console.groupEnd()
232
+ return false;
233
+ }
234
+ const rawToken = res.headers.get('x-access-token');
235
+ if (rawToken === null) {
236
+ this.setAccessToken(null);
237
+ // console.groupEnd()
238
+ return false;
239
+ }
240
+ const token = rawToken.split(/\s+/g).at(1) ?? '';
241
+ if (token !== this.accessToken.value)
242
+ this.setAccessToken(token);
243
+ // console.groupEnd()
244
+ return true;
245
+ }
246
+ async fetch(...args) {
247
+ // console.group('fetch')
248
+ const [endpoint, optionsOrPrevErrors = {}, _previousErrors = []] = args;
249
+ const options = Array.isArray(optionsOrPrevErrors)
250
+ ? {}
251
+ : optionsOrPrevErrors;
252
+ const previousErrors = Array.isArray(optionsOrPrevErrors)
253
+ ? optionsOrPrevErrors
254
+ : _previousErrors;
255
+ // console.log('->', { endpoint, options, previousErrors })
256
+ const { type, auth } = this.contractsDetails[endpoint];
257
+ // Refresh CSRF token if needed
258
+ const now = Date.now();
259
+ if (this.csrfToken.value === null
260
+ || this.csrfToken.fetchedAt === null
261
+ || now - this.csrfToken.fetchedAt > minutes(20).toMilliseconds())
262
+ await this.refreshCsrfToken();
263
+ // Refresh access token if needed
264
+ if (auth !== 'none' && this.accessToken.value === null)
265
+ await this.refreshAccessToken();
266
+ let res;
267
+ let formatted;
268
+ try {
269
+ res = await this.getServerResponse(endpoint, options);
270
+ formatted = await this.formatServerResponse(res, type);
271
+ if (endpoint === 'AUTH_LOGOUT'
272
+ || endpoint === 'AUTH_LOGOUT_EVERYWHERE')
273
+ this.setAccessToken(null);
274
+ }
275
+ catch (err) {
276
+ throw new Error(`Request failed: ${unknownToString(err)}`);
277
+ }
278
+ if (formatted === undefined) { /*console.log('<-', formatted); console.groupEnd(); */
279
+ return formatted;
280
+ }
281
+ if (formatted instanceof Blob) { /*console.log('<-', formatted); console.groupEnd(); */
282
+ return formatted;
283
+ }
284
+ if (formatted.success) { /*console.log('<-', formatted); console.groupEnd();*/
285
+ return formatted;
286
+ }
287
+ if (previousErrors.includes(formatted.error.code)) { /*console.log('<-', formatted); console.groupEnd();*/
288
+ return formatted;
289
+ }
290
+ const newPreviousErrors = [...previousErrors, formatted.error.code];
291
+ // Invalid CSRF token
292
+ if (formatted.error.code === Codes.INVALID_CSRF_TOKEN) {
293
+ await this.refreshCsrfToken();
294
+ // console.log('<-', formatted)
295
+ // console.groupEnd()
296
+ return this.fetch(endpoint, options, newPreviousErrors);
297
+ }
298
+ // Invalid access token
299
+ if (formatted.error.code === Codes.INVALID_ACCESS_TOKEN) {
300
+ await this.refreshAccessToken();
301
+ // console.log('<-', formatted)
302
+ // console.groupEnd()
303
+ return this.fetch(endpoint, options, newPreviousErrors);
304
+ }
305
+ // console.log('<-', formatted)
306
+ // console.groupEnd()
307
+ return formatted;
308
+ }
309
+ contractsDetails = {
310
+ // Core
311
+ ADMIN_TEMP_FLUSH: { type: 'void', auth: 'strong', upload: false },
312
+ ADMIN_USERS_CREATE: { type: 'json', auth: 'strong', upload: false },
313
+ ADMIN_USERS_DELETE: { type: 'json', auth: 'strong', upload: false },
314
+ ADMIN_USERS_GET: { type: 'json', auth: 'strong', upload: false },
315
+ ADMIN_USERS_GET_UPLOAD_QUOTA: { type: 'json', auth: 'strong', upload: false },
316
+ ADMIN_USERS_LIST: { type: 'list', auth: 'strong', upload: false },
317
+ ADMIN_USERS_RESET_UPLOAD_QUOTA: { type: 'json', auth: 'strong', upload: false },
318
+ ADMIN_USERS_REVOKE_AUTH_TOKENS: { type: 'void', auth: 'strong', upload: false },
319
+ ADMIN_USERS_REVOKE_EMAIL_VALIDATION_TOKENS: { type: 'void', auth: 'strong', upload: false },
320
+ ADMIN_USERS_REVOKE_PASSWORD_RENEWAL_TOKENS: { type: 'void', auth: 'strong', upload: false },
321
+ ADMIN_USERS_UPDATE: { type: 'json', auth: 'strong', upload: false },
322
+ AUTH_LOGIN: { type: 'json', auth: 'none', upload: false },
323
+ AUTH_LOGOUT: { type: 'void', auth: 'weak', upload: false },
324
+ AUTH_LOGOUT_EVERYWHERE: { type: 'void', auth: 'weak', upload: false },
325
+ AUTH_REFRESH_TOKEN: { type: 'void', auth: 'none', upload: false },
326
+ AUTH_REQUEST_EMAIL_VERIFICATION_TOKEN: { type: 'void', auth: 'none', upload: false },
327
+ AUTH_REQUEST_NEW_PASSWORD: { type: 'void', auth: 'none', upload: false },
328
+ AUTH_SIGNUP: { type: 'json', auth: 'none', upload: false },
329
+ AUTH_SUBMIT_NEW_PASSWORD: { type: 'void', auth: 'none', upload: false },
330
+ AUTH_VERIFY_EMAIL: { type: 'json', auth: 'none', upload: false },
331
+ AUTH_WHOAMI: { type: 'json', auth: 'strong', upload: false },
332
+ AUTH_WHOAMI_GET: { type: 'json', auth: 'strong', upload: false },
333
+ CSRF_GET_TOKEN: { type: 'json', auth: 'none', upload: false },
334
+ EXAMPLE_PLUGIN__SOMETHING: { type: 'json', auth: 'none', upload: false },
335
+ IMAGE_FORMAT: { type: 'file', auth: 'strong', upload: true },
336
+ IMAGE_TRANSFORM: { type: 'file', auth: 'strong', upload: true },
337
+ SYSTEM_KILL: { type: 'void', auth: 'strong', upload: false },
338
+ SYSTEM_PING: { type: 'json', auth: 'none', upload: false },
339
+ SYSTEM_SEND_MAIL: { type: 'void', auth: 'strong', upload: false },
340
+ SYSTEM_STATUS_CHECK_GET: { type: 'json', auth: 'none', upload: false },
341
+ SYSTEM_STATUS_CHECK_POST: { type: 'json', auth: 'none', upload: false },
342
+ USERS_GET: { type: 'json', auth: 'none', upload: false },
343
+ // Plugins
344
+ TEST_PLUGIN__FILE: { type: 'file', auth: 'none', upload: false },
345
+ TEST_PLUGIN__FILE_POST: { type: 'file', auth: 'none', upload: false },
346
+ TEST_PLUGIN__FILE_POST_AUTH: { type: 'file', auth: 'strong', upload: false },
347
+ TEST_PLUGIN__JSON: { type: 'json', auth: 'none', upload: false },
348
+ TEST_PLUGIN__JSON_POST: { type: 'json', auth: 'none', upload: false },
349
+ TEST_PLUGIN__JSON_POST_AUTH: { type: 'json', auth: 'strong', upload: false },
350
+ TEST_PLUGIN__LIST: { type: 'list', auth: 'none', upload: false },
351
+ TEST_PLUGIN__LIST_POST: { type: 'list', auth: 'none', upload: false },
352
+ TEST_PLUGIN__LIST_POST_AUTH: { type: 'list', auth: 'strong', upload: false },
353
+ TEST_PLUGIN__VOID: { type: 'void', auth: 'none', upload: false },
354
+ TEST_PLUGIN__VOID_POST: { type: 'void', auth: 'none', upload: false },
355
+ TEST_PLUGIN__VOID_POST_AUTH: { type: 'void', auth: 'strong', upload: false },
356
+ DESIGN_EDITO_THUMBS_UPLOAD_TEMP: { type: 'json', auth: 'strong', upload: true },
357
+ DESIGN_EDITO_THUMBS_DOWNLOAD_TEMP: { type: 'file', auth: 'strong', upload: false },
358
+ DESIGN_EDITO_THUMBS_TEMPLATE_BOOK_GENERATE: { type: 'file', auth: 'strong', upload: false }
359
+ };
360
+ }
361
+ export { Requester };
@@ -0,0 +1,23 @@
1
+ /** Default K-factor used in Elo rating updates. Determines the maximum change in rating per game. */
2
+ export declare const DEFAULT_K_FACTOR = 32;
3
+ /** Default scale used in Elo probability calculation. Represents the rating difference that corresponds to ~90% win probability. */
4
+ export declare const DEFAULT_SCALE = 400;
5
+ /**
6
+ * Calculates the expected probability of player A winning against player B.
7
+ *
8
+ * @param {number} eloA - Elo rating of player A.
9
+ * @param {number} eloB - Elo rating of player B.
10
+ * @param {number} [scale=DEFAULT_SCALE] - Scale factor controlling sensitivity to rating differences.
11
+ * @returns {number} Probability that player A wins (between 0 and 1).
12
+ */
13
+ export declare function getWinProbability(eloA: number, eloB: number, scale?: number): number;
14
+ /**
15
+ * Updates a player's Elo rating based on the expected probability and actual outcome.
16
+ *
17
+ * @param {number} winProbability - Expected probability of winning (from getWinProbability).
18
+ * @param {number} actualOutcome - Actual result of the match (0 = loss, 0.5 = draw, 1 = win; can be fractional for weighted outcomes).
19
+ * @param {number} currentElo - Player's current Elo rating.
20
+ * @param {number} [kFactor=DEFAULT_K_FACTOR] - Maximum rating change per game.
21
+ * @returns {number} Updated Elo rating.
22
+ */
23
+ export declare function updateRating(winProbability: number, actualOutcome: number, currentElo: number, kFactor?: number): number;
@@ -0,0 +1,28 @@
1
+ /** Default K-factor used in Elo rating updates. Determines the maximum change in rating per game. */
2
+ export const DEFAULT_K_FACTOR = 32;
3
+ /** Default scale used in Elo probability calculation. Represents the rating difference that corresponds to ~90% win probability. */
4
+ export const DEFAULT_SCALE = 400;
5
+ /**
6
+ * Calculates the expected probability of player A winning against player B.
7
+ *
8
+ * @param {number} eloA - Elo rating of player A.
9
+ * @param {number} eloB - Elo rating of player B.
10
+ * @param {number} [scale=DEFAULT_SCALE] - Scale factor controlling sensitivity to rating differences.
11
+ * @returns {number} Probability that player A wins (between 0 and 1).
12
+ */
13
+ export function getWinProbability(eloA, eloB, scale = DEFAULT_SCALE) {
14
+ const exponent = (eloB - eloA) / scale;
15
+ return 1 / (1 + Math.pow(10, exponent));
16
+ }
17
+ /**
18
+ * Updates a player's Elo rating based on the expected probability and actual outcome.
19
+ *
20
+ * @param {number} winProbability - Expected probability of winning (from getWinProbability).
21
+ * @param {number} actualOutcome - Actual result of the match (0 = loss, 0.5 = draw, 1 = win; can be fractional for weighted outcomes).
22
+ * @param {number} currentElo - Player's current Elo rating.
23
+ * @param {number} [kFactor=DEFAULT_K_FACTOR] - Maximum rating change per game.
24
+ * @returns {number} Updated Elo rating.
25
+ */
26
+ export function updateRating(winProbability, actualOutcome, currentElo, kFactor = DEFAULT_K_FACTOR) {
27
+ return currentElo + kFactor * (actualOutcome - winProbability);
28
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { DEFAULT_K_FACTOR, DEFAULT_SCALE, getWinProbability, updateRating } from './index.js';
3
+ describe('getWinProbability', () => {
4
+ it('returns 0.5 for equal ratings', () => {
5
+ expect(getWinProbability(1500, 1500)).toBeCloseTo(0.5);
6
+ });
7
+ it('returns higher than 0.5 when first player is stronger', () => {
8
+ expect(getWinProbability(1600, 1400)).toBeGreaterThan(0.5);
9
+ });
10
+ it('returns lower than 0.5 when first player is weaker', () => {
11
+ expect(getWinProbability(1400, 1600)).toBeLessThan(0.5);
12
+ });
13
+ it('respects a custom scale', () => {
14
+ const customScale = 200;
15
+ const p1 = getWinProbability(1600, 1400, customScale);
16
+ const p2 = getWinProbability(1600, 1400, DEFAULT_SCALE);
17
+ expect(p1).toBeGreaterThan(p2); // smaller scale → bigger difference
18
+ });
19
+ });
20
+ describe('updateRating', () => {
21
+ it('does not change rating if actual equals expected', () => {
22
+ const current = 1500;
23
+ const expected = 0.5;
24
+ const actual = 0.5;
25
+ const newRating = updateRating(expected, actual, current);
26
+ expect(newRating).toBeCloseTo(current);
27
+ });
28
+ it('increases rating if actual > expected', () => {
29
+ const current = 1500;
30
+ const expected = 0.3;
31
+ const actual = 1;
32
+ const newRating = updateRating(expected, actual, current);
33
+ expect(newRating).toBeGreaterThan(current);
34
+ });
35
+ it('decreases rating if actual < expected', () => {
36
+ const current = 1500;
37
+ const expected = 0.7;
38
+ const actual = 0;
39
+ const newRating = updateRating(expected, actual, current);
40
+ expect(newRating).toBeLessThan(current);
41
+ });
42
+ it('accepts fractional actualOutcome (for margin of victory)', () => {
43
+ const current = 1500;
44
+ const expected = 0.4;
45
+ const actual = 0.8;
46
+ const newRating = updateRating(expected, actual, current);
47
+ expect(newRating).toBeGreaterThan(current);
48
+ expect(newRating).toBeLessThan(current + DEFAULT_K_FACTOR); // should not exceed max K
49
+ });
50
+ it('respects custom K-factor', () => {
51
+ const current = 1500;
52
+ const expected = 0.5;
53
+ const actual = 1;
54
+ const customK = 50;
55
+ const newRatingDefault = updateRating(expected, actual, current);
56
+ const newRatingCustom = updateRating(expected, actual, current, customK);
57
+ expect(newRatingCustom - current).toBeGreaterThan(newRatingDefault - current);
58
+ });
59
+ });
60
+ describe('CONSTANTS', () => {
61
+ it('DEFAULT_K_FACTOR is 32', () => {
62
+ expect(DEFAULT_K_FACTOR).toBe(32);
63
+ });
64
+ it('DEFAULT_SCALE is 400', () => {
65
+ expect(DEFAULT_SCALE).toBe(400);
66
+ });
67
+ });
@@ -0,0 +1,5 @@
1
+ export * as publisher from './@publisher/index.js'
2
+ export * as elo from './elo/index.js'
3
+ export * as poetry from './poetry/index.js'
4
+ export * as scrapers from './scrapers/index.js'
5
+ export * as weather from './weather/index.js'
@@ -0,0 +1,5 @@
1
+ export * as publisher from './@publisher/index.js'
2
+ export * as elo from './elo/index.js'
3
+ export * as poetry from './poetry/index.js'
4
+ export * as scrapers from './scrapers/index.js'
5
+ export * as weather from './weather/index.js'
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,125 @@
1
+ /* Linting is suspended on this file — dormant project, left as it was found. See
2
+ « Two files opted out of linting » in README.md before removing this. */
3
+ /* eslint-disable
4
+ @typescript-eslint/no-unsafe-type-assertion,
5
+ @typescript-eslint/no-unused-vars,
6
+ no-console
7
+ */
8
+ import { shuffle } from '@design-edito/tools/agnostic/arrays/shuffle/index.js';
9
+ /* * * * * * * * * * * * * * * * *
10
+ *
11
+ * Rhyme Generator
12
+ *
13
+ * * * * * * * * * * * * * * * * */
14
+ function getRandomSum(n) {
15
+ if (n <= 0 || !Number.isInteger(n))
16
+ throw new Error('n doit être un entier positif');
17
+ const result = [];
18
+ let remaining = n;
19
+ while (remaining > 0) {
20
+ const value = Math.floor((1 - (Math.random() * Math.random())) * remaining) + 1; // [1, remaining]
21
+ result.push(value);
22
+ remaining -= value;
23
+ }
24
+ return result;
25
+ }
26
+ const getWordsByFeet = (feet, wordsRecord) => Object
27
+ .entries(wordsRecord)
28
+ .filter(([, value]) => value === feet)
29
+ .map(([key]) => key);
30
+ const getWords = (feet, wordsRecord, coeff) => {
31
+ const foundWordsThatLong = shuffle(getWordsByFeet(feet, wordsRecord))[0];
32
+ if (feet === 1) {
33
+ if (foundWordsThatLong === undefined)
34
+ throw new Error('need at least 1 word that is 1 foot long');
35
+ return [foundWordsThatLong];
36
+ }
37
+ if (foundWordsThatLong !== undefined
38
+ && Math.random() < coeff)
39
+ return [foundWordsThatLong];
40
+ let randomSubParts = [];
41
+ while (randomSubParts.length < 2) {
42
+ randomSubParts = getRandomSum(feet);
43
+ }
44
+ return randomSubParts.map(ft => getWords(ft, wordsRecord, coeff)).flat();
45
+ };
46
+ const flattenRhymeRecord = (rhymRec) => {
47
+ return Object.entries(rhymRec).reduce((acc, [rhyme, words]) => {
48
+ return { ...acc, ...words };
49
+ }, {});
50
+ };
51
+ const getRhyme = (suffix, feet, rhymeRecord, coeff) => {
52
+ const foundRhymeThatLong = shuffle(getWordsByFeet(feet, rhymeRecord[suffix] ?? {}))[0];
53
+ if (feet === 1) {
54
+ if (foundRhymeThatLong === undefined)
55
+ return [`${suffix} *`]; //throw new Error('need at least 1 word that is 1 foot long')
56
+ return [foundRhymeThatLong];
57
+ }
58
+ if (foundRhymeThatLong !== undefined
59
+ && Math.random() < coeff)
60
+ return [foundRhymeThatLong];
61
+ let randomSubParts = [];
62
+ while (randomSubParts.length < 2) {
63
+ randomSubParts = getRandomSum(feet);
64
+ }
65
+ return randomSubParts.map((ft, pos) => {
66
+ if (pos === randomSubParts.length - 1)
67
+ return getRhyme(suffix, ft, rhymeRecord, coeff);
68
+ return shuffle(getWords(ft, flattenRhymeRecord(rhymeRecord), coeff));
69
+ }).flat();
70
+ };
71
+ const getPoem = function (stanza, rhymeRecord, coeff) {
72
+ return stanza.map(({ length, rhyme }) => length === 0 ? '' : getRhyme(rhyme, length, rhymeRecord, coeff).join(' ')).join('\n');
73
+ };
74
+ /* * * * * * * * * * * * * * * * *
75
+ *
76
+ * TESTS - Rhyme Generator
77
+ *
78
+ * * * * * * * * * * * * * * * * */
79
+ const rhymeRecord = {
80
+ '-a': {
81
+ 'a': 1,
82
+ },
83
+ '-è': {
84
+ 'eh': 1,
85
+ }
86
+ };
87
+ const _rhymeRecordTypecheck = rhymeRecord;
88
+ const COEFF = .85;
89
+ console.log(`
90
+
91
+ ${getPoem([{
92
+ length: 4,
93
+ rhyme: '-a'
94
+ }, {
95
+ length: 4,
96
+ rhyme: '-a'
97
+ }, {
98
+ length: 3,
99
+ rhyme: '-è'
100
+ }, {
101
+ length: 0
102
+ }, {
103
+ length: 2,
104
+ rhyme: '-a'
105
+ }, {
106
+ length: 0
107
+ }, {
108
+ length: 2,
109
+ rhyme: '-è'
110
+ }, {
111
+ length: 4,
112
+ rhyme: '-è'
113
+ }, {
114
+ length: 0
115
+ }, {
116
+ length: 4,
117
+ rhyme: '-a'
118
+ }, {
119
+ length: 3,
120
+ rhyme: '-è'
121
+ }, {
122
+ length: 1542,
123
+ rhyme: '-a'
124
+ }], rhymeRecord, COEFF)}
125
+ `);
@@ -0,0 +1 @@
1
+ export * as generateRhymes from './generate-rhymes/index.js'