complete-common 1.0.0

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.
@@ -0,0 +1,304 @@
1
+ import { parseIntSafe } from "./utils.js";
2
+
3
+ // When regular expressions are located at the root instead of inside the function, the functions
4
+ // are tested to perform 11% faster.
5
+
6
+ const DIACRITIC_REGEX = /\p{Diacritic}/u;
7
+
8
+ /** This is what the Zod validator library uses. */
9
+ const EMOJI_REGEX = /(\p{Extended_Pictographic}|\p{Emoji_Component})/u;
10
+
11
+ const FIRST_LETTER_CAPITALIZED_REGEX = /^\p{Lu}/u;
12
+
13
+ /** From: https://github.com/expandjs/expandjs/blob/master/lib/kebabCaseRegex.js */
14
+ const KEBAB_CASE_REGEX =
15
+ /^([a-z](?!\d)|\d(?![a-z]))+(-?([a-z](?!\d)|\d(?![a-z])))*$|^$/;
16
+
17
+ const SEMANTIC_VERSION_REGEX = /^v*(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/;
18
+ const WHITESPACE_REGEX = /\s/g;
19
+
20
+ export function capitalizeFirstLetter(string: string): string {
21
+ if (string === "") {
22
+ return string;
23
+ }
24
+
25
+ const firstCharacter = string.charAt(0);
26
+ const capitalizedFirstLetter = firstCharacter.toUpperCase();
27
+ const restOfString = string.slice(1);
28
+
29
+ return `${capitalizedFirstLetter}${restOfString}`;
30
+ }
31
+
32
+ /**
33
+ * Helper function to replace all of the ampersands, less than signs, greater than signs, double
34
+ * quotes, and single quotes in a string with the escaped counterparts. For example, "<" will be
35
+ * replaced with "&lt;".
36
+ */
37
+ export function escapeHTMLCharacters(string: string): string {
38
+ return string
39
+ .replaceAll("&", "&amp;")
40
+ .replaceAll("<", "&lt;")
41
+ .replaceAll(">", "&gt;")
42
+ .replaceAll('"', "&quot;")
43
+ .replaceAll("'", "&#039;");
44
+ }
45
+
46
+ export function getNumConsecutiveDiacritics(string: string): number {
47
+ // First, normalize with Normalization Form Canonical Decomposition (NFD) so that diacritics are
48
+ // separated from other characters:
49
+ // https://en.wikipedia.org/wiki/Unicode_equivalence
50
+ const normalizedString = string.normalize("NFD");
51
+
52
+ let numConsecutiveDiacritic = 0;
53
+ let maxConsecutiveDiacritic = 0;
54
+
55
+ for (const character of normalizedString) {
56
+ if (hasDiacritic(character)) {
57
+ numConsecutiveDiacritic++;
58
+ if (numConsecutiveDiacritic > maxConsecutiveDiacritic) {
59
+ maxConsecutiveDiacritic = numConsecutiveDiacritic;
60
+ }
61
+ } else {
62
+ numConsecutiveDiacritic = 0;
63
+ }
64
+ }
65
+
66
+ return maxConsecutiveDiacritic;
67
+ }
68
+
69
+ export function hasDiacritic(string: string): boolean {
70
+ // First, normalize with Normalization Form Canonical Decomposition (NFD) so that diacritics are
71
+ // separated from other characters:
72
+ // https://en.wikipedia.org/wiki/Unicode_equivalence
73
+ const normalizedString = string.normalize("NFD");
74
+
75
+ return DIACRITIC_REGEX.test(normalizedString);
76
+ }
77
+
78
+ export function hasEmoji(string: string): boolean {
79
+ return EMOJI_REGEX.test(string);
80
+ }
81
+
82
+ /** From: https://stackoverflow.com/questions/1731190/check-if-a-string-has-white-space */
83
+ export function hasWhitespace(string: string): boolean {
84
+ return WHITESPACE_REGEX.test(string);
85
+ }
86
+
87
+ /**
88
+ * From:
89
+ * https://stackoverflow.com/questions/8334606/check-if-first-letter-of-word-is-a-capital-letter
90
+ */
91
+ export function isFirstLetterCapitalized(string: string): boolean {
92
+ return FIRST_LETTER_CAPITALIZED_REGEX.test(string);
93
+ }
94
+
95
+ /** Kebab case is the naming style of using all lowercase and hyphens, like "foo-bar". */
96
+ export function isKebabCase(string: string): boolean {
97
+ return KEBAB_CASE_REGEX.test(string);
98
+ }
99
+
100
+ /**
101
+ * Helper function to check if a given string is a valid Semantic Version.
102
+ *
103
+ * @see https://semver.org/
104
+ */
105
+ export function isSemanticVersion(versionString: string): boolean {
106
+ const match = versionString.match(SEMANTIC_VERSION_REGEX);
107
+ return match !== null;
108
+ }
109
+
110
+ export function kebabCaseToCamelCase(string: string): string {
111
+ return string.replaceAll(/-./g, (match) => {
112
+ const firstLetterOfWord = match[1];
113
+ return firstLetterOfWord === undefined
114
+ ? ""
115
+ : firstLetterOfWord.toUpperCase();
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Helper function to normalize a string. Specifically, this performs the following steps:
121
+ *
122
+ * - Removes any non-printable characters, if any.
123
+ * - Normalizes all newlines to "\n".
124
+ * - Normalizes all spaces to " ".
125
+ * - Removes leading/trailing whitespace.
126
+ *
127
+ * @see
128
+ * https://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript
129
+ */
130
+ export function normalizeString(string: string): string {
131
+ let sanitizedString = string;
132
+
133
+ sanitizedString = removeNonPrintableCharacters(sanitizedString);
134
+
135
+ // Normalize newlines.
136
+ sanitizedString = sanitizedString.replaceAll("\n\r", "\n");
137
+ sanitizedString = sanitizedString.replaceAll(/\p{Zl}/gu, "\n");
138
+ sanitizedString = sanitizedString.replaceAll(/\p{Zp}/gu, "\n");
139
+
140
+ // Normalize spaces.
141
+ sanitizedString = sanitizedString.replaceAll(/\p{Zs}/gu, " ");
142
+
143
+ // Remove leading/trailing whitespace.
144
+ sanitizedString = sanitizedString.trim();
145
+
146
+ return sanitizedString;
147
+ }
148
+
149
+ /**
150
+ * Helper function to parse a Semantic Versioning string into its individual constituents. Returns
151
+ * undefined if the submitted string was not a proper Semantic Version.
152
+ *
153
+ * @see https://semver.org/
154
+ */
155
+ export function parseSemanticVersion(versionString: string):
156
+ | {
157
+ majorVersion: number;
158
+ minorVersion: number;
159
+ patchVersion: number;
160
+ }
161
+ | undefined {
162
+ const match = versionString.match(SEMANTIC_VERSION_REGEX);
163
+ if (match === null || match.groups === undefined) {
164
+ return undefined;
165
+ }
166
+
167
+ const { major, minor, patch } = match.groups;
168
+ if (major === undefined || minor === undefined || patch === undefined) {
169
+ return undefined;
170
+ }
171
+
172
+ const majorVersion = parseIntSafe(major);
173
+ const minorVersion = parseIntSafe(minor);
174
+ const patchVersion = parseIntSafe(patch);
175
+
176
+ if (
177
+ majorVersion === undefined ||
178
+ minorVersion === undefined ||
179
+ patchVersion === undefined
180
+ ) {
181
+ return undefined;
182
+ }
183
+
184
+ return { majorVersion, minorVersion, patchVersion };
185
+ }
186
+
187
+ /**
188
+ * Helper function to remove lines from a multi-line string. This function looks for a "-start" and
189
+ * a "-end" suffix after the marker. Lines with markets will be completely removed from the output.
190
+ *
191
+ * For example, by using a marker of "@foo":
192
+ *
193
+ * ```text
194
+ * line1
195
+ * # @foo-start
196
+ * line2
197
+ * line3
198
+ * # @foo-end
199
+ * line4
200
+ * ```
201
+ *
202
+ * Would return:
203
+ *
204
+ * ```text
205
+ * line1
206
+ * line4
207
+ * ```
208
+ */
209
+ export function removeLinesBetweenMarkers(
210
+ string: string,
211
+ marker: string,
212
+ ): string {
213
+ const lines = string.split("\n");
214
+ const newLines: string[] = [];
215
+
216
+ let skippingLines = false;
217
+
218
+ for (const line of lines) {
219
+ if (line.includes(`${marker}-start`)) {
220
+ skippingLines = true;
221
+ continue;
222
+ }
223
+
224
+ if (line.includes(`${marker}-end`)) {
225
+ skippingLines = false;
226
+ continue;
227
+ }
228
+
229
+ if (!skippingLines) {
230
+ newLines.push(line);
231
+ }
232
+ }
233
+
234
+ return newLines.join("\n");
235
+ }
236
+
237
+ /** Helper function to remove lines from a multi-line string matching a certain other string. */
238
+ export function removeLinesMatching(string: string, match: string): string {
239
+ const lines = string.split("\n");
240
+ const newLines = lines.filter((line) => !line.includes(match));
241
+ return newLines.join("\n");
242
+ }
243
+
244
+ /**
245
+ * Helper function to remove all non-printable characters from a string.
246
+ *
247
+ * @see
248
+ * https://stackoverflow.com/questions/11598786/how-to-replace-non-printable-unicode-characters-javascript
249
+ */
250
+ export function removeNonPrintableCharacters(string: string): string {
251
+ return string.replaceAll(/\p{C}/gu, "");
252
+ }
253
+
254
+ /** Helper function to remove all whitespace characters from a string. */
255
+ export function removeWhitespace(string: string): string {
256
+ return string.replaceAll(WHITESPACE_REGEX, "");
257
+ }
258
+
259
+ /**
260
+ * Helper function to trim a prefix from a string, if it exists. Returns the trimmed string.
261
+ *
262
+ * @param string The string to trim.
263
+ * @param prefix The prefix to trim.
264
+ * @param trimAll Whether to remove multiple instances of the prefix, if they exist. If this is set
265
+ * to true, the prefix must only be a single character.
266
+ */
267
+ export function trimPrefix(
268
+ string: string,
269
+ prefix: string,
270
+ trimAll = false,
271
+ ): string {
272
+ if (trimAll) {
273
+ const regExp = new RegExp(`^${prefix}+`, "g");
274
+ return string.replaceAll(regExp, "");
275
+ }
276
+
277
+ if (!string.startsWith(prefix)) {
278
+ return string;
279
+ }
280
+
281
+ return string.slice(prefix.length);
282
+ }
283
+
284
+ /** Helper function to trim a suffix from a string, if it exists. Returns the trimmed string. */
285
+ export function trimSuffix(string: string, prefix: string): string {
286
+ if (!string.endsWith(prefix)) {
287
+ return string;
288
+ }
289
+
290
+ const endCharacter = string.length - prefix.length;
291
+ return string.slice(0, endCharacter);
292
+ }
293
+
294
+ /**
295
+ * Helper function to truncate a string to a maximum length. If the length of the string is less
296
+ * than or equal to the provided maximum length, the string will be returned unmodified.
297
+ */
298
+ export function truncateString(string: string, maxLength: number): string {
299
+ if (string.length <= maxLength) {
300
+ return string;
301
+ }
302
+
303
+ return string.slice(0, maxLength);
304
+ }
@@ -0,0 +1,31 @@
1
+ import type { Tuple } from "../types/Tuple.js";
2
+
3
+ type TupleKey<T extends readonly unknown[]> = {
4
+ [L in T["length"]]: Exclude<Partial<Tuple<unknown, L>>["length"], L>;
5
+ }[T["length"]];
6
+ type TupleValue<T extends readonly unknown[]> = T[0];
7
+ type TupleEntry<T extends readonly unknown[]> = [TupleKey<T>, TupleValue<T>];
8
+
9
+ /**
10
+ * Helper function to get the entries (i.e. indexes and values) of a tuple in a type-safe way.
11
+ *
12
+ * This is useful because the vanilla `Array.entries` method will always have the keys be of type
13
+ * `number`.
14
+ */
15
+ export function* tupleEntries<T extends readonly unknown[]>(
16
+ tuple: T,
17
+ ): Generator<TupleEntry<T>> {
18
+ yield* tuple.entries() as Generator<TupleEntry<T>>;
19
+ }
20
+
21
+ /**
22
+ * Helper function to get the keys (i.e. indexes) of a tuple in a type-safe way.
23
+ *
24
+ * This is useful because the vanilla `Array.keys` method will always have the keys be of type
25
+ * `number`.
26
+ */
27
+ export function* tupleKeys<T extends readonly unknown[]>(
28
+ tuple: T,
29
+ ): Generator<TupleKey<T>> {
30
+ yield* tuple.keys() as Generator<TupleKey<T>>;
31
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Helper function to narrow an unknown value to an object (i.e. a TypeScript record).
3
+ *
4
+ * Under the hood, this checks for `typeof variable === "object"`, `variable !== null`, and
5
+ * `!Array.isArray(variable)`.
6
+ */
7
+ export function isObject(
8
+ variable: unknown,
9
+ ): variable is Record<string, unknown> {
10
+ return (
11
+ typeof variable === "object" &&
12
+ variable !== null &&
13
+ !Array.isArray(variable)
14
+ );
15
+ }