tiny-essentials 1.20.0 → 1.20.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 (55) hide show
  1. package/README.md +5 -0
  2. package/dist/v1/TinyBasicsEs.min.js +1 -1
  3. package/dist/v1/TinyDragger.min.js +1 -1
  4. package/dist/v1/TinyEssentials.min.js +1 -1
  5. package/dist/v1/TinyHtml.min.js +1 -1
  6. package/dist/v1/TinySmartScroller.min.js +1 -1
  7. package/dist/v1/TinyUploadClicker.min.js +1 -1
  8. package/dist/v1/basics/html.cjs +33 -2
  9. package/dist/v1/basics/html.d.mts +14 -4
  10. package/dist/v1/basics/html.mjs +27 -2
  11. package/dist/v1/basics/index.cjs +2 -0
  12. package/dist/v1/basics/index.d.mts +3 -1
  13. package/dist/v1/basics/index.mjs +3 -3
  14. package/dist/v1/basics/simpleMath.cjs +23 -4
  15. package/dist/v1/basics/simpleMath.d.mts +18 -4
  16. package/dist/v1/basics/simpleMath.mjs +22 -4
  17. package/dist/v1/index.cjs +2 -0
  18. package/dist/v1/index.d.mts +3 -1
  19. package/dist/v1/index.mjs +3 -3
  20. package/dist/v1/libs/TinyDragger.cjs +1 -1
  21. package/dist/v1/libs/TinyDragger.mjs +1 -1
  22. package/dist/v1/libs/TinyHtml.cjs +500 -105
  23. package/dist/v1/libs/TinyHtml.d.mts +251 -42
  24. package/dist/v1/libs/TinyHtml.mjs +448 -96
  25. package/dist/v1/libs/TinyIframeEvents.cjs +2 -1
  26. package/dist/v1/libs/TinyNewWinEvents.cjs +4 -2
  27. package/docs/v1/basics/html.md +78 -22
  28. package/docs/v1/basics/simpleMath.md +22 -4
  29. package/docs/v1/libs/TinyHtml.md +268 -6
  30. package/package.json +1 -1
  31. package/dist/v1/ColorSafeStringify.js +0 -235
  32. package/dist/v1/TinyAfterScrollWatcher.js +0 -219
  33. package/dist/v1/TinyBasicsEs.js +0 -9334
  34. package/dist/v1/TinyClipboard.js +0 -459
  35. package/dist/v1/TinyColorConverter.js +0 -617
  36. package/dist/v1/TinyDomReadyManager.js +0 -213
  37. package/dist/v1/TinyDragDropDetector.js +0 -307
  38. package/dist/v1/TinyDragger.js +0 -6569
  39. package/dist/v1/TinyEssentials.js +0 -20792
  40. package/dist/v1/TinyEvents.js +0 -402
  41. package/dist/v1/TinyHtml.js +0 -5545
  42. package/dist/v1/TinyIframeEvents.js +0 -854
  43. package/dist/v1/TinyLevelUp.js +0 -291
  44. package/dist/v1/TinyLocalStorage.js +0 -1440
  45. package/dist/v1/TinyNewWinEvents.js +0 -888
  46. package/dist/v1/TinyNotifications.js +0 -408
  47. package/dist/v1/TinyNotifyCenter.js +0 -493
  48. package/dist/v1/TinyPromiseQueue.js +0 -299
  49. package/dist/v1/TinyRateLimiter.js +0 -611
  50. package/dist/v1/TinySmartScroller.js +0 -7039
  51. package/dist/v1/TinyTextRangeEditor.js +0 -497
  52. package/dist/v1/TinyTimeout.js +0 -233
  53. package/dist/v1/TinyToastNotify.js +0 -441
  54. package/dist/v1/TinyUploadClicker.js +0 -14353
  55. package/dist/v1/UltraRandomMsgGen.js +0 -995
@@ -1,611 +0,0 @@
1
- /******/ (() => { // webpackBootstrap
2
- /******/ "use strict";
3
- /******/ // The require scope
4
- /******/ var __webpack_require__ = {};
5
- /******/
6
- /************************************************************************/
7
- /******/ /* webpack/runtime/define property getters */
8
- /******/ (() => {
9
- /******/ // define getter functions for harmony exports
10
- /******/ __webpack_require__.d = (exports, definition) => {
11
- /******/ for(var key in definition) {
12
- /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
13
- /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
14
- /******/ }
15
- /******/ }
16
- /******/ };
17
- /******/ })();
18
- /******/
19
- /******/ /* webpack/runtime/hasOwnProperty shorthand */
20
- /******/ (() => {
21
- /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
22
- /******/ })();
23
- /******/
24
- /************************************************************************/
25
- var __webpack_exports__ = {};
26
-
27
- // EXPORTS
28
- __webpack_require__.d(__webpack_exports__, {
29
- TinyRateLimiter: () => (/* reexport */ libs_TinyRateLimiter)
30
- });
31
-
32
- ;// ./src/v1/libs/TinyRateLimiter.mjs
33
- /** @typedef {(groupId: string) => void} OnMemoryExceeded */
34
-
35
- /** @typedef {(groupId: string) => void} OnGroupExpired */
36
-
37
- /**
38
- * Class representing a flexible rate limiter per user or group.
39
- *
40
- * This rate limiter supports limiting per user or per group by mapping
41
- * userIds to a common groupId. All users within the same group share
42
- * rate limits.
43
- */
44
- class TinyRateLimiter {
45
- /** @type {number|null} */
46
- #maxMemory = null;
47
-
48
- /** @type {NodeJS.Timeout|null} */
49
- #cleanupTimer = null;
50
-
51
- /** @type {number|null|undefined} */
52
- #maxHits = null;
53
-
54
- /** @type {number|null|undefined} */
55
- #interval = null;
56
-
57
- /** @type {number|null|undefined} */
58
- #cleanupInterval = null;
59
-
60
- /** @type {number|null|undefined} */
61
- #maxIdle = null;
62
-
63
- /** @type {Map<string, number[]>} */
64
- groupData = new Map(); // groupId -> timestamps[]
65
-
66
- /** @type {Map<string, number>} */
67
- lastSeen = new Map(); // groupId -> timestamp
68
-
69
- /** @type {Map<string, string>} */
70
- userToGroup = new Map(); // userId -> groupId
71
-
72
- /** @type {Map<string, boolean>} */
73
- groupFlags = new Map(); // groupId -> boolean
74
-
75
- /**
76
- * @type {Map<string, number>}
77
- * Stores TTL (in ms) for each groupId individually
78
- */
79
- groupTTL = new Map();
80
-
81
- /**
82
- * @type {null|OnMemoryExceeded}
83
- */
84
- #onMemoryExceeded = null;
85
-
86
- /**
87
- * Set the callback to be triggered when a group exceeds its limit
88
- * @param {OnMemoryExceeded} callback
89
- */
90
- setOnMemoryExceeded(callback) {
91
- if (typeof callback !== 'function') throw new Error('onMemoryExceeded must be a function');
92
- this.#onMemoryExceeded = callback;
93
- }
94
-
95
- /**
96
- * Clear the onMemoryExceeded callback
97
- */
98
- clearOnMemoryExceeded() {
99
- this.#onMemoryExceeded = null;
100
- }
101
-
102
- /**
103
- * @type {null|OnGroupExpired}
104
- */
105
- #onGroupExpired = null;
106
-
107
- /**
108
- * Set the callback to be triggered when a group expires and is removed.
109
- *
110
- * This callback is called automatically during cleanup when a group
111
- * becomes inactive for longer than its TTL.
112
- *
113
- * @param {OnGroupExpired} callback - A function that receives the expired groupId.
114
- */
115
- setOnGroupExpired(callback) {
116
- if (typeof callback !== 'function') throw new Error('onGroupExpired must be a function');
117
- this.#onGroupExpired = callback;
118
- }
119
-
120
- /**
121
- * Clear the onGroupExpired callback
122
- */
123
- clearOnGroupExpired() {
124
- this.#onGroupExpired = null;
125
- }
126
-
127
- /**
128
- * @param {Object} options
129
- * @param {number|null} [options.maxMemory] - Max memory allowed
130
- * @param {number} [options.maxHits] - Max interactions allowed
131
- * @param {number} [options.interval] - Time window in milliseconds
132
- * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
133
- * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
134
- */
135
- constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
136
- /** @param {number|undefined} val */
137
- const isPositiveInteger = (val) =>
138
- typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
139
-
140
- const isMaxHitsValid = isPositiveInteger(maxHits);
141
- const isIntervalValid = isPositiveInteger(interval);
142
- const isCleanupValid = isPositiveInteger(cleanupInterval);
143
- const isMaxIdleValid = isPositiveInteger(maxIdle);
144
-
145
- if (!isMaxHitsValid && !isIntervalValid)
146
- throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
147
- if (maxHits !== undefined && !isMaxHitsValid)
148
- throw new Error("'maxHits' must be a positive integer if defined.");
149
- if (interval !== undefined && !isIntervalValid)
150
- throw new Error("'interval' must be a positive integer in milliseconds if defined.");
151
- if (cleanupInterval !== undefined && !isCleanupValid)
152
- throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
153
- if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
154
-
155
- if (typeof maxMemory === 'number' && Number.isFinite(maxMemory) && maxMemory > 0) {
156
- this.#maxMemory = Math.floor(maxMemory);
157
- } else if (maxMemory === null || maxMemory === undefined) {
158
- this.#maxMemory = null;
159
- } else {
160
- throw new Error('maxMemory must be a positive number or null');
161
- }
162
-
163
- this.#maxHits = isMaxHitsValid ? maxHits : null;
164
- this.#interval = isIntervalValid ? interval : null;
165
- this.#cleanupInterval = isCleanupValid ? cleanupInterval : null;
166
- this.#maxIdle = maxIdle;
167
-
168
- // Start automatic cleanup only if cleanupInterval is valid
169
- if (this.#cleanupInterval !== null)
170
- this.#cleanupTimer = setInterval(() => this._cleanup(), this.#cleanupInterval);
171
- }
172
-
173
- /**
174
- * Check if a given ID is a groupId (not a userId)
175
- * @param {string} id
176
- * @returns {boolean}
177
- */
178
- isGroupId(id) {
179
- const result = this.groupFlags.get(id);
180
- return typeof result === 'boolean' ? result : false;
181
- }
182
-
183
- /**
184
- * Get all user IDs that belong to a given group.
185
- * @param {string} groupId
186
- * @returns {string[]}
187
- */
188
- getUsersInGroup(groupId) {
189
- const users = [];
190
- for (const [userId, assignedGroup] of this.userToGroup.entries()) {
191
- if (assignedGroup === groupId) {
192
- users.push(userId);
193
- }
194
- }
195
-
196
- return users;
197
- }
198
-
199
- /**
200
- * Set TTL (in milliseconds) for a specific group
201
- * @param {string} groupId
202
- * @param {number} ttl
203
- */
204
- setGroupTTL(groupId, ttl) {
205
- if (typeof ttl !== 'number' || !Number.isFinite(ttl) || ttl <= 0)
206
- throw new Error('TTL must be a positive number in milliseconds');
207
- this.groupTTL.set(groupId, ttl);
208
- }
209
-
210
- /**
211
- * Get TTL (in ms) for a specific group.
212
- * @param {string} groupId
213
- * @returns {number|null}
214
- */
215
- getGroupTTL(groupId) {
216
- return this.groupTTL.get(groupId) ?? null;
217
- }
218
-
219
- /**
220
- * Delete the TTL setting for a specific group
221
- * @param {string} groupId
222
- */
223
- deleteGroupTTL(groupId) {
224
- this.groupTTL.delete(groupId);
225
- }
226
-
227
- /**
228
- * Assign a userId to a groupId, with merge if user has existing data.
229
- * @param {string} userId
230
- * @param {string} groupId
231
- * @throws {Error} If userId is already assigned to a different group
232
- */
233
- assignToGroup(userId, groupId) {
234
- const existingGroup = this.userToGroup.get(userId);
235
- if (existingGroup && existingGroup !== groupId)
236
- throw new Error(`User ${userId} is already assigned to group ${existingGroup}`);
237
-
238
- // If the user is already in the group, nothing needs to be done
239
- if (existingGroup === groupId) return;
240
- const userData = this.groupData.get(userId);
241
-
242
- // Associates the user to the group
243
- if (this.isGroupId(userId)) {
244
- for (const [uid, gId] of this.userToGroup.entries())
245
- if (gId === userId) this.userToGroup.set(uid, groupId);
246
- this.userToGroup.delete(userId);
247
- } else this.userToGroup.set(userId, groupId);
248
-
249
- // If the user has no data, nothing needs to be done
250
- if (!userData) return;
251
-
252
- const groupData = this.groupData.get(groupId);
253
- if (groupData) {
254
- for (const item of userData) groupData.push(item);
255
- } else {
256
- const newData = [];
257
- for (const item of userData) newData.push(item);
258
- this.groupData.set(groupId, newData);
259
- }
260
-
261
- this.lastSeen.set(groupId, Date.now());
262
-
263
- // Removes individual data as they are now in the group
264
- this.groupFlags.delete(userId);
265
- this.groupData.delete(userId);
266
- this.lastSeen.delete(userId);
267
- this.groupTTL.delete(userId);
268
- this.groupFlags.set(groupId, true);
269
- }
270
-
271
- /**
272
- * Get the groupId for a given userId
273
- * @param {string} userId
274
- * @returns {string}
275
- */
276
- getGroupId(userId) {
277
- return this.userToGroup.get(userId) || userId; // fallback: use userId as own group
278
- }
279
-
280
- /**
281
- * Register a hit for a specific user
282
- * @param {string} userId
283
- */
284
- hit(userId) {
285
- const groupId = this.getGroupId(userId);
286
- const now = Date.now();
287
-
288
- if (!this.groupData.has(groupId)) {
289
- this.groupData.set(groupId, []);
290
- this.groupFlags.set(groupId, false);
291
- }
292
-
293
- const history = this.groupData.get(groupId);
294
- if (!history) throw new Error(`No data found for groupId: ${groupId}`);
295
-
296
- history.push(now);
297
- this.lastSeen.set(groupId, now);
298
-
299
- // Clean up old entries
300
- if (this.#interval !== null) {
301
- const interval = this.getInterval();
302
- const cutoff = now - interval;
303
- while (history.length && history[0] < cutoff) {
304
- history.shift();
305
- }
306
- }
307
-
308
- // Optional: keep only the last N entries for memory optimization
309
- if (this.#maxMemory !== null && typeof this.#maxMemory === 'number') {
310
- if (history.length > this.#maxMemory) {
311
- history.splice(0, history.length - this.#maxMemory);
312
- if (typeof this.#onMemoryExceeded === 'function') this.#onMemoryExceeded(groupId);
313
- }
314
- }
315
- }
316
-
317
- /**
318
- * Check if the user (via their group) is currently rate limited
319
- * @param {string} userId
320
- * @returns {boolean}
321
- */
322
- isRateLimited(userId) {
323
- const groupId = this.getGroupId(userId);
324
- if (!this.groupData.has(groupId)) return false;
325
-
326
- const history = this.groupData.get(groupId);
327
- if (!history) throw new Error(`No data found for groupId: ${groupId}`);
328
-
329
- if (this.#interval !== null) {
330
- const now = Date.now();
331
- const interval = this.getInterval();
332
- const cutoff = now - interval;
333
- let count = 0;
334
- for (let i = history.length - 1; i >= 0; i--) {
335
- if (history[i] > cutoff) count++;
336
- else break;
337
- }
338
- if (this.#maxHits !== null) return count > this.getMaxHits();
339
- return count > 0;
340
- }
341
-
342
- if (this.#maxHits !== null) {
343
- return history.length > this.getMaxHits();
344
- }
345
-
346
- return false;
347
- }
348
-
349
- /**
350
- * Manually reset group data
351
- * @param {string} groupId
352
- */
353
- resetGroup(groupId) {
354
- this.groupFlags.delete(groupId);
355
- this.groupData.delete(groupId);
356
- this.lastSeen.delete(groupId);
357
- this.groupTTL.delete(groupId);
358
- }
359
-
360
- /**
361
- * Manually reset user data.
362
- *
363
- * @deprecated Use `resetUserGroup(userId)` instead. This method will be removed in future versions.
364
- * @param {string} userId
365
- * @returns {void}
366
- */
367
- reset(userId) {
368
- if (false)
369
- // removed by dead control flow
370
- {}
371
- return this.resetUserGroup(userId);
372
- }
373
-
374
- /**
375
- * Manually reset a user mapping
376
- * @param {string} userId
377
- */
378
- resetUserGroup(userId) {
379
- this.userToGroup.delete(userId);
380
- }
381
-
382
- /**
383
- * Set custom timestamps to a group
384
- * @param {string} groupId
385
- * @param {number[]} timestamps
386
- */
387
- setData(groupId, timestamps) {
388
- if (!Array.isArray(timestamps)) throw new Error('timestamps must be an array of numbers.');
389
- for (const t of timestamps) {
390
- if (typeof t !== 'number' || !Number.isFinite(t)) {
391
- throw new Error('All timestamps must be finite numbers.');
392
- }
393
- }
394
- if (!this.groupData.has(groupId)) this.groupFlags.set(groupId, false);
395
- this.groupData.set(groupId, timestamps);
396
- this.lastSeen.set(groupId, Date.now());
397
- }
398
-
399
- /**
400
- * Check if a group has data
401
- * @param {string} groupId
402
- * @returns {boolean}
403
- */
404
- hasData(groupId) {
405
- return this.groupData.has(groupId);
406
- }
407
-
408
- /**
409
- * Get timestamps from a group
410
- * @param {string} groupId
411
- * @returns {number[]}
412
- */
413
- getData(groupId) {
414
- return this.groupData.get(groupId) || [];
415
- }
416
-
417
- /**
418
- * Get the maximum idle time (in milliseconds) before a group is considered expired.
419
- * @returns {number}
420
- */
421
- getMaxIdle() {
422
- if (typeof this.#maxIdle !== 'number' || !Number.isFinite(this.#maxIdle) || this.#maxIdle < 0) {
423
- throw new Error("'maxIdle' must be a non-negative finite number.");
424
- }
425
- return this.#maxIdle;
426
- }
427
-
428
- /**
429
- * Set the maximum idle time (in milliseconds) before a group is considered expired.
430
- * @param {number} ms
431
- */
432
- setMaxIdle(ms) {
433
- if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) {
434
- throw new Error("'maxIdle' must be a non-negative finite number.");
435
- }
436
- this.#maxIdle = ms;
437
- }
438
-
439
- /**
440
- * Cleanup old/inactive groups with individual TTLs
441
- * @private
442
- */
443
- _cleanup() {
444
- const now = Date.now();
445
- for (const [groupId, last] of this.lastSeen.entries()) {
446
- const ttl = this.getGroupTTL(groupId) ?? this.getMaxIdle();
447
- if (now - last > ttl) {
448
- this.groupFlags.delete(groupId);
449
- this.groupData.delete(groupId);
450
- this.lastSeen.delete(groupId);
451
- this.groupTTL.delete(groupId);
452
-
453
- // Notify subclass or external binding
454
- if (typeof this.#onGroupExpired === 'function') {
455
- this.#onGroupExpired(groupId);
456
- }
457
- }
458
- }
459
- }
460
-
461
- /**
462
- * Get list of active group IDs
463
- * @returns {string[]}
464
- */
465
- getActiveGroups() {
466
- return Array.from(this.groupData.keys());
467
- }
468
-
469
- /**
470
- * Get a shallow copy of all user-to-group mappings as a plain object
471
- * @returns {Record<string, string>}
472
- */
473
- getAllUserMappings() {
474
- return Object.fromEntries(this.userToGroup);
475
- }
476
-
477
- /**
478
- * Get the interval window in milliseconds.
479
- * @returns {number}
480
- */
481
- getInterval() {
482
- if (typeof this.#interval !== 'number' || !Number.isFinite(this.#interval)) {
483
- throw new Error("'interval' is not a valid finite number.");
484
- }
485
- return this.#interval;
486
- }
487
-
488
- /**
489
- * Get the maximum number of allowed hits.
490
- * @returns {number}
491
- */
492
- getMaxHits() {
493
- if (typeof this.#maxHits !== 'number' || !Number.isFinite(this.#maxHits)) {
494
- throw new Error("'maxHits' is not a valid finite number.");
495
- }
496
- return this.#maxHits;
497
- }
498
-
499
- /**
500
- * Get the total number of hits recorded for a group.
501
- * @param {string} groupId
502
- * @returns {number}
503
- */
504
- getTotalHits(groupId) {
505
- const history = this.groupData.get(groupId);
506
- return Array.isArray(history) ? history.length : 0;
507
- }
508
-
509
- /**
510
- * Get the timestamp of the last hit for a group.
511
- * @param {string} groupId
512
- * @returns {number|null}
513
- */
514
- getLastHit(groupId) {
515
- const history = this.groupData.get(groupId);
516
- return history?.length ? history[history.length - 1] : null;
517
- }
518
-
519
- /**
520
- * Get milliseconds since the last hit for a group.
521
- * @param {string} groupId
522
- * @returns {number|null}
523
- */
524
- getTimeSinceLastHit(groupId) {
525
- const last = this.getLastHit(groupId);
526
- return last !== null ? Date.now() - last : null;
527
- }
528
-
529
- /**
530
- * Internal utility to compute average spacing
531
- * @private
532
- * @param {number[]|undefined} history
533
- * @returns {number|null}
534
- */
535
- _calculateAverageSpacing(history) {
536
- if (!Array.isArray(history) || history.length < 2) return null;
537
- let total = 0;
538
- for (let i = 1; i < history.length; i++) {
539
- total += history[i] - history[i - 1];
540
- }
541
- return total / (history.length - 1);
542
- }
543
-
544
- /**
545
- * Get average time between hits for a group (ms).
546
- * @param {string} groupId
547
- * @returns {number|null}
548
- */
549
- getAverageHitSpacing(groupId) {
550
- return this._calculateAverageSpacing(this.groupData.get(groupId));
551
- }
552
-
553
- /**
554
- * Get metrics about a group's activity.
555
- * @param {string} groupId
556
- * @returns {{
557
- * totalHits: number,
558
- * lastHit: number|null,
559
- * timeSinceLastHit: number|null,
560
- * averageHitSpacing: number|null
561
- * }}
562
- */
563
- getMetrics(groupId) {
564
- const history = this.groupData.get(groupId);
565
-
566
- if (!Array.isArray(history) || history.length === 0) {
567
- return {
568
- totalHits: 0,
569
- lastHit: null,
570
- timeSinceLastHit: null,
571
- averageHitSpacing: null,
572
- };
573
- }
574
-
575
- const totalHits = history.length;
576
- const lastHit = history[totalHits - 1];
577
- const timeSinceLastHit = Date.now() - lastHit;
578
- const averageHitSpacing = this._calculateAverageSpacing(history);
579
-
580
- return {
581
- totalHits,
582
- lastHit,
583
- timeSinceLastHit,
584
- averageHitSpacing,
585
- };
586
- }
587
-
588
- /**
589
- * Destroy the rate limiter, stopping cleanup and clearing data
590
- */
591
- destroy() {
592
- if (this.#cleanupTimer) clearInterval(this.#cleanupTimer);
593
- this._cleanup();
594
- this.groupData.clear();
595
- this.lastSeen.clear();
596
- this.userToGroup.clear();
597
- this.groupTTL.clear();
598
- this.groupFlags.clear();
599
- }
600
- }
601
-
602
- /* harmony default export */ const libs_TinyRateLimiter = (TinyRateLimiter);
603
-
604
- ;// ./src/v1/build/TinyRateLimiter.mjs
605
-
606
-
607
-
608
-
609
- window.TinyRateLimiter = __webpack_exports__.TinyRateLimiter;
610
- /******/ })()
611
- ;