@regardio/js 0.4.1 → 0.5.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.
- package/README.md +56 -105
- package/dist/{validation/invariant.d.ts → assert/index.d.ts} +10 -1
- package/dist/{validation/invariant.js → assert/index.js} +8 -2
- package/dist/{browser/base64.js → encoding/index.js} +1 -1
- package/dist/http/index.d.ts +49 -0
- package/dist/http/index.js +126 -0
- package/dist/text/index.d.ts +46 -0
- package/dist/text/index.js +109 -0
- package/dist/time/{time.d.ts → index.d.ts} +9 -1
- package/dist/time/{time.js → index.js} +23 -1
- package/package.json +22 -42
- package/dist/async/delay.d.ts +0 -7
- package/dist/async/delay.js +0 -8
- package/dist/format/bytes.d.ts +0 -3
- package/dist/format/bytes.js +0 -13
- package/dist/format/measure.d.ts +0 -3
- package/dist/format/measure.js +0 -16
- package/dist/http/cookie.d.ts +0 -21
- package/dist/http/cookie.js +0 -47
- package/dist/http/domain.d.ts +0 -9
- package/dist/http/domain.js +0 -16
- package/dist/http/request-helpers.d.ts +0 -3
- package/dist/http/request-helpers.js +0 -10
- package/dist/validation/verify-file-accept.d.ts +0 -10
- package/dist/validation/verify-file-accept.js +0 -7
- /package/dist/{browser/base64.d.ts → encoding/index.d.ts} +0 -0
- /package/dist/intl/{language-detector.d.ts → index.d.ts} +0 -0
- /package/dist/intl/{language-detector.js → index.js} +0 -0
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **TypeScript utilities for Regardio applications**
|
|
4
4
|
|
|
5
5
|
A collection of lightweight, tree-shakeable utility functions for common tasks
|
|
6
|
-
like HTTP handling, internationalization, time formatting, and validation.
|
|
6
|
+
like text manipulation, HTTP handling, internationalization, time formatting, and validation.
|
|
7
7
|
|
|
8
8
|
## ⚠️ Pre-release Notice
|
|
9
9
|
|
|
@@ -18,7 +18,7 @@ like HTTP handling, internationalization, time formatting, and validation.
|
|
|
18
18
|
We created `@regardio/js` to:
|
|
19
19
|
|
|
20
20
|
- **Share battle-tested utilities** — These functions power real Regardio projects and have been refined through actual use
|
|
21
|
-
- **Reduce boilerplate** — Common patterns like cookie handling, language detection, and time formatting in one place
|
|
21
|
+
- **Reduce boilerplate** — Common patterns like text formatting, cookie handling, language detection, and time formatting in one place
|
|
22
22
|
- **Stay framework-agnostic** — Works with any JavaScript/TypeScript project (React, Node, Deno, etc.)
|
|
23
23
|
- **Enable tree-shaking** — Import only what you need; unused utilities won't bloat your bundle
|
|
24
24
|
|
|
@@ -28,92 +28,71 @@ We created `@regardio/js` to:
|
|
|
28
28
|
pnpm add @regardio/js
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## Documentation
|
|
32
|
-
|
|
33
|
-
See the [docs](./docs) folder for detailed documentation on each module.
|
|
34
|
-
|
|
35
31
|
## Modules
|
|
36
32
|
|
|
37
|
-
###
|
|
33
|
+
### @regardio/js/text
|
|
38
34
|
|
|
39
|
-
|
|
35
|
+
String manipulation and formatting utilities.
|
|
40
36
|
|
|
41
37
|
```ts
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
import { urlBase64ToUint8Array } from '@regardio/js/browser/base64';
|
|
53
|
-
|
|
54
|
-
const bytes = urlBase64ToUint8Array(vapidPublicKey);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### format/bytes
|
|
58
|
-
|
|
59
|
-
Human-readable byte formatting.
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import { formatBytes } from '@regardio/js/format/bytes';
|
|
63
|
-
|
|
38
|
+
import {
|
|
39
|
+
typographicQuotes,
|
|
40
|
+
truncateText,
|
|
41
|
+
splitIntoSentences,
|
|
42
|
+
formatBytes,
|
|
43
|
+
parseAuthorString,
|
|
44
|
+
} from '@regardio/js/text';
|
|
45
|
+
|
|
46
|
+
typographicQuotes('"Hello"', 'de'); // „Hello"
|
|
47
|
+
truncateText('Hello world', 8); // "Hello..."
|
|
64
48
|
formatBytes(1500000); // "1.5 MB"
|
|
49
|
+
parseAuthorString('John <john@example.com>'); // { name: 'John', email: 'john@example.com' }
|
|
65
50
|
```
|
|
66
51
|
|
|
67
|
-
###
|
|
52
|
+
### @regardio/js/time
|
|
68
53
|
|
|
69
|
-
|
|
54
|
+
Date, time, and async utilities.
|
|
70
55
|
|
|
71
56
|
```ts
|
|
72
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
timeAgo,
|
|
59
|
+
friendlyDuration,
|
|
60
|
+
oneWeekFromNow,
|
|
61
|
+
delay,
|
|
62
|
+
measure,
|
|
63
|
+
} from '@regardio/js/time';
|
|
73
64
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Logs
|
|
65
|
+
timeAgo(new Date('2024-01-01')); // "3 months ago"
|
|
66
|
+
friendlyDuration(150, 'en', true); // { key: 'common:duration.hoursAndMinutesShort', vars: {...} }
|
|
67
|
+
await delay(1000); // Wait 1 second
|
|
68
|
+
await measure('fetchData', () => fetch('/api')); // Logs timing
|
|
78
69
|
```
|
|
79
70
|
|
|
80
|
-
### http
|
|
71
|
+
### @regardio/js/http
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
HTTP, cookie, and routing utilities.
|
|
83
74
|
|
|
84
75
|
```ts
|
|
85
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
getCookieValue,
|
|
78
|
+
setCookieValue,
|
|
79
|
+
createDomain,
|
|
80
|
+
getCleanUrl,
|
|
81
|
+
isRouteActive,
|
|
82
|
+
} from '@regardio/js/http';
|
|
86
83
|
|
|
87
84
|
setCookieValue('theme', 'dark', { path: '/', secure: true });
|
|
88
85
|
const theme = getCookieValue('theme');
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### http/domain
|
|
92
|
-
|
|
93
|
-
Domain extraction from requests (proxy-aware).
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
import { createDomain } from '@regardio/js/http/domain';
|
|
97
|
-
|
|
98
86
|
const domain = createDomain(request); // "https://example.com"
|
|
87
|
+
isRouteActive('/account', '/account/settings', false); // true
|
|
99
88
|
```
|
|
100
89
|
|
|
101
|
-
###
|
|
90
|
+
### @regardio/js/intl
|
|
102
91
|
|
|
103
|
-
|
|
92
|
+
Server-side language detection for i18n.
|
|
104
93
|
|
|
105
94
|
```ts
|
|
106
|
-
import {
|
|
107
|
-
|
|
108
|
-
getCleanUrl(request); // URL without search params
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### intl/language-detector
|
|
112
|
-
|
|
113
|
-
Server-side language detection for i18n (works with Lingui).
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { LanguageDetector } from '@regardio/js/intl/language-detector';
|
|
95
|
+
import { LanguageDetector } from '@regardio/js/intl';
|
|
117
96
|
|
|
118
97
|
const detector = new LanguageDetector({
|
|
119
98
|
supportedLanguages: ['en', 'de'],
|
|
@@ -123,66 +102,38 @@ const detector = new LanguageDetector({
|
|
|
123
102
|
const locale = await detector.detect(request);
|
|
124
103
|
```
|
|
125
104
|
|
|
126
|
-
###
|
|
105
|
+
### @regardio/js/assert
|
|
127
106
|
|
|
128
|
-
|
|
107
|
+
Runtime assertion and validation utilities.
|
|
129
108
|
|
|
130
109
|
```ts
|
|
131
|
-
import {
|
|
132
|
-
|
|
133
|
-
const locales = getClientLocales(request); // ['en-US', 'de']
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### time/time
|
|
137
|
-
|
|
138
|
-
Time formatting utilities.
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
import { timeAgo, friendlyDuration, oneWeekFromNow } from '@regardio/js/time/time';
|
|
142
|
-
|
|
143
|
-
timeAgo(new Date('2024-01-01')); // "3 months ago"
|
|
144
|
-
friendlyDuration(150, 'en', true); // { key: 'common:duration.hoursAndMinutesShort', vars: {...} }
|
|
145
|
-
const expires = oneWeekFromNow();
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### validation/invariant
|
|
149
|
-
|
|
150
|
-
Runtime assertion utilities.
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
import { invariant, invariantResponse } from '@regardio/js/validation/invariant';
|
|
110
|
+
import { invariant, invariantResponse, verifyAccept } from '@regardio/js/assert';
|
|
154
111
|
|
|
155
112
|
invariant(user, 'User not found'); // Throws Error if falsy
|
|
156
|
-
invariantResponse(user, 'User not found', { status: 404 }); // Throws Response
|
|
113
|
+
invariantResponse(user, 'User not found', { status: 404 }); // Throws Response
|
|
114
|
+
verifyAccept('image/png', 'image/*'); // true
|
|
157
115
|
```
|
|
158
116
|
|
|
159
|
-
###
|
|
117
|
+
### @regardio/js/encoding
|
|
160
118
|
|
|
161
|
-
|
|
119
|
+
Binary encoding utilities.
|
|
162
120
|
|
|
163
121
|
```ts
|
|
164
|
-
import {
|
|
122
|
+
import { urlBase64ToUint8Array } from '@regardio/js/encoding';
|
|
165
123
|
|
|
166
|
-
|
|
167
|
-
verifyAccept('video/mp4', 'image/*'); // false
|
|
124
|
+
const bytes = urlBase64ToUint8Array(vapidPublicKey);
|
|
168
125
|
```
|
|
169
126
|
|
|
170
127
|
## Module Overview
|
|
171
128
|
|
|
172
129
|
| Module | Description |
|
|
173
130
|
|--------|-------------|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
| `http/request-helpers` | URL cleaning utilities |
|
|
181
|
-
| `intl/language-detector` | Server-side language detection for i18n |
|
|
182
|
-
| `intl/locale` | Client locale extraction from headers |
|
|
183
|
-
| `time/time` | Time formatting and date utilities |
|
|
184
|
-
| `validation/invariant` | Runtime assertion utilities |
|
|
185
|
-
| `validation/verify-file-accept` | MIME type validation for file uploads |
|
|
131
|
+
| `@regardio/js/text` | String manipulation, formatting, author parsing |
|
|
132
|
+
| `@regardio/js/time` | Time formatting, delays, performance measurement |
|
|
133
|
+
| `@regardio/js/http` | Cookies, domain extraction, route matching |
|
|
134
|
+
| `@regardio/js/intl` | Server-side language detection for i18n |
|
|
135
|
+
| `@regardio/js/assert` | Runtime assertions and MIME validation |
|
|
136
|
+
| `@regardio/js/encoding` | Base64 and binary conversions |
|
|
186
137
|
|
|
187
138
|
## Contributing
|
|
188
139
|
|
|
@@ -31,4 +31,13 @@ declare function invariant(condition: unknown, message: string | (() => string))
|
|
|
31
31
|
*/
|
|
32
32
|
declare function invariantResponse(condition: unknown, message: string | (() => string), responseInit?: ResponseInit): asserts condition;
|
|
33
33
|
|
|
34
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/assert/invariant.ts
|
|
2
2
|
function invariant(condition, message) {
|
|
3
3
|
if (!condition) {
|
|
4
4
|
throw new Error(typeof message === "function" ? message() : message);
|
|
@@ -13,4 +13,10 @@ function invariantResponse(condition, message, responseInit) {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
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 };
|
|
@@ -0,0 +1,49 @@
|
|
|
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 };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/http/cookie.ts
|
|
2
|
+
function setCookieValue(name, value, options = {}) {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
console.warn("Cannot set cookie on server side");
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
8
|
+
if (options.expires) {
|
|
9
|
+
cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
10
|
+
}
|
|
11
|
+
if (options.path) {
|
|
12
|
+
cookieString += `; path=${options.path}`;
|
|
13
|
+
}
|
|
14
|
+
if (options.sameSite) {
|
|
15
|
+
cookieString += `; SameSite=${options.sameSite}`;
|
|
16
|
+
}
|
|
17
|
+
if (options.secure) {
|
|
18
|
+
cookieString += "; Secure";
|
|
19
|
+
}
|
|
20
|
+
if (options.domain) {
|
|
21
|
+
cookieString += `; domain=${options.domain}`;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
document.cookie = cookieString;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
console.error(`Failed to set cookie '${name}':`, error.message);
|
|
28
|
+
} else {
|
|
29
|
+
console.error(`Failed to set cookie '${name}': Unknown error`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getCookieValue(name) {
|
|
34
|
+
if (typeof window === "undefined") {
|
|
35
|
+
console.warn("Cannot get cookie on server side");
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const value = `; ${document.cookie}`;
|
|
39
|
+
const parts = value.split(`; ${name}=`);
|
|
40
|
+
if (parts.length === 2) {
|
|
41
|
+
const cookieValue = parts.pop()?.split(";").shift();
|
|
42
|
+
return cookieValue ? decodeURIComponent(cookieValue) : null;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/http/domain.ts
|
|
48
|
+
var createDomain = (request) => {
|
|
49
|
+
const headers = request.headers;
|
|
50
|
+
const maybeProto = headers.get("x-forwarded-proto");
|
|
51
|
+
const maybeHost = headers.get("host");
|
|
52
|
+
const url = new URL(request.url);
|
|
53
|
+
if (maybeProto) {
|
|
54
|
+
return `${maybeProto}://${maybeHost ?? url.host}`;
|
|
55
|
+
}
|
|
56
|
+
if (url.hostname === "localhost") {
|
|
57
|
+
return `http://${url.host}`;
|
|
58
|
+
}
|
|
59
|
+
return `https://${url.host}`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/http/is-route-active.ts
|
|
63
|
+
var ROOT_PATH = "/";
|
|
64
|
+
function isRouteActive(path, currentPath, end) {
|
|
65
|
+
if (path === currentPath) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (typeof end === "function") {
|
|
69
|
+
return !end(currentPath);
|
|
70
|
+
}
|
|
71
|
+
const defaultEnd = end ?? true;
|
|
72
|
+
const oneLevelDeep = 1;
|
|
73
|
+
const threeLevelsDeep = 3;
|
|
74
|
+
const depth = defaultEnd ? oneLevelDeep : threeLevelsDeep;
|
|
75
|
+
return checkIfRouteIsActive(path, currentPath, depth);
|
|
76
|
+
}
|
|
77
|
+
function checkIfRouteIsActive(targetLink, currentRoute, depth = 1) {
|
|
78
|
+
const currentRoutePath = currentRoute.split("?")[0] ?? "";
|
|
79
|
+
if (!isRoot(currentRoutePath) && isRoot(targetLink)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!currentRoutePath.includes(targetLink)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const isSameRoute = targetLink === currentRoutePath;
|
|
86
|
+
if (isSameRoute) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return hasMatchingSegments(targetLink, currentRoutePath, depth);
|
|
90
|
+
}
|
|
91
|
+
function splitIntoSegments(href) {
|
|
92
|
+
return href.split("/").filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
function hasMatchingSegments(targetLink, currentRoute, depth) {
|
|
95
|
+
const segments = splitIntoSegments(targetLink);
|
|
96
|
+
const matchingSegments = numberOfMatchingSegments(currentRoute, segments);
|
|
97
|
+
if (targetLink === currentRoute) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return matchingSegments > segments.length - (depth - 1);
|
|
101
|
+
}
|
|
102
|
+
function numberOfMatchingSegments(href, segments) {
|
|
103
|
+
let count = 0;
|
|
104
|
+
for (const segment of splitIntoSegments(href)) {
|
|
105
|
+
if (segments.includes(segment)) {
|
|
106
|
+
count += 1;
|
|
107
|
+
} else {
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
function isRoot(path) {
|
|
114
|
+
return path === ROOT_PATH;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/http/request-helpers.ts
|
|
118
|
+
function getCleanUrl(request) {
|
|
119
|
+
const url = new URL(request.url);
|
|
120
|
+
url.searchParams.forEach((_, key) => {
|
|
121
|
+
url.searchParams.delete(key);
|
|
122
|
+
});
|
|
123
|
+
return url.toString();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { checkIfRouteIsActive, createDomain, getCleanUrl, getCookieValue, isRouteActive, setCookieValue };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
declare function formatBytes(bytes: number, decimals?: number): string;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replace straight quotes with typographically correct quotes for the given locale.
|
|
5
|
+
*
|
|
6
|
+
* @param text - The text containing straight quotes
|
|
7
|
+
* @param locale - The locale to use for quote style (e.g., 'en', 'de', 'fr')
|
|
8
|
+
* @returns Text with typographic quotes
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* typographicQuotes('"Hello"', 'en') // → '"Hello"'
|
|
12
|
+
* typographicQuotes('"Hello"', 'de') // → '„Hello"'
|
|
13
|
+
* typographicQuotes('"Hello"', 'fr') // → '« Hello »'
|
|
14
|
+
*/
|
|
15
|
+
declare function typographicQuotes(text: string, locale: string): string;
|
|
16
|
+
declare function toBoolean(value: string | boolean | null | undefined): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Replace ­ HTML entity with Unicode soft hyphen
|
|
19
|
+
*/
|
|
20
|
+
declare function replaceShyInString(input: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Split text into sentences
|
|
23
|
+
*/
|
|
24
|
+
declare function splitIntoSentences(text: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Split text into words
|
|
27
|
+
*/
|
|
28
|
+
declare function splitIntoWords(text: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Truncate text to a maximum length
|
|
31
|
+
*/
|
|
32
|
+
declare function truncateText(text: string, maxLength: number, suffix?: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Author info parsed from a string like "Name <email> (url)"
|
|
35
|
+
*/
|
|
36
|
+
type AuthorInfo = {
|
|
37
|
+
name?: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
url?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Parse an author string in the format "Name <email> (url)"
|
|
43
|
+
*/
|
|
44
|
+
declare function parseAuthorString(input: string): AuthorInfo;
|
|
45
|
+
|
|
46
|
+
export { type AuthorInfo, formatBytes, parseAuthorString, replaceShyInString, splitIntoSentences, splitIntoWords, toBoolean, truncateText, typographicQuotes };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/text/bytes.ts
|
|
2
|
+
function formatBytes(bytes, decimals = 2) {
|
|
3
|
+
if (!+bytes) {
|
|
4
|
+
return "0 Bytes";
|
|
5
|
+
}
|
|
6
|
+
const k = 1e3;
|
|
7
|
+
const dm = decimals < 0 ? 0 : decimals;
|
|
8
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
9
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
10
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/text/text.ts
|
|
14
|
+
var quoteStyles = {
|
|
15
|
+
cs: { close: "\u201D", closeSingle: "\u2019", open: "\u201E", openSingle: "\u201A" },
|
|
16
|
+
// Danish, Norwegian - » « › ‹
|
|
17
|
+
da: { close: "\xAB", closeSingle: "\u203A", open: "\xBB", openSingle: "\u2039" },
|
|
18
|
+
// German (Germany, Austria) - „ " ‚ '
|
|
19
|
+
de: { close: "\u201D", closeSingle: "\u2019", open: "\u201E", openSingle: "\u201A" },
|
|
20
|
+
// German (Switzerland) - « » ‹ ›
|
|
21
|
+
"de-ch": { close: "\xBB", closeSingle: "\u203A", open: "\xAB", openSingle: "\u2039" },
|
|
22
|
+
// English (US, UK, etc.) - " " ' '
|
|
23
|
+
en: { close: "\u201D", closeSingle: "\u2019", open: "\u201C", openSingle: "\u2018" },
|
|
24
|
+
// Spanish, Italian, Portuguese - « » " "
|
|
25
|
+
es: { close: "\xBB", closeSingle: "\u201D", open: "\xAB", openSingle: "\u201C" },
|
|
26
|
+
fi: { close: "\u201D", closeSingle: "\u2019", open: "\u201D", openSingle: "\u2019" },
|
|
27
|
+
// French - « » ‹ › (with spaces)
|
|
28
|
+
fr: { close: " \xBB", closeSingle: " \u203A", open: "\xAB ", openSingle: "\u2039 " },
|
|
29
|
+
hu: { close: "\u201D", closeSingle: "\u2019", open: "\u201E", openSingle: "\u201A" },
|
|
30
|
+
it: { close: "\xBB", closeSingle: "\u201D", open: "\xAB", openSingle: "\u201C" },
|
|
31
|
+
// Japanese - 「 」 『 』
|
|
32
|
+
ja: { close: "\u300D", closeSingle: "\u300F", open: "\u300C", openSingle: "\u300E" },
|
|
33
|
+
// Dutch - ' ' ' '
|
|
34
|
+
nl: { close: "\u2019", closeSingle: "\u2019", open: "\u2018", openSingle: "\u2018" },
|
|
35
|
+
no: { close: "\xAB", closeSingle: "\u203A", open: "\xBB", openSingle: "\u2039" },
|
|
36
|
+
// Polish, Czech, Hungarian - „ " ‚ '
|
|
37
|
+
pl: { close: "\u201D", closeSingle: "\u2019", open: "\u201E", openSingle: "\u201A" },
|
|
38
|
+
pt: { close: "\xBB", closeSingle: "\u201D", open: "\xAB", openSingle: "\u201C" },
|
|
39
|
+
// Russian - « » ‚ '
|
|
40
|
+
ru: { close: "\xBB", closeSingle: "\u2019", open: "\xAB", openSingle: "\u201A" },
|
|
41
|
+
// Swedish, Finnish - " " ' '
|
|
42
|
+
sv: { close: "\u201D", closeSingle: "\u2019", open: "\u201D", openSingle: "\u2019" },
|
|
43
|
+
// Chinese - 「 」 『 』
|
|
44
|
+
zh: { close: "\u300D", closeSingle: "\u300F", open: "\u300C", openSingle: "\u300E" }
|
|
45
|
+
};
|
|
46
|
+
function getQuoteStyle(locale) {
|
|
47
|
+
const normalized = locale.toLowerCase();
|
|
48
|
+
const exactMatch = quoteStyles[normalized];
|
|
49
|
+
if (exactMatch) {
|
|
50
|
+
return exactMatch;
|
|
51
|
+
}
|
|
52
|
+
const baseLanguage = normalized.split("-")[0];
|
|
53
|
+
if (baseLanguage) {
|
|
54
|
+
const baseMatch = quoteStyles[baseLanguage];
|
|
55
|
+
if (baseMatch) {
|
|
56
|
+
return baseMatch;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return quoteStyles.en;
|
|
60
|
+
}
|
|
61
|
+
function typographicQuotes(text, locale) {
|
|
62
|
+
const style = getQuoteStyle(locale);
|
|
63
|
+
let result = text.replace(/"([^"]*)"/g, `${style.open}$1${style.close}`);
|
|
64
|
+
result = result.replace(/'([^']*)'/g, `${style.openSingle}$1${style.closeSingle}`);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function toBoolean(value) {
|
|
68
|
+
if (typeof value === "boolean") {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
return value === "true" || value === "1";
|
|
72
|
+
}
|
|
73
|
+
function replaceShyInString(input) {
|
|
74
|
+
return input.replace(/­/g, "\xAD");
|
|
75
|
+
}
|
|
76
|
+
function splitIntoSentences(text) {
|
|
77
|
+
return text.split(/(?<=[.!?])\s+/);
|
|
78
|
+
}
|
|
79
|
+
function splitIntoWords(text) {
|
|
80
|
+
return text.split(/\s+/);
|
|
81
|
+
}
|
|
82
|
+
function truncateText(text, maxLength, suffix = "...") {
|
|
83
|
+
if (text.length <= maxLength) {
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
87
|
+
}
|
|
88
|
+
var authorRegex = /^(.*?)\s*(?:<([^>]+)>)?\s*(?:\(([^)]+)\))?$/;
|
|
89
|
+
function parseAuthorString(input) {
|
|
90
|
+
const match = input.match(authorRegex);
|
|
91
|
+
if (match) {
|
|
92
|
+
const [, name, email, url] = match;
|
|
93
|
+
const result = {};
|
|
94
|
+
if (email) {
|
|
95
|
+
result.email = email;
|
|
96
|
+
}
|
|
97
|
+
const trimmedName = name?.trim();
|
|
98
|
+
if (trimmedName) {
|
|
99
|
+
result.name = trimmedName;
|
|
100
|
+
}
|
|
101
|
+
if (url) {
|
|
102
|
+
result.url = url;
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { formatBytes, parseAuthorString, replaceShyInString, splitIntoSentences, splitIntoWords, toBoolean, truncateText, typographicQuotes };
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Delays using a promise
|
|
2
|
+
* @param ms Milisecods of delay
|
|
3
|
+
* @return Promise
|
|
4
|
+
*/
|
|
5
|
+
declare function delay(ms: number): Promise<unknown>;
|
|
6
|
+
|
|
7
|
+
declare function measure<Result>(key: string, callback: () => Result | Promise<Result>): Promise<Result>;
|
|
8
|
+
|
|
1
9
|
declare function timeAgo(input: Date | string): string;
|
|
2
10
|
declare const oneWeekFromNow: () => Date;
|
|
3
11
|
declare const oneDayFromNow: () => Date;
|
|
@@ -37,4 +45,4 @@ declare function friendlyDuration(minutes: number | null, locale: string, short?
|
|
|
37
45
|
} | null;
|
|
38
46
|
declare const dateTimeInUnix: (date: number) => number;
|
|
39
47
|
|
|
40
|
-
export { dateTimeInUnix, friendlyDuration, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
|
|
48
|
+
export { dateTimeInUnix, delay, friendlyDuration, measure, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
// src/time/delay.ts
|
|
2
|
+
function delay(ms) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
return setTimeout(resolve, ms);
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/time/measure.ts
|
|
9
|
+
async function measure(key, callback) {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
try {
|
|
12
|
+
let result = callback();
|
|
13
|
+
if (result instanceof Promise) {
|
|
14
|
+
result = await result;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
} finally {
|
|
18
|
+
const end = Date.now();
|
|
19
|
+
console.log(`${key} took ${end - start}ms`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
// src/time/time.ts
|
|
2
24
|
function timeAgo(input) {
|
|
3
25
|
const date = new Date(input);
|
|
@@ -132,4 +154,4 @@ var dateTimeInUnix = (date) => {
|
|
|
132
154
|
return Math.floor(date / 1e3);
|
|
133
155
|
};
|
|
134
156
|
|
|
135
|
-
export { dateTimeInUnix, friendlyDuration, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
|
|
157
|
+
export { dateTimeInUnix, delay, friendlyDuration, measure, oneDayFromNow, oneMinuteFromNow, oneWeekFromNow, timeAgo };
|
package/package.json
CHANGED
|
@@ -10,56 +10,36 @@
|
|
|
10
10
|
},
|
|
11
11
|
"description": "Regardio JavaScript utilities",
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"@regardio/dev": "1.
|
|
13
|
+
"@regardio/dev": "1.10.2",
|
|
14
14
|
"@total-typescript/ts-reset": "0.6.1",
|
|
15
15
|
"@types/node": "25.0.3",
|
|
16
16
|
"tsup": "8.5.1",
|
|
17
17
|
"vitest": "4.0.16"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
|
-
"./
|
|
21
|
-
"import": "./dist/
|
|
22
|
-
"types": "./dist/
|
|
20
|
+
"./assert": {
|
|
21
|
+
"import": "./dist/assert/index.js",
|
|
22
|
+
"types": "./dist/assert/index.d.ts"
|
|
23
23
|
},
|
|
24
|
-
"./
|
|
25
|
-
"import": "./dist/
|
|
26
|
-
"types": "./dist/
|
|
24
|
+
"./encoding": {
|
|
25
|
+
"import": "./dist/encoding/index.js",
|
|
26
|
+
"types": "./dist/encoding/index.d.ts"
|
|
27
27
|
},
|
|
28
|
-
"./
|
|
29
|
-
"import": "./dist/
|
|
30
|
-
"types": "./dist/
|
|
28
|
+
"./http": {
|
|
29
|
+
"import": "./dist/http/index.js",
|
|
30
|
+
"types": "./dist/http/index.d.ts"
|
|
31
31
|
},
|
|
32
|
-
"./
|
|
33
|
-
"import": "./dist/
|
|
34
|
-
"types": "./dist/
|
|
32
|
+
"./intl": {
|
|
33
|
+
"import": "./dist/intl/index.js",
|
|
34
|
+
"types": "./dist/intl/index.d.ts"
|
|
35
35
|
},
|
|
36
|
-
"./
|
|
37
|
-
"import": "./dist/
|
|
38
|
-
"types": "./dist/
|
|
36
|
+
"./text": {
|
|
37
|
+
"import": "./dist/text/index.js",
|
|
38
|
+
"types": "./dist/text/index.d.ts"
|
|
39
39
|
},
|
|
40
|
-
"./
|
|
41
|
-
"import": "./dist/
|
|
42
|
-
"types": "./dist/
|
|
43
|
-
},
|
|
44
|
-
"./http/request-helpers": {
|
|
45
|
-
"import": "./dist/http/request-helpers.js",
|
|
46
|
-
"types": "./dist/http/request-helpers.d.ts"
|
|
47
|
-
},
|
|
48
|
-
"./intl/language-detector": {
|
|
49
|
-
"import": "./dist/intl/language-detector.js",
|
|
50
|
-
"types": "./dist/intl/language-detector.d.ts"
|
|
51
|
-
},
|
|
52
|
-
"./time/time": {
|
|
53
|
-
"import": "./dist/time/time.js",
|
|
54
|
-
"types": "./dist/time/time.d.ts"
|
|
55
|
-
},
|
|
56
|
-
"./validation/invariant": {
|
|
57
|
-
"import": "./dist/validation/invariant.js",
|
|
58
|
-
"types": "./dist/validation/invariant.d.ts"
|
|
59
|
-
},
|
|
60
|
-
"./validation/verify-file-accept": {
|
|
61
|
-
"import": "./dist/validation/verify-file-accept.js",
|
|
62
|
-
"types": "./dist/validation/verify-file-accept.d.ts"
|
|
40
|
+
"./time": {
|
|
41
|
+
"import": "./dist/time/index.js",
|
|
42
|
+
"types": "./dist/time/index.d.ts"
|
|
63
43
|
}
|
|
64
44
|
},
|
|
65
45
|
"files": ["dist"],
|
|
@@ -82,10 +62,10 @@
|
|
|
82
62
|
},
|
|
83
63
|
"repository": {
|
|
84
64
|
"type": "git",
|
|
85
|
-
"url": "https://github.com/regardio/js"
|
|
65
|
+
"url": "git+https://github.com/regardio/js.git"
|
|
86
66
|
},
|
|
87
67
|
"scripts": {
|
|
88
|
-
"build": "tsup",
|
|
68
|
+
"build": "tsup && post-build-exports && pnpm fix",
|
|
89
69
|
"clean": "exec-clean .turbo dist",
|
|
90
70
|
"dev": "tsup --watch",
|
|
91
71
|
"fix": "exec-p fix:*",
|
|
@@ -105,5 +85,5 @@
|
|
|
105
85
|
},
|
|
106
86
|
"sideEffects": false,
|
|
107
87
|
"type": "module",
|
|
108
|
-
"version": "0.
|
|
88
|
+
"version": "0.5.0"
|
|
109
89
|
}
|
package/dist/async/delay.d.ts
DELETED
package/dist/async/delay.js
DELETED
package/dist/format/bytes.d.ts
DELETED
package/dist/format/bytes.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// src/format/bytes.ts
|
|
2
|
-
function formatBytes(bytes, decimals = 2) {
|
|
3
|
-
if (!+bytes) {
|
|
4
|
-
return "0 Bytes";
|
|
5
|
-
}
|
|
6
|
-
const k = 1e3;
|
|
7
|
-
const dm = decimals < 0 ? 0 : decimals;
|
|
8
|
-
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
9
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
10
|
-
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export { formatBytes };
|
package/dist/format/measure.d.ts
DELETED
package/dist/format/measure.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// src/format/measure.ts
|
|
2
|
-
async function measure(key, callback) {
|
|
3
|
-
const start = Date.now();
|
|
4
|
-
try {
|
|
5
|
-
let result = callback();
|
|
6
|
-
if (result instanceof Promise) {
|
|
7
|
-
result = await result;
|
|
8
|
-
}
|
|
9
|
-
return result;
|
|
10
|
-
} finally {
|
|
11
|
-
const end = Date.now();
|
|
12
|
-
console.log(`${key} took ${end - start}ms`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export { measure };
|
package/dist/http/cookie.d.ts
DELETED
|
@@ -1,21 +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
|
-
export { getCookieValue, setCookieValue };
|
package/dist/http/cookie.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// src/http/cookie.ts
|
|
2
|
-
function setCookieValue(name, value, options = {}) {
|
|
3
|
-
if (typeof window === "undefined") {
|
|
4
|
-
console.warn("Cannot set cookie on server side");
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
8
|
-
if (options.expires) {
|
|
9
|
-
cookieString += `; expires=${options.expires.toUTCString()}`;
|
|
10
|
-
}
|
|
11
|
-
if (options.path) {
|
|
12
|
-
cookieString += `; path=${options.path}`;
|
|
13
|
-
}
|
|
14
|
-
if (options.sameSite) {
|
|
15
|
-
cookieString += `; SameSite=${options.sameSite}`;
|
|
16
|
-
}
|
|
17
|
-
if (options.secure) {
|
|
18
|
-
cookieString += "; Secure";
|
|
19
|
-
}
|
|
20
|
-
if (options.domain) {
|
|
21
|
-
cookieString += `; domain=${options.domain}`;
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
document.cookie = cookieString;
|
|
25
|
-
} catch (error) {
|
|
26
|
-
if (error instanceof Error) {
|
|
27
|
-
console.error(`Failed to set cookie '${name}':`, error.message);
|
|
28
|
-
} else {
|
|
29
|
-
console.error(`Failed to set cookie '${name}': Unknown error`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function getCookieValue(name) {
|
|
34
|
-
if (typeof window === "undefined") {
|
|
35
|
-
console.warn("Cannot get cookie on server side");
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
const value = `; ${document.cookie}`;
|
|
39
|
-
const parts = value.split(`; ${name}=`);
|
|
40
|
-
if (parts.length === 2) {
|
|
41
|
-
const cookieValue = parts.pop()?.split(";").shift();
|
|
42
|
-
return cookieValue ? decodeURIComponent(cookieValue) : null;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export { getCookieValue, setCookieValue };
|
package/dist/http/domain.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper utility used to extract the domain from the request even if it's
|
|
3
|
-
* behind a proxy. This is useful for sitemaps and other things.
|
|
4
|
-
* @param request Request object
|
|
5
|
-
* @returns Current domain
|
|
6
|
-
*/
|
|
7
|
-
declare const createDomain: (request: Request) => string;
|
|
8
|
-
|
|
9
|
-
export { createDomain };
|
package/dist/http/domain.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
// src/http/domain.ts
|
|
2
|
-
var createDomain = (request) => {
|
|
3
|
-
const headers = request.headers;
|
|
4
|
-
const maybeProto = headers.get("x-forwarded-proto");
|
|
5
|
-
const maybeHost = headers.get("host");
|
|
6
|
-
const url = new URL(request.url);
|
|
7
|
-
if (maybeProto) {
|
|
8
|
-
return `${maybeProto}://${maybeHost ?? url.host}`;
|
|
9
|
-
}
|
|
10
|
-
if (url.hostname === "localhost") {
|
|
11
|
-
return `http://${url.host}`;
|
|
12
|
-
}
|
|
13
|
-
return `https://${url.host}`;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export { createDomain };
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Check if a mime type matches the set given in accept
|
|
3
|
-
*
|
|
4
|
-
* @param type the mime type to test, ex image/png
|
|
5
|
-
* @param accept the mime types to accept, ex audio/*,video/*,image/png
|
|
6
|
-
* @returns true if the mime is accepted, false otherwise
|
|
7
|
-
*/
|
|
8
|
-
declare function verifyAccept(type: string, accept: string): boolean;
|
|
9
|
-
|
|
10
|
-
export { verifyAccept };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|