@regardio/js 0.6.0 → 0.7.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,223 @@
1
+ //#region src/text/bytes.ts
2
+ function formatBytes(bytes, decimals = 2) {
3
+ if (!+bytes) return "0 Bytes";
4
+ /** Converts on base 10
5
+ * In binary - change k to 1024
6
+ */
7
+ const k = 1e3;
8
+ const dm = decimals < 0 ? 0 : decimals;
9
+ const sizes = [
10
+ "Bytes",
11
+ "KB",
12
+ "MB",
13
+ "GB",
14
+ "TB",
15
+ "PB",
16
+ "EB",
17
+ "ZB",
18
+ "YB"
19
+ ];
20
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
21
+ return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
22
+ }
23
+
24
+ //#endregion
25
+ //#region src/text/text.ts
26
+ /**
27
+ * Locale-specific typographic quote styles
28
+ * Using Unicode escape sequences to avoid encoding issues
29
+ * @see https://en.wikipedia.org/wiki/Quotation_mark#Summary_table
30
+ */
31
+ const quoteStyles = {
32
+ cs: {
33
+ close: "”",
34
+ closeSingle: "’",
35
+ open: "„",
36
+ openSingle: "‚"
37
+ },
38
+ da: {
39
+ close: "«",
40
+ closeSingle: "›",
41
+ open: "»",
42
+ openSingle: "‹"
43
+ },
44
+ de: {
45
+ close: "”",
46
+ closeSingle: "’",
47
+ open: "„",
48
+ openSingle: "‚"
49
+ },
50
+ "de-ch": {
51
+ close: "»",
52
+ closeSingle: "›",
53
+ open: "«",
54
+ openSingle: "‹"
55
+ },
56
+ en: {
57
+ close: "”",
58
+ closeSingle: "’",
59
+ open: "“",
60
+ openSingle: "‘"
61
+ },
62
+ es: {
63
+ close: "»",
64
+ closeSingle: "”",
65
+ open: "«",
66
+ openSingle: "“"
67
+ },
68
+ fi: {
69
+ close: "”",
70
+ closeSingle: "’",
71
+ open: "”",
72
+ openSingle: "’"
73
+ },
74
+ fr: {
75
+ close: " »",
76
+ closeSingle: " ›",
77
+ open: "« ",
78
+ openSingle: "‹ "
79
+ },
80
+ hu: {
81
+ close: "”",
82
+ closeSingle: "’",
83
+ open: "„",
84
+ openSingle: "‚"
85
+ },
86
+ it: {
87
+ close: "»",
88
+ closeSingle: "”",
89
+ open: "«",
90
+ openSingle: "“"
91
+ },
92
+ ja: {
93
+ close: "」",
94
+ closeSingle: "』",
95
+ open: "「",
96
+ openSingle: "『"
97
+ },
98
+ nl: {
99
+ close: "’",
100
+ closeSingle: "’",
101
+ open: "‘",
102
+ openSingle: "‘"
103
+ },
104
+ no: {
105
+ close: "«",
106
+ closeSingle: "›",
107
+ open: "»",
108
+ openSingle: "‹"
109
+ },
110
+ pl: {
111
+ close: "”",
112
+ closeSingle: "’",
113
+ open: "„",
114
+ openSingle: "‚"
115
+ },
116
+ pt: {
117
+ close: "»",
118
+ closeSingle: "”",
119
+ open: "«",
120
+ openSingle: "“"
121
+ },
122
+ ru: {
123
+ close: "»",
124
+ closeSingle: "’",
125
+ open: "«",
126
+ openSingle: "‚"
127
+ },
128
+ sv: {
129
+ close: "”",
130
+ closeSingle: "’",
131
+ open: "”",
132
+ openSingle: "’"
133
+ },
134
+ zh: {
135
+ close: "」",
136
+ closeSingle: "』",
137
+ open: "「",
138
+ openSingle: "『"
139
+ }
140
+ };
141
+ /**
142
+ * Get the quote style for a given locale.
143
+ * Falls back to base language if region-specific style not found,
144
+ * then to English style as default.
145
+ */
146
+ function getQuoteStyle(locale) {
147
+ const normalized = locale.toLowerCase();
148
+ const exactMatch = quoteStyles[normalized];
149
+ if (exactMatch) return exactMatch;
150
+ const baseLanguage = normalized.split("-")[0];
151
+ if (baseLanguage) {
152
+ const baseMatch = quoteStyles[baseLanguage];
153
+ if (baseMatch) return baseMatch;
154
+ }
155
+ return quoteStyles.en;
156
+ }
157
+ /**
158
+ * Replace straight quotes with typographically correct quotes for the given locale.
159
+ *
160
+ * @param text - The text containing straight quotes
161
+ * @param locale - The locale to use for quote style (e.g., 'en', 'de', 'fr')
162
+ * @returns Text with typographic quotes
163
+ *
164
+ * @example
165
+ * typographicQuotes('"Hello"', 'en') // → '"Hello"'
166
+ * typographicQuotes('"Hello"', 'de') // → '„Hello"'
167
+ * typographicQuotes('"Hello"', 'fr') // → '« Hello »'
168
+ */
169
+ function typographicQuotes(text, locale) {
170
+ const style = getQuoteStyle(locale);
171
+ let result = text.replace(/"([^"]*)"/g, `${style.open}$1${style.close}`);
172
+ result = result.replace(/'([^']*)'/g, `${style.openSingle}$1${style.closeSingle}`);
173
+ return result;
174
+ }
175
+ function toBoolean(value) {
176
+ if (typeof value === "boolean") return value;
177
+ return value === "true" || value === "1";
178
+ }
179
+ /**
180
+ * Replace &shy; HTML entity with Unicode soft hyphen
181
+ */
182
+ function replaceShyInString(input) {
183
+ return input.replace(/&shy;/g, "­");
184
+ }
185
+ /**
186
+ * Split text into sentences
187
+ */
188
+ function splitIntoSentences(text) {
189
+ return text.split(/(?<=[.!?])\s+/);
190
+ }
191
+ /**
192
+ * Split text into words
193
+ */
194
+ function splitIntoWords(text) {
195
+ return text.split(/\s+/);
196
+ }
197
+ /**
198
+ * Truncate text to a maximum length
199
+ */
200
+ function truncateText(text, maxLength, suffix = "...") {
201
+ if (text.length <= maxLength) return text;
202
+ return text.slice(0, maxLength - suffix.length) + suffix;
203
+ }
204
+ const authorRegex = /^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$/;
205
+ /**
206
+ * Parse an author string in the format "Name <email> (url)"
207
+ */
208
+ function parseAuthorString(input) {
209
+ const match = input.match(authorRegex);
210
+ if (match) {
211
+ const [, name, email, url] = match;
212
+ const result = {};
213
+ if (email) result.email = email;
214
+ const trimmedName = name?.trim();
215
+ if (trimmedName) result.name = trimmedName;
216
+ if (url) result.url = url;
217
+ return result;
218
+ }
219
+ return {};
220
+ }
221
+
222
+ //#endregion
223
+ export { formatBytes, parseAuthorString, replaceShyInString, splitIntoSentences, splitIntoWords, toBoolean, truncateText, typographicQuotes };
@@ -0,0 +1,22 @@
1
+ //#region src/time/delay.d.ts
2
+ /** Delays using a promise
3
+ * @param ms Milisecods of delay
4
+ * @return Promise
5
+ */
6
+ declare function delay(ms: number): Promise<void>;
7
+ //#endregion
8
+ //#region src/time/measure.d.ts
9
+ declare function measure<Result>(key: string, callback: () => Result | Promise<Result>): Promise<Result>;
10
+ //#endregion
11
+ //#region src/time/time.d.ts
12
+ declare function timeAgo(input: Date | string): string;
13
+ declare const oneWeekFromNow: () => Date;
14
+ declare const oneDayFromNow: () => Date;
15
+ declare const oneMinuteFromNow: () => Date;
16
+ declare function friendlyDuration(minutes: number | null, locale: string, short?: boolean): {
17
+ key: string;
18
+ vars: Record<string, string | number>;
19
+ } | null;
20
+ declare const dateTimeInUnix: (date: number) => number;
21
+ //#endregion
22
+ export { dateTimeInUnix, delay, friendlyDuration, measure, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
@@ -0,0 +1,154 @@
1
+ //#region src/time/delay.ts
2
+ /** Delays using a promise
3
+ * @param ms Milisecods of delay
4
+ * @return Promise
5
+ */
6
+ function delay(ms) {
7
+ return new Promise((resolve) => {
8
+ return setTimeout(resolve, ms);
9
+ });
10
+ }
11
+
12
+ //#endregion
13
+ //#region src/time/measure.ts
14
+ async function measure(key, callback) {
15
+ const start = Date.now();
16
+ try {
17
+ let result = callback();
18
+ if (result instanceof Promise) result = await result;
19
+ return result;
20
+ } finally {
21
+ const end = Date.now();
22
+ console.log(`${key} took ${end - start}ms`);
23
+ }
24
+ }
25
+
26
+ //#endregion
27
+ //#region src/time/time.ts
28
+ function timeAgo(input) {
29
+ const date = new Date(input);
30
+ const formatter = new Intl.RelativeTimeFormat("en");
31
+ const ranges = {
32
+ days: 3600 * 24,
33
+ hours: 3600,
34
+ minutes: 60,
35
+ months: 3600 * 24 * 30,
36
+ seconds: 1,
37
+ weeks: 3600 * 24 * 7,
38
+ years: 3600 * 24 * 365
39
+ };
40
+ const secondsElapsed = (date.getTime() - Date.now()) / 1e3;
41
+ for (const key of Object.keys(ranges)) if (ranges[key] < Math.abs(secondsElapsed)) {
42
+ const delta = secondsElapsed / ranges[key];
43
+ return formatter.format(Math.round(delta), key);
44
+ }
45
+ return "Just now";
46
+ }
47
+ const oneWeekFromNow = () => {
48
+ const now = /* @__PURE__ */ new Date();
49
+ return new Date(now.getTime() + 10080 * 60 * 1e3);
50
+ };
51
+ const oneDayFromNow = () => {
52
+ const now = /* @__PURE__ */ new Date();
53
+ return new Date(now.getTime() + 1440 * 60 * 1e3);
54
+ };
55
+ const oneMinuteFromNow = () => {
56
+ const now = /* @__PURE__ */ new Date();
57
+ return new Date(now.getTime() + 60 * 1e3);
58
+ };
59
+ function friendlyDuration(minutes, locale, short = false) {
60
+ const numberFormatter = new Intl.NumberFormat(locale);
61
+ if (!minutes) return null;
62
+ if (short) {
63
+ if (minutes > 120) {
64
+ const hours = Math.floor(minutes / 60);
65
+ const remainingMinutes = minutes % 60;
66
+ if (remainingMinutes > 0) return {
67
+ key: "common:duration.hoursAndMinutesShort",
68
+ vars: {
69
+ hours: numberFormatter.format(hours),
70
+ minutes: numberFormatter.format(remainingMinutes)
71
+ }
72
+ };
73
+ return {
74
+ key: "common:duration.hoursShort",
75
+ vars: { hours: numberFormatter.format(hours) }
76
+ };
77
+ }
78
+ return {
79
+ key: "common:duration.minutesShort",
80
+ vars: { minutes: numberFormatter.format(minutes) }
81
+ };
82
+ }
83
+ if (minutes >= 525600) {
84
+ const years = Math.floor(minutes / 525600);
85
+ return {
86
+ key: "common:duration.years",
87
+ vars: {
88
+ count: years,
89
+ value: numberFormatter.format(years)
90
+ }
91
+ };
92
+ }
93
+ if (minutes >= 43200) {
94
+ const months = Math.floor(minutes / 43200);
95
+ return {
96
+ key: "common:duration.months",
97
+ vars: {
98
+ count: months,
99
+ value: numberFormatter.format(months)
100
+ }
101
+ };
102
+ }
103
+ if (minutes >= 10080) {
104
+ const weeks = Math.floor(minutes / 10080);
105
+ return {
106
+ key: "common:duration.weeks",
107
+ vars: {
108
+ count: weeks,
109
+ value: numberFormatter.format(weeks)
110
+ }
111
+ };
112
+ }
113
+ if (minutes >= 1440) {
114
+ const days = Math.floor(minutes / 1440);
115
+ return {
116
+ key: "common:duration.days",
117
+ vars: {
118
+ count: days,
119
+ value: numberFormatter.format(days)
120
+ }
121
+ };
122
+ }
123
+ if (minutes >= 240) {
124
+ const hours = Math.floor(minutes / 60);
125
+ const remainingMinutes = minutes % 60;
126
+ if (remainingMinutes > 0) return {
127
+ key: "common:duration.hoursAndMinutes",
128
+ vars: {
129
+ hours: numberFormatter.format(hours),
130
+ minutes: numberFormatter.format(remainingMinutes)
131
+ }
132
+ };
133
+ return {
134
+ key: "common:duration.hours",
135
+ vars: {
136
+ count: hours,
137
+ value: numberFormatter.format(hours)
138
+ }
139
+ };
140
+ }
141
+ return {
142
+ key: "common:duration.minutes",
143
+ vars: {
144
+ count: minutes,
145
+ value: numberFormatter.format(minutes)
146
+ }
147
+ };
148
+ }
149
+ const dateTimeInUnix = (date) => {
150
+ return Math.floor(date / 1e3);
151
+ };
152
+
153
+ //#endregion
154
+ export { dateTimeInUnix, delay, friendlyDuration, measure, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "@regardio/js",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "private": false,
6
6
  "description": "Regardio JavaScript utilities",
7
7
  "keywords": [
@@ -28,37 +28,37 @@
28
28
  "type": "module",
29
29
  "exports": {
30
30
  "./assert": {
31
- "types": "./dist/assert/index.d.ts",
32
- "import": "./dist/assert/index.js"
31
+ "types": "./dist/assert/index.d.mts",
32
+ "import": "./dist/assert/index.mjs"
33
33
  },
34
34
  "./encoding": {
35
- "types": "./dist/encoding/index.d.ts",
36
- "import": "./dist/encoding/index.js"
35
+ "types": "./dist/encoding/index.d.mts",
36
+ "import": "./dist/encoding/index.mjs"
37
37
  },
38
38
  "./http": {
39
- "types": "./dist/http/index.d.ts",
40
- "import": "./dist/http/index.js"
39
+ "types": "./dist/http/index.d.mts",
40
+ "import": "./dist/http/index.mjs"
41
41
  },
42
42
  "./intl": {
43
- "types": "./dist/intl/index.d.ts",
44
- "import": "./dist/intl/index.js"
43
+ "types": "./dist/intl/index.d.mts",
44
+ "import": "./dist/intl/index.mjs"
45
45
  },
46
46
  "./text": {
47
- "types": "./dist/text/index.d.ts",
48
- "import": "./dist/text/index.js"
47
+ "types": "./dist/text/index.d.mts",
48
+ "import": "./dist/text/index.mjs"
49
49
  },
50
50
  "./time": {
51
- "types": "./dist/time/index.d.ts",
52
- "import": "./dist/time/index.js"
51
+ "types": "./dist/time/index.d.mts",
52
+ "import": "./dist/time/index.mjs"
53
53
  }
54
54
  },
55
55
  "files": [
56
56
  "dist"
57
57
  ],
58
58
  "scripts": {
59
- "build": "tsup && post-build-exports && pnpm fix",
59
+ "build": "tsdown && pnpm fix",
60
60
  "clean": "exec-clean .turbo dist",
61
- "dev": "tsup --watch",
61
+ "dev": "tsdown --watch",
62
62
  "fix": "exec-p fix:*",
63
63
  "fix:biome": "lint-biome check --write --unsafe .",
64
64
  "fix:md": "lint-md --fix",
@@ -80,13 +80,13 @@
80
80
  "react-router": "7.12.0"
81
81
  },
82
82
  "devDependencies": {
83
- "@regardio/dev": "1.11.1",
83
+ "@regardio/dev": "1.12.0",
84
84
  "@total-typescript/ts-reset": "0.6.1",
85
- "@types/node": "25.0.3",
86
- "@vitest/coverage-v8": "4.0.16",
87
- "@vitest/ui": "4.0.16",
88
- "tsup": "8.5.1",
89
- "vitest": "4.0.16"
85
+ "@types/node": "25.0.8",
86
+ "@vitest/coverage-v8": "4.0.17",
87
+ "@vitest/ui": "4.0.17",
88
+ "tsdown": "0.20.0-beta.2",
89
+ "vitest": "4.0.17"
90
90
  },
91
91
  "publishConfig": {
92
92
  "access": "public"
@@ -1,43 +0,0 @@
1
- /**
2
- * Provide a condition and if that condition is falsey, this throws an error
3
- * with the given message.
4
- *
5
- * inspired by invariant from 'tiny-invariant' except will still include the
6
- * message in production.
7
- *
8
- * @example
9
- * invariant(typeof value === 'string', `value must be a string`)
10
- *
11
- * @param condition The condition to check
12
- * @param message The message to throw (or a callback to generate the message)
13
- *
14
- * @throws {Error} if condition is falsey
15
- */
16
- declare function invariant(condition: unknown, message: string | (() => string)): asserts condition;
17
- /**
18
- * Provide a condition and if that condition is falsey, this throws a 400
19
- * Response with the given message.
20
- *
21
- * inspired by invariant from 'tiny-invariant'
22
- *
23
- * @example
24
- * invariantResponse(typeof value === 'string', `value must be a string`)
25
- *
26
- * @param condition The condition to check
27
- * @param message The message to throw (or a callback to generate the message)
28
- * @param responseInit Additional response init options if a response is thrown
29
- *
30
- * @throws {Response} if condition is falsey
31
- */
32
- declare function invariantResponse(condition: unknown, message: string | (() => string), responseInit?: ResponseInit): asserts condition;
33
-
34
- /**
35
- * Check if a mime type matches the set given in accept
36
- *
37
- * @param type the mime type to test, ex image/png
38
- * @param accept the mime types to accept, ex audio/*,video/*,image/png
39
- * @returns true if the mime is accepted, false otherwise
40
- */
41
- declare function verifyAccept(type: string, accept: string): boolean;
42
-
43
- export { invariant, invariantResponse, verifyAccept };
@@ -1,22 +0,0 @@
1
- // src/assert/invariant.ts
2
- function invariant(condition, message) {
3
- if (!condition) {
4
- throw new Error(typeof message === "function" ? message() : message);
5
- }
6
- }
7
- function invariantResponse(condition, message, responseInit) {
8
- if (!condition) {
9
- throw new Response(typeof message === "function" ? message() : message, {
10
- status: 400,
11
- ...responseInit
12
- });
13
- }
14
- }
15
-
16
- // src/assert/verify-file-accept.ts
17
- function verifyAccept(type, accept) {
18
- const allowed = accept.split(",").map((x) => x.trim());
19
- return allowed.includes(type) || allowed.includes(`${type.split("/")[0]}/*`);
20
- }
21
-
22
- export { invariant, invariantResponse, verifyAccept };
@@ -1,13 +0,0 @@
1
- /**
2
- * Converts a base64 string to a Uint8Array
3
- * @param base64String The base64 string to convert
4
- * @returns A Uint8Array containing the decoded data
5
- */
6
- declare const urlBase64ToUint8Array: (base64String: string) => Uint8Array;
7
- /**
8
- * Generate a cryptographically secure random base64 string.
9
- * @returns A base64-encoded random string (16 bytes of entropy)
10
- */
11
- declare function generateRandomBase64(): string;
12
-
13
- export { generateRandomBase64, urlBase64ToUint8Array };
@@ -1,18 +0,0 @@
1
- // src/encoding/base64.ts
2
- var urlBase64ToUint8Array = (base64String) => {
3
- const padding = "=".repeat((4 - base64String.length % 4) % 4);
4
- const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
5
- const rawData = window.atob(base64);
6
- const outputArray = new Uint8Array(rawData.length);
7
- for (let i = 0; i < rawData.length; ++i) {
8
- outputArray[i] = rawData.charCodeAt(i);
9
- }
10
- return outputArray;
11
- };
12
- function generateRandomBase64() {
13
- const array = new Uint8Array(16);
14
- crypto.getRandomValues(array);
15
- return btoa(String.fromCharCode(...array));
16
- }
17
-
18
- export { generateRandomBase64, urlBase64ToUint8Array };
@@ -1,49 +0,0 @@
1
- /**
2
- * Helper function to set cookies in a more controlled way
3
- * @param name - The name of the cookie
4
- * @param value - The value to set
5
- * @param options - Cookie options
6
- */
7
- declare function setCookieValue(name: string, value: string, options?: {
8
- expires?: Date;
9
- path?: string;
10
- sameSite?: string;
11
- secure?: boolean;
12
- domain?: string;
13
- }): void;
14
- /**
15
- * Get a cookie value by name
16
- * @param name - The name of the cookie to get
17
- * @returns The cookie value or null if not found
18
- */
19
- declare function getCookieValue(name: string): string | null;
20
-
21
- /**
22
- * Helper utility used to extract the domain from the request even if it's
23
- * behind a proxy. This is useful for sitemaps and other things.
24
- * @param request Request object
25
- * @returns Current domain
26
- */
27
- declare const createDomain: (request: Request) => string;
28
-
29
- /**
30
- * @name isRouteActive
31
- * @description A function to check if a route is active. This is used to
32
- * @param end
33
- * @param path
34
- * @param currentPath
35
- */
36
- declare function isRouteActive(path: string, currentPath: string, end?: boolean | ((path: string) => boolean) | undefined): boolean;
37
- /**
38
- * @name checkIfRouteIsActive
39
- * @description A function to check if a route is active. This is used to
40
- * highlight the active link in the navigation.
41
- * @param targetLink - The link to check against
42
- * @param currentRoute - the current route
43
- * @param depth - how far down should segments be matched?
44
- */
45
- declare function checkIfRouteIsActive(targetLink: string, currentRoute: string, depth?: number): boolean;
46
-
47
- declare function getCleanUrl(request: Request): string;
48
-
49
- export { checkIfRouteIsActive, createDomain, getCleanUrl, getCookieValue, isRouteActive, setCookieValue };