@hichchi/utils 0.0.1-beta.2 → 0.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.
- package/index.cjs.default.js +1 -0
- package/index.cjs.js +3645 -0
- package/index.cjs.mjs +2 -0
- package/index.d.ts +1 -5
- package/index.esm.js +3575 -0
- package/package.json +13 -8
- package/src/index.d.ts +5 -0
- package/{interfaces → src/interfaces}/infinite-object.interface.d.ts +3 -3
- package/{interfaces → src/interfaces}/path-value-set.interface.d.ts +12 -12
- package/{types → src/types}/deep-partial.type.d.ts +6 -0
- package/{types → src/types}/literal-object.type.d.ts +2 -2
- package/{utils → src/utils}/index.d.ts +1 -0
- package/{utils → src/utils}/object.utils.d.ts +23 -19
- package/CHANGELOG.md +0 -19
- package/README.md +0 -7944
- package/constants/constants.js +0 -227
- package/constants/constants.js.map +0 -1
- package/constants/english-inflection-rules.js +0 -360
- package/constants/english-inflection-rules.js.map +0 -1
- package/constants/index.js +0 -6
- package/constants/index.js.map +0 -1
- package/enums/index.js +0 -5
- package/enums/index.js.map +0 -1
- package/enums/template-tag.enum.js +0 -39
- package/enums/template-tag.enum.js.map +0 -1
- package/index.js +0 -9
- package/index.js.map +0 -1
- package/interfaces/index.js +0 -7
- package/interfaces/index.js.map +0 -1
- package/interfaces/infinite-object.interface.js +0 -3
- package/interfaces/infinite-object.interface.js.map +0 -1
- package/interfaces/inflection-rule.interfaces.js +0 -3
- package/interfaces/inflection-rule.interfaces.js.map +0 -1
- package/interfaces/path-value-set.interface.js +0 -3
- package/interfaces/path-value-set.interface.js.map +0 -1
- package/readme-top.md +0 -187
- package/types/deep-partial.type.js +0 -3
- package/types/deep-partial.type.js.map +0 -1
- package/types/index.js +0 -38
- package/types/index.js.map +0 -1
- package/types/is-already-in-path.type.js +0 -3
- package/types/is-already-in-path.type.js.map +0 -1
- package/types/is-empty.type.js +0 -3
- package/types/is-empty.type.js.map +0 -1
- package/types/is-primitive.type.js +0 -4
- package/types/is-primitive.type.js.map +0 -1
- package/types/literal-object.type.js +0 -3
- package/types/literal-object.type.js.map +0 -1
- package/types/loose-autocomplete.type.js +0 -3
- package/types/loose-autocomplete.type.js.map +0 -1
- package/types/partial-with-null.type.js +0 -3
- package/types/partial-with-null.type.js.map +0 -1
- package/types/prettify.type.js +0 -3
- package/types/prettify.type.js.map +0 -1
- package/types/type.type.js +0 -3
- package/types/type.type.js.map +0 -1
- package/utils/assertions.utils.js +0 -163
- package/utils/assertions.utils.js.map +0 -1
- package/utils/file.utils.js +0 -1315
- package/utils/file.utils.js.map +0 -1
- package/utils/index.js +0 -9
- package/utils/index.js.map +0 -1
- package/utils/object.utils.js +0 -1069
- package/utils/object.utils.js.map +0 -1
- package/utils/string-template.utils.js +0 -269
- package/utils/string-template.utils.js.map +0 -1
- package/utils/string.utils.js +0 -1255
- package/utils/string.utils.js.map +0 -1
- package/utils/url.utils.js +0 -112
- package/utils/url.utils.js.map +0 -1
- /package/{constants → src/constants}/constants.d.ts +0 -0
- /package/{constants → src/constants}/english-inflection-rules.d.ts +0 -0
- /package/{constants → src/constants}/index.d.ts +0 -0
- /package/{enums → src/enums}/index.d.ts +0 -0
- /package/{enums → src/enums}/template-tag.enum.d.ts +0 -0
- /package/{interfaces → src/interfaces}/index.d.ts +0 -0
- /package/{interfaces → src/interfaces}/inflection-rule.interfaces.d.ts +0 -0
- /package/{types → src/types}/index.d.ts +0 -0
- /package/{types → src/types}/is-already-in-path.type.d.ts +0 -0
- /package/{types → src/types}/is-empty.type.d.ts +0 -0
- /package/{types → src/types}/is-primitive.type.d.ts +0 -0
- /package/{types → src/types}/loose-autocomplete.type.d.ts +0 -0
- /package/{types → src/types}/partial-with-null.type.d.ts +0 -0
- /package/{types → src/types}/prettify.type.d.ts +0 -0
- /package/{types → src/types}/type.type.d.ts +0 -0
- /package/{utils → src/utils}/assertions.utils.d.ts +0 -0
- /package/{utils → src/utils}/file.utils.d.ts +0 -0
- /package/{utils → src/utils}/string-template.utils.d.ts +0 -0
- /package/{utils → src/utils}/string.utils.d.ts +0 -0
- /package/{utils → src/utils}/url.utils.d.ts +0 -0
package/index.cjs.js
ADDED
|
@@ -0,0 +1,3645 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// noinspection JSUnusedGlobalSymbols
|
|
4
|
+
/**
|
|
5
|
+
* Validates if a redirect URL is allowed based on a whitelist of permitted domains.
|
|
6
|
+
*
|
|
7
|
+
* This security utility function checks whether a given URL's domain is included in a list
|
|
8
|
+
* of allowed domains, helping prevent open redirect vulnerabilities. It supports both exact
|
|
9
|
+
* domain matching and subdomain matching, where a URL's hostname must either exactly match
|
|
10
|
+
* an allowed domain or be a subdomain of an allowed domain.
|
|
11
|
+
*
|
|
12
|
+
* This is essential for secure redirect functionality in web applications, preventing
|
|
13
|
+
* attackers from redirecting users to malicious external sites. It's commonly used in
|
|
14
|
+
* authentication flows, logout redirects, and any feature that accepts user-provided
|
|
15
|
+
* redirect URLs.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} url - The URL to validate. Must be a valid URL string that can be parsed
|
|
18
|
+
* by the URL constructor. Can include protocol, path, query parameters, etc.
|
|
19
|
+
* @param {string[]} allowedDomains - Array of domain names that are permitted for redirects.
|
|
20
|
+
* Domains should be specified without protocol (e.g., "example.com").
|
|
21
|
+
* Subdomains of these domains will also be allowed.
|
|
22
|
+
* @returns {boolean} True if the URL's hostname matches or is a subdomain of any allowed domain,
|
|
23
|
+
* false if the domain is not allowed or if the URL is invalid
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Basic domain validation
|
|
28
|
+
* const allowedDomains = ["example.com", "myapp.com"];
|
|
29
|
+
*
|
|
30
|
+
* const isValid1 = isValidRedirectUrl("https://example.com/dashboard", allowedDomains);
|
|
31
|
+
* // Returns: true (exact domain match)
|
|
32
|
+
*
|
|
33
|
+
* const isValid2 = isValidRedirectUrl("https://api.example.com/callback", allowedDomains);
|
|
34
|
+
* // Returns: true (subdomain of allowed domain)
|
|
35
|
+
*
|
|
36
|
+
* const isValid3 = isValidRedirectUrl("https://malicious.com/phishing", allowedDomains);
|
|
37
|
+
* // Returns: false (domain not in allowed list)
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // Authentication redirect validation
|
|
43
|
+
* function handleAuthRedirect(redirectUrl: string): void {
|
|
44
|
+
* const trustedDomains = [
|
|
45
|
+
* "myapp.com",
|
|
46
|
+
* "admin.myapp.com",
|
|
47
|
+
* "localhost" // for development
|
|
48
|
+
* ];
|
|
49
|
+
*
|
|
50
|
+
* if (isValidRedirectUrl(redirectUrl, trustedDomains)) {
|
|
51
|
+
* window.location.href = redirectUrl;
|
|
52
|
+
* } else {
|
|
53
|
+
* // Redirect to safe default instead
|
|
54
|
+
* window.location.href = "/dashboard";
|
|
55
|
+
* console.warn("Attempted redirect to untrusted domain:", redirectUrl);
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // API endpoint validation
|
|
63
|
+
* app.post('/auth/login', (req, res) => {
|
|
64
|
+
* const { username, password, redirectUrl } = req.body;
|
|
65
|
+
*
|
|
66
|
+
* if (authenticateUser(username, password)) {
|
|
67
|
+
* const allowedDomains = ["myapp.com", "partner.com"];
|
|
68
|
+
*
|
|
69
|
+
* if (redirectUrl && isValidRedirectUrl(redirectUrl, allowedDomains)) {
|
|
70
|
+
* res.json({ success: true, redirectUrl });
|
|
71
|
+
* } else {
|
|
72
|
+
* res.json({ success: true, redirectUrl: "/dashboard" });
|
|
73
|
+
* }
|
|
74
|
+
* } else {
|
|
75
|
+
* res.status(401).json({ error: "Invalid credentials" });
|
|
76
|
+
* }
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* // Complex subdomain scenarios
|
|
83
|
+
* const domains = ["company.com"];
|
|
84
|
+
*
|
|
85
|
+
* isValidRedirectUrl("https://app.company.com", domains); // true
|
|
86
|
+
* isValidRedirectUrl("https://api.app.company.com", domains); // true
|
|
87
|
+
* isValidRedirectUrl("https://company.com.evil.com", domains); // false
|
|
88
|
+
* isValidRedirectUrl("https://fakecompany.com", domains); // false
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* - Uses the native URL constructor for reliable URL parsing
|
|
93
|
+
* - Supports both exact domain matches and subdomain matches
|
|
94
|
+
* - Returns false for invalid URLs that cannot be parsed
|
|
95
|
+
* - Domain matching is case-insensitive (handled by URL constructor)
|
|
96
|
+
* - Does not validate the protocol - focuses only on hostname validation
|
|
97
|
+
* - Subdomain matching uses endsWith() to ensure proper domain boundaries
|
|
98
|
+
* - Empty or null URL strings will cause the function to return false
|
|
99
|
+
*
|
|
100
|
+
* @throws {never} This function catches all URL parsing errors and returns false instead of throwing
|
|
101
|
+
*/
|
|
102
|
+
function isValidRedirectUrl(url, allowedDomains) {
|
|
103
|
+
try {
|
|
104
|
+
const parsedUrl = new URL(url);
|
|
105
|
+
return allowedDomains.some(domain => parsedUrl.hostname === domain || parsedUrl.hostname.endsWith(`.${domain}`));
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
112
|
+
// noinspection JSUnusedGlobalSymbols
|
|
113
|
+
/**
|
|
114
|
+
* Deep copy an object.
|
|
115
|
+
* @template T Type of the object.
|
|
116
|
+
* @param {T} obj Object to copy.
|
|
117
|
+
* @returns {T} Copied object.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```TypeScript
|
|
121
|
+
* // Example usage
|
|
122
|
+
* const object = {
|
|
123
|
+
* name: "John Doe"
|
|
124
|
+
* }
|
|
125
|
+
*
|
|
126
|
+
* const copiedObject = deepCopy(object);
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
function deepCopy(obj) {
|
|
130
|
+
if (Array.isArray(obj)) {
|
|
131
|
+
return obj.map(deepCopy);
|
|
132
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
133
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, deepCopy(value)]));
|
|
134
|
+
}
|
|
135
|
+
return obj;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the key of a map by value.
|
|
139
|
+
* @param {Map<string, unknown>} map Map to get key from.
|
|
140
|
+
* @param {unknown} value Value to get key for.
|
|
141
|
+
* @returns {string | undefined} Key of the map.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```TypeScript
|
|
145
|
+
* // Example usage
|
|
146
|
+
* const user = new Map<string, string>([
|
|
147
|
+
* ["firstName", "John"],
|
|
148
|
+
* ["lastName", "Doe"],
|
|
149
|
+
* ["preferredName", "John"],
|
|
150
|
+
* ["age", 30],
|
|
151
|
+
* ]);
|
|
152
|
+
*
|
|
153
|
+
* const key = getMapKey(user, "value2");
|
|
154
|
+
*
|
|
155
|
+
* // Example output: "firstName"
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
function getMapKey(map, value) {
|
|
159
|
+
return [...Array.from(map.entries())].find(([, v]) => v === value)?.[0];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Retrieves all keys from a Map where the corresponding values contain a specified substring.
|
|
163
|
+
*
|
|
164
|
+
* This utility function searches through a Map's values and returns an array of keys
|
|
165
|
+
* whose associated values include the provided partial string. The search is case-sensitive
|
|
166
|
+
* and uses JavaScript's native string.includes() method for matching.
|
|
167
|
+
*
|
|
168
|
+
* This is particularly useful for implementing search functionality, filtering operations,
|
|
169
|
+
* or finding related entries in configuration maps, user preference stores, or any
|
|
170
|
+
* key-value data structure where you need to locate entries by partial value matching.
|
|
171
|
+
*
|
|
172
|
+
* @param {Map<string, string>} map - The Map to search through for matching values
|
|
173
|
+
* @param {string} partialValue - The substring to search for within the Map's values
|
|
174
|
+
* @returns {string[]} An array of keys whose corresponding values contain the partial value
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* // Search user preferences for entries containing "John"
|
|
179
|
+
* const userPreferences = new Map<string, string>([
|
|
180
|
+
* ["displayName", "John Doe"],
|
|
181
|
+
* ["firstName", "John"],
|
|
182
|
+
* ["lastName", "Doe"],
|
|
183
|
+
* ["preferredName", "Johnny"],
|
|
184
|
+
* ["email", "john.doe@example.com"]
|
|
185
|
+
* ]);
|
|
186
|
+
*
|
|
187
|
+
* const johnKeys = getMapKeys(userPreferences, "John");
|
|
188
|
+
* // Returns: ["displayName", "firstName", "email"]
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* // Find configuration keys related to database settings
|
|
194
|
+
* const config = new Map<string, string>([
|
|
195
|
+
* ["db_host", "localhost"],
|
|
196
|
+
* ["db_port", "5432"],
|
|
197
|
+
* ["cache_host", "redis-server"],
|
|
198
|
+
* ["db_name", "myapp_database"],
|
|
199
|
+
* ["log_level", "debug"]
|
|
200
|
+
* ]);
|
|
201
|
+
*
|
|
202
|
+
* const dbKeys = getMapKeys(config, "db");
|
|
203
|
+
* // Returns: ["db_name"] (only values containing "db", not keys)
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```typescript
|
|
208
|
+
* // Search in a translations map
|
|
209
|
+
* const translations = new Map<string, string>([
|
|
210
|
+
* ["welcome_message", "Welcome to our application"],
|
|
211
|
+
* ["error_message", "An error occurred"],
|
|
212
|
+
* ["success_message", "Operation completed successfully"],
|
|
213
|
+
* ["info_message", "Please check your information"]
|
|
214
|
+
* ]);
|
|
215
|
+
*
|
|
216
|
+
* const messageKeys = getMapKeys(translations, "message");
|
|
217
|
+
* // Returns: ["welcome_message", "error_message", "success_message", "info_message"]
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @remarks
|
|
221
|
+
* - The search is case-sensitive; "John" will not match "john"
|
|
222
|
+
* - Returns an empty array if no matches are found
|
|
223
|
+
* - The function iterates through all Map entries, so performance scales with Map size
|
|
224
|
+
* - Only works with Maps that have string values; other value types will cause runtime errors
|
|
225
|
+
*/
|
|
226
|
+
const getMapKeys = (map, partialValue) => {
|
|
227
|
+
const keys = [];
|
|
228
|
+
for (const [key, value] of Array.from(map.entries())) {
|
|
229
|
+
if (value.includes(partialValue)) {
|
|
230
|
+
keys.push(key);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return keys;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Groups an array of objects into a Map based on a key extraction function.
|
|
237
|
+
*
|
|
238
|
+
* This utility function takes an array of objects and organizes them into groups
|
|
239
|
+
* based on a common key or property. The grouping is performed using a key extraction
|
|
240
|
+
* function that you provide, which determines how each object should be categorized.
|
|
241
|
+
* The result is a Map where each key represents a group and the value is an array
|
|
242
|
+
* of objects belonging to that group.
|
|
243
|
+
*
|
|
244
|
+
* This is particularly useful for data analysis, reporting, organizing collections
|
|
245
|
+
* for display purposes, or preparing data for further processing where items need
|
|
246
|
+
* to be categorized by shared characteristics.
|
|
247
|
+
*
|
|
248
|
+
* @template K - The type of the grouping key (can be string, number, boolean, etc.)
|
|
249
|
+
* @template V - The type of the objects being grouped
|
|
250
|
+
* @param {Array<V>} list - The array of objects to group
|
|
251
|
+
* @param {(input: V) => K} keyGetter - A function that extracts the grouping key from each object.
|
|
252
|
+
* This function receives an object and should return the value
|
|
253
|
+
* to group by. If the function returns null or undefined,
|
|
254
|
+
* the object will be grouped under a null key.
|
|
255
|
+
* @returns {Map<K | null, Array<V>>} A Map where keys are the grouping values and values are arrays
|
|
256
|
+
* of objects that share that grouping key
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* // Group users by age
|
|
261
|
+
* interface User {
|
|
262
|
+
* name: string;
|
|
263
|
+
* age: number;
|
|
264
|
+
* department: string;
|
|
265
|
+
* }
|
|
266
|
+
*
|
|
267
|
+
* const users: User[] = [
|
|
268
|
+
* { name: "John", age: 30, department: "Engineering" },
|
|
269
|
+
* { name: "Jane", age: 25, department: "Marketing" },
|
|
270
|
+
* { name: "Bob", age: 30, department: "Engineering" },
|
|
271
|
+
* { name: "Alice", age: 25, department: "Sales" },
|
|
272
|
+
* { name: "Charlie", age: 35, department: "Engineering" }
|
|
273
|
+
* ];
|
|
274
|
+
*
|
|
275
|
+
* const usersByAge = groupBy(users, user => user.age);
|
|
276
|
+
* // Returns:
|
|
277
|
+
* // Map {
|
|
278
|
+
* // 30 => [
|
|
279
|
+
* // { name: "John", age: 30, department: "Engineering" },
|
|
280
|
+
* // { name: "Bob", age: 30, department: "Engineering" }
|
|
281
|
+
* // ],
|
|
282
|
+
* // 25 => [
|
|
283
|
+
* // { name: "Jane", age: 25, department: "Marketing" },
|
|
284
|
+
* // { name: "Alice", age: 25, department: "Sales" }
|
|
285
|
+
* // ],
|
|
286
|
+
* // 35 => [
|
|
287
|
+
* // { name: "Charlie", age: 35, department: "Engineering" }
|
|
288
|
+
* // ]
|
|
289
|
+
* // }
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // Group products by category
|
|
295
|
+
* interface Product {
|
|
296
|
+
* id: number;
|
|
297
|
+
* name: string;
|
|
298
|
+
* category: string;
|
|
299
|
+
* price: number;
|
|
300
|
+
* }
|
|
301
|
+
*
|
|
302
|
+
* const products: Product[] = [
|
|
303
|
+
* { id: 1, name: "Laptop", category: "Electronics", price: 999 },
|
|
304
|
+
* { id: 2, name: "Book", category: "Education", price: 29 },
|
|
305
|
+
* { id: 3, name: "Phone", category: "Electronics", price: 699 },
|
|
306
|
+
* { id: 4, name: "Pen", category: "Office", price: 5 }
|
|
307
|
+
* ];
|
|
308
|
+
*
|
|
309
|
+
* const productsByCategory = groupBy(products, product => product.category);
|
|
310
|
+
* // Useful for creating category-based product listings
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* // Group tasks by completion status with boolean keys
|
|
316
|
+
* interface Task {
|
|
317
|
+
* id: number;
|
|
318
|
+
* title: string;
|
|
319
|
+
* completed: boolean;
|
|
320
|
+
* }
|
|
321
|
+
*
|
|
322
|
+
* const tasks: Task[] = [
|
|
323
|
+
* { id: 1, title: "Review code", completed: true },
|
|
324
|
+
* { id: 2, title: "Write tests", completed: false },
|
|
325
|
+
* { id: 3, title: "Deploy app", completed: true },
|
|
326
|
+
* { id: 4, title: "Update docs", completed: false }
|
|
327
|
+
* ];
|
|
328
|
+
*
|
|
329
|
+
* const tasksByStatus = groupBy(tasks, task => task.completed);
|
|
330
|
+
* // Returns Map with boolean keys: true and false
|
|
331
|
+
* ```
|
|
332
|
+
*
|
|
333
|
+
* @remarks
|
|
334
|
+
* - The function preserves the original order of objects within each group
|
|
335
|
+
* - If the keyGetter function returns null or undefined, those objects will be grouped under a null key
|
|
336
|
+
* - The Map structure allows for efficient lookups and iteration over groups
|
|
337
|
+
* - Works with any type of grouping key (string, number, boolean, objects, etc.)
|
|
338
|
+
* - Empty arrays will return an empty Map
|
|
339
|
+
*/
|
|
340
|
+
const groupBy = (list, keyGetter) => {
|
|
341
|
+
const map = new Map();
|
|
342
|
+
list.forEach(item => {
|
|
343
|
+
const key = keyGetter(item);
|
|
344
|
+
const collection = map.get(key);
|
|
345
|
+
if (!collection) {
|
|
346
|
+
map.set(key, [item]);
|
|
347
|
+
} else {
|
|
348
|
+
collection.push(item);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return map;
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Retrieves all values from a Map that contain a specified substring.
|
|
355
|
+
*
|
|
356
|
+
* This utility function searches through a Map's values and returns an array of all values
|
|
357
|
+
* that include the provided partial string. The search is case-sensitive and uses JavaScript's
|
|
358
|
+
* native string.includes() method for matching. This is the counterpart to getMapKeys(),
|
|
359
|
+
* but instead of returning the keys, it returns the actual values that match.
|
|
360
|
+
*
|
|
361
|
+
* This is particularly useful for implementing search functionality where you need to find
|
|
362
|
+
* and display matching content, filtering data for autocomplete features, or extracting
|
|
363
|
+
* related values from configuration maps, user data, or any key-value store where you
|
|
364
|
+
* need to locate entries by partial content matching.
|
|
365
|
+
*
|
|
366
|
+
* @param {Map<string, string>} map - The Map to search through for matching values
|
|
367
|
+
* @param {string} partialValue - The substring to search for within the Map's values
|
|
368
|
+
* @returns {string[]} An array of values that contain the specified partial value
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```typescript
|
|
372
|
+
* // Search user information for values containing "John"
|
|
373
|
+
* const userInfo = new Map<string, string>([
|
|
374
|
+
* ["fullName", "John Doe"],
|
|
375
|
+
* ["firstName", "John"],
|
|
376
|
+
* ["lastName", "Doe"],
|
|
377
|
+
* ["nickname", "Johnny"],
|
|
378
|
+
* ["email", "john.doe@example.com"],
|
|
379
|
+
* ["phone", "555-0123"]
|
|
380
|
+
* ]);
|
|
381
|
+
*
|
|
382
|
+
* const johnValues = searchMapValues(userInfo, "John");
|
|
383
|
+
* // Returns: ["John Doe", "John", "Johnny", "john.doe@example.com"]
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* // Find error messages containing specific keywords
|
|
389
|
+
* const errorMessages = new Map<string, string>([
|
|
390
|
+
* ["auth_failed", "Authentication failed. Please check your credentials."],
|
|
391
|
+
* ["network_error", "Network connection failed. Please try again."],
|
|
392
|
+
* ["validation_error", "Validation failed for the provided data."],
|
|
393
|
+
* ["timeout_error", "Request timeout. The server took too long to respond."],
|
|
394
|
+
* ["permission_denied", "Access denied. You don't have permission."]
|
|
395
|
+
* ]);
|
|
396
|
+
*
|
|
397
|
+
* const failedMessages = searchMapValues(errorMessages, "failed");
|
|
398
|
+
* // Returns: ["Authentication failed. Please check your credentials.",
|
|
399
|
+
* // "Network connection failed. Please try again.",
|
|
400
|
+
* // "Validation failed for the provided data."]
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* // Search configuration values for environment-specific settings
|
|
406
|
+
* const config = new Map<string, string>([
|
|
407
|
+
* ["api_url", "https://api.production.example.com"],
|
|
408
|
+
* ["db_host", "production-db.example.com"],
|
|
409
|
+
* ["cache_url", "redis://production-cache.example.com"],
|
|
410
|
+
* ["log_level", "error"],
|
|
411
|
+
* ["debug_mode", "false"]
|
|
412
|
+
* ]);
|
|
413
|
+
*
|
|
414
|
+
* const productionValues = searchMapValues(config, "production");
|
|
415
|
+
* // Returns: ["https://api.production.example.com",
|
|
416
|
+
* // "production-db.example.com",
|
|
417
|
+
* // "redis://production-cache.example.com"]
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* @remarks
|
|
421
|
+
* - The search is case-sensitive; "John" will not match "john"
|
|
422
|
+
* - Returns an empty array if no matches are found
|
|
423
|
+
* - The function iterates through all Map entries, so performance scales with Map size
|
|
424
|
+
* - Only works with Maps that have string values; other value types will cause runtime errors
|
|
425
|
+
* - Values are returned in the order they appear in the Map iteration
|
|
426
|
+
*
|
|
427
|
+
* @see {@link getMapKeys} Function to get keys whose values contain a substring
|
|
428
|
+
*/
|
|
429
|
+
const searchMapValues = (map, partialValue) => {
|
|
430
|
+
const values = [];
|
|
431
|
+
for (const [, value] of Array.from(map.entries())) {
|
|
432
|
+
if (value.includes(partialValue)) {
|
|
433
|
+
values.push(value);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return values;
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* Gets a value from a nested object using a dot-notation path string.
|
|
440
|
+
*
|
|
441
|
+
* This function safely traverses a deeply nested object structure using a string path
|
|
442
|
+
* with dot notation. It handles both object properties and array indices within the path.
|
|
443
|
+
*
|
|
444
|
+
* @template T - Type of the value to be returned.
|
|
445
|
+
* @param {InfiniteObject} obj - The object to retrieve the value from.
|
|
446
|
+
* @param {string} path - The dot-notation path to the desired value.
|
|
447
|
+
* - Use dots to navigate through nested objects: 'user.profile.address'
|
|
448
|
+
* - Use array notation for accessing array elements: 'items[0]' or 'users[2].name'
|
|
449
|
+
* @returns {T | undefined} - The value at the specified path, or undefined if:
|
|
450
|
+
* - Any part of the path doesn't exist
|
|
451
|
+
* - An array index is out of bounds
|
|
452
|
+
* - The path format is invalid
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```typescript
|
|
456
|
+
* // Simple nested object property
|
|
457
|
+
* const user = {
|
|
458
|
+
* profile: {
|
|
459
|
+
* name: "John Doe",
|
|
460
|
+
* contact: { email: "john@example.com" }
|
|
461
|
+
* }
|
|
462
|
+
* };
|
|
463
|
+
* const email = getValueByPath<string>(user, "profile.contact.email");
|
|
464
|
+
* // Returns: "john@example.com"
|
|
465
|
+
* ```
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```typescript
|
|
469
|
+
* // Accessing array elements
|
|
470
|
+
* const data = {
|
|
471
|
+
* users: [
|
|
472
|
+
* { id: 1, name: "Alice" },
|
|
473
|
+
* { id: 2, name: "Bob" }
|
|
474
|
+
* ]
|
|
475
|
+
* };
|
|
476
|
+
* const name = getValueByPath<string>(data, "users[1].name");
|
|
477
|
+
* // Returns: "Bob"
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
const getValueByPath = (obj, path) => {
|
|
481
|
+
const keys = path.split("."); // Split the path into an array of keys
|
|
482
|
+
let value = obj;
|
|
483
|
+
for (const key of keys) {
|
|
484
|
+
// noinspection RegExpRedundantEscape
|
|
485
|
+
const regExp = /^(\w+)\[(\d+)\]$/;
|
|
486
|
+
const isArrayIndex = regExp.exec(key); // Check if the key is an array index
|
|
487
|
+
if (isArrayIndex) {
|
|
488
|
+
const arrayKey = isArrayIndex[1];
|
|
489
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
490
|
+
const index = Number(isArrayIndex[2]);
|
|
491
|
+
if (arrayKey && Array.isArray(value[arrayKey]) && index >= 0 && index < value[arrayKey].length) {
|
|
492
|
+
value = value[arrayKey][index]; // Update the value to the array element
|
|
493
|
+
} else {
|
|
494
|
+
return undefined; // Return undefined if the array index is invalid
|
|
495
|
+
}
|
|
496
|
+
} else if (value && typeof value === "object" && key in value) {
|
|
497
|
+
value = value[key]; // Update the value to the nested property
|
|
498
|
+
} else {
|
|
499
|
+
return undefined; // Return undefined if any key is not found
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return value;
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Converts a nested object into a flattened DottedPathValueObject representation.
|
|
506
|
+
*
|
|
507
|
+
* This function transforms a hierarchical object structure into a flat key-value map
|
|
508
|
+
* where keys represent paths to values in the original object using dot notation.
|
|
509
|
+
*
|
|
510
|
+
* The function recursively traverses the object and flattens nested properties,
|
|
511
|
+
* converting object hierarchies like `{ user: { name: 'John' } }` into
|
|
512
|
+
* path-based entries like `{ 'user.name': 'John' }`.
|
|
513
|
+
*
|
|
514
|
+
* @param {LiteralObject} obj - The nested object to flatten
|
|
515
|
+
* @returns {DottedPathValueObject} - A flattened representation where:
|
|
516
|
+
* - Keys are dot-notation paths to values in the original object
|
|
517
|
+
* - Values are primitive values (strings, numbers, booleans) from the original object
|
|
518
|
+
*
|
|
519
|
+
* @remarks
|
|
520
|
+
* - Array values will be preserved as-is (not flattened into separate paths)
|
|
521
|
+
* - Only primitive values (string, number, boolean) are supported as leaf values
|
|
522
|
+
* - Circular references are not handled and will cause a stack overflow
|
|
523
|
+
*
|
|
524
|
+
* @see {@link dottedPathObjectToNested} The inverse operation to convert a DottedPathValueObject back to a nested object
|
|
525
|
+
* @see {@link DottedPathValueObject} The interface for the returned flattened object
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
529
|
+
* // Flatten a nested user object
|
|
530
|
+
* const user = {
|
|
531
|
+
* id: 123,
|
|
532
|
+
* name: "John Doe",
|
|
533
|
+
* isActive: true,
|
|
534
|
+
* profile: {
|
|
535
|
+
* age: 30,
|
|
536
|
+
* address: {
|
|
537
|
+
* city: "New York",
|
|
538
|
+
* zip: "10001"
|
|
539
|
+
* }
|
|
540
|
+
* }
|
|
541
|
+
* };
|
|
542
|
+
*
|
|
543
|
+
* const flattened = objectToDottedPathValueObject(user);
|
|
544
|
+
*
|
|
545
|
+
* // Result:
|
|
546
|
+
* // {
|
|
547
|
+
* // "id": 123,
|
|
548
|
+
* // "name": "John Doe",
|
|
549
|
+
* // "isActive": true,
|
|
550
|
+
* // "profile.age": 30,
|
|
551
|
+
* // "profile.address.city": "New York",
|
|
552
|
+
* // "profile.address.zip": "10001"
|
|
553
|
+
* // }
|
|
554
|
+
* ```
|
|
555
|
+
*/
|
|
556
|
+
function objectToDottedPathValueObject(obj) {
|
|
557
|
+
const result = {};
|
|
558
|
+
function traverse(obj, path = []) {
|
|
559
|
+
for (const key in obj) {
|
|
560
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
561
|
+
const value = obj[key];
|
|
562
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
563
|
+
traverse(value, [...path, key]);
|
|
564
|
+
} else {
|
|
565
|
+
result[[...path, key].join(".")] = value;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
traverse(obj);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Converts a flattened DottedPathValueObject back into a nested object structure.
|
|
575
|
+
*
|
|
576
|
+
* This function is the inverse of `objectToDottedPathValueObject`. It takes a flat map of
|
|
577
|
+
* dot-notation paths to values and reconstructs a hierarchical object structure.
|
|
578
|
+
*
|
|
579
|
+
* Each key in the input DottedPathValueObject represents a path through the object hierarchy,
|
|
580
|
+
* with dots separating each level. The function builds a nested object structure by
|
|
581
|
+
* parsing these paths and placing values at the appropriate locations.
|
|
582
|
+
*
|
|
583
|
+
* @template R - The type of the returned object (defaults to object)
|
|
584
|
+
* @param {DottedPathValueObject} pathValueSet - A flattened object with dot-notation path keys
|
|
585
|
+
* @returns {R} - A reconstructed nested object with the original hierarchy
|
|
586
|
+
*
|
|
587
|
+
* @remarks
|
|
588
|
+
* - Paths are validated for safety to prevent injection attacks
|
|
589
|
+
* - Invalid paths are silently skipped (not included in the result)
|
|
590
|
+
* - Path components should contain only alphanumeric characters, underscores, hyphens, and dots
|
|
591
|
+
*
|
|
592
|
+
* @see {@link objectToDottedPathValueObject} The inverse operation to convert an object to DottedPathValueObject
|
|
593
|
+
* @see {@link DottedPathValueObject} The interface for the input flattened object
|
|
594
|
+
*
|
|
595
|
+
* @example
|
|
596
|
+
* ```typescript
|
|
597
|
+
* // Convert a flat DottedPathValueObject to a nested object
|
|
598
|
+
* const flatData = {
|
|
599
|
+
* "id": 123,
|
|
600
|
+
* "name": "John Doe",
|
|
601
|
+
* "profile.age": 30,
|
|
602
|
+
* "profile.address.city": "New York",
|
|
603
|
+
* "profile.address.zip": "10001"
|
|
604
|
+
* };
|
|
605
|
+
*
|
|
606
|
+
* const nestedObject = dottedPathObjectToNested(flatData);
|
|
607
|
+
*
|
|
608
|
+
* Result:
|
|
609
|
+
* // {
|
|
610
|
+
* // id: 123,
|
|
611
|
+
* // name: "John Doe",
|
|
612
|
+
* // profile: {
|
|
613
|
+
* // age: 30,
|
|
614
|
+
* // address: {
|
|
615
|
+
* // city: "New York",
|
|
616
|
+
* // zip: "10001"
|
|
617
|
+
* // }
|
|
618
|
+
* // }
|
|
619
|
+
* // }
|
|
620
|
+
* ```
|
|
621
|
+
*
|
|
622
|
+
* @example
|
|
623
|
+
* ```typescript
|
|
624
|
+
* // Typed return value
|
|
625
|
+
* interface User {
|
|
626
|
+
* id: number;
|
|
627
|
+
* name: string;
|
|
628
|
+
* profile: {
|
|
629
|
+
* age: number;
|
|
630
|
+
* address: {
|
|
631
|
+
* city: string;
|
|
632
|
+
* zip: string;
|
|
633
|
+
* };
|
|
634
|
+
* };
|
|
635
|
+
* }
|
|
636
|
+
*
|
|
637
|
+
* const userData = dottedPathObjectToNested<User>(flatData);
|
|
638
|
+
* // Returns object with User type
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
function dottedPathObjectToNested(pathValueSet) {
|
|
642
|
+
const object = {};
|
|
643
|
+
// Helper function to validate paths
|
|
644
|
+
const isValidPath = path => {
|
|
645
|
+
const regex = /^[a-zA-Z0-9_.-]+$/;
|
|
646
|
+
return path.split(".").every(part => regex.test(part));
|
|
647
|
+
};
|
|
648
|
+
// Helper function to set nested properties
|
|
649
|
+
const setObjectValue = (obj, keys, value) => {
|
|
650
|
+
const [firstKey, ...remainingKeys] = keys;
|
|
651
|
+
if (remainingKeys.length === 0) {
|
|
652
|
+
obj[firstKey] = value; // Set value at the final key
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
// Initialize the key if it doesn't exist
|
|
656
|
+
obj[firstKey] = obj[firstKey] || {};
|
|
657
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
658
|
+
setObjectValue(obj[firstKey], remainingKeys, value); // Recurse for the rest of the keys
|
|
659
|
+
// TODO: v2.0 Fix type for above
|
|
660
|
+
};
|
|
661
|
+
for (const path in pathValueSet) {
|
|
662
|
+
if (Object.prototype.hasOwnProperty.call(pathValueSet, path)) {
|
|
663
|
+
if (!isValidPath(path)) {
|
|
664
|
+
continue; // Skip invalid paths
|
|
665
|
+
}
|
|
666
|
+
const value = pathValueSet[path];
|
|
667
|
+
const keys = path.split("."); // Split path into keys
|
|
668
|
+
setObjectValue(object, keys, value); // Use helper to populate the object
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return object;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Creates a new object by omitting specified properties and undefined values from the original object.
|
|
675
|
+
*
|
|
676
|
+
* This utility function performs a selective copy of an object, excluding both explicitly
|
|
677
|
+
* specified properties and any properties with undefined values. The function modifies
|
|
678
|
+
* the original object in-place by deleting the unwanted properties, making it useful for
|
|
679
|
+
* cleaning up objects before serialization, API calls, or data processing.
|
|
680
|
+
*
|
|
681
|
+
* This is particularly useful for preparing data for API requests where you need to remove
|
|
682
|
+
* sensitive fields, clean up form data, or exclude certain properties from being sent to
|
|
683
|
+
* external services. It's also helpful for removing undefined values that might cause
|
|
684
|
+
* issues in JSON serialization or database operations.
|
|
685
|
+
*
|
|
686
|
+
* @template T - The type of the object being processed
|
|
687
|
+
* @param {Partial<T>} obj - The object to process and remove properties from.
|
|
688
|
+
* This object will be modified in-place.
|
|
689
|
+
* @param {(keyof T)[]} [keys] - Optional array of property keys to omit from the object.
|
|
690
|
+
* If not provided, only undefined properties will be removed.
|
|
691
|
+
* @returns {Partial<T>} The same object reference with specified properties and undefined values removed
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```typescript
|
|
695
|
+
* // Remove specific properties and undefined values
|
|
696
|
+
* interface User {
|
|
697
|
+
* id: number;
|
|
698
|
+
* name: string;
|
|
699
|
+
* email: string;
|
|
700
|
+
* role: string;
|
|
701
|
+
* address?: string;
|
|
702
|
+
* phone?: string;
|
|
703
|
+
* }
|
|
704
|
+
*
|
|
705
|
+
* const user: Partial<User> = {
|
|
706
|
+
* id: 123,
|
|
707
|
+
* name: "John Doe",
|
|
708
|
+
* email: "john@example.com",
|
|
709
|
+
* role: "admin",
|
|
710
|
+
* address: undefined,
|
|
711
|
+
* phone: "555-0123"
|
|
712
|
+
* };
|
|
713
|
+
*
|
|
714
|
+
* const cleanUser = omit(user, ["role"]);
|
|
715
|
+
* // Returns: { id: 123, name: "John Doe", email: "john@example.com", phone: "555-0123" }
|
|
716
|
+
* // Note: 'role' and 'address' (undefined) are removed
|
|
717
|
+
* ```
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* ```typescript
|
|
721
|
+
* // Remove only undefined values (no specific keys)
|
|
722
|
+
* const formData = {
|
|
723
|
+
* firstName: "John",
|
|
724
|
+
* lastName: "Doe",
|
|
725
|
+
* middleName: undefined,
|
|
726
|
+
* email: "john@example.com",
|
|
727
|
+
* phone: undefined
|
|
728
|
+
* };
|
|
729
|
+
*
|
|
730
|
+
* const cleanFormData = omit(formData);
|
|
731
|
+
* // Returns: { firstName: "John", lastName: "Doe", email: "john@example.com" }
|
|
732
|
+
* ```
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* ```typescript
|
|
736
|
+
* // Prepare data for API request by removing sensitive fields
|
|
737
|
+
* interface UserProfile {
|
|
738
|
+
* id: number;
|
|
739
|
+
* username: string;
|
|
740
|
+
* email: string;
|
|
741
|
+
* password: string;
|
|
742
|
+
* internalNotes: string;
|
|
743
|
+
* createdAt: Date;
|
|
744
|
+
* }
|
|
745
|
+
*
|
|
746
|
+
* const userProfile: Partial<UserProfile> = {
|
|
747
|
+
* id: 1,
|
|
748
|
+
* username: "johndoe",
|
|
749
|
+
* email: "john@example.com",
|
|
750
|
+
* password: "secret123",
|
|
751
|
+
* internalNotes: "VIP customer",
|
|
752
|
+
* createdAt: new Date()
|
|
753
|
+
* };
|
|
754
|
+
*
|
|
755
|
+
* const publicProfile = omit(userProfile, ["password", "internalNotes"]);
|
|
756
|
+
* // Safe to send to client: { id: 1, username: "johndoe", email: "john@example.com", createdAt: Date }
|
|
757
|
+
* ```
|
|
758
|
+
*
|
|
759
|
+
* @remarks
|
|
760
|
+
* - This function modifies the original object in-place rather than creating a copy
|
|
761
|
+
* - Both explicitly specified keys and undefined properties are removed
|
|
762
|
+
* - If the input object is falsy (null, undefined), it's returned as-is
|
|
763
|
+
* - The function uses delete operator, which may affect object performance in some JavaScript engines
|
|
764
|
+
* - Properties with null values are preserved (only undefined values are automatically removed)
|
|
765
|
+
*
|
|
766
|
+
* @see {@link prune} Function for more comprehensive object cleaning including null and empty values
|
|
767
|
+
*/
|
|
768
|
+
const omit = (obj, keys) => {
|
|
769
|
+
if (obj) {
|
|
770
|
+
Object.keys(obj).forEach(key => {
|
|
771
|
+
return (obj[key] === undefined || keys?.includes(key)) && delete obj[key];
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return obj;
|
|
775
|
+
};
|
|
776
|
+
/**
|
|
777
|
+
* Creates a deep copy of an object with all empty, null, undefined, and optionally prototype properties removed.
|
|
778
|
+
*
|
|
779
|
+
* This utility function recursively traverses an object and creates a clean copy by excluding
|
|
780
|
+
* properties that are null, undefined, or empty strings. It performs a deep cleaning operation,
|
|
781
|
+
* processing nested objects recursively to ensure that the entire object hierarchy is pruned
|
|
782
|
+
* of unwanted values.
|
|
783
|
+
*
|
|
784
|
+
* This is particularly useful for preparing data for serialization, cleaning up API responses,
|
|
785
|
+
* removing empty form fields, or ensuring that only meaningful data is processed or stored.
|
|
786
|
+
* The function is especially valuable when working with deeply nested objects that may contain
|
|
787
|
+
* sparse data or when you need to eliminate all traces of empty values from complex data structures.
|
|
788
|
+
*
|
|
789
|
+
* @template T - The type of the object being pruned
|
|
790
|
+
* @param {PartialWithNull<T>} obj - The object to prune. Can contain null, undefined, or empty values.
|
|
791
|
+
* @param {boolean} [omitPrototype=false] - Whether to exclude inherited properties from the prototype chain.
|
|
792
|
+
* If true, only own properties will be included in the result.
|
|
793
|
+
* If false (default), inherited enumerable properties will be processed.
|
|
794
|
+
* @returns {T} A new object with all empty, null, and undefined properties recursively removed
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```typescript
|
|
798
|
+
* // Clean up a user profile with nested empty values
|
|
799
|
+
* interface UserProfile {
|
|
800
|
+
* id: number;
|
|
801
|
+
* name: string;
|
|
802
|
+
* contact: {
|
|
803
|
+
* email: string;
|
|
804
|
+
* phone?: string;
|
|
805
|
+
* address?: {
|
|
806
|
+
* street: string;
|
|
807
|
+
* city: string;
|
|
808
|
+
* zip?: string;
|
|
809
|
+
* };
|
|
810
|
+
* };
|
|
811
|
+
* preferences: {
|
|
812
|
+
* theme: string;
|
|
813
|
+
* notifications?: boolean;
|
|
814
|
+
* };
|
|
815
|
+
* }
|
|
816
|
+
*
|
|
817
|
+
* const userProfile = {
|
|
818
|
+
* id: 123,
|
|
819
|
+
* name: "John Doe",
|
|
820
|
+
* contact: {
|
|
821
|
+
* email: "john@example.com",
|
|
822
|
+
* phone: "",
|
|
823
|
+
* address: {
|
|
824
|
+
* street: "123 Main St",
|
|
825
|
+
* city: "New York",
|
|
826
|
+
* zip: null
|
|
827
|
+
* }
|
|
828
|
+
* },
|
|
829
|
+
* preferences: {
|
|
830
|
+
* theme: "dark",
|
|
831
|
+
* notifications: undefined
|
|
832
|
+
* }
|
|
833
|
+
* };
|
|
834
|
+
*
|
|
835
|
+
* const cleanProfile = prune(userProfile);
|
|
836
|
+
* // Returns:
|
|
837
|
+
* // {
|
|
838
|
+
* // id: 123,
|
|
839
|
+
* // name: "John Doe",
|
|
840
|
+
* // contact: {
|
|
841
|
+
* // email: "john@example.com",
|
|
842
|
+
* // address: {
|
|
843
|
+
* // street: "123 Main St",
|
|
844
|
+
* // city: "New York"
|
|
845
|
+
* // }
|
|
846
|
+
* // },
|
|
847
|
+
* // preferences: {
|
|
848
|
+
* // theme: "dark"
|
|
849
|
+
* // }
|
|
850
|
+
* // }
|
|
851
|
+
* ```
|
|
852
|
+
*
|
|
853
|
+
* @example
|
|
854
|
+
* ```typescript
|
|
855
|
+
* // Clean up form data before submission
|
|
856
|
+
* const formData = {
|
|
857
|
+
* firstName: "John",
|
|
858
|
+
* lastName: "Doe",
|
|
859
|
+
* middleName: "",
|
|
860
|
+
* email: "john@example.com",
|
|
861
|
+
* phone: null,
|
|
862
|
+
* address: {
|
|
863
|
+
* street: "123 Main St",
|
|
864
|
+
* apartment: "",
|
|
865
|
+
* city: "New York",
|
|
866
|
+
* state: undefined,
|
|
867
|
+
* zip: "10001"
|
|
868
|
+
* },
|
|
869
|
+
* emergencyContact: {
|
|
870
|
+
* name: "",
|
|
871
|
+
* phone: null
|
|
872
|
+
* }
|
|
873
|
+
* };
|
|
874
|
+
*
|
|
875
|
+
* const cleanFormData = prune(formData);
|
|
876
|
+
* // Returns only fields with meaningful values:
|
|
877
|
+
* // {
|
|
878
|
+
* // firstName: "John",
|
|
879
|
+
* // lastName: "Doe",
|
|
880
|
+
* // email: "john@example.com",
|
|
881
|
+
* // address: {
|
|
882
|
+
* // street: "123 Main St",
|
|
883
|
+
* // city: "New York",
|
|
884
|
+
* // zip: "10001"
|
|
885
|
+
* // }
|
|
886
|
+
* // }
|
|
887
|
+
* // Note: emergencyContact object is completely removed as all its properties were empty
|
|
888
|
+
* ```
|
|
889
|
+
*
|
|
890
|
+
* @example
|
|
891
|
+
* ```typescript
|
|
892
|
+
* // Control prototype property inclusion
|
|
893
|
+
* class BaseUser {
|
|
894
|
+
* role = "user";
|
|
895
|
+
* }
|
|
896
|
+
*
|
|
897
|
+
* class ExtendedUser extends BaseUser {
|
|
898
|
+
* name = "John";
|
|
899
|
+
* email = "";
|
|
900
|
+
* }
|
|
901
|
+
*
|
|
902
|
+
* const user = new ExtendedUser();
|
|
903
|
+
*
|
|
904
|
+
* const prunedWithPrototype = prune(user, false);
|
|
905
|
+
* // Includes inherited 'role' property: { role: "user", name: "John" }
|
|
906
|
+
*
|
|
907
|
+
* const prunedOwnOnly = prune(user, true);
|
|
908
|
+
* // Only own properties: { name: "John" }
|
|
909
|
+
* ```
|
|
910
|
+
*
|
|
911
|
+
* @remarks
|
|
912
|
+
* - Creates a new object rather than modifying the original (unlike the omit function)
|
|
913
|
+
* - Recursively processes nested objects to ensure deep cleaning
|
|
914
|
+
* - Removes properties with values: null, undefined, or empty string ("")
|
|
915
|
+
* - Preserves properties with falsy but meaningful values like 0, false
|
|
916
|
+
* - If a nested object becomes empty after pruning, it will be excluded from the result
|
|
917
|
+
* - Non-object types are returned as an empty object of the target type
|
|
918
|
+
* - The function preserves the type structure while removing empty values
|
|
919
|
+
*
|
|
920
|
+
* @see {@link omit} Function for selective property removal without deep cleaning
|
|
921
|
+
*/
|
|
922
|
+
const prune = (obj, omitPrototype) => {
|
|
923
|
+
const objClone = {};
|
|
924
|
+
if (typeof obj !== "object") {
|
|
925
|
+
return objClone;
|
|
926
|
+
}
|
|
927
|
+
for (const key in obj) {
|
|
928
|
+
if (!omitPrototype || Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
929
|
+
if (obj[key] !== null && typeof obj[key] === "object") {
|
|
930
|
+
objClone[key] = prune(obj[key], omitPrototype);
|
|
931
|
+
} else if (obj[key] !== null && obj[key] !== undefined && obj[key] !== "") {
|
|
932
|
+
objClone[key] = obj[key];
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return objClone;
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Checks if an object has all specified properties as its own properties (not inherited).
|
|
940
|
+
*
|
|
941
|
+
* This utility function verifies that an object contains all the properties listed in the
|
|
942
|
+
* provided array as direct (own) properties, not inherited from the prototype chain.
|
|
943
|
+
* It uses the modern Object.hasOwn() method for reliable property existence checking,
|
|
944
|
+
* making it safer than using hasOwnProperty() directly.
|
|
945
|
+
*
|
|
946
|
+
* This is particularly useful for validating object structures, ensuring required properties
|
|
947
|
+
* exist before processing, implementing type guards, or verifying that objects conform to
|
|
948
|
+
* expected interfaces. It's commonly used in data validation, API request validation,
|
|
949
|
+
* and ensuring objects have all necessary properties before performing operations.
|
|
950
|
+
*
|
|
951
|
+
* @param {Record<PropertyKey, unknown>} obj - The object to check for property existence
|
|
952
|
+
* @param {PropertyKey[]} props - Array of property keys to verify exist on the object.
|
|
953
|
+
* PropertyKey includes string, number, and symbol types.
|
|
954
|
+
* @returns {boolean} True if the object has all specified properties as own properties, false otherwise
|
|
955
|
+
*
|
|
956
|
+
* @example
|
|
957
|
+
* ```typescript
|
|
958
|
+
* // Validate that a user object has required properties
|
|
959
|
+
* interface User {
|
|
960
|
+
* id: number;
|
|
961
|
+
* name: string;
|
|
962
|
+
* email: string;
|
|
963
|
+
* }
|
|
964
|
+
*
|
|
965
|
+
* const user = {
|
|
966
|
+
* id: 123,
|
|
967
|
+
* name: "John Doe",
|
|
968
|
+
* email: "john@example.com",
|
|
969
|
+
* role: "admin"
|
|
970
|
+
* };
|
|
971
|
+
*
|
|
972
|
+
* const hasRequiredProps = hasOwnAll(user, ["id", "name", "email"]);
|
|
973
|
+
* // Returns: true
|
|
974
|
+
*
|
|
975
|
+
* const hasAllProps = hasOwnAll(user, ["id", "name", "email", "phone"]);
|
|
976
|
+
* // Returns: false (missing 'phone' property)
|
|
977
|
+
* ```
|
|
978
|
+
*
|
|
979
|
+
* @example
|
|
980
|
+
* ```typescript
|
|
981
|
+
* // Validate API request payload
|
|
982
|
+
* function processUserUpdate(payload: unknown): User | null {
|
|
983
|
+
* if (typeof payload === 'object' && payload !== null) {
|
|
984
|
+
* const requiredFields = ['id', 'name', 'email'];
|
|
985
|
+
*
|
|
986
|
+
* if (hasOwnAll(payload as Record<string, unknown>, requiredFields)) {
|
|
987
|
+
* // Safe to process as we know all required fields exist
|
|
988
|
+
* return updateUser(payload as User);
|
|
989
|
+
* }
|
|
990
|
+
* }
|
|
991
|
+
*
|
|
992
|
+
* throw new Error('Invalid payload: missing required fields');
|
|
993
|
+
* }
|
|
994
|
+
* ```
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* ```typescript
|
|
998
|
+
* // Check configuration object completeness
|
|
999
|
+
* interface DatabaseConfig {
|
|
1000
|
+
* host: string;
|
|
1001
|
+
* port: number;
|
|
1002
|
+
* database: string;
|
|
1003
|
+
* username: string;
|
|
1004
|
+
* password: string;
|
|
1005
|
+
* }
|
|
1006
|
+
*
|
|
1007
|
+
* const config = {
|
|
1008
|
+
* host: "localhost",
|
|
1009
|
+
* port: 5432,
|
|
1010
|
+
* database: "myapp",
|
|
1011
|
+
* username: "admin"
|
|
1012
|
+
* // password is missing
|
|
1013
|
+
* };
|
|
1014
|
+
*
|
|
1015
|
+
* const requiredConfigKeys = ['host', 'port', 'database', 'username', 'password'];
|
|
1016
|
+
* const isConfigComplete = hasOwnAll(config, requiredConfigKeys);
|
|
1017
|
+
* // Returns: false
|
|
1018
|
+
*
|
|
1019
|
+
* if (!isConfigComplete) {
|
|
1020
|
+
* throw new Error('Database configuration is incomplete');
|
|
1021
|
+
* }
|
|
1022
|
+
* ```
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```typescript
|
|
1026
|
+
* // Work with symbol properties
|
|
1027
|
+
* const symbolKey = Symbol('secret');
|
|
1028
|
+
* const obj = {
|
|
1029
|
+
* name: "test",
|
|
1030
|
+
* [symbolKey]: "hidden value"
|
|
1031
|
+
* };
|
|
1032
|
+
*
|
|
1033
|
+
* const hasSymbolProp = hasOwnAll(obj, ['name', symbolKey]);
|
|
1034
|
+
* // Returns: true
|
|
1035
|
+
* ```
|
|
1036
|
+
*
|
|
1037
|
+
* @remarks
|
|
1038
|
+
* - Uses Object.hasOwn() which is more reliable than hasOwnProperty()
|
|
1039
|
+
* - Only checks for own properties, not inherited properties from the prototype chain
|
|
1040
|
+
* - Returns false immediately if any property is missing (short-circuit evaluation)
|
|
1041
|
+
* - Works with string, number, and symbol property keys
|
|
1042
|
+
* - Returns true for an empty properties array (vacuous truth)
|
|
1043
|
+
* - Does not check property values, only existence
|
|
1044
|
+
*
|
|
1045
|
+
* @see {@link Object.hasOwn} The underlying method used for property checking
|
|
1046
|
+
*/
|
|
1047
|
+
function hasOwnAll(obj, props) {
|
|
1048
|
+
return props.every(prop => Object.hasOwn(obj, prop));
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Extracts all values from a TypeScript enum, handling both string and numeric enum types correctly.
|
|
1052
|
+
*
|
|
1053
|
+
* This utility function safely retrieves the actual values from TypeScript enums, accounting for
|
|
1054
|
+
* the different internal representations of string and numeric enums. For numeric enums, TypeScript
|
|
1055
|
+
* creates reverse mappings (value → key), so this function filters out the reverse mapping entries
|
|
1056
|
+
* to return only the actual enum values. For string enums, it returns all values directly.
|
|
1057
|
+
*
|
|
1058
|
+
* This is particularly useful when you need to iterate over enum values, validate input against
|
|
1059
|
+
* enum values, create dropdown options from enums, or perform any operation that requires access
|
|
1060
|
+
* to the actual enum values rather than the keys.
|
|
1061
|
+
*
|
|
1062
|
+
* @template T - The enum type extending object
|
|
1063
|
+
* @param {T} e - The enum object to extract values from
|
|
1064
|
+
* @returns {T[keyof T][]} An array containing all the actual values of the enum
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* ```typescript
|
|
1068
|
+
* // String enum example
|
|
1069
|
+
* enum Color {
|
|
1070
|
+
* RED = "red",
|
|
1071
|
+
* GREEN = "green",
|
|
1072
|
+
* BLUE = "blue"
|
|
1073
|
+
* }
|
|
1074
|
+
*
|
|
1075
|
+
* const colorValues = getEnumValues(Color);
|
|
1076
|
+
* // Returns: ["red", "green", "blue"]
|
|
1077
|
+
*
|
|
1078
|
+
* // Use for validation
|
|
1079
|
+
* function isValidColor(value: string): value is Color {
|
|
1080
|
+
* return getEnumValues(Color).includes(value as Color);
|
|
1081
|
+
* }
|
|
1082
|
+
* ```
|
|
1083
|
+
*
|
|
1084
|
+
* @example
|
|
1085
|
+
* ```typescript
|
|
1086
|
+
* // Numeric enum example
|
|
1087
|
+
* enum Status {
|
|
1088
|
+
* PENDING, // 0
|
|
1089
|
+
* APPROVED, // 1
|
|
1090
|
+
* REJECTED // 2
|
|
1091
|
+
* }
|
|
1092
|
+
*
|
|
1093
|
+
* const statusValues = getEnumValues(Status);
|
|
1094
|
+
* // Returns: [0, 1, 2] (not ["PENDING", "APPROVED", "REJECTED", 0, 1, 2])
|
|
1095
|
+
*
|
|
1096
|
+
* // Use for creating select options
|
|
1097
|
+
* const statusOptions = getEnumValues(Status).map(value => ({
|
|
1098
|
+
* value,
|
|
1099
|
+
* label: Status[value] // Get the key name for display
|
|
1100
|
+
* }));
|
|
1101
|
+
* ```
|
|
1102
|
+
*
|
|
1103
|
+
* @example
|
|
1104
|
+
* ```typescript
|
|
1105
|
+
* // Mixed numeric enum example
|
|
1106
|
+
* enum HttpStatus {
|
|
1107
|
+
* OK = 200,
|
|
1108
|
+
* NOT_FOUND = 404,
|
|
1109
|
+
* SERVER_ERROR = 500
|
|
1110
|
+
* }
|
|
1111
|
+
*
|
|
1112
|
+
* const httpStatusValues = getEnumValues(HttpStatus);
|
|
1113
|
+
* // Returns: [200, 404, 500]
|
|
1114
|
+
*
|
|
1115
|
+
* // Use for status code validation
|
|
1116
|
+
* function isValidHttpStatus(code: number): code is HttpStatus {
|
|
1117
|
+
* return getEnumValues(HttpStatus).includes(code as HttpStatus);
|
|
1118
|
+
* }
|
|
1119
|
+
* ```
|
|
1120
|
+
*
|
|
1121
|
+
* @example
|
|
1122
|
+
* ```typescript
|
|
1123
|
+
* // Creating dropdown options from enum
|
|
1124
|
+
* enum UserRole {
|
|
1125
|
+
* ADMIN = "admin",
|
|
1126
|
+
* USER = "user",
|
|
1127
|
+
* MODERATOR = "moderator"
|
|
1128
|
+
* }
|
|
1129
|
+
*
|
|
1130
|
+
* const roleOptions = getEnumValues(UserRole).map(role => ({
|
|
1131
|
+
* value: role,
|
|
1132
|
+
* label: role.charAt(0).toUpperCase() + role.slice(1)
|
|
1133
|
+
* }));
|
|
1134
|
+
* // Returns: [
|
|
1135
|
+
* // { value: "admin", label: "Admin" },
|
|
1136
|
+
* // { value: "user", label: "User" },
|
|
1137
|
+
* // { value: "moderator", label: "Moderator" }
|
|
1138
|
+
* // ]
|
|
1139
|
+
* ```
|
|
1140
|
+
*
|
|
1141
|
+
* @remarks
|
|
1142
|
+
* - Automatically detects numeric vs string enums and handles them appropriately
|
|
1143
|
+
* - For numeric enums, filters out reverse mapping entries that TypeScript automatically creates
|
|
1144
|
+
* - For string enums, returns all values as-is since there are no reverse mappings
|
|
1145
|
+
* - Works with mixed numeric enums (enums with explicit numeric values)
|
|
1146
|
+
* - The returned array maintains the order of enum declaration
|
|
1147
|
+
* - Type-safe: the return type is correctly inferred as an array of the enum's value types
|
|
1148
|
+
*
|
|
1149
|
+
* @see {@link Object.values} The underlying method used to extract enum entries
|
|
1150
|
+
*/
|
|
1151
|
+
function getEnumValues(e) {
|
|
1152
|
+
const values = Object.values(e);
|
|
1153
|
+
// In numeric enums, values can appear as keys (reverse mapping)
|
|
1154
|
+
const isNumericEnum = values.some(v => typeof v === "number");
|
|
1155
|
+
return isNumericEnum ? values.filter(v => typeof v !== "string") : values;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Filters items by recursively matching nested property values.
|
|
1159
|
+
*/
|
|
1160
|
+
function filterByObject(items, filters) {
|
|
1161
|
+
function matchesObject(item, filter) {
|
|
1162
|
+
if (typeof filter !== "object" || filter === null) {
|
|
1163
|
+
return item === filter;
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof item !== "object" || item === null) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
const itemObj = item;
|
|
1169
|
+
const filterObj = filter;
|
|
1170
|
+
for (const key in filterObj) {
|
|
1171
|
+
if (!matchesObject(itemObj[key], filterObj[key])) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
return items.filter(item => matchesObject(item, filters));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// noinspection JSUnusedGlobalSymbols
|
|
1181
|
+
/**
|
|
1182
|
+
* Comprehensive mapping of file extensions to their corresponding MIME types.
|
|
1183
|
+
*
|
|
1184
|
+
* This map provides a bidirectional mapping between file extensions and MIME types,
|
|
1185
|
+
* enabling applications to determine the appropriate content type for files or
|
|
1186
|
+
* to identify the likely file extension for a given MIME type.
|
|
1187
|
+
*
|
|
1188
|
+
* The map includes entries for common file formats across various categories:
|
|
1189
|
+
* - Documents (PDF, DOC, DOCX, XLS, XLSX, etc.)
|
|
1190
|
+
* - Images (JPG, PNG, GIF, SVG, WebP, etc.)
|
|
1191
|
+
* - Audio (MP3, WAV, OGG, etc.)
|
|
1192
|
+
* - Video (MP4, WebM, AVI, etc.)
|
|
1193
|
+
* - Archives (ZIP, RAR, 7Z, etc.)
|
|
1194
|
+
* - Web resources (HTML, CSS, JS, etc.)
|
|
1195
|
+
* - And many more specialized formats
|
|
1196
|
+
*
|
|
1197
|
+
* @remarks
|
|
1198
|
+
* The map is indexed by file extension (without the dot prefix) and contains
|
|
1199
|
+
* the corresponding MIME type as the value. Use utility functions like
|
|
1200
|
+
* getMimeTypeFromExtension and getExtensionFromMimeType to perform lookups
|
|
1201
|
+
* rather than accessing this map directly.
|
|
1202
|
+
*
|
|
1203
|
+
* Extensions are stored in lowercase to ensure case-insensitive matching.
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* ```typescript
|
|
1207
|
+
* // Get MIME type for a file extension
|
|
1208
|
+
* const mimeType = getMimeTypeFromExtension('pdf'); // 'application/pdf'
|
|
1209
|
+
*
|
|
1210
|
+
* // Get file extension for a MIME type
|
|
1211
|
+
* const extension = getExtensionFromMimeType('image/jpeg'); // 'jpg'
|
|
1212
|
+
* ```
|
|
1213
|
+
*/
|
|
1214
|
+
const mimeTypes = new Map([["123", "application/vnd.lotus-1-2-3"], ["1km", "application/vnd.1000minds.decision-model+xml"], ["3dml", "text/vnd.in3d.3dml"], ["3ds", "image/x-3ds"], ["3g2", "video/3gpp2"], ["3gp", "video/3gpp"], ["3gpp", "video/3gpp"], ["3mf", "model/3mf"], ["7z", "application/x-7z-compressed"], ["aab", "application/x-authorware-bin"], ["aac", "audio/x-aac"], ["aam", "application/x-authorware-map"], ["aas", "application/x-authorware-seg"], ["abw", "application/x-abiword"], ["ac", "application/vnd.nokia.n-gage.ac+xml"], ["acc", "application/vnd.americandynamics.acc"], ["ace", "application/x-ace-compressed"], ["acu", "application/vnd.acucobol"], ["acutc", "application/vnd.acucorp"], ["adp", "audio/adpcm"], ["adts", "audio/aac"], ["aep", "application/vnd.audiograph"], ["afm", "application/x-font-type1"], ["afp", "application/vnd.ibm.modcap"], ["age", "application/vnd.age"], ["ahead", "application/vnd.ahead.space"], ["ai", "application/postscript"], ["aif", "audio/x-aiff"], ["aifc", "audio/x-aiff"], ["aiff", "audio/x-aiff"], ["air", "application/vnd.adobe.air-application-installer-package+zip"], ["ait", "application/vnd.dvb.ait"], ["ami", "application/vnd.amiga.ami"], ["aml", "application/automationml-aml+xml"], ["amlx", "application/automationml-amlx+zip"], ["amr", "audio/amr"], ["apk", "application/vnd.android.package-archive"], ["apng", "image/apng"], ["appcache", "text/cache-manifest"], ["appinstaller", "application/appinstaller"], ["application", "application/x-ms-application"], ["appx", "application/appx"], ["appxbundle", "application/appxbundle"], ["apr", "application/vnd.lotus-approach"], ["arc", "application/x-freearc"], ["arj", "application/x-arj"], ["asc", "application/pgp-signature"], ["asf", "video/x-ms-asf"], ["asm", "text/x-asm"], ["aso", "application/vnd.accpac.simply.aso"], ["asx", "video/x-ms-asf"], ["atc", "application/vnd.acucorp"], ["atom", "application/atom+xml"], ["atomcat", "application/atomcat+xml"], ["atomdeleted", "application/atomdeleted+xml"], ["atomsvc", "application/atomsvc+xml"], ["atx", "application/vnd.antix.game-component"], ["au", "audio/basic"], ["avci", "image/avci"], ["avcs", "image/avcs"], ["avi", "video/x-msvideo"], ["avif", "image/avif"], ["aw", "application/applixware"], ["azf", "application/vnd.airzip.filesecure.azf"], ["azs", "application/vnd.airzip.filesecure.azs"], ["azv", "image/vnd.airzip.accelerator.azv"], ["azw", "application/vnd.amazon.ebook"], ["b16", "image/vnd.pco.b16"], ["bat", "application/x-msdownload"], ["bcpio", "application/x-bcpio"], ["bdf", "application/x-font-bdf"], ["bdm", "application/vnd.syncml.dm+wbxml"], ["bdoc", "application/x-bdoc"], ["bed", "application/vnd.realvnc.bed"], ["bh2", "application/vnd.fujitsu.oasysprs"], ["bin", "application/octet-stream"], ["blb", "application/x-blorb"], ["blorb", "application/x-blorb"], ["bmi", "application/vnd.bmi"], ["bmml", "application/vnd.balsamiq.bmml+xml"], ["bmp", "image/x-ms-bmp"], ["book", "application/vnd.framemaker"], ["box", "application/vnd.previewsystems.box"], ["boz", "application/x-bzip2"], ["bpk", "application/octet-stream"], ["bsp", "model/vnd.valve.source.compiled-map"], ["btf", "image/prs.btif"], ["btif", "image/prs.btif"], ["buffer", "application/octet-stream"], ["bz", "application/x-bzip"], ["bz2", "application/x-bzip2"], ["c", "text/x-c"], ["c11amc", "application/vnd.cluetrust.cartomobile-config"], ["c11amz", "application/vnd.cluetrust.cartomobile-config-pkg"], ["c4d", "application/vnd.clonk.c4group"], ["c4f", "application/vnd.clonk.c4group"], ["c4g", "application/vnd.clonk.c4group"], ["c4p", "application/vnd.clonk.c4group"], ["c4u", "application/vnd.clonk.c4group"], ["cab", "application/vnd.ms-cab-compressed"], ["caf", "audio/x-caf"], ["cap", "application/vnd.tcpdump.pcap"], ["car", "application/vnd.curl.car"], ["cat", "application/vnd.ms-pki.seccat"], ["cb7", "application/x-cbr"], ["cba", "application/x-cbr"], ["cbr", "application/x-cbr"], ["cbt", "application/x-cbr"], ["cbz", "application/x-cbr"], ["cc", "text/x-c"], ["cco", "application/x-cocoa"], ["cct", "application/x-director"], ["ccxml", "application/ccxml+xml"], ["cdbcmsg", "application/vnd.contact.cmsg"], ["cdf", "application/x-netcdf"], ["cdfx", "application/cdfx+xml"], ["cdkey", "application/vnd.mediastation.cdkey"], ["cdmia", "application/cdmi-capability"], ["cdmic", "application/cdmi-container"], ["cdmid", "application/cdmi-domain"], ["cdmio", "application/cdmi-object"], ["cdmiq", "application/cdmi-queue"], ["cdx", "chemical/x-cdx"], ["cdxml", "application/vnd.chemdraw+xml"], ["cdy", "application/vnd.cinderella"], ["cer", "application/pkix-cert"], ["cfs", "application/x-cfs-compressed"], ["cgm", "image/cgm"], ["chat", "application/x-chat"], ["chm", "application/vnd.ms-htmlhelp"], ["chrt", "application/vnd.kde.kchart"], ["cif", "chemical/x-cif"], ["cii", "application/vnd.anser-web-certificate-issue-initiation"], ["cil", "application/vnd.ms-artgalry"], ["cjs", "application/node"], ["cla", "application/vnd.claymore"], ["class", "application/java-vm"], ["cld", "model/vnd.cld"], ["clkk", "application/vnd.crick.clicker.keyboard"], ["clkp", "application/vnd.crick.clicker.palette"], ["clkt", "application/vnd.crick.clicker.template"], ["clkw", "application/vnd.crick.clicker.wordbank"], ["clkx", "application/vnd.crick.clicker"], ["clp", "application/x-msclip"], ["cmc", "application/vnd.cosmocaller"], ["cmdf", "chemical/x-cmdf"], ["cml", "chemical/x-cml"], ["cmp", "application/vnd.yellowriver-custom-menu"], ["cmx", "image/x-cmx"], ["cod", "application/vnd.rim.cod"], ["coffee", "text/coffeescript"], ["com", "application/x-msdownload"], ["conf", "text/plain"], ["cpio", "application/x-cpio"], ["cpl", "application/cpl+xml"], ["cpp", "text/x-c"], ["cpt", "application/mac-compactpro"], ["crd", "application/x-mscardfile"], ["crl", "application/pkix-crl"], ["crt", "application/x-x509-ca-cert"], ["crx", "application/x-chrome-extension"], ["cryptonote", "application/vnd.rig.cryptonote"], ["csh", "application/x-csh"], ["csl", "application/vnd.citationstyles.style+xml"], ["csml", "chemical/x-csml"], ["csp", "application/vnd.commonspace"], ["css", "text/css"], ["cst", "application/x-director"], ["csv", "text/csv"], ["cu", "application/cu-seeme"], ["curl", "text/vnd.curl"], ["cwl", "application/cwl"], ["cww", "application/prs.cww"], ["cxt", "application/x-director"], ["cxx", "text/x-c"], ["dae", "model/vnd.collada+xml"], ["daf", "application/vnd.mobius.daf"], ["dart", "application/vnd.dart"], ["dataless", "application/vnd.fdsn.seed"], ["davmount", "application/davmount+xml"], ["dbf", "application/vnd.dbf"], ["dbk", "application/docbook+xml"], ["dcr", "application/x-director"], ["dcurl", "text/vnd.curl.dcurl"], ["dd2", "application/vnd.oma.dd2+xml"], ["ddd", "application/vnd.fujixerox.ddd"], ["ddf", "application/vnd.syncml.dmddf+xml"], ["dds", "image/vnd.ms-dds"], ["deb", "application/x-debian-package"], ["def", "text/plain"], ["deploy", "application/octet-stream"], ["der", "application/x-x509-ca-cert"], ["dfac", "application/vnd.dreamfactory"], ["dgc", "application/x-dgc-compressed"], ["dib", "image/bmp"], ["dic", "text/x-c"], ["dir", "application/x-director"], ["dis", "application/vnd.mobius.dis"], ["disposition-notification", "message/disposition-notification"], ["dist", "application/octet-stream"], ["distz", "application/octet-stream"], ["djv", "image/vnd.djvu"], ["djvu", "image/vnd.djvu"], ["dll", "application/x-msdownload"], ["dmg", "application/x-apple-diskimage"], ["dmp", "application/vnd.tcpdump.pcap"], ["dms", "application/octet-stream"], ["dna", "application/vnd.dna"], ["doc", "application/msword"], ["docm", "application/vnd.ms-word.document.macroenabled.12"], ["docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"], ["dot", "application/msword"], ["dotm", "application/vnd.ms-word.template.macroenabled.12"], ["dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"], ["dp", "application/vnd.osgi.dp"], ["dpg", "application/vnd.dpgraph"], ["dpx", "image/dpx"], ["dra", "audio/vnd.dra"], ["drle", "image/dicom-rle"], ["dsc", "text/prs.lines.tag"], ["dssc", "application/dssc+der"], ["dtb", "application/x-dtbook+xml"], ["dtd", "application/xml-dtd"], ["dts", "audio/vnd.dts"], ["dtshd", "audio/vnd.dts.hd"], ["dump", "application/octet-stream"], ["dvb", "video/vnd.dvb.file"], ["dvi", "application/x-dvi"], ["dwd", "application/atsc-dwd+xml"], ["dwf", "model/vnd.dwf"], ["dwg", "image/vnd.dwg"], ["dxf", "image/vnd.dxf"], ["dxp", "application/vnd.spotfire.dxp"], ["dxr", "application/x-director"], ["ear", "application/java-archive"], ["ecelp4800", "audio/vnd.nuera.ecelp4800"], ["ecelp7470", "audio/vnd.nuera.ecelp7470"], ["ecelp9600", "audio/vnd.nuera.ecelp9600"], ["ecma", "application/ecmascript"], ["edm", "application/vnd.novadigm.edm"], ["edx", "application/vnd.novadigm.edx"], ["efif", "application/vnd.picsel"], ["ei6", "application/vnd.pg.osasli"], ["elc", "application/octet-stream"], ["emf", "image/emf"], ["eml", "message/rfc822"], ["emma", "application/emma+xml"], ["emotionml", "application/emotionml+xml"], ["emz", "application/x-msmetafile"], ["eol", "audio/vnd.digital-winds"], ["eot", "application/vnd.ms-fontobject"], ["eps", "application/postscript"], ["epub", "application/epub+zip"], ["es3", "application/vnd.eszigno3+xml"], ["esa", "application/vnd.osgi.subsystem"], ["esf", "application/vnd.epson.esf"], ["et3", "application/vnd.eszigno3+xml"], ["etx", "text/x-setext"], ["eva", "application/x-eva"], ["evy", "application/x-envoy"], ["exe", "application/x-msdownload"], ["exi", "application/exi"], ["exp", "application/express"], ["exr", "image/aces"], ["ext", "application/vnd.novadigm.ext"], ["ez", "application/andrew-inset"], ["ez2", "application/vnd.ezpix-album"], ["ez3", "application/vnd.ezpix-package"], ["f", "text/x-fortran"], ["f4v", "video/x-f4v"], ["f77", "text/x-fortran"], ["f90", "text/x-fortran"], ["fbs", "image/vnd.fastbidsheet"], ["fcdt", "application/vnd.adobe.formscentral.fcdt"], ["fcs", "application/vnd.isac.fcs"], ["fdf", "application/vnd.fdf"], ["fdt", "application/fdt+xml"], ["fe_launch", "application/vnd.denovo.fcselayout-link"], ["fg5", "application/vnd.fujitsu.oasysgp"], ["fgd", "application/x-director"], ["fh", "image/x-freehand"], ["fh4", "image/x-freehand"], ["fh5", "image/x-freehand"], ["fh7", "image/x-freehand"], ["fhc", "image/x-freehand"], ["fig", "application/x-xfig"], ["fits", "image/fits"], ["flac", "audio/x-flac"], ["fli", "video/x-fli"], ["flo", "application/vnd.micrografx.flo"], ["flv", "video/x-flv"], ["flw", "application/vnd.kde.kivio"], ["flx", "text/vnd.fmi.flexstor"], ["fly", "text/vnd.fly"], ["fm", "application/vnd.framemaker"], ["fnc", "application/vnd.frogans.fnc"], ["fo", "application/vnd.software602.filler.form+xml"], ["for", "text/x-fortran"], ["fpx", "image/vnd.fpx"], ["frame", "application/vnd.framemaker"], ["fsc", "application/vnd.fsc.weblaunch"], ["fst", "image/vnd.fst"], ["ftc", "application/vnd.fluxtime.clip"], ["fti", "application/vnd.anser-web-funds-transfer-initiation"], ["fvt", "video/vnd.fvt"], ["fxp", "application/vnd.adobe.fxp"], ["fxpl", "application/vnd.adobe.fxp"], ["fzs", "application/vnd.fuzzysheet"], ["g2w", "application/vnd.geoplan"], ["g3", "image/g3fax"], ["g3w", "application/vnd.geospace"], ["gac", "application/vnd.groove-account"], ["gam", "application/x-tads"], ["gbr", "application/rpki-ghostbusters"], ["gca", "application/x-gca-compressed"], ["gdl", "model/vnd.gdl"], ["gdoc", "application/vnd.google-apps.document"], ["ged", "text/vnd.familysearch.gedcom"], ["geo", "application/vnd.dynageo"], ["geojson", "application/geo+json"], ["gex", "application/vnd.geometry-explorer"], ["ggb", "application/vnd.geogebra.file"], ["ggt", "application/vnd.geogebra.tool"], ["ghf", "application/vnd.groove-help"], ["gif", "image/gif"], ["gim", "application/vnd.groove-identity-message"], ["glb", "model/gltf-binary"], ["gltf", "model/gltf+json"], ["gml", "application/gml+xml"], ["gmx", "application/vnd.gmx"], ["gnumeric", "application/x-gnumeric"], ["gph", "application/vnd.flographit"], ["gpx", "application/gpx+xml"], ["gqf", "application/vnd.grafeq"], ["gqs", "application/vnd.grafeq"], ["gram", "application/srgs"], ["gramps", "application/x-gramps-xml"], ["gre", "application/vnd.geometry-explorer"], ["grv", "application/vnd.groove-injector"], ["grxml", "application/srgs+xml"], ["gsf", "application/x-font-ghostscript"], ["gsheet", "application/vnd.google-apps.spreadsheet"], ["gslides", "application/vnd.google-apps.presentation"], ["gtar", "application/x-gtar"], ["gtm", "application/vnd.groove-tool-message"], ["gtw", "model/vnd.gtw"], ["gv", "text/vnd.graphviz"], ["gxf", "application/gxf"], ["gxt", "application/vnd.geonext"], ["gz", "application/gzip"], ["h", "text/x-c"], ["h261", "video/h261"], ["h263", "video/h263"], ["h264", "video/h264"], ["hal", "application/vnd.hal+xml"], ["hbci", "application/vnd.hbci"], ["hbs", "text/x-handlebars-template"], ["hdd", "application/x-virtualbox-hdd"], ["hdf", "application/x-hdf"], ["heic", "image/heic"], ["heics", "image/heic-sequence"], ["heif", "image/heif"], ["heifs", "image/heif-sequence"], ["hej2", "image/hej2k"], ["held", "application/atsc-held+xml"], ["hh", "text/x-c"], ["hjson", "application/hjson"], ["hlp", "application/winhlp"], ["hpgl", "application/vnd.hp-hpgl"], ["hpid", "application/vnd.hp-hpid"], ["hps", "application/vnd.hp-hps"], ["hqx", "application/mac-binhex40"], ["hsj2", "image/hsj2"], ["htc", "text/x-component"], ["htke", "application/vnd.kenameaapp"], ["htm", "text/html"], ["html", "text/html"], ["hvd", "application/vnd.yamaha.hv-dic"], ["hvp", "application/vnd.yamaha.hv-voice"], ["hvs", "application/vnd.yamaha.hv-script"], ["i2g", "application/vnd.intergeo"], ["icc", "application/vnd.iccprofile"], ["ice", "x-conference/x-cooltalk"], ["icm", "application/vnd.iccprofile"], ["ico", "image/x-icon"], ["ics", "text/calendar"], ["ief", "image/ief"], ["ifb", "text/calendar"], ["ifm", "application/vnd.shana.informed.formdata"], ["iges", "model/iges"], ["igl", "application/vnd.igloader"], ["igm", "application/vnd.insors.igm"], ["igs", "model/iges"], ["igx", "application/vnd.micrografx.igx"], ["iif", "application/vnd.shana.informed.interchange"], ["img", "application/octet-stream"], ["imp", "application/vnd.accpac.simply.imp"], ["ims", "application/vnd.ms-ims"], ["in", "text/plain"], ["ini", "text/plain"], ["ink", "application/inkml+xml"], ["inkml", "application/inkml+xml"], ["install", "application/x-install-instructions"], ["iota", "application/vnd.astraea-software.iota"], ["ipfix", "application/ipfix"], ["ipk", "application/vnd.shana.informed.package"], ["irm", "application/vnd.ibm.rights-management"], ["irp", "application/vnd.irepository.package+xml"], ["iso", "application/x-iso9660-image"], ["itp", "application/vnd.shana.informed.formtemplate"], ["its", "application/its+xml"], ["ivp", "application/vnd.immervision-ivp"], ["ivu", "application/vnd.immervision-ivu"], ["jad", "text/vnd.sun.j2me.app-descriptor"], ["jade", "text/jade"], ["jam", "application/vnd.jam"], ["jar", "application/java-archive"], ["jardiff", "application/x-java-archive-diff"], ["java", "text/x-java-source"], ["jhc", "image/jphc"], ["jisp", "application/vnd.jisp"], ["jls", "image/jls"], ["jlt", "application/vnd.hp-jlyt"], ["jng", "image/x-jng"], ["jnlp", "application/x-java-jnlp-file"], ["joda", "application/vnd.joost.joda-archive"], ["jp2", "image/jp2"], ["jpe", "image/jpeg"], ["jpeg", "image/jpeg"], ["jpf", "image/jpx"], ["jpg", "image/jpeg"], ["jpg2", "image/jp2"], ["jpgm", "video/jpm"], ["jpgv", "video/jpeg"], ["jph", "image/jph"], ["jpm", "video/jpm"], ["jpx", "image/jpx"], ["js", "text/javascript"], ["json", "application/json"], ["json5", "application/json5"], ["jsonld", "application/ld+json"], ["jsonml", "application/jsonml+json"], ["jsx", "text/jsx"], ["jt", "model/jt"], ["jxr", "image/jxr"], ["jxra", "image/jxra"], ["jxrs", "image/jxrs"], ["jxs", "image/jxs"], ["jxsc", "image/jxsc"], ["jxsi", "image/jxsi"], ["jxss", "image/jxss"], ["kar", "audio/midi"], ["karbon", "application/vnd.kde.karbon"], ["kdbx", "application/x-keepass2"], ["key", "application/x-iwork-keynote-sffkey"], ["kfo", "application/vnd.kde.kformula"], ["kia", "application/vnd.kidspiration"], ["kml", "application/vnd.google-earth.kml+xml"], ["kmz", "application/vnd.google-earth.kmz"], ["kne", "application/vnd.kinar"], ["knp", "application/vnd.kinar"], ["kon", "application/vnd.kde.kontour"], ["kpr", "application/vnd.kde.kpresenter"], ["kpt", "application/vnd.kde.kpresenter"], ["kpxx", "application/vnd.ds-keypoint"], ["ksp", "application/vnd.kde.kspread"], ["ktr", "application/vnd.kahootz"], ["ktx", "image/ktx"], ["ktx2", "image/ktx2"], ["ktz", "application/vnd.kahootz"], ["kwd", "application/vnd.kde.kword"], ["kwt", "application/vnd.kde.kword"], ["lasxml", "application/vnd.las.las+xml"], ["latex", "application/x-latex"], ["lbd", "application/vnd.llamagraphics.life-balance.desktop"], ["lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"], ["les", "application/vnd.hhe.lesson-player"], ["less", "text/less"], ["lgr", "application/lgr+xml"], ["lha", "application/x-lzh-compressed"], ["link66", "application/vnd.route66.link66+xml"], ["list", "text/plain"], ["list3820", "application/vnd.ibm.modcap"], ["listafp", "application/vnd.ibm.modcap"], ["litcoffee", "text/coffeescript"], ["lnk", "application/x-ms-shortcut"], ["log", "text/plain"], ["lostxml", "application/lost+xml"], ["lrf", "application/octet-stream"], ["lrm", "application/vnd.ms-lrm"], ["ltf", "application/vnd.frogans.ltf"], ["lua", "text/x-lua"], ["luac", "application/x-lua-bytecode"], ["lvp", "audio/vnd.lucent.voice"], ["lwp", "application/vnd.lotus-wordpro"], ["lzh", "application/x-lzh-compressed"], ["m13", "application/x-msmediaview"], ["m14", "application/x-msmediaview"], ["m1v", "video/mpeg"], ["m21", "application/mp21"], ["m2a", "audio/mpeg"], ["m2v", "video/mpeg"], ["m3a", "audio/mpeg"], ["m3u", "audio/x-mpegurl"], ["m3u8", "application/vnd.apple.mpegurl"], ["m4a", "audio/x-m4a"], ["m4p", "application/mp4"], ["m4s", "video/iso.segment"], ["m4u", "video/vnd.mpegurl"], ["m4v", "video/x-m4v"], ["ma", "application/mathematica"], ["mads", "application/mads+xml"], ["maei", "application/mmt-aei+xml"], ["mag", "application/vnd.ecowin.chart"], ["maker", "application/vnd.framemaker"], ["man", "text/troff"], ["manifest", "text/cache-manifest"], ["map", "application/json"], ["mar", "application/octet-stream"], ["markdown", "text/markdown"], ["mathml", "application/mathml+xml"], ["mb", "application/mathematica"], ["mbk", "application/vnd.mobius.mbk"], ["mbox", "application/mbox"], ["mc1", "application/vnd.medcalcdata"], ["mcd", "application/vnd.mcd"], ["mcurl", "text/vnd.curl.mcurl"], ["md", "text/markdown"], ["mdb", "application/x-msaccess"], ["mdi", "image/vnd.ms-modi"], ["mdx", "text/mdx"], ["me", "text/troff"], ["mesh", "model/mesh"], ["meta4", "application/metalink4+xml"], ["metalink", "application/metalink+xml"], ["mets", "application/mets+xml"], ["mfm", "application/vnd.mfmp"], ["mft", "application/rpki-manifest"], ["mgp", "application/vnd.osgeo.mapguide.package"], ["mgz", "application/vnd.proteus.magazine"], ["mid", "audio/midi"], ["midi", "audio/midi"], ["mie", "application/x-mie"], ["mif", "application/vnd.mif"], ["mime", "message/rfc822"], ["mj2", "video/mj2"], ["mjp2", "video/mj2"], ["mjs", "text/javascript"], ["mk3d", "video/x-matroska"], ["mka", "audio/x-matroska"], ["mkd", "text/x-markdown"], ["mks", "video/x-matroska"], ["mkv", "video/x-matroska"], ["mlp", "application/vnd.dolby.mlp"], ["mmd", "application/vnd.chipnuts.karaoke-mmd"], ["mmf", "application/vnd.smaf"], ["mml", "text/mathml"], ["mmr", "image/vnd.fujixerox.edmics-mmr"], ["mng", "video/x-mng"], ["mny", "application/x-msmoney"], ["mobi", "application/x-mobipocket-ebook"], ["mods", "application/mods+xml"], ["mov", "video/quicktime"], ["movie", "video/x-sgi-movie"], ["mp2", "audio/mpeg"], ["mp21", "application/mp21"], ["mp2a", "audio/mpeg"], ["mp3", "audio/mpeg"], ["mp4", "video/mp4"], ["mp4a", "audio/mp4"], ["mp4s", "application/mp4"], ["mp4v", "video/mp4"], ["mpc", "application/vnd.mophun.certificate"], ["mpd", "application/dash+xml"], ["mpe", "video/mpeg"], ["mpeg", "video/mpeg"], ["mpf", "application/media-policy-dataset+xml"], ["mpg", "video/mpeg"], ["mpg4", "video/mp4"], ["mpga", "audio/mpeg"], ["mpkg", "application/vnd.apple.installer+xml"], ["mpm", "application/vnd.blueice.multipass"], ["mpn", "application/vnd.mophun.application"], ["mpp", "application/vnd.ms-project"], ["mpt", "application/vnd.ms-project"], ["mpy", "application/vnd.ibm.minipay"], ["mqy", "application/vnd.mobius.mqy"], ["mrc", "application/marc"], ["mrcx", "application/marcxml+xml"], ["ms", "text/troff"], ["mscml", "application/mediaservercontrol+xml"], ["mseed", "application/vnd.fdsn.mseed"], ["mseq", "application/vnd.mseq"], ["msf", "application/vnd.epson.msf"], ["msg", "application/vnd.ms-outlook"], ["msh", "model/mesh"], ["msi", "application/x-msdownload"], ["msix", "application/msix"], ["msixbundle", "application/msixbundle"], ["msl", "application/vnd.mobius.msl"], ["msm", "application/octet-stream"], ["msp", "application/octet-stream"], ["msty", "application/vnd.muvee.style"], ["mtl", "model/mtl"], ["mts", "model/vnd.mts"], ["mus", "application/vnd.musician"], ["musd", "application/mmt-usd+xml"], ["musicxml", "application/vnd.recordare.musicxml+xml"], ["mvb", "application/x-msmediaview"], ["mvt", "application/vnd.mapbox-vector-tile"], ["mwf", "application/vnd.mfer"], ["mxf", "application/mxf"], ["mxl", "application/vnd.recordare.musicxml"], ["mxmf", "audio/mobile-xmf"], ["mxml", "application/xv+xml"], ["mxs", "application/vnd.triscape.mxs"], ["mxu", "video/vnd.mpegurl"], ["n-gage", "application/vnd.nokia.n-gage.symbian.install"], ["n3", "text/n3"], ["nb", "application/mathematica"], ["nbp", "application/vnd.wolfram.player"], ["nc", "application/x-netcdf"], ["ncx", "application/x-dtbncx+xml"], ["nfo", "text/x-nfo"], ["ngdat", "application/vnd.nokia.n-gage.data"], ["nitf", "application/vnd.nitf"], ["nlu", "application/vnd.neurolanguage.nlu"], ["nml", "application/vnd.enliven"], ["nnd", "application/vnd.noblenet-directory"], ["nns", "application/vnd.noblenet-sealer"], ["nnw", "application/vnd.noblenet-web"], ["npx", "image/vnd.net-fpx"], ["nq", "application/n-quads"], ["nsc", "application/x-conference"], ["nsf", "application/vnd.lotus-notes"], ["nt", "application/n-triples"], ["ntf", "application/vnd.nitf"], ["numbers", "application/x-iwork-numbers-sffnumbers"], ["nzb", "application/x-nzb"], ["oa2", "application/vnd.fujitsu.oasys2"], ["oa3", "application/vnd.fujitsu.oasys3"], ["oas", "application/vnd.fujitsu.oasys"], ["obd", "application/x-msbinder"], ["obgx", "application/vnd.openblox.game+xml"], ["obj", "model/obj"], ["oda", "application/oda"], ["odb", "application/vnd.oasis.opendocument.database"], ["odc", "application/vnd.oasis.opendocument.chart"], ["odf", "application/vnd.oasis.opendocument.formula"], ["odft", "application/vnd.oasis.opendocument.formula-template"], ["odg", "application/vnd.oasis.opendocument.graphics"], ["odi", "application/vnd.oasis.opendocument.image"], ["odm", "application/vnd.oasis.opendocument.text-master"], ["odp", "application/vnd.oasis.opendocument.presentation"], ["ods", "application/vnd.oasis.opendocument.spreadsheet"], ["odt", "application/vnd.oasis.opendocument.text"], ["oga", "audio/ogg"], ["ogex", "model/vnd.opengex"], ["ogg", "audio/ogg"], ["ogv", "video/ogg"], ["ogx", "application/ogg"], ["omdoc", "application/omdoc+xml"], ["onepkg", "application/onenote"], ["onetmp", "application/onenote"], ["onetoc", "application/onenote"], ["onetoc2", "application/onenote"], ["opf", "application/oebps-package+xml"], ["opml", "text/x-opml"], ["oprc", "application/vnd.palm"], ["opus", "audio/ogg"], ["org", "text/x-org"], ["osf", "application/vnd.yamaha.openscoreformat"], ["osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"], ["osm", "application/vnd.openstreetmap.data+xml"], ["otc", "application/vnd.oasis.opendocument.chart-template"], ["otf", "font/otf"], ["otg", "application/vnd.oasis.opendocument.graphics-template"], ["oth", "application/vnd.oasis.opendocument.text-web"], ["oti", "application/vnd.oasis.opendocument.image-template"], ["otp", "application/vnd.oasis.opendocument.presentation-template"], ["ots", "application/vnd.oasis.opendocument.spreadsheet-template"], ["ott", "application/vnd.oasis.opendocument.text-template"], ["ova", "application/x-virtualbox-ova"], ["ovf", "application/x-virtualbox-ovf"], ["owl", "application/rdf+xml"], ["oxps", "application/oxps"], ["oxt", "application/vnd.openofficeorg.extension"], ["p", "text/x-pascal"], ["p10", "application/pkcs10"], ["p12", "application/x-pkcs12"], ["p7b", "application/x-pkcs7-certificates"], ["p7c", "application/pkcs7-mime"], ["p7m", "application/pkcs7-mime"], ["p7r", "application/x-pkcs7-certreqresp"], ["p7s", "application/pkcs7-signature"], ["p8", "application/pkcs8"], ["pac", "application/x-ns-proxy-autoconfig"], ["pages", "application/x-iwork-pages-sffpages"], ["pas", "text/x-pascal"], ["paw", "application/vnd.pawaafile"], ["pbd", "application/vnd.powerbuilder6"], ["pbm", "image/x-portable-bitmap"], ["pcap", "application/vnd.tcpdump.pcap"], ["pcf", "application/x-font-pcf"], ["pcl", "application/vnd.hp-pcl"], ["pclxl", "application/vnd.hp-pclxl"], ["pct", "image/x-pict"], ["pcurl", "application/vnd.curl.pcurl"], ["pcx", "image/x-pcx"], ["pdb", "application/x-pilot"], ["pde", "text/x-processing"], ["pem", "application/x-x509-ca-cert"], ["pfa", "application/x-font-type1"], ["pfb", "application/x-font-type1"], ["pfm", "application/x-font-type1"], ["pfr", "application/font-tdpfr"], ["pfx", "application/x-pkcs12"], ["pgm", "image/x-portable-graymap"], ["pgn", "application/x-chess-pgn"], ["pgp", "application/pgp-encrypted"], ["php", "application/x-httpd-php"], ["pic", "image/x-pict"], ["pkg", "application/octet-stream"], ["pki", "application/pkixcmp"], ["pkipath", "application/pkix-pkipath"], ["pkpass", "application/vnd.apple.pkpass"], ["pl", "application/x-perl"], ["plb", "application/vnd.3gpp.pic-bw-large"], ["plc", "application/vnd.mobius.plc"], ["plf", "application/vnd.pocketlearn"], ["pls", "application/pls+xml"], ["pm", "application/x-perl"], ["pml", "application/vnd.ctc-posml"], ["png", "image/png"], ["pnm", "image/x-portable-anymap"], ["portpkg", "application/vnd.macports.portpkg"], ["pot", "application/vnd.ms-powerpoint"], ["potm", "application/vnd.ms-powerpoint.template.macroenabled.12"], ["potx", "application/vnd.openxmlformats-officedocument.presentationml.template"], ["ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"], ["ppd", "application/vnd.cups-ppd"], ["ppm", "image/x-portable-pixmap"], ["pps", "application/vnd.ms-powerpoint"], ["ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"], ["ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"], ["ppt", "application/vnd.ms-powerpoint"], ["pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"], ["pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"], ["pqa", "application/vnd.palm"], ["prc", "model/prc"], ["pre", "application/vnd.lotus-freelance"], ["prf", "application/pics-rules"], ["provx", "application/provenance+xml"], ["ps", "application/postscript"], ["psb", "application/vnd.3gpp.pic-bw-small"], ["psd", "image/vnd.adobe.photoshop"], ["psf", "application/x-font-linux-psf"], ["pskcxml", "application/pskc+xml"], ["pti", "image/prs.pti"], ["ptid", "application/vnd.pvi.ptid1"], ["pub", "application/x-mspublisher"], ["pvb", "application/vnd.3gpp.pic-bw-var"], ["pwn", "application/vnd.3m.post-it-notes"], ["pya", "audio/vnd.ms-playready.media.pya"], ["pyo", "model/vnd.pytha.pyox"], ["pyox", "model/vnd.pytha.pyox"], ["pyv", "video/vnd.ms-playready.media.pyv"], ["qam", "application/vnd.epson.quickanime"], ["qbo", "application/vnd.intu.qbo"], ["qfx", "application/vnd.intu.qfx"], ["qps", "application/vnd.publishare-delta-tree"], ["qt", "video/quicktime"], ["qwd", "application/vnd.quark.quarkxpress"], ["qwt", "application/vnd.quark.quarkxpress"], ["qxb", "application/vnd.quark.quarkxpress"], ["qxd", "application/vnd.quark.quarkxpress"], ["qxl", "application/vnd.quark.quarkxpress"], ["qxt", "application/vnd.quark.quarkxpress"], ["ra", "audio/x-realaudio"], ["ram", "audio/x-pn-realaudio"], ["raml", "application/raml+yaml"], ["rapd", "application/route-apd+xml"], ["rar", "application/x-rar-compressed"], ["ras", "image/x-cmu-raster"], ["rcprofile", "application/vnd.ipunplugged.rcprofile"], ["rdf", "application/rdf+xml"], ["rdz", "application/vnd.data-vision.rdz"], ["relo", "application/p2p-overlay+xml"], ["rep", "application/vnd.businessobjects"], ["res", "application/x-dtbresource+xml"], ["rgb", "image/x-rgb"], ["rif", "application/reginfo+xml"], ["rip", "audio/vnd.rip"], ["ris", "application/x-research-info-systems"], ["rl", "application/resource-lists+xml"], ["rlc", "image/vnd.fujixerox.edmics-rlc"], ["rld", "application/resource-lists-diff+xml"], ["rm", "application/vnd.rn-realmedia"], ["rmi", "audio/midi"], ["rmp", "audio/x-pn-realaudio-plugin"], ["rms", "application/vnd.jcp.javame.midlet-rms"], ["rmvb", "application/vnd.rn-realmedia-vbr"], ["rnc", "application/relax-ng-compact-syntax"], ["rng", "application/xml"], ["roa", "application/rpki-roa"], ["roff", "text/troff"], ["rp9", "application/vnd.cloanto.rp9"], ["rpm", "application/x-redhat-package-manager"], ["rpss", "application/vnd.nokia.radio-presets"], ["rpst", "application/vnd.nokia.radio-preset"], ["rq", "application/sparql-query"], ["rs", "application/rls-services+xml"], ["rsat", "application/atsc-rsat+xml"], ["rsd", "application/rsd+xml"], ["rsheet", "application/urc-ressheet+xml"], ["rss", "application/rss+xml"], ["rtf", "text/rtf"], ["rtx", "text/richtext"], ["run", "application/x-makeself"], ["rusd", "application/route-usd+xml"], ["s", "text/x-asm"], ["s3m", "audio/s3m"], ["saf", "application/vnd.yamaha.smaf-audio"], ["sass", "text/x-sass"], ["sbml", "application/sbml+xml"], ["sc", "application/vnd.ibm.secure-container"], ["scd", "application/x-msschedule"], ["scm", "application/vnd.lotus-screencam"], ["scq", "application/scvp-cv-request"], ["scs", "application/scvp-cv-response"], ["scss", "text/x-scss"], ["scurl", "text/vnd.curl.scurl"], ["sda", "application/vnd.stardivision.draw"], ["sdc", "application/vnd.stardivision.calc"], ["sdd", "application/vnd.stardivision.impress"], ["sdkd", "application/vnd.solent.sdkm+xml"], ["sdkm", "application/vnd.solent.sdkm+xml"], ["sdp", "application/sdp"], ["sdw", "application/vnd.stardivision.writer"], ["sea", "application/x-sea"], ["see", "application/vnd.seemail"], ["seed", "application/vnd.fdsn.seed"], ["sema", "application/vnd.sema"], ["semd", "application/vnd.semd"], ["semf", "application/vnd.semf"], ["senmlx", "application/senml+xml"], ["sensmlx", "application/sensml+xml"], ["ser", "application/java-serialized-object"], ["setpay", "application/set-payment-initiation"], ["setreg", "application/set-registration-initiation"], ["sfd-hdstx", "application/vnd.hydrostatix.sof-data"], ["sfs", "application/vnd.spotfire.sfs"], ["sfv", "text/x-sfv"], ["sgi", "image/sgi"], ["sgl", "application/vnd.stardivision.writer-global"], ["sgm", "text/sgml"], ["sgml", "text/sgml"], ["sh", "application/x-sh"], ["shar", "application/x-shar"], ["shex", "text/shex"], ["shf", "application/shf+xml"], ["shtml", "text/html"], ["sid", "image/x-mrsid-image"], ["sieve", "application/sieve"], ["sig", "application/pgp-signature"], ["sil", "audio/silk"], ["silo", "model/mesh"], ["sis", "application/vnd.symbian.install"], ["sisx", "application/vnd.symbian.install"], ["sit", "application/x-stuffit"], ["sitx", "application/x-stuffitx"], ["siv", "application/sieve"], ["skd", "application/vnd.koan"], ["skm", "application/vnd.koan"], ["skp", "application/vnd.koan"], ["skt", "application/vnd.koan"], ["sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"], ["sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"], ["slim", "text/slim"], ["slm", "text/slim"], ["sls", "application/route-s-tsid+xml"], ["slt", "application/vnd.epson.salt"], ["sm", "application/vnd.stepmania.stepchart"], ["smf", "application/vnd.stardivision.math"], ["smi", "application/smil+xml"], ["smil", "application/smil+xml"], ["smv", "video/x-smv"], ["smzip", "application/vnd.stepmania.package"], ["snd", "audio/basic"], ["snf", "application/x-font-snf"], ["so", "application/octet-stream"], ["spc", "application/x-pkcs7-certificates"], ["spdx", "text/spdx"], ["spf", "application/vnd.yamaha.smaf-phrase"], ["spl", "application/x-futuresplash"], ["spot", "text/vnd.in3d.spot"], ["spp", "application/scvp-vp-response"], ["spq", "application/scvp-vp-request"], ["spx", "audio/ogg"], ["sql", "application/x-sql"], ["src", "application/x-wais-source"], ["srt", "application/x-subrip"], ["sru", "application/sru+xml"], ["srx", "application/sparql-results+xml"], ["ssdl", "application/ssdl+xml"], ["sse", "application/vnd.kodak-descriptor"], ["ssf", "application/vnd.epson.ssf"], ["ssml", "application/ssml+xml"], ["st", "application/vnd.sailingtracker.track"], ["stc", "application/vnd.sun.xml.calc.template"], ["std", "application/vnd.sun.xml.draw.template"], ["stf", "application/vnd.wt.stf"], ["sti", "application/vnd.sun.xml.impress.template"], ["stk", "application/hyperstudio"], ["stl", "model/stl"], ["stpx", "model/step+xml"], ["stpxz", "model/step-xml+zip"], ["stpz", "model/step+zip"], ["str", "application/vnd.pg.format"], ["stw", "application/vnd.sun.xml.writer.template"], ["styl", "text/stylus"], ["stylus", "text/stylus"], ["sub", "text/vnd.dvb.subtitle"], ["sus", "application/vnd.sus-calendar"], ["susp", "application/vnd.sus-calendar"], ["sv4cpio", "application/x-sv4cpio"], ["sv4crc", "application/x-sv4crc"], ["svc", "application/vnd.dvb.service"], ["svd", "application/vnd.svd"], ["svg", "image/svg+xml"], ["svgz", "image/svg+xml"], ["swa", "application/x-director"], ["swf", "application/x-shockwave-flash"], ["swi", "application/vnd.aristanetworks.swi"], ["swidtag", "application/swid+xml"], ["sxc", "application/vnd.sun.xml.calc"], ["sxd", "application/vnd.sun.xml.draw"], ["sxg", "application/vnd.sun.xml.writer.global"], ["sxi", "application/vnd.sun.xml.impress"], ["sxm", "application/vnd.sun.xml.math"], ["sxw", "application/vnd.sun.xml.writer"], ["t", "text/troff"], ["t3", "application/x-t3vm-image"], ["t38", "image/t38"], ["taglet", "application/vnd.mynfc"], ["tao", "application/vnd.tao.intent-module-archive"], ["tap", "image/vnd.tencent.tap"], ["tar", "application/x-tar"], ["tcap", "application/vnd.3gpp2.tcap"], ["tcl", "application/x-tcl"], ["td", "application/urc-targetdesc+xml"], ["teacher", "application/vnd.smart.teacher"], ["tei", "application/tei+xml"], ["teicorpus", "application/tei+xml"], ["tex", "application/x-tex"], ["texi", "application/x-texinfo"], ["texinfo", "application/x-texinfo"], ["text", "text/plain"], ["tfi", "application/thraud+xml"], ["tfm", "application/x-tex-tfm"], ["tfx", "image/tiff-fx"], ["tga", "image/x-tga"], ["thmx", "application/vnd.ms-officetheme"], ["tif", "image/tiff"], ["tiff", "image/tiff"], ["tk", "application/x-tcl"], ["tmo", "application/vnd.tmobile-livetv"], ["toml", "application/toml"], ["torrent", "application/x-bittorrent"], ["tpl", "application/vnd.groove-tool-template"], ["tpt", "application/vnd.trid.tpt"], ["tr", "text/troff"], ["tra", "application/vnd.trueapp"], ["trig", "application/trig"], ["trm", "application/x-msterminal"], ["ts", "video/mp2t"], ["tsd", "application/timestamped-data"], ["tsv", "text/tab-separated-values"], ["ttc", "font/collection"], ["ttf", "font/ttf"], ["ttl", "text/turtle"], ["ttml", "application/ttml+xml"], ["twd", "application/vnd.simtech-mindmapper"], ["twds", "application/vnd.simtech-mindmapper"], ["txd", "application/vnd.genomatix.tuxedo"], ["txf", "application/vnd.mobius.txf"], ["txt", "text/plain"], ["u32", "application/x-authorware-bin"], ["u3d", "model/u3d"], ["u8dsn", "message/global-delivery-status"], ["u8hdr", "message/global-headers"], ["u8mdn", "message/global-disposition-notification"], ["u8msg", "message/global"], ["ubj", "application/ubjson"], ["udeb", "application/x-debian-package"], ["ufd", "application/vnd.ufdl"], ["ufdl", "application/vnd.ufdl"], ["ulx", "application/x-glulx"], ["umj", "application/vnd.umajin"], ["unityweb", "application/vnd.unity"], ["uo", "application/vnd.uoml+xml"], ["uoml", "application/vnd.uoml+xml"], ["uri", "text/uri-list"], ["uris", "text/uri-list"], ["urls", "text/uri-list"], ["usda", "model/vnd.usda"], ["usdz", "model/vnd.usdz+zip"], ["ustar", "application/x-ustar"], ["utz", "application/vnd.uiq.theme"], ["uu", "text/x-uuencode"], ["uva", "audio/vnd.dece.audio"], ["uvd", "application/vnd.dece.data"], ["uvf", "application/vnd.dece.data"], ["uvg", "image/vnd.dece.graphic"], ["uvh", "video/vnd.dece.hd"], ["uvi", "image/vnd.dece.graphic"], ["uvm", "video/vnd.dece.mobile"], ["uvp", "video/vnd.dece.pd"], ["uvs", "video/vnd.dece.sd"], ["uvt", "application/vnd.dece.ttml+xml"], ["uvu", "video/vnd.uvvu.mp4"], ["uvv", "video/vnd.dece.video"], ["uvva", "audio/vnd.dece.audio"], ["uvvd", "application/vnd.dece.data"], ["uvvf", "application/vnd.dece.data"], ["uvvg", "image/vnd.dece.graphic"], ["uvvh", "video/vnd.dece.hd"], ["uvvi", "image/vnd.dece.graphic"], ["uvvm", "video/vnd.dece.mobile"], ["uvvp", "video/vnd.dece.pd"], ["uvvs", "video/vnd.dece.sd"], ["uvvt", "application/vnd.dece.ttml+xml"], ["uvvu", "video/vnd.uvvu.mp4"], ["uvvv", "video/vnd.dece.video"], ["uvvx", "application/vnd.dece.unspecified"], ["uvvz", "application/vnd.dece.zip"], ["uvx", "application/vnd.dece.unspecified"], ["uvz", "application/vnd.dece.zip"], ["vbox", "application/x-virtualbox-vbox"], ["vbox-extpack", "application/x-virtualbox-vbox-extpack"], ["vcard", "text/vcard"], ["vcd", "application/x-cdlink"], ["vcf", "text/x-vcard"], ["vcg", "application/vnd.groove-vcard"], ["vcs", "text/x-vcalendar"], ["vcx", "application/vnd.vcx"], ["vdi", "application/x-virtualbox-vdi"], ["vds", "model/vnd.sap.vds"], ["vhd", "application/x-virtualbox-vhd"], ["vis", "application/vnd.visionary"], ["viv", "video/vnd.vivo"], ["vmdk", "application/x-virtualbox-vmdk"], ["vob", "video/x-ms-vob"], ["vor", "application/vnd.stardivision.writer"], ["vox", "application/x-authorware-bin"], ["vrml", "model/vrml"], ["vsd", "application/vnd.visio"], ["vsf", "application/vnd.vsf"], ["vss", "application/vnd.visio"], ["vst", "application/vnd.visio"], ["vsw", "application/vnd.visio"], ["vtf", "image/vnd.valve.source.texture"], ["vtt", "text/vtt"], ["vtu", "model/vnd.vtu"], ["vxml", "application/voicexml+xml"], ["w3d", "application/x-director"], ["wad", "application/x-doom"], ["wadl", "application/vnd.sun.wadl+xml"], ["war", "application/java-archive"], ["wasm", "application/wasm"], ["wav", "audio/x-wav"], ["wax", "audio/x-ms-wax"], ["wbmp", "image/vnd.wap.wbmp"], ["wbs", "application/vnd.criticaltools.wbs+xml"], ["wbxml", "application/vnd.wap.wbxml"], ["wcm", "application/vnd.ms-works"], ["wdb", "application/vnd.ms-works"], ["wdp", "image/vnd.ms-photo"], ["weba", "audio/webm"], ["webapp", "application/x-web-app-manifest+json"], ["webm", "video/webm"], ["webmanifest", "application/manifest+json"], ["webp", "image/webp"], ["wg", "application/vnd.pmi.widget"], ["wgsl", "text/wgsl"], ["wgt", "application/widget"], ["wif", "application/watcherinfo+xml"], ["wks", "application/vnd.ms-works"], ["wm", "video/x-ms-wm"], ["wma", "audio/x-ms-wma"], ["wmd", "application/x-ms-wmd"], ["wmf", "image/wmf"], ["wml", "text/vnd.wap.wml"], ["wmlc", "application/vnd.wap.wmlc"], ["wmls", "text/vnd.wap.wmlscript"], ["wmlsc", "application/vnd.wap.wmlscriptc"], ["wmv", "video/x-ms-wmv"], ["wmx", "video/x-ms-wmx"], ["wmz", "application/x-msmetafile"], ["woff", "font/woff"], ["woff2", "font/woff2"], ["wpd", "application/vnd.wordperfect"], ["wpl", "application/vnd.ms-wpl"], ["wps", "application/vnd.ms-works"], ["wqd", "application/vnd.wqd"], ["wri", "application/x-mswrite"], ["wrl", "model/vrml"], ["wsc", "message/vnd.wfa.wsc"], ["wsdl", "application/wsdl+xml"], ["wspolicy", "application/wspolicy+xml"], ["wtb", "application/vnd.webturbo"], ["wvx", "video/x-ms-wvx"], ["x32", "application/x-authorware-bin"], ["x3d", "model/x3d+xml"], ["x3db", "model/x3d+fastinfoset"], ["x3dbz", "model/x3d+binary"], ["x3dv", "model/x3d-vrml"], ["x3dvz", "model/x3d+vrml"], ["x3dz", "model/x3d+xml"], ["x_b", "model/vnd.parasolid.transmit.binary"], ["x_t", "model/vnd.parasolid.transmit.text"], ["xaml", "application/xaml+xml"], ["xap", "application/x-silverlight-app"], ["xar", "application/vnd.xara"], ["xav", "application/xcap-att+xml"], ["xbap", "application/x-ms-xbap"], ["xbd", "application/vnd.fujixerox.docuworks.binder"], ["xbm", "image/x-xbitmap"], ["xca", "application/xcap-caps+xml"], ["xcs", "application/calendar+xml"], ["xdf", "application/xcap-diff+xml"], ["xdm", "application/vnd.syncml.dm+xml"], ["xdp", "application/vnd.adobe.xdp+xml"], ["xdssc", "application/dssc+xml"], ["xdw", "application/vnd.fujixerox.docuworks"], ["xel", "application/xcap-el+xml"], ["xenc", "application/xenc+xml"], ["xer", "application/patch-ops-error+xml"], ["xfdf", "application/xfdf"], ["xfdl", "application/vnd.xfdl"], ["xht", "application/xhtml+xml"], ["xhtm", "application/vnd.pwg-xhtml-print+xml"], ["xhtml", "application/xhtml+xml"], ["xhvml", "application/xv+xml"], ["xif", "image/vnd.xiff"], ["xla", "application/vnd.ms-excel"], ["xlam", "application/vnd.ms-excel.addin.macroenabled.12"], ["xlc", "application/vnd.ms-excel"], ["xlf", "application/xliff+xml"], ["xlm", "application/vnd.ms-excel"], ["xls", "application/vnd.ms-excel"], ["xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"], ["xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"], ["xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], ["xlt", "application/vnd.ms-excel"], ["xltm", "application/vnd.ms-excel.template.macroenabled.12"], ["xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"], ["xlw", "application/vnd.ms-excel"], ["xm", "audio/xm"], ["xml", "text/xml"], ["xns", "application/xcap-ns+xml"], ["xo", "application/vnd.olpc-sugar"], ["xop", "application/xop+xml"], ["xpi", "application/x-xpinstall"], ["xpl", "application/xproc+xml"], ["xpm", "image/x-xpixmap"], ["xpr", "application/vnd.is-xpr"], ["xps", "application/vnd.ms-xpsdocument"], ["xpw", "application/vnd.intercon.formnet"], ["xpx", "application/vnd.intercon.formnet"], ["xsd", "application/xml"], ["xsf", "application/prs.xsf+xml"], ["xsl", "application/xslt+xml"], ["xslt", "application/xslt+xml"], ["xsm", "application/vnd.syncml+xml"], ["xspf", "application/xspf+xml"], ["xul", "application/vnd.mozilla.xul+xml"], ["xvm", "application/xv+xml"], ["xvml", "application/xv+xml"], ["xwd", "image/x-xwindowdump"], ["xyz", "chemical/x-xyz"], ["xz", "application/x-xz"], ["yaml", "text/yaml"], ["yang", "application/yang"], ["yin", "application/yin+xml"], ["yml", "text/yaml"], ["ymp", "text/x-suse-ymp"], ["z1", "application/x-zmachine"], ["z2", "application/x-zmachine"], ["z3", "application/x-zmachine"], ["z4", "application/x-zmachine"], ["z5", "application/x-zmachine"], ["z6", "application/x-zmachine"], ["z7", "application/x-zmachine"], ["z8", "application/x-zmachine"], ["zaz", "application/vnd.zzazz.deck+xml"], ["zip", "application/zip"], ["zir", "application/vnd.zul"], ["zirz", "application/vnd.zul"], ["zmm", "application/vnd.handheld-entertainment+xml"]]);
|
|
1215
|
+
/**
|
|
1216
|
+
* Get the file extension of the given mime type.
|
|
1217
|
+
* @param {string} mimeType - Mime type.
|
|
1218
|
+
* @param {Map<string, string>} [allowedMimeTypes] - Allowed mime types. If not provided, the default mime types map will be used.
|
|
1219
|
+
* @returns {string | undefined} File extension or undefined if the mime type is not found.
|
|
1220
|
+
*
|
|
1221
|
+
* @example
|
|
1222
|
+
* ```TypeScript
|
|
1223
|
+
* // Using the default mime types map
|
|
1224
|
+
* const extension = getFileExt('application/pdf');
|
|
1225
|
+
* // Output: 'pdf'
|
|
1226
|
+
* ```
|
|
1227
|
+
*
|
|
1228
|
+
* @example
|
|
1229
|
+
* ```TypeScript
|
|
1230
|
+
* // Using a custom mime types map
|
|
1231
|
+
* const customMimeTypes = new Map([
|
|
1232
|
+
* ['jpg', 'image/jpeg'],
|
|
1233
|
+
* ['png', 'image/png']
|
|
1234
|
+
* ]);
|
|
1235
|
+
* const extension = getFileExt('image/jpeg', customMimeTypes);
|
|
1236
|
+
* // Output: 'jpg'
|
|
1237
|
+
* ```
|
|
1238
|
+
*/
|
|
1239
|
+
const getFileExt = (mimeType, allowedMimeTypes) => {
|
|
1240
|
+
return getMapKey(allowedMimeTypes ?? mimeTypes, mimeType);
|
|
1241
|
+
};
|
|
1242
|
+
/**
|
|
1243
|
+
* Get file size in human-readable format.
|
|
1244
|
+
* @param {number} size - File size in bytes.
|
|
1245
|
+
* @param {boolean} [round] - Whether to round the size to whole numbers. If true, decimals will be removed.
|
|
1246
|
+
* @returns {string} File size in human-readable format (B, KB, MB, or GB).
|
|
1247
|
+
*
|
|
1248
|
+
* @example
|
|
1249
|
+
* ```TypeScript
|
|
1250
|
+
* // Get file size with decimals
|
|
1251
|
+
* const readableSize = getFileSize(1536);
|
|
1252
|
+
* // Output: '1.50 KB'
|
|
1253
|
+
* ```
|
|
1254
|
+
*
|
|
1255
|
+
* @example
|
|
1256
|
+
* ```TypeScript
|
|
1257
|
+
* // Get rounded file size
|
|
1258
|
+
* const roundedSize = getFileSize(1536, true);
|
|
1259
|
+
* // Output: '2 KB'
|
|
1260
|
+
* ```
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```TypeScript
|
|
1264
|
+
* // Get size for larger files
|
|
1265
|
+
* const largeFileSize = getFileSize(1073741824);
|
|
1266
|
+
* // Output: '1.00 GB'
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
const getFileSize = (size, round) => {
|
|
1270
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1271
|
+
if (size < 1024) {
|
|
1272
|
+
return `${Math.round(size)} B`;
|
|
1273
|
+
}
|
|
1274
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1275
|
+
if (size < 1024 * 1024) {
|
|
1276
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1277
|
+
return `${(size / 1024).toFixed(round ? 0 : 2)} KB`;
|
|
1278
|
+
}
|
|
1279
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1280
|
+
if (size < 1024 * 1024 * 1024) {
|
|
1281
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1282
|
+
return `${(size / 1024 / 1024).toFixed(round ? 0 : 2)} MB`;
|
|
1283
|
+
}
|
|
1284
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1285
|
+
return `${(size / 1024 / 1024 / 1024).toFixed(round ? 0 : 2)} GB`;
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Base for hexadecimal number conversion
|
|
1290
|
+
*
|
|
1291
|
+
* This constant defines the radix (base) used for hexadecimal number conversion.
|
|
1292
|
+
* The value 16 represents the standard hexadecimal numbering system (0-9, A-F).
|
|
1293
|
+
* It's used in toString() and parseInt() operations involving hex values.
|
|
1294
|
+
*
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```typescript
|
|
1297
|
+
* // Converting a number to hexadecimal string
|
|
1298
|
+
* const hexString = number.toString(HEX_RADIX);
|
|
1299
|
+
*
|
|
1300
|
+
* // Converting a hexadecimal string to a number
|
|
1301
|
+
* const number = parseInt(hexString, HEX_RADIX);
|
|
1302
|
+
* ```
|
|
1303
|
+
*
|
|
1304
|
+
* @see {@link HEX_PADDING_LENGTH} Related constant for hex string formatting
|
|
1305
|
+
* @see {@link HEX_PADDING_CHAR} Related constant for hex string padding
|
|
1306
|
+
*/
|
|
1307
|
+
const HEX_RADIX = 16;
|
|
1308
|
+
/**
|
|
1309
|
+
* Minimum length for padded hexadecimal strings
|
|
1310
|
+
*
|
|
1311
|
+
* This constant defines the minimum length for hexadecimal strings after padding.
|
|
1312
|
+
* It ensures that hex values have a consistent length (e.g., "0A" instead of "A").
|
|
1313
|
+
* The value 2 ensures that single-digit hex values are padded with a leading zero.
|
|
1314
|
+
*
|
|
1315
|
+
* @example
|
|
1316
|
+
* ```typescript
|
|
1317
|
+
* // Padding a hex string to ensure consistent length
|
|
1318
|
+
* const paddedHex = hexString.padStart(HEX_PADDING_LENGTH, HEX_PADDING_CHAR);
|
|
1319
|
+
*
|
|
1320
|
+
* // Full example with conversion and padding
|
|
1321
|
+
* const byteValue = 10;
|
|
1322
|
+
* const hexByte = byteValue.toString(HEX_RADIX).padStart(HEX_PADDING_LENGTH, HEX_PADDING_CHAR);
|
|
1323
|
+
* // hexByte = "0A"
|
|
1324
|
+
* ```
|
|
1325
|
+
*
|
|
1326
|
+
* @see {@link HEX_RADIX} Related constant for hex conversion
|
|
1327
|
+
* @see {@link HEX_PADDING_CHAR} Related constant for the padding character
|
|
1328
|
+
*/
|
|
1329
|
+
const HEX_PADDING_LENGTH = 2;
|
|
1330
|
+
/**
|
|
1331
|
+
* Character used for padding hexadecimal strings
|
|
1332
|
+
*
|
|
1333
|
+
* This constant defines the character used to pad hexadecimal strings to reach
|
|
1334
|
+
* the minimum length defined by HEX_PADDING_LENGTH. The value "0" is the standard
|
|
1335
|
+
* padding character for hexadecimal values.
|
|
1336
|
+
*
|
|
1337
|
+
* @example
|
|
1338
|
+
* ```typescript
|
|
1339
|
+
* // Padding a hex string with zeros
|
|
1340
|
+
* const paddedHex = hexString.padStart(HEX_PADDING_LENGTH, HEX_PADDING_CHAR);
|
|
1341
|
+
*
|
|
1342
|
+
* // Example with a single-digit hex value
|
|
1343
|
+
* const hexValue = "F";
|
|
1344
|
+
* const paddedHex = hexValue.padStart(HEX_PADDING_LENGTH, HEX_PADDING_CHAR);
|
|
1345
|
+
* // paddedHex = "0F"
|
|
1346
|
+
* ```
|
|
1347
|
+
*
|
|
1348
|
+
* @see {@link HEX_RADIX} Related constant for hex conversion
|
|
1349
|
+
* @see {@link HEX_PADDING_LENGTH} Related constant for the minimum length
|
|
1350
|
+
*/
|
|
1351
|
+
const HEX_PADDING_CHAR = "0";
|
|
1352
|
+
/**
|
|
1353
|
+
* Number of characters to remove in string truncation operations
|
|
1354
|
+
*
|
|
1355
|
+
* This constant defines the default number of characters to remove when
|
|
1356
|
+
* truncating strings in various utility functions. The value 2 is commonly
|
|
1357
|
+
* used when removing the last two characters of a string (e.g., removing
|
|
1358
|
+
* a trailing comma and space).
|
|
1359
|
+
*
|
|
1360
|
+
* @example
|
|
1361
|
+
* ```typescript
|
|
1362
|
+
* // Removing trailing characters from a string
|
|
1363
|
+
* const items = ["apple", "banana", "cherry"];
|
|
1364
|
+
* let result = "";
|
|
1365
|
+
*
|
|
1366
|
+
* for (const item of items) {
|
|
1367
|
+
* result += item + ", ";
|
|
1368
|
+
* }
|
|
1369
|
+
*
|
|
1370
|
+
* // Remove trailing comma and space
|
|
1371
|
+
* if (result.length > 0) {
|
|
1372
|
+
* result = result.slice(0, -CHARACTERS_TO_REMOVE);
|
|
1373
|
+
* }
|
|
1374
|
+
* // result = "apple, banana, cherry"
|
|
1375
|
+
* ```
|
|
1376
|
+
*/
|
|
1377
|
+
const CHARACTERS_TO_REMOVE = 2;
|
|
1378
|
+
/**
|
|
1379
|
+
* Default maximum line length for word wrapping
|
|
1380
|
+
*
|
|
1381
|
+
* This constant defines the default maximum length of a line when performing
|
|
1382
|
+
* word wrapping operations. The value 80 is a common standard for line length
|
|
1383
|
+
* in many text formats and terminal displays.
|
|
1384
|
+
*
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```typescript
|
|
1387
|
+
* // Wrapping a long text to the default line length
|
|
1388
|
+
* function wrapText(text: string, maxLength = DEFAULT_LINE_LENGTH, breakChar = DEFAULT_BREAK_CHAR): string {
|
|
1389
|
+
* // Word wrapping implementation
|
|
1390
|
+
* const result = [];
|
|
1391
|
+
* let line = "";
|
|
1392
|
+
*
|
|
1393
|
+
* for (const word of text.split(" ")) {
|
|
1394
|
+
* if ((line + word).length > maxLength) {
|
|
1395
|
+
* result.push(line);
|
|
1396
|
+
* line = word;
|
|
1397
|
+
* } else {
|
|
1398
|
+
* line += (line ? " " : "") + word;
|
|
1399
|
+
* }
|
|
1400
|
+
* }
|
|
1401
|
+
*
|
|
1402
|
+
* if (line) {
|
|
1403
|
+
* result.push(line);
|
|
1404
|
+
* }
|
|
1405
|
+
*
|
|
1406
|
+
* return result.join(breakChar);
|
|
1407
|
+
* }
|
|
1408
|
+
* ```
|
|
1409
|
+
*
|
|
1410
|
+
* @see {@link DEFAULT_BREAK_CHAR} Related constant for line break character
|
|
1411
|
+
*/
|
|
1412
|
+
const DEFAULT_LINE_LENGTH = 80;
|
|
1413
|
+
/**
|
|
1414
|
+
* Default line break character for word wrapping
|
|
1415
|
+
*
|
|
1416
|
+
* This constant defines the default character or string used to separate lines
|
|
1417
|
+
* when performing word wrapping operations. The value "\n" represents a standard
|
|
1418
|
+
* line feed character used for line breaks in most text formats.
|
|
1419
|
+
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```typescript
|
|
1422
|
+
* // Using the default break character in word wrapping
|
|
1423
|
+
* const wrappedText = longText.match(new RegExp(`.{1,${DEFAULT_LINE_LENGTH}}(\\s|$)`, 'g'))
|
|
1424
|
+
* ?.join(DEFAULT_BREAK_CHAR) || longText;
|
|
1425
|
+
* ```
|
|
1426
|
+
*
|
|
1427
|
+
* @see {@link DEFAULT_LINE_LENGTH} Related constant for maximum line length
|
|
1428
|
+
*/
|
|
1429
|
+
const DEFAULT_BREAK_CHAR = "\n";
|
|
1430
|
+
/**
|
|
1431
|
+
* Default context length for text excerpts
|
|
1432
|
+
*
|
|
1433
|
+
* This constant defines the default number of characters to include before and after
|
|
1434
|
+
* a search term when generating text excerpts or snippets. The value 40 provides
|
|
1435
|
+
* enough context to understand the meaning while keeping the excerpt concise.
|
|
1436
|
+
*
|
|
1437
|
+
* @example
|
|
1438
|
+
* ```typescript
|
|
1439
|
+
* // Generating an excerpt around a search term
|
|
1440
|
+
* function generateExcerpt(text: string, searchTerm: string): string {
|
|
1441
|
+
* const index = text.toLowerCase().indexOf(searchTerm.toLowerCase());
|
|
1442
|
+
* if (index === -1) return text.substring(0, DEFAULT_CONTEXT_LENGTH * 2) + DEFAULT_ELLIPSIS;
|
|
1443
|
+
*
|
|
1444
|
+
* const start = Math.max(0, index - DEFAULT_CONTEXT_LENGTH);
|
|
1445
|
+
* const end = Math.min(text.length, index + searchTerm.length + DEFAULT_CONTEXT_LENGTH);
|
|
1446
|
+
*
|
|
1447
|
+
* let excerpt = text.substring(start, end);
|
|
1448
|
+
* if (start > 0) excerpt = DEFAULT_ELLIPSIS + excerpt;
|
|
1449
|
+
* if (end < text.length) excerpt = excerpt + DEFAULT_ELLIPSIS;
|
|
1450
|
+
*
|
|
1451
|
+
* return excerpt;
|
|
1452
|
+
* }
|
|
1453
|
+
* ```
|
|
1454
|
+
*
|
|
1455
|
+
* @see {@link DEFAULT_ELLIPSIS} Related constant for indicating truncated text
|
|
1456
|
+
* @see {@link CONTEXT_MULTIPLIER} Related constant for adjusting context length
|
|
1457
|
+
*/
|
|
1458
|
+
const DEFAULT_CONTEXT_LENGTH = 40;
|
|
1459
|
+
/**
|
|
1460
|
+
* Default ellipsis string for indicating truncated text
|
|
1461
|
+
*
|
|
1462
|
+
* This constant defines the default string used to indicate that text has been
|
|
1463
|
+
* truncated or omitted in excerpts and summaries. The value "..." is the standard
|
|
1464
|
+
* ellipsis used to represent omitted content.
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```typescript
|
|
1468
|
+
* // Truncating a long string with ellipsis
|
|
1469
|
+
* function truncate(text: string, maxLength: number): string {
|
|
1470
|
+
* if (text.length <= maxLength) return text;
|
|
1471
|
+
* return text.substring(0, maxLength - DEFAULT_ELLIPSIS.length) + DEFAULT_ELLIPSIS;
|
|
1472
|
+
* }
|
|
1473
|
+
*
|
|
1474
|
+
* const longText = "This is a very long text that needs to be truncated";
|
|
1475
|
+
* const truncated = truncate(longText, 20);
|
|
1476
|
+
* // truncated = "This is a very lo..."
|
|
1477
|
+
* ```
|
|
1478
|
+
*
|
|
1479
|
+
* @see {@link DEFAULT_CONTEXT_LENGTH} Related constant for excerpt generation
|
|
1480
|
+
*/
|
|
1481
|
+
const DEFAULT_ELLIPSIS = "...";
|
|
1482
|
+
/**
|
|
1483
|
+
* Multiplier for adjusting context length in excerpts
|
|
1484
|
+
*
|
|
1485
|
+
* This constant defines a multiplier used to adjust the context length when
|
|
1486
|
+
* generating excerpts under different conditions. The value 2 is typically
|
|
1487
|
+
* used to double the context length for certain scenarios, such as when no
|
|
1488
|
+
* search term is found.
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```typescript
|
|
1492
|
+
* // Using the multiplier to adjust context length
|
|
1493
|
+
* function getContextLength(hasSearchTerm: boolean): number {
|
|
1494
|
+
* return hasSearchTerm
|
|
1495
|
+
* ? DEFAULT_CONTEXT_LENGTH
|
|
1496
|
+
* : DEFAULT_CONTEXT_LENGTH * CONTEXT_MULTIPLIER;
|
|
1497
|
+
* }
|
|
1498
|
+
*
|
|
1499
|
+
* // Getting the first part of a text when no search term is found
|
|
1500
|
+
* function getFirstPart(text: string): string {
|
|
1501
|
+
* const length = DEFAULT_CONTEXT_LENGTH * CONTEXT_MULTIPLIER;
|
|
1502
|
+
* return text.length > length
|
|
1503
|
+
* ? text.substring(0, length) + DEFAULT_ELLIPSIS
|
|
1504
|
+
* : text;
|
|
1505
|
+
* }
|
|
1506
|
+
* ```
|
|
1507
|
+
*
|
|
1508
|
+
* @see {@link DEFAULT_CONTEXT_LENGTH} Related constant for base context length
|
|
1509
|
+
*/
|
|
1510
|
+
const CONTEXT_MULTIPLIER = 2;
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Comprehensive rules for English word inflection (singular/plural transformations)
|
|
1514
|
+
*
|
|
1515
|
+
* This module provides a structured collection of rules for transforming English words
|
|
1516
|
+
* between singular and plural forms. The rules are organized into categories based on
|
|
1517
|
+
* linguistic patterns and exceptions, ensuring accurate transformations for various
|
|
1518
|
+
* word types.
|
|
1519
|
+
*
|
|
1520
|
+
* Key features:
|
|
1521
|
+
* - Invariant words that are identical in singular and plural forms
|
|
1522
|
+
* - Irregular plural forms that don't follow standard patterns
|
|
1523
|
+
* - Latin and Greek origin words with their specific plural formations
|
|
1524
|
+
* - Pattern-based rules for common English word endings
|
|
1525
|
+
* - Default fallback rules for standard English pluralization
|
|
1526
|
+
*
|
|
1527
|
+
* The rules are applied in a specific priority order, from most specific to most general,
|
|
1528
|
+
* to ensure correct handling of special cases. Each rule consists of a regular expression
|
|
1529
|
+
* pattern and a replacement string or function.
|
|
1530
|
+
*
|
|
1531
|
+
* @example
|
|
1532
|
+
* ```typescript
|
|
1533
|
+
* // Using the rules with the plural() function
|
|
1534
|
+
* import { EnglishInflectionRules } from '@hichchi/utils/constants';
|
|
1535
|
+
* import { InflectionRule } from '@hichchi/utils/interfaces';
|
|
1536
|
+
*
|
|
1537
|
+
* function plural(word: string): string {
|
|
1538
|
+
* // Combine all rules in priority order
|
|
1539
|
+
* const rules: InflectionRule[] = [
|
|
1540
|
+
* ...EnglishInflectionRules.INVARIANT,
|
|
1541
|
+
* ...EnglishInflectionRules.IRREGULAR.toPlural,
|
|
1542
|
+
* ...EnglishInflectionRules.LATIN_GREEK.toPlural,
|
|
1543
|
+
* ...EnglishInflectionRules.PATTERN_RULES.toPlural,
|
|
1544
|
+
* ...EnglishInflectionRules.DEFAULT.toPlural,
|
|
1545
|
+
* ];
|
|
1546
|
+
*
|
|
1547
|
+
* // Apply the first matching rule
|
|
1548
|
+
* for (const rule of rules) {
|
|
1549
|
+
* if (rule.regex.test(word)) {
|
|
1550
|
+
* return word.replace(rule.regex, rule.replacement as string);
|
|
1551
|
+
* }
|
|
1552
|
+
* }
|
|
1553
|
+
*
|
|
1554
|
+
* return word;
|
|
1555
|
+
* }
|
|
1556
|
+
* ```
|
|
1557
|
+
*
|
|
1558
|
+
* @see {@link plural} Function that uses these rules to convert words to plural form
|
|
1559
|
+
* @see {@link singular} Function that uses these rules to convert words to singular form
|
|
1560
|
+
* @see {@link InflectionRuleCategories} Interface defining the structure of the rules
|
|
1561
|
+
* @see {@link InflectionRule} Interface for individual transformation rules
|
|
1562
|
+
*/
|
|
1563
|
+
const EnglishInflectionRules = {
|
|
1564
|
+
/**
|
|
1565
|
+
* Words that are the same in singular and plural form
|
|
1566
|
+
*
|
|
1567
|
+
* This category contains rules for words that don't change between singular and plural forms.
|
|
1568
|
+
* These invariant words (also called "zero plurals") have identical singular and plural forms
|
|
1569
|
+
* in English. The rule simply returns the word unchanged.
|
|
1570
|
+
*
|
|
1571
|
+
* Common examples include:
|
|
1572
|
+
* - Animal names: "fish", "deer", "sheep"
|
|
1573
|
+
* - Scientific terms: "species", "series"
|
|
1574
|
+
* - Uncountable nouns: "equipment"
|
|
1575
|
+
*
|
|
1576
|
+
* @example
|
|
1577
|
+
* ```typescript
|
|
1578
|
+
* // These words remain unchanged in plural form
|
|
1579
|
+
* plural("fish"); // "fish"
|
|
1580
|
+
* plural("deer"); // "deer"
|
|
1581
|
+
* plural("sheep"); // "sheep"
|
|
1582
|
+
* plural("species"); // "species"
|
|
1583
|
+
* plural("equipment"); // "equipment"
|
|
1584
|
+
* ```
|
|
1585
|
+
*
|
|
1586
|
+
* @see {@link plural} Function that applies these rules
|
|
1587
|
+
* @see {@link singular} Function that applies these rules
|
|
1588
|
+
*/
|
|
1589
|
+
INVARIANT: [{
|
|
1590
|
+
regex: /^(fish|deer|sheep|species|series|equipment)$/i,
|
|
1591
|
+
replacement: "$1"
|
|
1592
|
+
}],
|
|
1593
|
+
/**
|
|
1594
|
+
* Irregular plural forms
|
|
1595
|
+
*
|
|
1596
|
+
* This category contains rules for words with irregular plural forms that don't
|
|
1597
|
+
* follow standard English pluralization patterns. These words have unique
|
|
1598
|
+
* transformations between singular and plural forms that must be handled as
|
|
1599
|
+
* special cases.
|
|
1600
|
+
*
|
|
1601
|
+
* The category is divided into two rule sets:
|
|
1602
|
+
* - toPlural: Rules for converting singular forms to their irregular plural forms
|
|
1603
|
+
* - toSingular: Rules for converting irregular plural forms back to singular
|
|
1604
|
+
*
|
|
1605
|
+
* These rules handle common irregular English plurals like "child/children",
|
|
1606
|
+
* "man/men", "tooth/teeth", etc. Many of these irregular forms have historical
|
|
1607
|
+
* origins in Old English and other Germanic languages.
|
|
1608
|
+
*
|
|
1609
|
+
* @example
|
|
1610
|
+
* ```typescript
|
|
1611
|
+
* // Singular to plural transformations
|
|
1612
|
+
* plural("child"); // "children"
|
|
1613
|
+
* plural("man"); // "men"
|
|
1614
|
+
* plural("tooth"); // "teeth"
|
|
1615
|
+
* plural("person"); // "people"
|
|
1616
|
+
*
|
|
1617
|
+
* // Plural to singular transformations
|
|
1618
|
+
* singular("children"); // "child"
|
|
1619
|
+
* singular("men"); // "man"
|
|
1620
|
+
* singular("teeth"); // "tooth"
|
|
1621
|
+
* singular("people"); // "person"
|
|
1622
|
+
* ```
|
|
1623
|
+
*
|
|
1624
|
+
* @see {@link plural} Function that applies these rules for pluralization
|
|
1625
|
+
* @see {@link singular} Function that applies these rules for singularization
|
|
1626
|
+
*/
|
|
1627
|
+
IRREGULAR: {
|
|
1628
|
+
// Singular to plural transformations
|
|
1629
|
+
toPlural: [{
|
|
1630
|
+
regex: /^child$/i,
|
|
1631
|
+
replacement: "children"
|
|
1632
|
+
}, {
|
|
1633
|
+
regex: /^man$/i,
|
|
1634
|
+
replacement: "men"
|
|
1635
|
+
}, {
|
|
1636
|
+
regex: /^woman$/i,
|
|
1637
|
+
replacement: "women"
|
|
1638
|
+
}, {
|
|
1639
|
+
regex: /^tooth$/i,
|
|
1640
|
+
replacement: "teeth"
|
|
1641
|
+
}, {
|
|
1642
|
+
regex: /^foot$/i,
|
|
1643
|
+
replacement: "feet"
|
|
1644
|
+
}, {
|
|
1645
|
+
regex: /^goose$/i,
|
|
1646
|
+
replacement: "geese"
|
|
1647
|
+
}, {
|
|
1648
|
+
regex: /^mouse$/i,
|
|
1649
|
+
replacement: "mice"
|
|
1650
|
+
}, {
|
|
1651
|
+
regex: /^person$/i,
|
|
1652
|
+
replacement: "people"
|
|
1653
|
+
}, {
|
|
1654
|
+
regex: /^ox$/i,
|
|
1655
|
+
replacement: "oxen"
|
|
1656
|
+
}],
|
|
1657
|
+
// Plural to singular transformations
|
|
1658
|
+
toSingular: [{
|
|
1659
|
+
regex: /children$/i,
|
|
1660
|
+
replacement: "child"
|
|
1661
|
+
}, {
|
|
1662
|
+
regex: /men$/i,
|
|
1663
|
+
replacement: "man"
|
|
1664
|
+
}, {
|
|
1665
|
+
regex: /women$/i,
|
|
1666
|
+
replacement: "woman"
|
|
1667
|
+
}, {
|
|
1668
|
+
regex: /teeth$/i,
|
|
1669
|
+
replacement: "tooth"
|
|
1670
|
+
}, {
|
|
1671
|
+
regex: /feet$/i,
|
|
1672
|
+
replacement: "foot"
|
|
1673
|
+
}, {
|
|
1674
|
+
regex: /geese$/i,
|
|
1675
|
+
replacement: "goose"
|
|
1676
|
+
}, {
|
|
1677
|
+
regex: /mice$/i,
|
|
1678
|
+
replacement: "mouse"
|
|
1679
|
+
}, {
|
|
1680
|
+
regex: /people$/i,
|
|
1681
|
+
replacement: "person"
|
|
1682
|
+
}, {
|
|
1683
|
+
regex: /oxen$/i,
|
|
1684
|
+
replacement: "ox"
|
|
1685
|
+
}]
|
|
1686
|
+
},
|
|
1687
|
+
/**
|
|
1688
|
+
* Latin/Greek origin words
|
|
1689
|
+
*
|
|
1690
|
+
* This category contains rules for words borrowed from Latin and Greek that
|
|
1691
|
+
* retain their original pluralization patterns. These words follow different
|
|
1692
|
+
* rules than native English words and often have distinctive ending changes.
|
|
1693
|
+
*
|
|
1694
|
+
* The category is divided into two rule sets:
|
|
1695
|
+
* - toPlural: Rules for converting Latin/Greek singular forms to their plural forms
|
|
1696
|
+
* - toSingular: Rules for converting Latin/Greek plural forms back to singular
|
|
1697
|
+
*
|
|
1698
|
+
* Common patterns include:
|
|
1699
|
+
* - "-us" → "-i" (cactus/cacti, fungus/fungi)
|
|
1700
|
+
* - "-is" → "-es" (analysis/analyses, thesis/theses)
|
|
1701
|
+
* - "-on"/"-um" → "-a" (criterion/criteria, datum/data)
|
|
1702
|
+
* - "-ex"/"-ix" → "-ices" (index/indices, matrix/matrices)
|
|
1703
|
+
*
|
|
1704
|
+
* These words are often found in academic, scientific, and technical contexts.
|
|
1705
|
+
* Note that some of these words have alternative anglicized plural forms that
|
|
1706
|
+
* are also acceptable in modern English (e.g., "cactuses" alongside "cacti").
|
|
1707
|
+
*
|
|
1708
|
+
* @example
|
|
1709
|
+
* ```typescript
|
|
1710
|
+
* // Singular to plural transformations
|
|
1711
|
+
* plural("cactus"); // "cacti"
|
|
1712
|
+
* plural("analysis"); // "analyses"
|
|
1713
|
+
* plural("criterion"); // "criteria"
|
|
1714
|
+
* plural("index"); // "indices"
|
|
1715
|
+
* plural("matrix"); // "matrices"
|
|
1716
|
+
*
|
|
1717
|
+
* // Plural to singular transformations
|
|
1718
|
+
* singular("cacti"); // "cactus"
|
|
1719
|
+
* singular("analyses"); // "analysis"
|
|
1720
|
+
* singular("criteria"); // "criterion"
|
|
1721
|
+
* singular("indices"); // "index"
|
|
1722
|
+
* singular("matrices"); // "matrix"
|
|
1723
|
+
* ```
|
|
1724
|
+
*
|
|
1725
|
+
* @see {@link plural} Function that applies these rules for pluralization
|
|
1726
|
+
* @see {@link singular} Function that applies these rules for singularization
|
|
1727
|
+
*/
|
|
1728
|
+
LATIN_GREEK: {
|
|
1729
|
+
// Singular to plural transformations
|
|
1730
|
+
toPlural: [{
|
|
1731
|
+
regex: /^cactus$/i,
|
|
1732
|
+
replacement: "cacti"
|
|
1733
|
+
}, {
|
|
1734
|
+
regex: /^focus$/i,
|
|
1735
|
+
replacement: "foci"
|
|
1736
|
+
}, {
|
|
1737
|
+
regex: /^fungus$/i,
|
|
1738
|
+
replacement: "fungi"
|
|
1739
|
+
}, {
|
|
1740
|
+
regex: /^nucleus$/i,
|
|
1741
|
+
replacement: "nuclei"
|
|
1742
|
+
}, {
|
|
1743
|
+
regex: /^syllabus$/i,
|
|
1744
|
+
replacement: "syllabi"
|
|
1745
|
+
}, {
|
|
1746
|
+
regex: /^analysis$/i,
|
|
1747
|
+
replacement: "analyses"
|
|
1748
|
+
}, {
|
|
1749
|
+
regex: /^diagnosis$/i,
|
|
1750
|
+
replacement: "diagnoses"
|
|
1751
|
+
}, {
|
|
1752
|
+
regex: /^thesis$/i,
|
|
1753
|
+
replacement: "theses"
|
|
1754
|
+
}, {
|
|
1755
|
+
regex: /^criterion$/i,
|
|
1756
|
+
replacement: "criteria"
|
|
1757
|
+
}, {
|
|
1758
|
+
regex: /^datum$/i,
|
|
1759
|
+
replacement: "data"
|
|
1760
|
+
}, {
|
|
1761
|
+
regex: /^phenomenon$/i,
|
|
1762
|
+
replacement: "phenomena"
|
|
1763
|
+
}, {
|
|
1764
|
+
regex: /^index$/i,
|
|
1765
|
+
replacement: "indices"
|
|
1766
|
+
}, {
|
|
1767
|
+
regex: /^matrix$/i,
|
|
1768
|
+
replacement: "matrices"
|
|
1769
|
+
}, {
|
|
1770
|
+
regex: /^vertex$/i,
|
|
1771
|
+
replacement: "vertices"
|
|
1772
|
+
}, {
|
|
1773
|
+
regex: /^appendix$/i,
|
|
1774
|
+
replacement: "appendices"
|
|
1775
|
+
}],
|
|
1776
|
+
// Plural to singular transformations
|
|
1777
|
+
toSingular: [{
|
|
1778
|
+
regex: /matrices$/i,
|
|
1779
|
+
replacement: "matrix"
|
|
1780
|
+
}, {
|
|
1781
|
+
regex: /indices$/i,
|
|
1782
|
+
replacement: "index"
|
|
1783
|
+
}, {
|
|
1784
|
+
regex: /vertices$/i,
|
|
1785
|
+
replacement: "vertex"
|
|
1786
|
+
}, {
|
|
1787
|
+
regex: /appendices$/i,
|
|
1788
|
+
replacement: "appendix"
|
|
1789
|
+
}, {
|
|
1790
|
+
regex: /cacti$/i,
|
|
1791
|
+
replacement: "cactus"
|
|
1792
|
+
}, {
|
|
1793
|
+
regex: /foci$/i,
|
|
1794
|
+
replacement: "focus"
|
|
1795
|
+
}, {
|
|
1796
|
+
regex: /fungi$/i,
|
|
1797
|
+
replacement: "fungus"
|
|
1798
|
+
}, {
|
|
1799
|
+
regex: /nuclei$/i,
|
|
1800
|
+
replacement: "nucleus"
|
|
1801
|
+
}, {
|
|
1802
|
+
regex: /syllabi$/i,
|
|
1803
|
+
replacement: "syllabus"
|
|
1804
|
+
}, {
|
|
1805
|
+
regex: /analyses$/i,
|
|
1806
|
+
replacement: "analysis"
|
|
1807
|
+
}, {
|
|
1808
|
+
regex: /diagnoses$/i,
|
|
1809
|
+
replacement: "diagnosis"
|
|
1810
|
+
}, {
|
|
1811
|
+
regex: /theses$/i,
|
|
1812
|
+
replacement: "thesis"
|
|
1813
|
+
}, {
|
|
1814
|
+
regex: /criteria$/i,
|
|
1815
|
+
replacement: "criterion"
|
|
1816
|
+
}, {
|
|
1817
|
+
regex: /data$/i,
|
|
1818
|
+
replacement: "datum"
|
|
1819
|
+
}, {
|
|
1820
|
+
regex: /phenomena$/i,
|
|
1821
|
+
replacement: "phenomenon"
|
|
1822
|
+
}]
|
|
1823
|
+
},
|
|
1824
|
+
/**
|
|
1825
|
+
* Words with specific ending patterns
|
|
1826
|
+
*
|
|
1827
|
+
* This category contains rules for words that follow regular patterns based on
|
|
1828
|
+
* their endings. These patterns represent the most common rules for English
|
|
1829
|
+
* pluralization and singularization, covering a wide range of everyday words.
|
|
1830
|
+
*
|
|
1831
|
+
* The category is divided into two rule sets:
|
|
1832
|
+
* - toPlural: Rules for converting singular forms to plural based on word endings
|
|
1833
|
+
* - toSingular: Rules for converting plural forms back to singular
|
|
1834
|
+
*
|
|
1835
|
+
* Key pattern groups include:
|
|
1836
|
+
*
|
|
1837
|
+
* 1. Words ending in -f or -fe:
|
|
1838
|
+
* - "knife" → "knives", "life" → "lives", "wolf" → "wolves"
|
|
1839
|
+
*
|
|
1840
|
+
* 2. Words ending in -y:
|
|
1841
|
+
* - After consonant: "city" → "cities", "baby" → "babies"
|
|
1842
|
+
* - After vowel: "boy" → "boys", "key" → "keys"
|
|
1843
|
+
*
|
|
1844
|
+
* 3. Words ending in -o:
|
|
1845
|
+
* - Some specific words: "hero" → "heroes", "potato" → "potatoes"
|
|
1846
|
+
* - After consonant: "echo" → "echoes", "tomato" → "tomatoes"
|
|
1847
|
+
*
|
|
1848
|
+
* 4. Words ending in -s, -ss, -sh, -ch, -x, -z:
|
|
1849
|
+
* - "bus" → "buses", "box" → "boxes", "dish" → "dishes"
|
|
1850
|
+
*
|
|
1851
|
+
* These patterns cover the majority of regular English pluralization rules
|
|
1852
|
+
* and are applied after checking for invariant words, irregular forms, and
|
|
1853
|
+
* Latin/Greek words.
|
|
1854
|
+
*
|
|
1855
|
+
* @example
|
|
1856
|
+
* ```typescript
|
|
1857
|
+
* // Words ending in -f or -fe
|
|
1858
|
+
* plural("knife"); // "knives"
|
|
1859
|
+
* plural("life"); // "lives"
|
|
1860
|
+
* plural("wolf"); // "wolves"
|
|
1861
|
+
*
|
|
1862
|
+
* // Words ending in -y
|
|
1863
|
+
* plural("city"); // "cities"
|
|
1864
|
+
* plural("boy"); // "boys"
|
|
1865
|
+
*
|
|
1866
|
+
* // Words ending in -o
|
|
1867
|
+
* plural("hero"); // "heroes"
|
|
1868
|
+
* plural("photo"); // "photos"
|
|
1869
|
+
*
|
|
1870
|
+
* // Words ending in -s, -ss, -sh, -ch, -x, -z
|
|
1871
|
+
* plural("bus"); // "buses"
|
|
1872
|
+
* plural("box"); // "boxes"
|
|
1873
|
+
* plural("dish"); // "dishes"
|
|
1874
|
+
*
|
|
1875
|
+
* // Plural to singular
|
|
1876
|
+
* singular("knives"); // "knife"
|
|
1877
|
+
* singular("cities"); // "city"
|
|
1878
|
+
* singular("boxes"); // "box"
|
|
1879
|
+
* ```
|
|
1880
|
+
*
|
|
1881
|
+
* @see {@link plural} Function that applies these rules for pluralization
|
|
1882
|
+
* @see {@link singular} Function that applies these rules for singularization
|
|
1883
|
+
* @see {@link CHARACTERS_TO_REMOVE} Constant used in some replacement functions
|
|
1884
|
+
*/
|
|
1885
|
+
PATTERN_RULES: {
|
|
1886
|
+
// Singular to plural transformations
|
|
1887
|
+
toPlural: [
|
|
1888
|
+
// Words ending in -f or -fe
|
|
1889
|
+
{
|
|
1890
|
+
regex: /^((?:wi|li)fe)$/i,
|
|
1891
|
+
replacement: "$1ves"
|
|
1892
|
+
}, {
|
|
1893
|
+
regex: /(fe)$/i,
|
|
1894
|
+
replacement: "ves"
|
|
1895
|
+
}, {
|
|
1896
|
+
regex: /([^f])f$/i,
|
|
1897
|
+
replacement: "$1ves"
|
|
1898
|
+
},
|
|
1899
|
+
// Words ending in -y
|
|
1900
|
+
{
|
|
1901
|
+
regex: /([^aeiou])y$/i,
|
|
1902
|
+
replacement: "$1ies"
|
|
1903
|
+
}, {
|
|
1904
|
+
regex: /([aeiou]y)$/i,
|
|
1905
|
+
replacement: "$1s"
|
|
1906
|
+
},
|
|
1907
|
+
// Words ending in -o
|
|
1908
|
+
{
|
|
1909
|
+
regex: /(hero|potato|tomato|torpedo|veto)$/i,
|
|
1910
|
+
replacement: "$1es"
|
|
1911
|
+
}, {
|
|
1912
|
+
regex: /([^aeiou])o$/i,
|
|
1913
|
+
replacement: "$1oes"
|
|
1914
|
+
},
|
|
1915
|
+
// Words ending in -is
|
|
1916
|
+
{
|
|
1917
|
+
regex: /(is)$/i,
|
|
1918
|
+
replacement: "es"
|
|
1919
|
+
},
|
|
1920
|
+
// Words ending in -us
|
|
1921
|
+
{
|
|
1922
|
+
regex: /(us)$/i,
|
|
1923
|
+
replacement: "uses"
|
|
1924
|
+
},
|
|
1925
|
+
// Words ending in -s, -ss, -sh, -ch, -x, -z
|
|
1926
|
+
{
|
|
1927
|
+
regex: /(s|ss|sh|ch|x|z)$/i,
|
|
1928
|
+
replacement: "$1es"
|
|
1929
|
+
}],
|
|
1930
|
+
// Plural to singular transformations
|
|
1931
|
+
toSingular: [
|
|
1932
|
+
// Special spelling changes
|
|
1933
|
+
{
|
|
1934
|
+
regex: /knives$/i,
|
|
1935
|
+
replacement: "knife"
|
|
1936
|
+
}, {
|
|
1937
|
+
regex: /lives$/i,
|
|
1938
|
+
replacement: "life"
|
|
1939
|
+
}, {
|
|
1940
|
+
regex: /shelves$/i,
|
|
1941
|
+
replacement: "shelf"
|
|
1942
|
+
},
|
|
1943
|
+
// Common word patterns
|
|
1944
|
+
{
|
|
1945
|
+
regex: /(buses|dishes|matches)$/i,
|
|
1946
|
+
replacement: match => match.slice(0, -CHARACTERS_TO_REMOVE)
|
|
1947
|
+
}, {
|
|
1948
|
+
regex: /([^aeiouy]|qu)ies$/i,
|
|
1949
|
+
replacement: "$1y"
|
|
1950
|
+
}, {
|
|
1951
|
+
regex: /(x|ch|ss|sh)es$/i,
|
|
1952
|
+
replacement: "$1"
|
|
1953
|
+
}, {
|
|
1954
|
+
regex: /oes$/i,
|
|
1955
|
+
replacement: "o"
|
|
1956
|
+
}, {
|
|
1957
|
+
regex: /ves$/i,
|
|
1958
|
+
replacement: "f"
|
|
1959
|
+
}]
|
|
1960
|
+
},
|
|
1961
|
+
/**
|
|
1962
|
+
* Default rules for standard transformations
|
|
1963
|
+
*
|
|
1964
|
+
* This category contains fallback rules that are applied when no other rules match.
|
|
1965
|
+
* These represent the most basic and common English pluralization pattern: adding
|
|
1966
|
+
* an "s" to form plurals and removing an "s" to form singulars.
|
|
1967
|
+
*
|
|
1968
|
+
* The category is divided into two rule sets:
|
|
1969
|
+
* - toPlural: Adds "s" to the end of any word to form its plural
|
|
1970
|
+
* - toSingular: Removes a trailing "s" from any word to form its singular
|
|
1971
|
+
*
|
|
1972
|
+
* These rules are applied as a last resort after all other more specific rules
|
|
1973
|
+
* have been checked. They handle the majority of regular English words that don't
|
|
1974
|
+
* fall into any of the special categories or patterns.
|
|
1975
|
+
*
|
|
1976
|
+
* While simple, these rules can sometimes produce incorrect results for words that
|
|
1977
|
+
* should follow more specific patterns but weren't included in the other rule sets.
|
|
1978
|
+
* They're designed as a reasonable fallback rather than a comprehensive solution.
|
|
1979
|
+
*
|
|
1980
|
+
* @example
|
|
1981
|
+
* ```typescript
|
|
1982
|
+
* // Singular to plural with default rule
|
|
1983
|
+
* plural("book"); // "books"
|
|
1984
|
+
* plural("car"); // "cars"
|
|
1985
|
+
* plural("apple"); // "apples"
|
|
1986
|
+
*
|
|
1987
|
+
* // Plural to singular with default rule
|
|
1988
|
+
* singular("books"); // "book"
|
|
1989
|
+
* singular("cars"); // "car"
|
|
1990
|
+
* singular("apples"); // "apple"
|
|
1991
|
+
* ```
|
|
1992
|
+
*
|
|
1993
|
+
* @see {@link plural} Function that applies these rules for pluralization
|
|
1994
|
+
* @see {@link singular} Function that applies these rules for singularization
|
|
1995
|
+
*/
|
|
1996
|
+
DEFAULT: {
|
|
1997
|
+
toPlural: [{
|
|
1998
|
+
regex: /(.*)$/i,
|
|
1999
|
+
replacement: "$1s"
|
|
2000
|
+
}],
|
|
2001
|
+
toSingular: [{
|
|
2002
|
+
regex: /s$/i,
|
|
2003
|
+
replacement: ""
|
|
2004
|
+
}]
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
|
|
2008
|
+
// noinspection JSUnusedGlobalSymbols
|
|
2009
|
+
function breakToWords(str, format) {
|
|
2010
|
+
if (!str) {
|
|
2011
|
+
return format ? "" : [];
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
const words = str.match(/[A-Z]{2,}(?=[A-Z][a-z]+\d*|\b)|[A-Z]?[a-z]+\d*|[A-Z]+|\d+/g)?.map(s => s.toLowerCase()) ?? [];
|
|
2015
|
+
return format ? words.map(format).join(" ") : words;
|
|
2016
|
+
} catch {
|
|
2017
|
+
return format ? "" : [];
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Converts a string to lowercase with safe handling of undefined or null values.
|
|
2022
|
+
*
|
|
2023
|
+
* This utility function provides a safe way to convert strings to lowercase, automatically
|
|
2024
|
+
* handling edge cases where the input might be undefined, null, or empty. Unlike the native
|
|
2025
|
+
* String.prototype.toLowerCase() method, this function won't throw an error when called
|
|
2026
|
+
* with undefined or null values, instead returning an empty string.
|
|
2027
|
+
*
|
|
2028
|
+
* This is particularly useful when working with user input, API responses, or any scenario
|
|
2029
|
+
* where string values might be optional or potentially undefined. It's commonly used for
|
|
2030
|
+
* case-insensitive comparisons, search functionality, or normalizing text data.
|
|
2031
|
+
*
|
|
2032
|
+
* @param {string} [str] - The string to convert to lowercase. Can be undefined or null.
|
|
2033
|
+
* @returns {string} The lowercase version of the input string, or an empty string if input is falsy
|
|
2034
|
+
*
|
|
2035
|
+
* @example
|
|
2036
|
+
* ```typescript
|
|
2037
|
+
* // Basic string conversion
|
|
2038
|
+
* const result1 = toLowerCase("Hello World");
|
|
2039
|
+
* // Returns: "hello world"
|
|
2040
|
+
*
|
|
2041
|
+
* const result2 = toLowerCase("JAVASCRIPT");
|
|
2042
|
+
* // Returns: "javascript"
|
|
2043
|
+
* ```
|
|
2044
|
+
*
|
|
2045
|
+
* @example
|
|
2046
|
+
* ```typescript
|
|
2047
|
+
* // Safe handling of undefined/null values
|
|
2048
|
+
* const result1 = toLowerCase(undefined);
|
|
2049
|
+
* // Returns: ""
|
|
2050
|
+
*
|
|
2051
|
+
* const result2 = toLowerCase(null);
|
|
2052
|
+
* // Returns: ""
|
|
2053
|
+
*
|
|
2054
|
+
* const result3 = toLowerCase("");
|
|
2055
|
+
* // Returns: ""
|
|
2056
|
+
* ```
|
|
2057
|
+
*
|
|
2058
|
+
* @example
|
|
2059
|
+
* ```typescript
|
|
2060
|
+
* // Use in case-insensitive search
|
|
2061
|
+
* function searchUsers(users: User[], searchTerm: string): User[] {
|
|
2062
|
+
* const normalizedSearch = toLowerCase(searchTerm);
|
|
2063
|
+
* return users.filter(user =>
|
|
2064
|
+
* toLowerCase(user.name).includes(normalizedSearch) ||
|
|
2065
|
+
* toLowerCase(user.email).includes(normalizedSearch)
|
|
2066
|
+
* );
|
|
2067
|
+
* }
|
|
2068
|
+
* ```
|
|
2069
|
+
*
|
|
2070
|
+
* @example
|
|
2071
|
+
* ```typescript
|
|
2072
|
+
* // Use in form validation
|
|
2073
|
+
* function validateEmail(email?: string): boolean {
|
|
2074
|
+
* const normalizedEmail = toLowerCase(email);
|
|
2075
|
+
* const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2076
|
+
* return emailRegex.test(normalizedEmail);
|
|
2077
|
+
* }
|
|
2078
|
+
* ```
|
|
2079
|
+
*
|
|
2080
|
+
* @remarks
|
|
2081
|
+
* - Returns an empty string for any falsy input (undefined, null, empty string)
|
|
2082
|
+
* - Uses the native String.prototype.toLowerCase() method for actual conversion
|
|
2083
|
+
* - Does not modify the original string (strings are immutable in JavaScript)
|
|
2084
|
+
* - Handles all Unicode characters correctly, following locale-independent rules
|
|
2085
|
+
* - More robust than direct .toLowerCase() calls when input might be undefined
|
|
2086
|
+
*
|
|
2087
|
+
* @see {@link toUpperCase} Function for converting strings to uppercase
|
|
2088
|
+
* @see {@link toLowerCaseBreak} Function for converting to lowercase and breaking into words
|
|
2089
|
+
*/
|
|
2090
|
+
function toLowerCase(str) {
|
|
2091
|
+
if (!str) {
|
|
2092
|
+
return "";
|
|
2093
|
+
}
|
|
2094
|
+
return str.toLowerCase();
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Converts a string to lowercase and breaks it into words, joining them with a specified separator.
|
|
2098
|
+
*
|
|
2099
|
+
* This utility function combines word breaking and case conversion in a single operation.
|
|
2100
|
+
* It first breaks the input string into individual words using intelligent pattern recognition
|
|
2101
|
+
* (handling camelCase, snake_case, kebab-case, etc.), converts each word to lowercase,
|
|
2102
|
+
* and then joins them with the specified separator or a space by default.
|
|
2103
|
+
*
|
|
2104
|
+
* This is particularly useful for converting various naming conventions to a consistent
|
|
2105
|
+
* lowercase format, creating readable labels from variable names, generating search-friendly
|
|
2106
|
+
* text, or preparing strings for further processing where consistent word separation is needed.
|
|
2107
|
+
*
|
|
2108
|
+
* @param {string} [str] - The string to convert and break into words. Can be undefined or null.
|
|
2109
|
+
* @param {string} [join=" "] - The separator to use when joining the words. Defaults to a single space.
|
|
2110
|
+
* @returns {string} A lowercase string with words separated by the specified join character,
|
|
2111
|
+
* or an empty string if input is falsy
|
|
2112
|
+
*
|
|
2113
|
+
* @example
|
|
2114
|
+
* ```typescript
|
|
2115
|
+
* // Basic camelCase conversion
|
|
2116
|
+
* const result1 = toLowerCaseBreak("HelloWorld");
|
|
2117
|
+
* // Returns: "hello world"
|
|
2118
|
+
*
|
|
2119
|
+
* const result2 = toLowerCaseBreak("getUserProfile");
|
|
2120
|
+
* // Returns: "get user profile"
|
|
2121
|
+
* ```
|
|
2122
|
+
*
|
|
2123
|
+
* @example
|
|
2124
|
+
* ```typescript
|
|
2125
|
+
* // Custom separators
|
|
2126
|
+
* const result1 = toLowerCaseBreak("HelloWorld", "-");
|
|
2127
|
+
* // Returns: "hello-world"
|
|
2128
|
+
*
|
|
2129
|
+
* const result2 = toLowerCaseBreak("APIResponseHandler", "_");
|
|
2130
|
+
* // Returns: "api_response_handler"
|
|
2131
|
+
*
|
|
2132
|
+
* const result3 = toLowerCaseBreak("userAccountSettings", " | ");
|
|
2133
|
+
* // Returns: "user | account | settings"
|
|
2134
|
+
* ```
|
|
2135
|
+
*
|
|
2136
|
+
* @example
|
|
2137
|
+
* ```typescript
|
|
2138
|
+
* // Various input formats
|
|
2139
|
+
* const result1 = toLowerCaseBreak("snake_case_example");
|
|
2140
|
+
* // Returns: "snake case example"
|
|
2141
|
+
*
|
|
2142
|
+
* const result2 = toLowerCaseBreak("kebab-case-example");
|
|
2143
|
+
* // Returns: "kebab case example"
|
|
2144
|
+
*
|
|
2145
|
+
* const result3 = toLowerCaseBreak("CONSTANT_VALUE");
|
|
2146
|
+
* // Returns: "constant value"
|
|
2147
|
+
* ```
|
|
2148
|
+
*
|
|
2149
|
+
* @example
|
|
2150
|
+
* ```typescript
|
|
2151
|
+
* // Creating user-friendly labels
|
|
2152
|
+
* function createLabel(fieldName: string): string {
|
|
2153
|
+
* return toLowerCaseBreak(fieldName)
|
|
2154
|
+
* .split(' ')
|
|
2155
|
+
* .map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
2156
|
+
* .join(' ');
|
|
2157
|
+
* }
|
|
2158
|
+
*
|
|
2159
|
+
* createLabel("firstName"); // "First Name"
|
|
2160
|
+
* createLabel("emailAddress"); // "Email Address"
|
|
2161
|
+
* ```
|
|
2162
|
+
*
|
|
2163
|
+
* @example
|
|
2164
|
+
* ```typescript
|
|
2165
|
+
* // Safe handling of edge cases
|
|
2166
|
+
* const result1 = toLowerCaseBreak(undefined);
|
|
2167
|
+
* // Returns: ""
|
|
2168
|
+
*
|
|
2169
|
+
* const result2 = toLowerCaseBreak("", "-");
|
|
2170
|
+
* // Returns: ""
|
|
2171
|
+
*
|
|
2172
|
+
* const result3 = toLowerCaseBreak("singleword");
|
|
2173
|
+
* // Returns: "singleword"
|
|
2174
|
+
* ```
|
|
2175
|
+
*
|
|
2176
|
+
* @remarks
|
|
2177
|
+
* - Returns an empty string for any falsy input (undefined, null, empty string)
|
|
2178
|
+
* - Uses the same intelligent word-breaking algorithm as the breakToWords function
|
|
2179
|
+
* - Handles camelCase, PascalCase, snake_case, kebab-case, and mixed formats
|
|
2180
|
+
* - Preserves numbers as separate words when they appear in the string
|
|
2181
|
+
* - The join parameter can be any string, including empty string for concatenation
|
|
2182
|
+
* - More efficient than calling breakToWords and toLowerCase separately
|
|
2183
|
+
*
|
|
2184
|
+
* @see {@link breakToWords} Function for breaking strings into word arrays
|
|
2185
|
+
* @see {@link toLowerCase} Function for simple lowercase conversion
|
|
2186
|
+
* @see {@link toUpperCaseBreak} Function for uppercase word breaking
|
|
2187
|
+
*/
|
|
2188
|
+
function toLowerCaseBreak(str, join) {
|
|
2189
|
+
if (!str) {
|
|
2190
|
+
return "";
|
|
2191
|
+
}
|
|
2192
|
+
return breakToWords(str).map(s => s.toLowerCase()).join(join ?? " ");
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Converts a string to uppercase with safe handling of undefined or null values.
|
|
2196
|
+
*
|
|
2197
|
+
* This utility function provides a safe way to convert strings to uppercase, automatically
|
|
2198
|
+
* handling edge cases where the input might be undefined, null, or empty. Unlike the native
|
|
2199
|
+
* String.prototype.toUpperCase() method, this function won't throw an error when called
|
|
2200
|
+
* with undefined or null values, instead returning an empty string.
|
|
2201
|
+
*
|
|
2202
|
+
* This is particularly useful when working with user input, API responses, or any scenario
|
|
2203
|
+
* where string values might be optional or potentially undefined. It's commonly used for
|
|
2204
|
+
* creating constants, formatting display text, generating identifiers, or normalizing text
|
|
2205
|
+
* data that needs to be in uppercase format.
|
|
2206
|
+
*
|
|
2207
|
+
* @param {string} [str] - The string to convert to uppercase. Can be undefined or null.
|
|
2208
|
+
* @returns {string} The uppercase version of the input string, or an empty string if input is falsy
|
|
2209
|
+
*
|
|
2210
|
+
* @example
|
|
2211
|
+
* ```typescript
|
|
2212
|
+
* // Basic string conversion
|
|
2213
|
+
* const result1 = toUpperCase("hello world");
|
|
2214
|
+
* // Returns: "HELLO WORLD"
|
|
2215
|
+
*
|
|
2216
|
+
* const result2 = toUpperCase("javascript");
|
|
2217
|
+
* // Returns: "JAVASCRIPT"
|
|
2218
|
+
* ```
|
|
2219
|
+
*
|
|
2220
|
+
* @example
|
|
2221
|
+
* ```typescript
|
|
2222
|
+
* // Safe handling of undefined/null values
|
|
2223
|
+
* const result1 = toUpperCase(undefined);
|
|
2224
|
+
* // Returns: ""
|
|
2225
|
+
*
|
|
2226
|
+
* const result2 = toUpperCase(null);
|
|
2227
|
+
* // Returns: ""
|
|
2228
|
+
*
|
|
2229
|
+
* const result3 = toUpperCase("");
|
|
2230
|
+
* // Returns: ""
|
|
2231
|
+
* ```
|
|
2232
|
+
*
|
|
2233
|
+
* @example
|
|
2234
|
+
* ```typescript
|
|
2235
|
+
* // Creating constants or identifiers
|
|
2236
|
+
* function generateConstantName(variableName: string): string {
|
|
2237
|
+
* return toUpperCase(variableName.replace(/[^a-zA-Z0-9]/g, '_'));
|
|
2238
|
+
* }
|
|
2239
|
+
*
|
|
2240
|
+
* generateConstantName("user-profile"); // "USER_PROFILE"
|
|
2241
|
+
* generateConstantName("api.endpoint"); // "API_ENDPOINT"
|
|
2242
|
+
* ```
|
|
2243
|
+
*
|
|
2244
|
+
* @example
|
|
2245
|
+
* ```typescript
|
|
2246
|
+
* // Use in text formatting
|
|
2247
|
+
* function formatTitle(title?: string): string {
|
|
2248
|
+
* if (!title) return "UNTITLED";
|
|
2249
|
+
* return toUpperCase(title.trim());
|
|
2250
|
+
* }
|
|
2251
|
+
*
|
|
2252
|
+
* formatTitle("my document"); // "MY DOCUMENT"
|
|
2253
|
+
* formatTitle(undefined); // "UNTITLED"
|
|
2254
|
+
* ```
|
|
2255
|
+
*
|
|
2256
|
+
* @example
|
|
2257
|
+
* ```typescript
|
|
2258
|
+
* // Use in case-insensitive comparisons
|
|
2259
|
+
* function compareIgnoreCase(str1?: string, str2?: string): boolean {
|
|
2260
|
+
* return toUpperCase(str1) === toUpperCase(str2);
|
|
2261
|
+
* }
|
|
2262
|
+
*
|
|
2263
|
+
* compareIgnoreCase("Hello", "HELLO"); // true
|
|
2264
|
+
* compareIgnoreCase("Test", "test"); // true
|
|
2265
|
+
* ```
|
|
2266
|
+
*
|
|
2267
|
+
* @remarks
|
|
2268
|
+
* - Returns an empty string for any falsy input (undefined, null, empty string)
|
|
2269
|
+
* - Uses the native String.prototype.toUpperCase() method for actual conversion
|
|
2270
|
+
* - Does not modify the original string (strings are immutable in JavaScript)
|
|
2271
|
+
* - Handles all Unicode characters correctly, following locale-independent rules
|
|
2272
|
+
* - More robust than direct .toUpperCase() calls when input might be undefined
|
|
2273
|
+
* - Particularly useful for creating constants, headers, or display text
|
|
2274
|
+
*
|
|
2275
|
+
* @see {@link toLowerCase} Function for converting strings to lowercase
|
|
2276
|
+
* @see {@link toUpperCaseBreak} Function for converting to uppercase and breaking into words
|
|
2277
|
+
*/
|
|
2278
|
+
function toUpperCase(str) {
|
|
2279
|
+
if (!str) {
|
|
2280
|
+
return "";
|
|
2281
|
+
}
|
|
2282
|
+
return str.toUpperCase();
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Compares two strings for similarity using Levenshtein distance algorithm.
|
|
2286
|
+
*
|
|
2287
|
+
* The Levenshtein distance is a measure of the difference between two strings
|
|
2288
|
+
* by counting the minimum number of single-character edits (insertions, deletions,
|
|
2289
|
+
* or substitutions) required to change one string into another.
|
|
2290
|
+
*
|
|
2291
|
+
* @param {string} str1 - First string to compare
|
|
2292
|
+
* @param {string} str2 - Second string to compare
|
|
2293
|
+
* @returns {number} - A value between 0 and 1, where 1 means identical strings
|
|
2294
|
+
*
|
|
2295
|
+
* @example
|
|
2296
|
+
* ```typescript
|
|
2297
|
+
* stringSimilarity("hello", "hallo"); // 0.8 (4/5 characters match)
|
|
2298
|
+
* stringSimilarity("kitten", "sitting"); // ~0.57 (higher distance)
|
|
2299
|
+
* stringSimilarity("same", "same"); // 1.0 (identical)
|
|
2300
|
+
* ```
|
|
2301
|
+
*/
|
|
2302
|
+
function stringSimilarity(str1, str2) {
|
|
2303
|
+
if (str1 === str2) return 1.0;
|
|
2304
|
+
if (!str1 || !str2) return 0.0;
|
|
2305
|
+
const len1 = str1.length;
|
|
2306
|
+
const len2 = str2.length;
|
|
2307
|
+
// Initialize the distance matrix
|
|
2308
|
+
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
|
2309
|
+
// Fill the first row and column
|
|
2310
|
+
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
|
2311
|
+
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
|
2312
|
+
// Fill the rest of the matrix
|
|
2313
|
+
for (let i = 1; i <= len1; i++) {
|
|
2314
|
+
for (let j = 1; j <= len2; j++) {
|
|
2315
|
+
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
|
2316
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1,
|
|
2317
|
+
// deletion
|
|
2318
|
+
matrix[i][j - 1] + 1,
|
|
2319
|
+
// insertion
|
|
2320
|
+
matrix[i - 1][j - 1] + cost);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
// Calculate similarity from Levenshtein distance
|
|
2324
|
+
const maxLen = Math.max(len1, len2);
|
|
2325
|
+
if (maxLen === 0) return 1.0;
|
|
2326
|
+
return 1.0 - matrix[len1][len2] / maxLen;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Generates a slug from a string, suitable for URLs or file names.
|
|
2330
|
+
*
|
|
2331
|
+
* @param {string} str - The string to convert to a slug
|
|
2332
|
+
* @param {string} [separator="-"] - The character to use as separator
|
|
2333
|
+
* @returns {string} - The slug string
|
|
2334
|
+
*
|
|
2335
|
+
* @example
|
|
2336
|
+
* ```typescript
|
|
2337
|
+
* slugify("Hello World"); // "hello-world"
|
|
2338
|
+
* slugify("10 Tips & Tricks!"); // "10-tips-tricks"
|
|
2339
|
+
* slugify("My Product (2023 Edition)"); // "my-product-2023-edition"
|
|
2340
|
+
* slugify("Über résumé"); // "uber-resume"
|
|
2341
|
+
* slugify("Blog Post", "_"); // "blog_post"
|
|
2342
|
+
* ```
|
|
2343
|
+
*/
|
|
2344
|
+
function slugify(str, separator = "-") {
|
|
2345
|
+
if (!str) return "";
|
|
2346
|
+
return str.normalize("NFD") // Normalize to decomposed form for handling accents
|
|
2347
|
+
.replace(/[\u0300-\u036f]/g, "") // Remove accents/diacritics
|
|
2348
|
+
.toLowerCase() // Convert to lowercase
|
|
2349
|
+
.trim() // Trim whitespace
|
|
2350
|
+
.replace(/[^\w\s-]/g, "") // Remove non-word chars (except spaces and hyphens)
|
|
2351
|
+
.replace(/[\s_]+/g, separator) // Replace spaces and underscores with separator
|
|
2352
|
+
.replace(new RegExp(`${separator}+`, "g"), separator) // Replace multiple separators with single
|
|
2353
|
+
.replace(new RegExp(`^${separator}|${separator}$`, "g"), ""); // Remove leading/trailing separators
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Extracts all email addresses from a string.
|
|
2357
|
+
*
|
|
2358
|
+
* @param {string} str - The string to extract emails from
|
|
2359
|
+
* @returns {string[]} - Array of found email addresses
|
|
2360
|
+
*
|
|
2361
|
+
* @example
|
|
2362
|
+
* ```typescript
|
|
2363
|
+
* extractEmails("Contact us at support@example.com or sales@example.com");
|
|
2364
|
+
* // ["support@example.com", "sales@example.com"]
|
|
2365
|
+
* ```
|
|
2366
|
+
*/
|
|
2367
|
+
function extractEmails(str) {
|
|
2368
|
+
if (!str) return [];
|
|
2369
|
+
const emailRegex = /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi;
|
|
2370
|
+
return str.match(emailRegex) || [];
|
|
2371
|
+
}
|
|
2372
|
+
/**
|
|
2373
|
+
* Extracts all URLs from a string.
|
|
2374
|
+
*
|
|
2375
|
+
* @param {string} str - The string to extract URLs from
|
|
2376
|
+
* @returns {string[]} - Array of found URLs
|
|
2377
|
+
*
|
|
2378
|
+
* @example
|
|
2379
|
+
* ```typescript
|
|
2380
|
+
* extractUrls("Visit https://example.com or http://test.org/page?id=5");
|
|
2381
|
+
* // ["https://example.com", "http://test.org/page?id=5"]
|
|
2382
|
+
* ```
|
|
2383
|
+
*/
|
|
2384
|
+
function extractUrls(str) {
|
|
2385
|
+
if (!str) return [];
|
|
2386
|
+
// noinspection RegExpSimplifiable
|
|
2387
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
2388
|
+
return str.match(urlRegex) || [];
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Converts a string to a secure hash using SHA-256.
|
|
2392
|
+
*
|
|
2393
|
+
* Note: This is a browser-compatible implementation using the Web Crypto API.
|
|
2394
|
+
* For Node.js environments, you might want to use the crypto module instead.
|
|
2395
|
+
*
|
|
2396
|
+
* @param {string} str - The string to hash
|
|
2397
|
+
* @returns {Promise<string>} - Promise resolving to the hex hash string
|
|
2398
|
+
*
|
|
2399
|
+
* @example
|
|
2400
|
+
* ```typescript
|
|
2401
|
+
* // Browser usage
|
|
2402
|
+
* await hashString("password123");
|
|
2403
|
+
* // "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
|
|
2404
|
+
* ```
|
|
2405
|
+
*/
|
|
2406
|
+
async function hashString(str) {
|
|
2407
|
+
if (!str) return "";
|
|
2408
|
+
// Convert string to Uint8Array
|
|
2409
|
+
const encoder = new TextEncoder();
|
|
2410
|
+
const data = encoder.encode(str);
|
|
2411
|
+
// Hash the data
|
|
2412
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
2413
|
+
// Convert ArrayBuffer to hex string
|
|
2414
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2415
|
+
return hashArray.map(b => b.toString(HEX_RADIX).padStart(HEX_PADDING_LENGTH, HEX_PADDING_CHAR)).join("");
|
|
2416
|
+
}
|
|
2417
|
+
/**
|
|
2418
|
+
* Generates a random string with specified length and character set.
|
|
2419
|
+
*
|
|
2420
|
+
* @param {number} length - The length of the random string
|
|
2421
|
+
* @param {string} [charset] - The characters to use (defaults to alphanumeric)
|
|
2422
|
+
* @returns {string} - The generated random string
|
|
2423
|
+
*
|
|
2424
|
+
* @example
|
|
2425
|
+
* ```typescript
|
|
2426
|
+
* // Default alphanumeric string
|
|
2427
|
+
* randomString(8);
|
|
2428
|
+
* // e.g. "A7bC9Df3"
|
|
2429
|
+
*
|
|
2430
|
+
* // Hex string
|
|
2431
|
+
* randomString(6, "0123456789abcdef");
|
|
2432
|
+
* // e.g. "a3f9d2"
|
|
2433
|
+
*
|
|
2434
|
+
* // PIN code
|
|
2435
|
+
* randomString(4, "0123456789");
|
|
2436
|
+
* // e.g. "8302"
|
|
2437
|
+
* ```
|
|
2438
|
+
*/
|
|
2439
|
+
function randomString(length, charset) {
|
|
2440
|
+
if (length <= 0) return "";
|
|
2441
|
+
const chars = charset || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2442
|
+
let result = "";
|
|
2443
|
+
// Generate cryptographically secure random values if available
|
|
2444
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
2445
|
+
const values = new Uint32Array(length);
|
|
2446
|
+
crypto.getRandomValues(values);
|
|
2447
|
+
for (let i = 0; i < length; i++) {
|
|
2448
|
+
result += chars[values[i] % chars.length];
|
|
2449
|
+
}
|
|
2450
|
+
} else {
|
|
2451
|
+
// Fallback to Math.random (less secure)
|
|
2452
|
+
for (let i = 0; i < length; i++) {
|
|
2453
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return result;
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Masks a portion of a string, useful for displaying sensitive information.
|
|
2460
|
+
*
|
|
2461
|
+
* @param {string} str - The string to mask
|
|
2462
|
+
* @param {number} [visibleStart=0] - Number of characters to show at the beginning
|
|
2463
|
+
* @param {number} [visibleEnd=0] - Number of characters to show at the end
|
|
2464
|
+
* @param {string} [maskChar="*"] - Character to use for masking
|
|
2465
|
+
* @returns {string} - The masked string
|
|
2466
|
+
*
|
|
2467
|
+
* @example
|
|
2468
|
+
* ```typescript
|
|
2469
|
+
* // Mask credit card
|
|
2470
|
+
* maskString("4111111111111111", 4, 4);
|
|
2471
|
+
* // "4111********1111"
|
|
2472
|
+
*
|
|
2473
|
+
* // Mask email
|
|
2474
|
+
* maskString("user@example.com", 2, 4, "x");
|
|
2475
|
+
* // "usxxxxxx.com"
|
|
2476
|
+
*
|
|
2477
|
+
* // Mask phone number
|
|
2478
|
+
* maskString("+1-555-123-4567", 0, 4);
|
|
2479
|
+
* // "*********4567"
|
|
2480
|
+
* ```
|
|
2481
|
+
*/
|
|
2482
|
+
function maskString(str, visibleStart = 0, visibleEnd = 0, maskChar = "*") {
|
|
2483
|
+
if (!str) return "";
|
|
2484
|
+
const length = str.length;
|
|
2485
|
+
if (visibleStart + visibleEnd >= length) return str;
|
|
2486
|
+
const start = str.substring(0, visibleStart);
|
|
2487
|
+
const middle = maskChar.repeat(length - visibleStart - visibleEnd);
|
|
2488
|
+
const end = str.substring(length - visibleEnd);
|
|
2489
|
+
return start + middle + end;
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Convert a string to first case (Capitalize first letter of the string).
|
|
2493
|
+
* @param {string} [str] Optional string to join the words with.
|
|
2494
|
+
* @returns {string} String in first case.
|
|
2495
|
+
*
|
|
2496
|
+
* @example
|
|
2497
|
+
* ```TypeScript
|
|
2498
|
+
* toFirstCase("hello world"); // "Hello world"
|
|
2499
|
+
* ```
|
|
2500
|
+
*/
|
|
2501
|
+
function toFirstCase(str) {
|
|
2502
|
+
if (!str) {
|
|
2503
|
+
return "";
|
|
2504
|
+
}
|
|
2505
|
+
return (str[0]?.toUpperCase() || "") + str.slice(1).toLowerCase();
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Converts a string to a camelCase or PascalCase variable name.
|
|
2509
|
+
*
|
|
2510
|
+
* @param {string} str - The string to convert
|
|
2511
|
+
* @param {boolean} [pascalCase=false] - Whether to use PascalCase (true) or camelCase (false)
|
|
2512
|
+
* @returns {string} - The variable name
|
|
2513
|
+
*
|
|
2514
|
+
* @example
|
|
2515
|
+
* ```typescript
|
|
2516
|
+
* toVariableName("Hello World"); // "helloWorld"
|
|
2517
|
+
* toVariableName("API Response"); // "apiResponse"
|
|
2518
|
+
* toVariableName("first-name"); // "firstName"
|
|
2519
|
+
* toVariableName("user_id", true); // "UserId"
|
|
2520
|
+
* ```
|
|
2521
|
+
*/
|
|
2522
|
+
function toVariableName(str, pascalCase = false) {
|
|
2523
|
+
if (!str) return "";
|
|
2524
|
+
// Remove special characters and replace with spaces
|
|
2525
|
+
const cleaned = str.replace(/[^\w\s]/g, " ");
|
|
2526
|
+
// Break into words and process
|
|
2527
|
+
return breakToWords(cleaned).map((word, index) => {
|
|
2528
|
+
// Skip first word for camelCase unless it's PascalCase
|
|
2529
|
+
if (index === 0 && !pascalCase) {
|
|
2530
|
+
return word.toLowerCase();
|
|
2531
|
+
}
|
|
2532
|
+
// Capitalize first letter
|
|
2533
|
+
return toFirstCase(word);
|
|
2534
|
+
}).join("");
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Wraps words in a string to ensure each line is no longer than a specified length.
|
|
2538
|
+
*
|
|
2539
|
+
* @param {string} str - The string to wrap
|
|
2540
|
+
* @param {number} [lineLength=80] - Maximum length of each line
|
|
2541
|
+
* @param {string} [breakChar="\n"] - Character(s) to insert at line breaks
|
|
2542
|
+
* @returns {string} - The wrapped string
|
|
2543
|
+
*
|
|
2544
|
+
* @example
|
|
2545
|
+
* ```typescript
|
|
2546
|
+
* const text = "This is a long sentence that needs to be wrapped to fit within a certain width.";
|
|
2547
|
+
*
|
|
2548
|
+
* wordWrap(text, 20);
|
|
2549
|
+
* // "This is a long\nsentence that needs\nto be wrapped to\nfit within a\ncertain width."
|
|
2550
|
+
*
|
|
2551
|
+
* wordWrap(text, 30, "<br>");
|
|
2552
|
+
* // "This is a long sentence that<br>needs to be wrapped to fit<br>within a certain width."
|
|
2553
|
+
* ```
|
|
2554
|
+
*/
|
|
2555
|
+
function wordWrap(str, lineLength = DEFAULT_LINE_LENGTH, breakChar = DEFAULT_BREAK_CHAR) {
|
|
2556
|
+
if (!str) return "";
|
|
2557
|
+
if (str.length <= lineLength) return str;
|
|
2558
|
+
const words = str.split(" ");
|
|
2559
|
+
let result = "";
|
|
2560
|
+
let currentLine = "";
|
|
2561
|
+
for (const word of words) {
|
|
2562
|
+
// If adding this word exceeds line length, start a new line
|
|
2563
|
+
if (currentLine.length + word.length + 1 > lineLength) {
|
|
2564
|
+
result += currentLine.trim() + breakChar;
|
|
2565
|
+
currentLine = `${word} `;
|
|
2566
|
+
} else {
|
|
2567
|
+
currentLine += `${word} `;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// Add the last line
|
|
2571
|
+
result += currentLine.trim();
|
|
2572
|
+
return result;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Creates an excerpt from a longer text by extracting a portion around a search term.
|
|
2576
|
+
* Useful for search result highlighting or previews.
|
|
2577
|
+
*
|
|
2578
|
+
* @param {string} text - The full text to create an excerpt from
|
|
2579
|
+
* @param {string} searchTerm - The search term to find in the text
|
|
2580
|
+
* @param {number} [contextLength=40] - Number of characters to include before and after the search term
|
|
2581
|
+
* @param {string} [ellipsis="..."] - Characters to use for indicating truncated text
|
|
2582
|
+
* @returns {string} - The excerpt with the search term in context
|
|
2583
|
+
*
|
|
2584
|
+
* @example
|
|
2585
|
+
* ```typescript
|
|
2586
|
+
* const article = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam in dui mauris.";
|
|
2587
|
+
*
|
|
2588
|
+
* createExcerpt(article, "dolor", 10);
|
|
2589
|
+
* // "...ipsum dolor sit amet..."
|
|
2590
|
+
*
|
|
2591
|
+
* createExcerpt(article, "consectetur", 15, "[...]");
|
|
2592
|
+
* // "[...]sit amet, consectetur adipiscing[...]"
|
|
2593
|
+
* ```
|
|
2594
|
+
*/
|
|
2595
|
+
function createExcerpt(text, searchTerm, contextLength = DEFAULT_CONTEXT_LENGTH, ellipsis = DEFAULT_ELLIPSIS) {
|
|
2596
|
+
if (!text || !searchTerm) return text || "";
|
|
2597
|
+
// Case-insensitive search
|
|
2598
|
+
const lowerText = text.toLowerCase();
|
|
2599
|
+
const lowerSearchTerm = searchTerm.toLowerCase();
|
|
2600
|
+
const index = lowerText.indexOf(lowerSearchTerm);
|
|
2601
|
+
if (index === -1) {
|
|
2602
|
+
// If search term not found, return beginning of text
|
|
2603
|
+
return text.length > contextLength * CONTEXT_MULTIPLIER ? text.substring(0, contextLength * CONTEXT_MULTIPLIER) + ellipsis : text;
|
|
2604
|
+
}
|
|
2605
|
+
// Calculate the start and end positions for the excerpt
|
|
2606
|
+
const startPos = Math.max(0, index - contextLength);
|
|
2607
|
+
const endPos = Math.min(text.length, index + searchTerm.length + contextLength);
|
|
2608
|
+
// Add ellipsis if we're not at the beginning or end
|
|
2609
|
+
const prefix = startPos > 0 ? ellipsis : "";
|
|
2610
|
+
const suffix = endPos < text.length ? ellipsis : "";
|
|
2611
|
+
// Extract the excerpt
|
|
2612
|
+
return prefix + text.substring(startPos, endPos) + suffix;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Normalizes a string by removing accents, diacritics, and converting to lowercase.
|
|
2616
|
+
*
|
|
2617
|
+
* @param {string} str - The string to normalize
|
|
2618
|
+
* @returns {string} - The normalized string
|
|
2619
|
+
*
|
|
2620
|
+
* @example
|
|
2621
|
+
* ```typescript
|
|
2622
|
+
* normalizeString("Café"); // "cafe"
|
|
2623
|
+
* normalizeString("Über"); // "uber"
|
|
2624
|
+
* normalizeString("résumé"); // "resume"
|
|
2625
|
+
* normalizeString("ESPAÑA"); // "espana"
|
|
2626
|
+
* ```
|
|
2627
|
+
*/
|
|
2628
|
+
function normalizeString(str) {
|
|
2629
|
+
if (!str) return "";
|
|
2630
|
+
return str.normalize("NFD") // Decompose accented characters
|
|
2631
|
+
.replace(/[\u0300-\u036f]/g, "") // Remove accent marks
|
|
2632
|
+
.toLowerCase(); // Convert to lowercase
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Convert a string to upper cases and break into words with optional join or space.
|
|
2636
|
+
* @param {string} [str] String to convert to upper cases and break into words.
|
|
2637
|
+
* @param {string} [join] Optional string to join the words with.
|
|
2638
|
+
* @returns {string} String in upper cases and broken into words.
|
|
2639
|
+
*
|
|
2640
|
+
* @example
|
|
2641
|
+
* ```TypeScript
|
|
2642
|
+
* toUpperCaseBreak("HelloWorld"); // "HELLO WORLD"
|
|
2643
|
+
* ```
|
|
2644
|
+
*
|
|
2645
|
+
* @example
|
|
2646
|
+
* ```TypeScript
|
|
2647
|
+
* toUpperCaseBreak("HelloWorld", "! "); // "HELLO! WORLD"
|
|
2648
|
+
*/
|
|
2649
|
+
function toUpperCaseBreak(str, join) {
|
|
2650
|
+
if (!str) {
|
|
2651
|
+
return "";
|
|
2652
|
+
}
|
|
2653
|
+
return breakToWords(str).map(s => s.toUpperCase()).join(join ?? " ");
|
|
2654
|
+
}
|
|
2655
|
+
/**
|
|
2656
|
+
* Converts a string to Sentence case (first word capitalized, rest lowercase) with customizable word separators.
|
|
2657
|
+
*
|
|
2658
|
+
* This function breaks a string into words, capitalizes the first word, converts remaining
|
|
2659
|
+
* words to lowercase, and then joins them using a custom separator (or space by default).
|
|
2660
|
+
*
|
|
2661
|
+
* Unlike `toFirstCase()` which works on a single word, this function processes multi-word
|
|
2662
|
+
* strings from various formats (camelCase, snake_case, etc.) and gives control over how
|
|
2663
|
+
* the words are joined back together.
|
|
2664
|
+
*
|
|
2665
|
+
* @param {string} [str] - The input string to convert
|
|
2666
|
+
* @param {string} [join] - Optional separator to join words (defaults to space)
|
|
2667
|
+
* @returns {string} - The formatted string with first word capitalized and custom separators,
|
|
2668
|
+
* or empty string if input is falsy
|
|
2669
|
+
*
|
|
2670
|
+
* @example
|
|
2671
|
+
* ```typescript
|
|
2672
|
+
* // With default space separator
|
|
2673
|
+
* toFirstCaseBreak("helloWorld"); // "Hello world"
|
|
2674
|
+
* toFirstCaseBreak("SYSTEM_ERROR"); // "System error"
|
|
2675
|
+
* toFirstCaseBreak("api-request-log"); // "Api request log"
|
|
2676
|
+
*
|
|
2677
|
+
* // With custom separators
|
|
2678
|
+
* toFirstCaseBreak("hello_world", "-"); // "Hello-world"
|
|
2679
|
+
* toFirstCaseBreak("userAuthConfig", "."); // "User.auth.config"
|
|
2680
|
+
* toFirstCaseBreak("GET_USER_DATA", "/"); // "Get/user/data"
|
|
2681
|
+
* ```
|
|
2682
|
+
*
|
|
2683
|
+
* @see {@link toFirstCase} For capitalizing a single word
|
|
2684
|
+
* @see {@link toTitleCase} For capitalizing the first letter of every word
|
|
2685
|
+
* @see {@link breakToWords} The underlying function used to split input into words
|
|
2686
|
+
*/
|
|
2687
|
+
function toFirstCaseBreak(str, join) {
|
|
2688
|
+
if (!str) {
|
|
2689
|
+
return "";
|
|
2690
|
+
}
|
|
2691
|
+
return breakToWords(str).map((s, i) => i === 0 ? toFirstCase(s) : s.toLowerCase()).join(join ?? " ");
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Convert a string to title case (Capitalize first letter of each word).
|
|
2695
|
+
* @param {string} [str] String to convert to title case.
|
|
2696
|
+
* @returns {string} String in title case.
|
|
2697
|
+
*
|
|
2698
|
+
* @example
|
|
2699
|
+
* ```TypeScript
|
|
2700
|
+
* toTitleCase("hello world"); // "Hello World"
|
|
2701
|
+
* ```
|
|
2702
|
+
*/
|
|
2703
|
+
function toTitleCase(str) {
|
|
2704
|
+
if (!str) {
|
|
2705
|
+
return "";
|
|
2706
|
+
}
|
|
2707
|
+
return breakToWords(str).map(s => toFirstCase(s)).join(" ");
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Convert a string to camel case.
|
|
2711
|
+
* @param {string} [str] String to convert to camel case.
|
|
2712
|
+
* @returns {string} String in camel case.
|
|
2713
|
+
*
|
|
2714
|
+
* @example
|
|
2715
|
+
* ```TypeScript
|
|
2716
|
+
* toCamelCase("hello world"); // "helloWorld"
|
|
2717
|
+
* ```
|
|
2718
|
+
*/
|
|
2719
|
+
function toCamelCase(str) {
|
|
2720
|
+
if (!str) {
|
|
2721
|
+
return "";
|
|
2722
|
+
}
|
|
2723
|
+
return breakToWords(str).map((s, i) => i === 0 ? s.toLowerCase() : toFirstCase(s)).join("");
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Convert a string to pascal case.
|
|
2727
|
+
* @param {string} [str] String to convert to pascal case.
|
|
2728
|
+
* @returns {string} String in pascal case.
|
|
2729
|
+
*
|
|
2730
|
+
* @example
|
|
2731
|
+
* ```TypeScript
|
|
2732
|
+
* toPascalCase("hello world"); // "HelloWorld"
|
|
2733
|
+
* ```
|
|
2734
|
+
*/
|
|
2735
|
+
function toPascalCase(str) {
|
|
2736
|
+
if (!str) {
|
|
2737
|
+
return "";
|
|
2738
|
+
}
|
|
2739
|
+
return breakToWords(str).map(s => toFirstCase(s)).join("");
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Convert a string to sentence case. (Capitalize first letter of every sentence).
|
|
2743
|
+
* @param {string} [str] String to convert to sentence case.
|
|
2744
|
+
* @returns {string} String in sentence case.
|
|
2745
|
+
*
|
|
2746
|
+
* @example
|
|
2747
|
+
* ```TypeScript
|
|
2748
|
+
* toSentenceCase("hello world. how are you?"); // "Hello world. How are you?"
|
|
2749
|
+
* ```
|
|
2750
|
+
*/
|
|
2751
|
+
function toSentenceCase(str) {
|
|
2752
|
+
return str.split(".").map(s => toFirstCase(s.trim())).join(". ");
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Converts a string to snake_case format.
|
|
2756
|
+
*
|
|
2757
|
+
* This function transforms a string from any common case format (camel, pascal, kebab, etc.)
|
|
2758
|
+
* into snake_case, where words are lowercase and separated by underscores.
|
|
2759
|
+
*
|
|
2760
|
+
* When the optional `caps` parameter is set to true, the result will be UPPER_SNAKE_CASE
|
|
2761
|
+
* (also known as SCREAMING_SNAKE_CASE or CONSTANT_CASE).
|
|
2762
|
+
*
|
|
2763
|
+
* @param {string} [str] - The input string to convert
|
|
2764
|
+
* @param {boolean} [caps=false] - When true, converts to UPPER_SNAKE_CASE
|
|
2765
|
+
* @returns {string} - The converted snake_case string, or empty string if input is falsy
|
|
2766
|
+
*
|
|
2767
|
+
* @example
|
|
2768
|
+
* ```typescript
|
|
2769
|
+
* // From various formats to snake_case
|
|
2770
|
+
* toSnakeCase("hello world"); // "hello_world"
|
|
2771
|
+
* toSnakeCase("HelloWorld"); // "hello_world"
|
|
2772
|
+
* toSnakeCase("hello-world"); // "hello_world"
|
|
2773
|
+
* toSnakeCase("HELLO WORLD"); // "hello_world"
|
|
2774
|
+
*
|
|
2775
|
+
* // To UPPER_SNAKE_CASE (CONSTANT_CASE)
|
|
2776
|
+
* toSnakeCase("hello world", true); // "HELLO_WORLD"
|
|
2777
|
+
* toSnakeCase("helloWorld", true); // "HELLO_WORLD"
|
|
2778
|
+
* ```
|
|
2779
|
+
*
|
|
2780
|
+
* @remarks
|
|
2781
|
+
* This function first splits the string into words using `breakToWords()`,
|
|
2782
|
+
* then joins them with underscores, applying the requested case transformation.
|
|
2783
|
+
*/
|
|
2784
|
+
function toSnakeCase(str, caps) {
|
|
2785
|
+
if (!str) {
|
|
2786
|
+
return "";
|
|
2787
|
+
}
|
|
2788
|
+
const snake = breakToWords(str).join("_");
|
|
2789
|
+
return caps ? snake.toUpperCase() : snake.toLowerCase();
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Convert a string to kebab case.
|
|
2793
|
+
* @param {string} [str] String to convert to kebab case.
|
|
2794
|
+
* @param {boolean} [caps] Whether to convert to upper case.
|
|
2795
|
+
* @returns {string} String in kebab case.
|
|
2796
|
+
*
|
|
2797
|
+
* @example
|
|
2798
|
+
* ```TypeScript
|
|
2799
|
+
* toKebabCase("hello world"); // "hello-world"
|
|
2800
|
+
* ```
|
|
2801
|
+
*
|
|
2802
|
+
* @example
|
|
2803
|
+
* ```TypeScript
|
|
2804
|
+
* toKebabCase("hello world", true); // "HELLO-WORLD"
|
|
2805
|
+
* ```
|
|
2806
|
+
*/
|
|
2807
|
+
function toKebabCase(str, caps) {
|
|
2808
|
+
if (!str) {
|
|
2809
|
+
return "";
|
|
2810
|
+
}
|
|
2811
|
+
const kebab = breakToWords(str).join("-");
|
|
2812
|
+
return caps ? kebab.toUpperCase() : kebab.toLowerCase();
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Converts a string to a number.
|
|
2816
|
+
* @param {number|string} str String to convert to a number.
|
|
2817
|
+
* @returns {number|undefined} Number or undefined.
|
|
2818
|
+
*
|
|
2819
|
+
* @example
|
|
2820
|
+
* ```TypeScript
|
|
2821
|
+
* toNumber("123"); // 123
|
|
2822
|
+
* ```
|
|
2823
|
+
*/
|
|
2824
|
+
function toNumber(str) {
|
|
2825
|
+
return !isNaN(Number(str)) ? Number(str) : undefined;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Remove HTML tags from a string and return plain text.
|
|
2829
|
+
* @param {string} str String to remove HTML tags from.
|
|
2830
|
+
* @returns {string} Plain text.
|
|
2831
|
+
*
|
|
2832
|
+
* @example
|
|
2833
|
+
* ```TypeScript
|
|
2834
|
+
* htmlToText("<h1>Hello World</h1>"); // "Hello World"
|
|
2835
|
+
* ```
|
|
2836
|
+
*/
|
|
2837
|
+
function htmlToText(str) {
|
|
2838
|
+
return str.replace(/<[^>]*>?/gm, "");
|
|
2839
|
+
}
|
|
2840
|
+
/**
|
|
2841
|
+
* Converts a singular English word to its plural form.
|
|
2842
|
+
*
|
|
2843
|
+
* This function applies common English pluralization rules to transform
|
|
2844
|
+
* singular words into their plural equivalents. It handles regular patterns
|
|
2845
|
+
* as well as many irregular cases and special rules for different word endings.
|
|
2846
|
+
*
|
|
2847
|
+
* @param {string} str - The singular word to convert to plural form
|
|
2848
|
+
* @returns {string} - The plural form of the word, or the original string if:
|
|
2849
|
+
* - Input is empty/falsy
|
|
2850
|
+
* - The word is already plural
|
|
2851
|
+
*
|
|
2852
|
+
* @example
|
|
2853
|
+
* ```typescript
|
|
2854
|
+
* // Regular plurals
|
|
2855
|
+
* plural("cat"); // "cats"
|
|
2856
|
+
* plural("dog"); // "dogs"
|
|
2857
|
+
*
|
|
2858
|
+
* // Words ending in -y
|
|
2859
|
+
* plural("party"); // "parties"
|
|
2860
|
+
* plural("day"); // "days"
|
|
2861
|
+
*
|
|
2862
|
+
* // Words ending in -f or -fe
|
|
2863
|
+
* plural("wolf"); // "wolves"
|
|
2864
|
+
* plural("knife"); // "knives"
|
|
2865
|
+
*
|
|
2866
|
+
* // Words ending in -o
|
|
2867
|
+
* plural("potato"); // "potatoes"
|
|
2868
|
+
* plural("photo"); // "photos"
|
|
2869
|
+
*
|
|
2870
|
+
* // Irregular plurals
|
|
2871
|
+
* plural("child"); // "children"
|
|
2872
|
+
* plural("man"); // "men"
|
|
2873
|
+
* plural("foot"); // "feet"
|
|
2874
|
+
*
|
|
2875
|
+
* // Latin/Greek words
|
|
2876
|
+
* plural("cactus"); // "cacti"
|
|
2877
|
+
* plural("analysis"); // "analyses"
|
|
2878
|
+
* ```
|
|
2879
|
+
*
|
|
2880
|
+
* @remarks
|
|
2881
|
+
* - This function is designed for English language words only
|
|
2882
|
+
* - It pairs well with the `singular()` function for round-trip conversion
|
|
2883
|
+
*/
|
|
2884
|
+
function plural(str) {
|
|
2885
|
+
if (!str) return str;
|
|
2886
|
+
// Combine all rules in priority order
|
|
2887
|
+
const rules = [...EnglishInflectionRules.INVARIANT, ...EnglishInflectionRules.IRREGULAR.toPlural, ...EnglishInflectionRules.LATIN_GREEK.toPlural, ...EnglishInflectionRules.PATTERN_RULES.toPlural, ...EnglishInflectionRules.DEFAULT.toPlural];
|
|
2888
|
+
// Apply the first matching rule
|
|
2889
|
+
for (const rule of rules) {
|
|
2890
|
+
if (rule.regex.test(str)) {
|
|
2891
|
+
return str.replace(rule.regex, typeof rule.replacement === "string" ? rule.replacement : rule.replacement(str));
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
return str;
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Truncates a string to a specified length and adds an ellipsis if needed.
|
|
2898
|
+
*
|
|
2899
|
+
* @param {string} str - The string to truncate
|
|
2900
|
+
* @param {number} length - Maximum length of the truncated string (excluding ellipsis)
|
|
2901
|
+
* @param {string} [ellipsis="..."] - The ellipsis to add to truncated strings
|
|
2902
|
+
* @returns {string} - The truncated string with ellipsis if needed
|
|
2903
|
+
*
|
|
2904
|
+
* @example
|
|
2905
|
+
* ```typescript
|
|
2906
|
+
* truncate("This is a long sentence", 10); // "This is a..."
|
|
2907
|
+
* truncate("Short", 10); // "Short"
|
|
2908
|
+
* truncate("Custom ellipsis", 6, " [more]"); // "Custom [more]"
|
|
2909
|
+
* ```
|
|
2910
|
+
*/
|
|
2911
|
+
function truncate(str, length, ellipsis = "...") {
|
|
2912
|
+
if (!str) return "";
|
|
2913
|
+
if (str.length <= length) return str;
|
|
2914
|
+
return str.substring(0, length) + ellipsis;
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* Pads a string to a specified length with a specified character.
|
|
2918
|
+
*
|
|
2919
|
+
* @param {string} str - The string to pad
|
|
2920
|
+
* @param {number} length - The target length of the resulting string
|
|
2921
|
+
* @param {string} [char=" "] - The character to pad with (defaults to space)
|
|
2922
|
+
* @param {boolean} [padEnd=true] - Whether to pad at the end (true) or beginning (false)
|
|
2923
|
+
* @returns {string} - The padded string
|
|
2924
|
+
*
|
|
2925
|
+
* @example
|
|
2926
|
+
* ```typescript
|
|
2927
|
+
* padString("Hello", 10); // "Hello "
|
|
2928
|
+
* padString("Hello", 10, "*"); // "Hello*****"
|
|
2929
|
+
* padString("Hello", 10, "0", false); // "00000Hello"
|
|
2930
|
+
* ```
|
|
2931
|
+
*/
|
|
2932
|
+
function padString(str, length, char = " ", padEnd = true) {
|
|
2933
|
+
if (!str) return "";
|
|
2934
|
+
if (str.length >= length) return str;
|
|
2935
|
+
const padding = char.repeat(length - str.length);
|
|
2936
|
+
return padEnd ? str + padding : padding + str;
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Escapes special characters in a string for use in regular expressions.
|
|
2940
|
+
*
|
|
2941
|
+
* @param {string} str - The string to escape
|
|
2942
|
+
* @returns {string} - The escaped string safe for use in RegExp
|
|
2943
|
+
*
|
|
2944
|
+
* @example
|
|
2945
|
+
* ```typescript
|
|
2946
|
+
* escapeRegExp("hello.world*"); // "hello\.world\*"
|
|
2947
|
+
*
|
|
2948
|
+
* // Usage in RegExp:
|
|
2949
|
+
* const userInput = "hello.world*";
|
|
2950
|
+
* const regex = new RegExp(escapeRegExp(userInput));
|
|
2951
|
+
* ```
|
|
2952
|
+
*/
|
|
2953
|
+
function escapeRegExp(str) {
|
|
2954
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Capitalizes each word in a string according to title case rules.
|
|
2958
|
+
*
|
|
2959
|
+
* This function implements proper title case rules, where:
|
|
2960
|
+
* - The first and last words are always capitalized
|
|
2961
|
+
* - All other major words are capitalized
|
|
2962
|
+
* - Minor words (articles, conjunctions, short prepositions) are not capitalized
|
|
2963
|
+
* unless they are the first or last word
|
|
2964
|
+
*
|
|
2965
|
+
* @param {string} str - The string to convert to title case
|
|
2966
|
+
* @returns {string} - The string in proper title case format
|
|
2967
|
+
*
|
|
2968
|
+
* @example
|
|
2969
|
+
* ```typescript
|
|
2970
|
+
* toProperTitleCase("the quick brown fox jumps over the lazy dog");
|
|
2971
|
+
* // "The Quick Brown Fox Jumps over the Lazy Dog"
|
|
2972
|
+
*
|
|
2973
|
+
* toProperTitleCase("a tale of two cities");
|
|
2974
|
+
* // "A Tale of Two Cities"
|
|
2975
|
+
* ```
|
|
2976
|
+
*/
|
|
2977
|
+
function toProperTitleCase(str) {
|
|
2978
|
+
if (!str) return "";
|
|
2979
|
+
const minorWords = ["a", "an", "and", "as", "at", "but", "by", "for", "in", "nor", "of", "on", "or", "per", "the", "to", "via", "vs"];
|
|
2980
|
+
const words = str.toLowerCase().split(/\s+/);
|
|
2981
|
+
const result = words.map((word, index) => {
|
|
2982
|
+
// Always capitalize first and last word
|
|
2983
|
+
if (index === 0 || index === words.length - 1) {
|
|
2984
|
+
return toFirstCase(word);
|
|
2985
|
+
}
|
|
2986
|
+
// Don't capitalize minor words
|
|
2987
|
+
if (minorWords.includes(word.toLowerCase())) {
|
|
2988
|
+
return word.toLowerCase();
|
|
2989
|
+
}
|
|
2990
|
+
// Capitalize major words
|
|
2991
|
+
return toFirstCase(word);
|
|
2992
|
+
});
|
|
2993
|
+
return result.join(" ");
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* Implementation of the format function that handles both overloads.
|
|
2997
|
+
*/
|
|
2998
|
+
function format(template, ...values) {
|
|
2999
|
+
if (!template) return "";
|
|
3000
|
+
// If the first value is an object (for named placeholders), use it directly
|
|
3001
|
+
if (values.length === 1 && typeof values[0] === "object" && values[0] !== null && !Array.isArray(values[0])) {
|
|
3002
|
+
const replacements = values[0];
|
|
3003
|
+
return template.replace(/{([^{}]+)}/g, (match, key) => {
|
|
3004
|
+
const value = replacements[key];
|
|
3005
|
+
return value !== undefined ? String(value) : match;
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
// Otherwise use indexed placeholders
|
|
3009
|
+
return template.replace(/{(\d+)}/g, (match, index) => {
|
|
3010
|
+
const value = values[Number(index)];
|
|
3011
|
+
return value !== undefined ? String(value) : match;
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Counts the occurrences of a substring within a string.
|
|
3016
|
+
*
|
|
3017
|
+
* @param {string} str - The string to search within
|
|
3018
|
+
* @param {string} searchValue - The substring to search for
|
|
3019
|
+
* @param {boolean} [caseSensitive=true] - Whether the search should be case-sensitive
|
|
3020
|
+
* @returns {number} - The number of occurrences
|
|
3021
|
+
*
|
|
3022
|
+
* @example
|
|
3023
|
+
* ```typescript
|
|
3024
|
+
* countOccurrences("hello hello world", "hello"); // 2
|
|
3025
|
+
* countOccurrences("Hello hello", "hello", false); // 2
|
|
3026
|
+
* countOccurrences("Hello hello", "hello", true); // 1
|
|
3027
|
+
* ```
|
|
3028
|
+
*/
|
|
3029
|
+
function countOccurrences(str, searchValue, caseSensitive = true) {
|
|
3030
|
+
if (!str || !searchValue) return 0;
|
|
3031
|
+
const regex = new RegExp(escapeRegExp(searchValue), caseSensitive ? "g" : "gi");
|
|
3032
|
+
const matches = str.match(regex);
|
|
3033
|
+
return matches ? matches.length : 0;
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Reverses a string.
|
|
3037
|
+
*
|
|
3038
|
+
* @param {string} str - The string to reverse
|
|
3039
|
+
* @returns {string} - The reversed string
|
|
3040
|
+
*
|
|
3041
|
+
* @example
|
|
3042
|
+
* ```typescript
|
|
3043
|
+
* reverse("hello"); // "olleh"
|
|
3044
|
+
* reverse("12345"); // "54321"
|
|
3045
|
+
* ```
|
|
3046
|
+
*/
|
|
3047
|
+
function reverse(str) {
|
|
3048
|
+
if (!str) return "";
|
|
3049
|
+
return str.split("").reverse().join("");
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Checks if a string contains only alphanumeric characters.
|
|
3053
|
+
*
|
|
3054
|
+
* @param {string} str - The string to check
|
|
3055
|
+
* @returns {boolean} - True if the string contains only alphanumeric characters
|
|
3056
|
+
*
|
|
3057
|
+
* @example
|
|
3058
|
+
* ```typescript
|
|
3059
|
+
* isAlphanumeric("abc123"); // true
|
|
3060
|
+
* isAlphanumeric("abc-123"); // false
|
|
3061
|
+
* ```
|
|
3062
|
+
*/
|
|
3063
|
+
function isAlphanumeric(str) {
|
|
3064
|
+
if (!str) return false;
|
|
3065
|
+
return /^[a-zA-Z0-9]+$/.test(str);
|
|
3066
|
+
}
|
|
3067
|
+
/**
|
|
3068
|
+
* Removes all whitespace from a string.
|
|
3069
|
+
*
|
|
3070
|
+
* @param {string} str - The string to process
|
|
3071
|
+
* @returns {string} - The string with all whitespace removed
|
|
3072
|
+
*
|
|
3073
|
+
* @example
|
|
3074
|
+
* ```typescript
|
|
3075
|
+
* removeWhitespace("hello world"); // "helloworld"
|
|
3076
|
+
* removeWhitespace(" spaces tabs "); // "spacestabs"
|
|
3077
|
+
* ```
|
|
3078
|
+
*/
|
|
3079
|
+
function removeWhitespace(str) {
|
|
3080
|
+
if (!str) return "";
|
|
3081
|
+
return str.replace(/\s+/g, "");
|
|
3082
|
+
}
|
|
3083
|
+
/**
|
|
3084
|
+
* Converts plural English words to their singular form.
|
|
3085
|
+
*
|
|
3086
|
+
* This function applies a series of linguistic rules and exceptions to transform
|
|
3087
|
+
* plural English words into their singular equivalents. It handles irregular plurals
|
|
3088
|
+
* (e.g., "children" → "child"), common suffix patterns (e.g., "parties" → "party"),
|
|
3089
|
+
* and Latin-derived words (e.g., "cacti" → "cactus").
|
|
3090
|
+
*
|
|
3091
|
+
* The function applies rules in priority order, from most specific to most general,
|
|
3092
|
+
* to ensure correct handling of special cases.
|
|
3093
|
+
*
|
|
3094
|
+
* @param {string} str - The plural word to convert to singular form
|
|
3095
|
+
* @returns {string} - The singular form of the word, or the original string if:
|
|
3096
|
+
* - Input is empty/falsy
|
|
3097
|
+
* - No singular form is recognized
|
|
3098
|
+
* - The word is already singular
|
|
3099
|
+
*
|
|
3100
|
+
* @example
|
|
3101
|
+
* ```typescript
|
|
3102
|
+
* // Irregular plurals
|
|
3103
|
+
* singular("children"); // "child"
|
|
3104
|
+
* singular("men"); // "man"
|
|
3105
|
+
* singular("feet"); // "foot"
|
|
3106
|
+
*
|
|
3107
|
+
* // Common patterns
|
|
3108
|
+
* singular("cats"); // "cat"
|
|
3109
|
+
* singular("boxes"); // "box"
|
|
3110
|
+
* singular("parties"); // "party"
|
|
3111
|
+
* singular("wolves"); // "wolf"
|
|
3112
|
+
*
|
|
3113
|
+
* // Latin-derived words
|
|
3114
|
+
* singular("cacti"); // "cactus"
|
|
3115
|
+
* singular("phenomena"); // "phenomenon"
|
|
3116
|
+
* ```
|
|
3117
|
+
*
|
|
3118
|
+
* @remarks
|
|
3119
|
+
* - This function is designed for English language words only
|
|
3120
|
+
* - It handles many common cases but isn't exhaustive for all English plurals
|
|
3121
|
+
* - Words that are identical in singular and plural form (e.g., "sheep") are returned unchanged
|
|
3122
|
+
*/
|
|
3123
|
+
function singular(str) {
|
|
3124
|
+
if (!str) return str;
|
|
3125
|
+
// Combine all rules in priority order
|
|
3126
|
+
const rules = [...EnglishInflectionRules.INVARIANT, ...EnglishInflectionRules.IRREGULAR.toSingular, ...EnglishInflectionRules.LATIN_GREEK.toSingular, ...EnglishInflectionRules.PATTERN_RULES.toSingular, ...EnglishInflectionRules.DEFAULT.toSingular];
|
|
3127
|
+
// Iterate over rules and apply the first matching one
|
|
3128
|
+
for (const rule of rules) {
|
|
3129
|
+
if (rule.regex.test(str)) {
|
|
3130
|
+
return str.replace(rule.regex, typeof rule.replacement === "string" ? rule.replacement : rule.replacement(str));
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
// Return the original string if no rules match
|
|
3134
|
+
return str;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
// noinspection JSUnusedGlobalSymbols
|
|
3138
|
+
/**
|
|
3139
|
+
* Type-safe utility to check if a value is an array of a specific type.
|
|
3140
|
+
*
|
|
3141
|
+
* This function acts as a type guard that not only checks if the provided value
|
|
3142
|
+
* is an array (using the native Array.isArray method), but also narrows the TypeScript
|
|
3143
|
+
* type to an array of the generic type T. This enables safer handling of potentially
|
|
3144
|
+
* array-typed values with full type inference in the conditional branches.
|
|
3145
|
+
*
|
|
3146
|
+
* @template T The expected element type of the array
|
|
3147
|
+
* @param {T | T[] | undefined} value The value to check, which could be a single item,
|
|
3148
|
+
* an array of items, or undefined
|
|
3149
|
+
* @returns {value is T[]} Type predicate that narrows the type to T[] when true
|
|
3150
|
+
*
|
|
3151
|
+
* @remarks
|
|
3152
|
+
* While this function essentially wraps Array.isArray, its value comes from the
|
|
3153
|
+
* TypeScript type narrowing it provides, making it especially useful in code that
|
|
3154
|
+
* needs to handle both single items and collections of items with type safety.
|
|
3155
|
+
*
|
|
3156
|
+
* The function properly handles undefined values by returning false, making it safe
|
|
3157
|
+
* to use with optional parameters or potentially undefined values.
|
|
3158
|
+
*
|
|
3159
|
+
* @example
|
|
3160
|
+
* ```typescript
|
|
3161
|
+
* // Function that can accept either a single user or multiple users
|
|
3162
|
+
* async function createUser(userOrUsers: UserDto | UserDto[] | undefined): Promise<User | User[]> {
|
|
3163
|
+
* // TypeScript knows userOrUsers is UserDto[] in this branch
|
|
3164
|
+
* if (isArray<UserDto>(userOrUsers)) {
|
|
3165
|
+
* return Promise.all(userOrUsers.map(user => userService.createUser(user)));
|
|
3166
|
+
* }
|
|
3167
|
+
* // TypeScript knows userOrUsers is UserDto or undefined in this branch
|
|
3168
|
+
* else if (userOrUsers) {
|
|
3169
|
+
* return userService.createUser(userOrUsers);
|
|
3170
|
+
* }
|
|
3171
|
+
* else {
|
|
3172
|
+
* throw new BadRequestException('User data is required');
|
|
3173
|
+
* }
|
|
3174
|
+
* }
|
|
3175
|
+
* ```
|
|
3176
|
+
*/
|
|
3177
|
+
function isArray(value) {
|
|
3178
|
+
return Array.isArray(value);
|
|
3179
|
+
}
|
|
3180
|
+
/**
|
|
3181
|
+
* Type-safe utility to check if a value is a non-array object of a specific type.
|
|
3182
|
+
*
|
|
3183
|
+
* This function serves as a type guard that checks if the provided value is an object
|
|
3184
|
+
* (but not an array) and narrows the TypeScript type to the generic type T. It properly
|
|
3185
|
+
* excludes null, arrays, and primitive values, focusing only on object instances.
|
|
3186
|
+
*
|
|
3187
|
+
* @template T The expected object type to narrow to when the check passes
|
|
3188
|
+
* @param {T | T[] | undefined} [value] The value to check, which could be a single object,
|
|
3189
|
+
* an array of objects, or undefined
|
|
3190
|
+
* @returns {value is T} Type predicate that narrows the type to T when true
|
|
3191
|
+
*
|
|
3192
|
+
* @remarks
|
|
3193
|
+
* This function performs three checks to ensure the value is a proper object:
|
|
3194
|
+
* 1. It's not an array (using Array.isArray)
|
|
3195
|
+
* 2. It's of type "object" according to JavaScript's typeof operator
|
|
3196
|
+
* 3. It's not null (which would pass the typeof check but isn't a valid object)
|
|
3197
|
+
*
|
|
3198
|
+
* This is particularly useful when handling parameters that could be either an object
|
|
3199
|
+
* or a simpler identifier (like an ID), allowing for type-safe conditional logic.
|
|
3200
|
+
*
|
|
3201
|
+
* @example
|
|
3202
|
+
* ```typescript
|
|
3203
|
+
* // Function that can accept either a user ID or a user object
|
|
3204
|
+
* async function getUserInfo(userIdOrUser: number | User | undefined): Promise<UserInfo> {
|
|
3205
|
+
* // TypeScript knows userIdOrUser is User in this branch
|
|
3206
|
+
* if (isObject<User>(userIdOrUser)) {
|
|
3207
|
+
* // Can safely access object properties
|
|
3208
|
+
* return await userService.getUserInfo(userIdOrUser.id);
|
|
3209
|
+
* }
|
|
3210
|
+
* // TypeScript knows userIdOrUser is number or undefined in this branch
|
|
3211
|
+
* else {
|
|
3212
|
+
* return await userService.getUserInfo(userIdOrUser);
|
|
3213
|
+
* }
|
|
3214
|
+
* }
|
|
3215
|
+
*
|
|
3216
|
+
* // Works with optional parameters too
|
|
3217
|
+
* function formatUserName(user?: User | string): string {
|
|
3218
|
+
* if (isObject<User>(user)) {
|
|
3219
|
+
* return `${user.firstName} ${user.lastName}`;
|
|
3220
|
+
* }
|
|
3221
|
+
* return user || 'Guest';
|
|
3222
|
+
* }
|
|
3223
|
+
* ```
|
|
3224
|
+
*/
|
|
3225
|
+
function isObject(value) {
|
|
3226
|
+
return !Array.isArray(value) && typeof value === "object" && value !== null;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Type-safe utility to check if a value is an object with a specific property.
|
|
3230
|
+
*
|
|
3231
|
+
* This function acts as a type guard that determines if an unknown value is an object
|
|
3232
|
+
* that contains a specified property, and narrows the TypeScript type to the generic
|
|
3233
|
+
* type T when true. This provides a robust way to implement duck typing in TypeScript,
|
|
3234
|
+
* allowing for safe property access in the conditional branches.
|
|
3235
|
+
*
|
|
3236
|
+
* @template T The expected object type that should contain the property
|
|
3237
|
+
* @param {unknown} value Any value to check, with no type constraints
|
|
3238
|
+
* @param {keyof T} propertyName The name of the property that should exist on the object
|
|
3239
|
+
* @returns {value is T} Type predicate that narrows the type to T when true
|
|
3240
|
+
*
|
|
3241
|
+
* @remarks
|
|
3242
|
+
* This function uses Object.prototype.hasOwnProperty.call to safely check for property
|
|
3243
|
+
* existence, even if the object has a custom implementation of hasOwnProperty or if the
|
|
3244
|
+
* property is named 'hasOwnProperty'.
|
|
3245
|
+
*
|
|
3246
|
+
* Unlike the simpler isObject utility, this function can distinguish between different
|
|
3247
|
+
* object types based on their properties, making it ideal for discriminating between
|
|
3248
|
+
* different interface implementations or handling union types.
|
|
3249
|
+
*
|
|
3250
|
+
* @example
|
|
3251
|
+
* ```typescript
|
|
3252
|
+
* // Check if an object has the properties of a User interface
|
|
3253
|
+
* interface User {
|
|
3254
|
+
* id: number;
|
|
3255
|
+
* name: string;
|
|
3256
|
+
* }
|
|
3257
|
+
*
|
|
3258
|
+
* interface Order {
|
|
3259
|
+
* orderNumber: string;
|
|
3260
|
+
* items: string[];
|
|
3261
|
+
* }
|
|
3262
|
+
*
|
|
3263
|
+
* // Function that can handle different object types
|
|
3264
|
+
* function processEntity(entity: unknown): void {
|
|
3265
|
+
* // Check if entity has 'id' property, indicating it's a User
|
|
3266
|
+
* if (isObjectWith<User>(entity, 'id')) {
|
|
3267
|
+
* // TypeScript knows entity is User in this branch
|
|
3268
|
+
* console.log(`Processing user: ${entity.name}`);
|
|
3269
|
+
* }
|
|
3270
|
+
* // Check if entity has 'orderNumber' property, indicating it's an Order
|
|
3271
|
+
* else if (isObjectWith<Order>(entity, 'orderNumber')) {
|
|
3272
|
+
* // TypeScript knows entity is Order in this branch
|
|
3273
|
+
* console.log(`Processing order: ${entity.orderNumber} with ${entity.items.length} items`);
|
|
3274
|
+
* }
|
|
3275
|
+
* else {
|
|
3276
|
+
* console.log('Unknown entity type');
|
|
3277
|
+
* }
|
|
3278
|
+
* }
|
|
3279
|
+
*
|
|
3280
|
+
* // Useful for API responses where types may vary
|
|
3281
|
+
* async function handleResponse(response: unknown): Promise<void> {
|
|
3282
|
+
* if (isObjectWith<ErrorResponse>(response, 'errorCode')) {
|
|
3283
|
+
* throw new ApiException(response.errorCode, response.message);
|
|
3284
|
+
* }
|
|
3285
|
+
* else if (isObjectWith<SuccessResponse>(response, 'data')) {
|
|
3286
|
+
* await processData(response.data);
|
|
3287
|
+
* }
|
|
3288
|
+
* }
|
|
3289
|
+
* ```
|
|
3290
|
+
*/
|
|
3291
|
+
function isObjectWith(value, propertyName) {
|
|
3292
|
+
return Object.prototype.hasOwnProperty.call(value, propertyName);
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
/**
|
|
3296
|
+
* Template tags for string transformations
|
|
3297
|
+
*
|
|
3298
|
+
* This enum defines all available template tags that can be used with the `applyTemplate`
|
|
3299
|
+
* and `applyTemplates` functions. Each tag corresponds to a specific string transformation
|
|
3300
|
+
* function from the string.utils module.
|
|
3301
|
+
*
|
|
3302
|
+
* Template tags are represented as strings in the format `#{transformationName}`. When these
|
|
3303
|
+
* tags appear in template strings, they are replaced with the result of applying the
|
|
3304
|
+
* corresponding transformation to the provided value.
|
|
3305
|
+
*
|
|
3306
|
+
* @see {@link applyTemplate} For applying a single value to a template
|
|
3307
|
+
* @see {@link applyTemplates} For applying multiple values to a template
|
|
3308
|
+
*/
|
|
3309
|
+
exports.TemplateTag = void 0;
|
|
3310
|
+
(function (TemplateTag) {
|
|
3311
|
+
TemplateTag["UPPER_CASE"] = "#{upperCase}";
|
|
3312
|
+
TemplateTag["SNAKE_CASE"] = "#{snakeCase}";
|
|
3313
|
+
TemplateTag["UPPER_SNAKE_CASE"] = "#{upperSnakeCase}";
|
|
3314
|
+
TemplateTag["LOWER_CASE"] = "#{lowerCase}";
|
|
3315
|
+
TemplateTag["SENTENCE_CASE"] = "#{sentenceCase}";
|
|
3316
|
+
TemplateTag["FIRST_CASE"] = "#{firstCase}";
|
|
3317
|
+
TemplateTag["CAMEL_CASE"] = "#{camelCase}";
|
|
3318
|
+
TemplateTag["PASCAL_CASE"] = "#{pascalCase}";
|
|
3319
|
+
TemplateTag["KEBAB_CASE"] = "#{kebabCase}";
|
|
3320
|
+
TemplateTag["UPPER_KEBAB_CASE"] = "#{upperKebabCase}";
|
|
3321
|
+
TemplateTag["TITLE_CASE"] = "#{titleCase}";
|
|
3322
|
+
TemplateTag["LOWER_CASE_BREAK"] = "#{lowerCaseBreak}";
|
|
3323
|
+
TemplateTag["UPPER_CASE_BREAK"] = "#{upperCaseBreak}";
|
|
3324
|
+
TemplateTag["FIRST_CASE_BREAK"] = "#{firstCaseBreak}";
|
|
3325
|
+
TemplateTag["SINGULAR"] = "#{singular}";
|
|
3326
|
+
TemplateTag["NUMBER"] = "#{number}";
|
|
3327
|
+
TemplateTag["HTML_TO_TEXT"] = "#{htmlToText}";
|
|
3328
|
+
// FORMAT = "#{format}",
|
|
3329
|
+
})(exports.TemplateTag || (exports.TemplateTag = {}));
|
|
3330
|
+
|
|
3331
|
+
// noinspection JSUnusedGlobalSymbols
|
|
3332
|
+
/**
|
|
3333
|
+
* Applies a value to a template string containing transformation tags.
|
|
3334
|
+
*
|
|
3335
|
+
* This function replaces template tags in a string with transformed versions of a provided value.
|
|
3336
|
+
* It's particularly useful for creating dynamic messages, error notifications, or any text
|
|
3337
|
+
* that needs to include the same value in different formats.
|
|
3338
|
+
*
|
|
3339
|
+
* Each template tag is replaced with the result of applying the corresponding transformation
|
|
3340
|
+
* function to the provided value. If a tag isn't present in the template string, that
|
|
3341
|
+
* transformation is skipped.
|
|
3342
|
+
*
|
|
3343
|
+
* Available template tags:
|
|
3344
|
+
* - `#{upperCase}` - Converts to UPPERCASE ("USER")
|
|
3345
|
+
* - `#{snakeCase}` - Converts to snake_case ("user_profile")
|
|
3346
|
+
* - `#{upperSnakeCase}` - Converts to UPPER_SNAKE_CASE ("USER_PROFILE")
|
|
3347
|
+
* - `#{lowerCase}` - Converts to lowercase ("user")
|
|
3348
|
+
* - `#{sentenceCase}` - Converts to Sentence case ("User profile")
|
|
3349
|
+
* - `#{firstCase}` - Converts to First case ("User")
|
|
3350
|
+
* - `#{camelCase}` - Converts to camelCase ("userProfile")
|
|
3351
|
+
* - `#{pascalCase}` - Converts to PascalCase ("UserProfile")
|
|
3352
|
+
* - `#{kebabCase}` - Converts to kebab-case ("user-profile")
|
|
3353
|
+
* - `#{upperKebabCase}` - Converts to KEBAB-CASE ("USER-PROFILE")
|
|
3354
|
+
* - `#{titleCase}` - Converts to Title Case ("User Profile")
|
|
3355
|
+
* - `#{lowerCaseBreak}` - Breaks words and converts to lowercase ("user profile")
|
|
3356
|
+
* - `#{upperCaseBreak}` - Breaks words and converts to UPPERCASE ("USER PROFILE")
|
|
3357
|
+
* - `#{firstCaseBreak}` - Breaks words and applies First case ("User profile")
|
|
3358
|
+
* - `#{singular}` - Converts plural to singular form ("users" → "user")
|
|
3359
|
+
* - `#{number}` - Converts to a number if possible ("123" → 123)
|
|
3360
|
+
* - `#{htmlToText}` - Removes HTML tags ("<b>User</b>" → "User")
|
|
3361
|
+
* - `#{format}` - Formats a string with placeholders (used with additional parameters)
|
|
3362
|
+
*
|
|
3363
|
+
* @example
|
|
3364
|
+
* ```typescript
|
|
3365
|
+
* // Error message with different versions of the same term
|
|
3366
|
+
* applyTemplate(
|
|
3367
|
+
* 'Cannot create a #{lowerCase} with this email. #{sentenceCase} already exists.',
|
|
3368
|
+
* 'User'
|
|
3369
|
+
* );
|
|
3370
|
+
* // Output: "Cannot create a user with this email. User already exists."
|
|
3371
|
+
* ```
|
|
3372
|
+
*
|
|
3373
|
+
* @example
|
|
3374
|
+
* ```typescript
|
|
3375
|
+
* // Using multiple transformations of the same value
|
|
3376
|
+
* applyTemplate(
|
|
3377
|
+
* 'Model: #{pascalCase}\nTable: #{snakeCase}\nAPI Path: #{kebabCase}',
|
|
3378
|
+
* 'blogPost'
|
|
3379
|
+
* );
|
|
3380
|
+
* // Output:
|
|
3381
|
+
* // Model: BlogPost
|
|
3382
|
+
* // Table: blog_post
|
|
3383
|
+
* // API Path: blog-post
|
|
3384
|
+
* ```
|
|
3385
|
+
*
|
|
3386
|
+
* @param {string} str - Template string containing transformation tags
|
|
3387
|
+
* @param {string} prefix - The prefix to update with
|
|
3388
|
+
* @returns {string} - The template with all tags replaced by transformed values
|
|
3389
|
+
*
|
|
3390
|
+
* @see {@link TemplateTag} For the full list of available template tags
|
|
3391
|
+
* @see {@link applyTemplates} For applying multiple different values to a template
|
|
3392
|
+
*/
|
|
3393
|
+
function applyTemplate(str, prefix) {
|
|
3394
|
+
// Apply all available template tags
|
|
3395
|
+
return str.replace(exports.TemplateTag.UPPER_CASE, toUpperCase(prefix)).replace(exports.TemplateTag.SNAKE_CASE, toSnakeCase(prefix)).replace(exports.TemplateTag.UPPER_SNAKE_CASE, toSnakeCase(prefix, true)).replace(exports.TemplateTag.LOWER_CASE, toLowerCase(prefix)).replace(exports.TemplateTag.SENTENCE_CASE, toSentenceCase(prefix)).replace(exports.TemplateTag.FIRST_CASE, toFirstCase(prefix)).replace(exports.TemplateTag.CAMEL_CASE, toCamelCase(prefix)).replace(exports.TemplateTag.PASCAL_CASE, toPascalCase(prefix)).replace(exports.TemplateTag.KEBAB_CASE, toKebabCase(prefix)).replace(exports.TemplateTag.UPPER_KEBAB_CASE, toKebabCase(prefix, true)).replace(exports.TemplateTag.TITLE_CASE, toTitleCase(prefix)).replace(exports.TemplateTag.LOWER_CASE_BREAK, toLowerCaseBreak(prefix)).replace(exports.TemplateTag.UPPER_CASE_BREAK, toUpperCaseBreak(prefix)).replace(exports.TemplateTag.FIRST_CASE_BREAK, toFirstCaseBreak(prefix)).replace(exports.TemplateTag.SINGULAR, singular(prefix)).replace(exports.TemplateTag.NUMBER, String(toNumber(prefix) ?? prefix)).replace(exports.TemplateTag.HTML_TO_TEXT, htmlToText(prefix));
|
|
3396
|
+
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Applies multiple named template transformations to a string with different values for each prefix.
|
|
3399
|
+
*
|
|
3400
|
+
* This advanced templating function allows you to use multiple different values within a single
|
|
3401
|
+
* template string, each with their own set of transformation tags. Unlike `applyTemplate` which
|
|
3402
|
+
* applies one value to all tags, this function lets you specify different values for different
|
|
3403
|
+
* prefixes, enabling complex template scenarios with multiple entities.
|
|
3404
|
+
*
|
|
3405
|
+
* Each prefix in the template is identified by a namespace (e.g., `user`, `post`) followed by
|
|
3406
|
+
* a dot and the transformation tag (e.g., `#{user.lowerCase}`, `#{post.titleCase}`). This allows
|
|
3407
|
+
* for sophisticated template generation where different parts of the text need different source
|
|
3408
|
+
* values with various transformations applied.
|
|
3409
|
+
*
|
|
3410
|
+
* This is particularly useful for generating complex messages, code templates, documentation,
|
|
3411
|
+
* or any text that involves multiple entities that need to be formatted differently within
|
|
3412
|
+
* the same template.
|
|
3413
|
+
*
|
|
3414
|
+
* @param {string} str - Template string containing namespaced transformation tags in the format
|
|
3415
|
+
* `#{prefix.transformationType}` where prefix matches keys in the prefixes object
|
|
3416
|
+
* @param {Record<string, string>} prefixes - Object mapping prefix names to their corresponding values.
|
|
3417
|
+
* Each key becomes a namespace in the template, and the value
|
|
3418
|
+
* is the string that will be transformed according to the tags.
|
|
3419
|
+
* @returns {string} The template string with all namespaced tags replaced by their transformed values
|
|
3420
|
+
*
|
|
3421
|
+
* @example
|
|
3422
|
+
* ```typescript
|
|
3423
|
+
* // Multi-entity message generation
|
|
3424
|
+
* const message = applyTemplates(
|
|
3425
|
+
* 'User #{user.lowerCase} created a new #{entity.sentenceCase} titled "#{title.titleCase}"',
|
|
3426
|
+
* {
|
|
3427
|
+
* user: 'JohnDoe',
|
|
3428
|
+
* entity: 'blogPost',
|
|
3429
|
+
* title: 'my first programming tutorial'
|
|
3430
|
+
* }
|
|
3431
|
+
* );
|
|
3432
|
+
* // Returns: "User johndoe created a new Blog post titled "My First Programming Tutorial""
|
|
3433
|
+
* ```
|
|
3434
|
+
*
|
|
3435
|
+
* @example
|
|
3436
|
+
* ```typescript
|
|
3437
|
+
* // Code generation with multiple entities
|
|
3438
|
+
* const codeTemplate = applyTemplates(
|
|
3439
|
+
* `class #{model.pascalCase} {
|
|
3440
|
+
* constructor(private #{service.camelCase}: #{service.pascalCase}Service) {}
|
|
3441
|
+
*
|
|
3442
|
+
* async create#{model.pascalCase}(data: #{model.pascalCase}Data): Promise<#{model.pascalCase}> {
|
|
3443
|
+
* return this.#{service.camelCase}.create(data);
|
|
3444
|
+
* }
|
|
3445
|
+
* }`,
|
|
3446
|
+
* {
|
|
3447
|
+
* model: 'user-profile',
|
|
3448
|
+
* service: 'database'
|
|
3449
|
+
* }
|
|
3450
|
+
* );
|
|
3451
|
+
* // Generates a complete class with proper naming conventions
|
|
3452
|
+
* ```
|
|
3453
|
+
*
|
|
3454
|
+
* @example
|
|
3455
|
+
* ```typescript
|
|
3456
|
+
* // API documentation generation
|
|
3457
|
+
* const apiDoc = applyTemplates(
|
|
3458
|
+
* `## #{endpoint.titleCase}
|
|
3459
|
+
*
|
|
3460
|
+
* **URL:** \`/api/#{endpoint.kebabCase}\`
|
|
3461
|
+
* **Method:** #{method.upperCase}
|
|
3462
|
+
* **Model:** #{model.pascalCase}
|
|
3463
|
+
*
|
|
3464
|
+
* Creates a new #{model.lowerCaseBreak} in the system.`,
|
|
3465
|
+
* {
|
|
3466
|
+
* endpoint: 'userProfiles',
|
|
3467
|
+
* method: 'post',
|
|
3468
|
+
* model: 'UserProfile'
|
|
3469
|
+
* }
|
|
3470
|
+
* );
|
|
3471
|
+
* ```
|
|
3472
|
+
*
|
|
3473
|
+
* @example
|
|
3474
|
+
* ```typescript
|
|
3475
|
+
* // Database migration script generation
|
|
3476
|
+
* const migration = applyTemplates(
|
|
3477
|
+
* `CREATE TABLE #{table.snakeCase} (
|
|
3478
|
+
* id SERIAL PRIMARY KEY,
|
|
3479
|
+
* #{field.snakeCase} VARCHAR(255) NOT NULL,
|
|
3480
|
+
* created_at TIMESTAMP DEFAULT NOW()
|
|
3481
|
+
* );
|
|
3482
|
+
*
|
|
3483
|
+
* CREATE INDEX idx_#{table.snakeCase}_#{field.snakeCase} ON #{table.snakeCase}(#{field.snakeCase});`,
|
|
3484
|
+
* {
|
|
3485
|
+
* table: 'UserProfiles',
|
|
3486
|
+
* field: 'emailAddress'
|
|
3487
|
+
* }
|
|
3488
|
+
* );
|
|
3489
|
+
* ```
|
|
3490
|
+
*
|
|
3491
|
+
* @remarks
|
|
3492
|
+
* - Each prefix in the prefixes object becomes a namespace in the template
|
|
3493
|
+
* - Template tags must follow the format `#{prefix.transformationType}`
|
|
3494
|
+
* - All transformation types available in `applyTemplate` are supported
|
|
3495
|
+
* - If a prefixed tag is not found in the template, it's simply ignored
|
|
3496
|
+
* - The function processes all prefixes and their transformations in the order they appear
|
|
3497
|
+
* - Supports the same transformation types as the TemplateTag enum
|
|
3498
|
+
* - More flexible than `applyTemplate` but with slightly more complex syntax
|
|
3499
|
+
*
|
|
3500
|
+
* @see {@link applyTemplate} For applying a single value to multiple transformation tags
|
|
3501
|
+
* @see {@link TemplateTag} For the complete list of available transformation types
|
|
3502
|
+
*/
|
|
3503
|
+
function applyTemplates(str, prefixes) {
|
|
3504
|
+
let result = str;
|
|
3505
|
+
// Process each prefix
|
|
3506
|
+
for (const [key, prefix] of Object.entries(prefixes)) {
|
|
3507
|
+
// Replace all possible template tags for this prefix
|
|
3508
|
+
// eslint-disable-next-line no-loop-func
|
|
3509
|
+
Object.values(exports.TemplateTag).forEach(tag => {
|
|
3510
|
+
// Convert #{tag} to #{prefix.tag}
|
|
3511
|
+
const prefixedTag = tag.replace("#{", `#{${key}.`);
|
|
3512
|
+
if (result.includes(prefixedTag)) {
|
|
3513
|
+
// Get the transformation function name
|
|
3514
|
+
const templateFunction = tag.replace(/#{(.+)}/, "$1");
|
|
3515
|
+
// Apply the appropriate transformation based on the tag
|
|
3516
|
+
let replacement = prefix;
|
|
3517
|
+
switch (templateFunction) {
|
|
3518
|
+
case exports.TemplateTag.UPPER_CASE:
|
|
3519
|
+
replacement = toUpperCase(prefix);
|
|
3520
|
+
break;
|
|
3521
|
+
case exports.TemplateTag.SNAKE_CASE:
|
|
3522
|
+
replacement = toSnakeCase(prefix);
|
|
3523
|
+
break;
|
|
3524
|
+
case exports.TemplateTag.UPPER_SNAKE_CASE:
|
|
3525
|
+
replacement = toSnakeCase(prefix, true);
|
|
3526
|
+
break;
|
|
3527
|
+
case exports.TemplateTag.LOWER_CASE:
|
|
3528
|
+
replacement = toLowerCase(prefix);
|
|
3529
|
+
break;
|
|
3530
|
+
case exports.TemplateTag.SENTENCE_CASE:
|
|
3531
|
+
replacement = toSentenceCase(prefix);
|
|
3532
|
+
break;
|
|
3533
|
+
case exports.TemplateTag.FIRST_CASE:
|
|
3534
|
+
replacement = toFirstCase(prefix);
|
|
3535
|
+
break;
|
|
3536
|
+
case exports.TemplateTag.CAMEL_CASE:
|
|
3537
|
+
replacement = toCamelCase(prefix);
|
|
3538
|
+
break;
|
|
3539
|
+
case exports.TemplateTag.PASCAL_CASE:
|
|
3540
|
+
replacement = toPascalCase(prefix);
|
|
3541
|
+
break;
|
|
3542
|
+
case exports.TemplateTag.KEBAB_CASE:
|
|
3543
|
+
replacement = toKebabCase(prefix);
|
|
3544
|
+
break;
|
|
3545
|
+
case exports.TemplateTag.UPPER_KEBAB_CASE:
|
|
3546
|
+
replacement = toKebabCase(prefix, true);
|
|
3547
|
+
break;
|
|
3548
|
+
case exports.TemplateTag.TITLE_CASE:
|
|
3549
|
+
replacement = toTitleCase(prefix);
|
|
3550
|
+
break;
|
|
3551
|
+
case exports.TemplateTag.LOWER_CASE_BREAK:
|
|
3552
|
+
replacement = toLowerCaseBreak(prefix);
|
|
3553
|
+
break;
|
|
3554
|
+
case exports.TemplateTag.UPPER_CASE_BREAK:
|
|
3555
|
+
replacement = toUpperCaseBreak(prefix);
|
|
3556
|
+
break;
|
|
3557
|
+
case exports.TemplateTag.FIRST_CASE_BREAK:
|
|
3558
|
+
replacement = toFirstCaseBreak(prefix);
|
|
3559
|
+
break;
|
|
3560
|
+
case exports.TemplateTag.SINGULAR:
|
|
3561
|
+
replacement = singular(prefix);
|
|
3562
|
+
break;
|
|
3563
|
+
case exports.TemplateTag.NUMBER:
|
|
3564
|
+
replacement = String(toNumber(prefix) ?? prefix);
|
|
3565
|
+
break;
|
|
3566
|
+
case exports.TemplateTag.HTML_TO_TEXT:
|
|
3567
|
+
replacement = htmlToText(prefix);
|
|
3568
|
+
break;
|
|
3569
|
+
}
|
|
3570
|
+
result = result.replace(prefixedTag, replacement);
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
return result;
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3577
|
+
exports.CHARACTERS_TO_REMOVE = CHARACTERS_TO_REMOVE;
|
|
3578
|
+
exports.CONTEXT_MULTIPLIER = CONTEXT_MULTIPLIER;
|
|
3579
|
+
exports.DEFAULT_BREAK_CHAR = DEFAULT_BREAK_CHAR;
|
|
3580
|
+
exports.DEFAULT_CONTEXT_LENGTH = DEFAULT_CONTEXT_LENGTH;
|
|
3581
|
+
exports.DEFAULT_ELLIPSIS = DEFAULT_ELLIPSIS;
|
|
3582
|
+
exports.DEFAULT_LINE_LENGTH = DEFAULT_LINE_LENGTH;
|
|
3583
|
+
exports.EnglishInflectionRules = EnglishInflectionRules;
|
|
3584
|
+
exports.HEX_PADDING_CHAR = HEX_PADDING_CHAR;
|
|
3585
|
+
exports.HEX_PADDING_LENGTH = HEX_PADDING_LENGTH;
|
|
3586
|
+
exports.HEX_RADIX = HEX_RADIX;
|
|
3587
|
+
exports.applyTemplate = applyTemplate;
|
|
3588
|
+
exports.applyTemplates = applyTemplates;
|
|
3589
|
+
exports.breakToWords = breakToWords;
|
|
3590
|
+
exports.countOccurrences = countOccurrences;
|
|
3591
|
+
exports.createExcerpt = createExcerpt;
|
|
3592
|
+
exports.deepCopy = deepCopy;
|
|
3593
|
+
exports.dottedPathObjectToNested = dottedPathObjectToNested;
|
|
3594
|
+
exports.escapeRegExp = escapeRegExp;
|
|
3595
|
+
exports.extractEmails = extractEmails;
|
|
3596
|
+
exports.extractUrls = extractUrls;
|
|
3597
|
+
exports.filterByObject = filterByObject;
|
|
3598
|
+
exports.format = format;
|
|
3599
|
+
exports.getEnumValues = getEnumValues;
|
|
3600
|
+
exports.getFileExt = getFileExt;
|
|
3601
|
+
exports.getFileSize = getFileSize;
|
|
3602
|
+
exports.getMapKey = getMapKey;
|
|
3603
|
+
exports.getMapKeys = getMapKeys;
|
|
3604
|
+
exports.getValueByPath = getValueByPath;
|
|
3605
|
+
exports.groupBy = groupBy;
|
|
3606
|
+
exports.hasOwnAll = hasOwnAll;
|
|
3607
|
+
exports.hashString = hashString;
|
|
3608
|
+
exports.htmlToText = htmlToText;
|
|
3609
|
+
exports.isAlphanumeric = isAlphanumeric;
|
|
3610
|
+
exports.isArray = isArray;
|
|
3611
|
+
exports.isObject = isObject;
|
|
3612
|
+
exports.isObjectWith = isObjectWith;
|
|
3613
|
+
exports.isValidRedirectUrl = isValidRedirectUrl;
|
|
3614
|
+
exports.maskString = maskString;
|
|
3615
|
+
exports.mimeTypes = mimeTypes;
|
|
3616
|
+
exports.normalizeString = normalizeString;
|
|
3617
|
+
exports.objectToDottedPathValueObject = objectToDottedPathValueObject;
|
|
3618
|
+
exports.omit = omit;
|
|
3619
|
+
exports.padString = padString;
|
|
3620
|
+
exports.plural = plural;
|
|
3621
|
+
exports.prune = prune;
|
|
3622
|
+
exports.randomString = randomString;
|
|
3623
|
+
exports.removeWhitespace = removeWhitespace;
|
|
3624
|
+
exports.reverse = reverse;
|
|
3625
|
+
exports.searchMapValues = searchMapValues;
|
|
3626
|
+
exports.singular = singular;
|
|
3627
|
+
exports.slugify = slugify;
|
|
3628
|
+
exports.stringSimilarity = stringSimilarity;
|
|
3629
|
+
exports.toCamelCase = toCamelCase;
|
|
3630
|
+
exports.toFirstCase = toFirstCase;
|
|
3631
|
+
exports.toFirstCaseBreak = toFirstCaseBreak;
|
|
3632
|
+
exports.toKebabCase = toKebabCase;
|
|
3633
|
+
exports.toLowerCase = toLowerCase;
|
|
3634
|
+
exports.toLowerCaseBreak = toLowerCaseBreak;
|
|
3635
|
+
exports.toNumber = toNumber;
|
|
3636
|
+
exports.toPascalCase = toPascalCase;
|
|
3637
|
+
exports.toProperTitleCase = toProperTitleCase;
|
|
3638
|
+
exports.toSentenceCase = toSentenceCase;
|
|
3639
|
+
exports.toSnakeCase = toSnakeCase;
|
|
3640
|
+
exports.toTitleCase = toTitleCase;
|
|
3641
|
+
exports.toUpperCase = toUpperCase;
|
|
3642
|
+
exports.toUpperCaseBreak = toUpperCaseBreak;
|
|
3643
|
+
exports.toVariableName = toVariableName;
|
|
3644
|
+
exports.truncate = truncate;
|
|
3645
|
+
exports.wordWrap = wordWrap;
|