@regardio/js 0.5.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  # The MIT License (MIT)
2
2
 
3
- Copyright © 2025 Regardio
3
+ Copyright © 2026 Regardio
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
6
 
@@ -0,0 +1,45 @@
1
+ //#region src/assert/invariant.d.ts
2
+ /**
3
+ * Provide a condition and if that condition is falsey, this throws an error
4
+ * with the given message.
5
+ *
6
+ * inspired by invariant from 'tiny-invariant' except will still include the
7
+ * message in production.
8
+ *
9
+ * @example
10
+ * invariant(typeof value === 'string', `value must be a string`)
11
+ *
12
+ * @param condition The condition to check
13
+ * @param message The message to throw (or a callback to generate the message)
14
+ *
15
+ * @throws {Error} if condition is falsey
16
+ */
17
+ declare function invariant(condition: unknown, message: string | (() => string)): asserts condition;
18
+ /**
19
+ * Provide a condition and if that condition is falsey, this throws a 400
20
+ * Response with the given message.
21
+ *
22
+ * inspired by invariant from 'tiny-invariant'
23
+ *
24
+ * @example
25
+ * invariantResponse(typeof value === 'string', `value must be a string`)
26
+ *
27
+ * @param condition The condition to check
28
+ * @param message The message to throw (or a callback to generate the message)
29
+ * @param responseInit Additional response init options if a response is thrown
30
+ *
31
+ * @throws {Response} if condition is falsey
32
+ */
33
+ declare function invariantResponse(condition: unknown, message: string | (() => string), responseInit?: ResponseInit): asserts condition;
34
+ //#endregion
35
+ //#region src/assert/verify-file-accept.d.ts
36
+ /**
37
+ * Check if a mime type matches the set given in accept
38
+ *
39
+ * @param type the mime type to test, ex image/png
40
+ * @param accept the mime types to accept, ex audio/*,video/*,image/png
41
+ * @returns true if the mime is accepted, false otherwise
42
+ */
43
+ declare function verifyAccept(type: string, accept: string): boolean;
44
+ //#endregion
45
+ export { invariant, invariantResponse, verifyAccept };
@@ -0,0 +1,57 @@
1
+ //#region src/assert/invariant.ts
2
+ /**
3
+ * Provide a condition and if that condition is falsey, this throws an error
4
+ * with the given message.
5
+ *
6
+ * inspired by invariant from 'tiny-invariant' except will still include the
7
+ * message in production.
8
+ *
9
+ * @example
10
+ * invariant(typeof value === 'string', `value must be a string`)
11
+ *
12
+ * @param condition The condition to check
13
+ * @param message The message to throw (or a callback to generate the message)
14
+ *
15
+ * @throws {Error} if condition is falsey
16
+ */
17
+ function invariant(condition, message) {
18
+ if (!condition) throw new Error(typeof message === "function" ? message() : message);
19
+ }
20
+ /**
21
+ * Provide a condition and if that condition is falsey, this throws a 400
22
+ * Response with the given message.
23
+ *
24
+ * inspired by invariant from 'tiny-invariant'
25
+ *
26
+ * @example
27
+ * invariantResponse(typeof value === 'string', `value must be a string`)
28
+ *
29
+ * @param condition The condition to check
30
+ * @param message The message to throw (or a callback to generate the message)
31
+ * @param responseInit Additional response init options if a response is thrown
32
+ *
33
+ * @throws {Response} if condition is falsey
34
+ */
35
+ function invariantResponse(condition, message, responseInit) {
36
+ if (!condition) throw new Response(typeof message === "function" ? message() : message, {
37
+ status: 400,
38
+ ...responseInit
39
+ });
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/assert/verify-file-accept.ts
44
+ /**
45
+ * Check if a mime type matches the set given in accept
46
+ *
47
+ * @param type the mime type to test, ex image/png
48
+ * @param accept the mime types to accept, ex audio/*,video/*,image/png
49
+ * @returns true if the mime is accepted, false otherwise
50
+ */
51
+ function verifyAccept(type, accept) {
52
+ const allowed = accept.split(",").map((x) => x.trim());
53
+ return allowed.includes(type) || allowed.includes(`${type.split("/")[0]}/*`);
54
+ }
55
+
56
+ //#endregion
57
+ export { invariant, invariantResponse, verifyAccept };
@@ -0,0 +1,14 @@
1
+ //#region src/encoding/base64.d.ts
2
+ /**
3
+ * Converts a base64 string to a Uint8Array
4
+ * @param base64String The base64 string to convert
5
+ * @returns A Uint8Array containing the decoded data
6
+ */
7
+ declare const urlBase64ToUint8Array: (base64String: string) => Uint8Array;
8
+ /**
9
+ * Generate a cryptographically secure random base64 string.
10
+ * @returns A base64-encoded random string (16 bytes of entropy)
11
+ */
12
+ declare function generateRandomBase64(): string;
13
+ //#endregion
14
+ export { generateRandomBase64, urlBase64ToUint8Array };
@@ -0,0 +1,25 @@
1
+ //#region src/encoding/base64.ts
2
+ /**
3
+ * Converts a base64 string to a Uint8Array
4
+ * @param base64String The base64 string to convert
5
+ * @returns A Uint8Array containing the decoded data
6
+ */
7
+ const urlBase64ToUint8Array = (base64String) => {
8
+ const base64 = (base64String + "=".repeat((4 - base64String.length % 4) % 4)).replace(/-/g, "+").replace(/_/g, "/");
9
+ const rawData = window.atob(base64);
10
+ const outputArray = new Uint8Array(rawData.length);
11
+ for (let i = 0; i < rawData.length; ++i) outputArray[i] = rawData.charCodeAt(i);
12
+ return outputArray;
13
+ };
14
+ /**
15
+ * Generate a cryptographically secure random base64 string.
16
+ * @returns A base64-encoded random string (16 bytes of entropy)
17
+ */
18
+ function generateRandomBase64() {
19
+ const array = new Uint8Array(16);
20
+ crypto.getRandomValues(array);
21
+ return btoa(String.fromCharCode(...array));
22
+ }
23
+
24
+ //#endregion
25
+ export { generateRandomBase64, urlBase64ToUint8Array };
@@ -0,0 +1,53 @@
1
+ //#region src/http/cookie.d.ts
2
+ /**
3
+ * Helper function to set cookies in a more controlled way
4
+ * @param name - The name of the cookie
5
+ * @param value - The value to set
6
+ * @param options - Cookie options
7
+ */
8
+ declare function setCookieValue(name: string, value: string, options?: {
9
+ expires?: Date;
10
+ path?: string;
11
+ sameSite?: string;
12
+ secure?: boolean;
13
+ domain?: string;
14
+ }): void;
15
+ /**
16
+ * Get a cookie value by name
17
+ * @param name - The name of the cookie to get
18
+ * @returns The cookie value or null if not found
19
+ */
20
+ declare function getCookieValue(name: string): string | null;
21
+ //#endregion
22
+ //#region src/http/domain.d.ts
23
+ /**
24
+ * Helper utility used to extract the domain from the request even if it's
25
+ * behind a proxy. This is useful for sitemaps and other things.
26
+ * @param request Request object
27
+ * @returns Current domain
28
+ */
29
+ declare const createDomain: (request: Request) => string;
30
+ //#endregion
31
+ //#region src/http/is-route-active.d.ts
32
+ /**
33
+ * @name isRouteActive
34
+ * @description A function to check if a route is active. This is used to
35
+ * @param end
36
+ * @param path
37
+ * @param currentPath
38
+ */
39
+ declare function isRouteActive(path: string, currentPath: string, end?: boolean | ((path: string) => boolean) | undefined): boolean;
40
+ /**
41
+ * @name checkIfRouteIsActive
42
+ * @description A function to check if a route is active. This is used to
43
+ * highlight the active link in the navigation.
44
+ * @param targetLink - The link to check against
45
+ * @param currentRoute - the current route
46
+ * @param depth - how far down should segments be matched?
47
+ */
48
+ declare function checkIfRouteIsActive(targetLink: string, currentRoute: string, depth?: number): boolean;
49
+ //#endregion
50
+ //#region src/http/request-helpers.d.ts
51
+ declare function getCleanUrl(request: Request): string;
52
+ //#endregion
53
+ export { checkIfRouteIsActive, createDomain, getCleanUrl, getCookieValue, isRouteActive, setCookieValue };
@@ -0,0 +1,122 @@
1
+ //#region src/http/cookie.ts
2
+ /**
3
+ * Helper function to set cookies in a more controlled way
4
+ * @param name - The name of the cookie
5
+ * @param value - The value to set
6
+ * @param options - Cookie options
7
+ */
8
+ function setCookieValue(name, value, options = {}) {
9
+ if (typeof window === "undefined") {
10
+ console.warn("Cannot set cookie on server side");
11
+ return;
12
+ }
13
+ let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
14
+ if (options.expires) cookieString += `; expires=${options.expires.toUTCString()}`;
15
+ if (options.path) cookieString += `; path=${options.path}`;
16
+ if (options.sameSite) cookieString += `; SameSite=${options.sameSite}`;
17
+ if (options.secure) cookieString += "; Secure";
18
+ if (options.domain) cookieString += `; domain=${options.domain}`;
19
+ try {
20
+ document.cookie = cookieString;
21
+ } catch (error) {
22
+ if (error instanceof Error) console.error(`Failed to set cookie '${name}':`, error.message);
23
+ else console.error(`Failed to set cookie '${name}': Unknown error`);
24
+ }
25
+ }
26
+ /**
27
+ * Get a cookie value by name
28
+ * @param name - The name of the cookie to get
29
+ * @returns The cookie value or null if not found
30
+ */
31
+ function getCookieValue(name) {
32
+ if (typeof window === "undefined") {
33
+ console.warn("Cannot get cookie on server side");
34
+ return null;
35
+ }
36
+ const parts = `; ${document.cookie}`.split(`; ${name}=`);
37
+ if (parts.length === 2) {
38
+ const cookieValue = parts.pop()?.split(";").shift();
39
+ return cookieValue ? decodeURIComponent(cookieValue) : null;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/http/domain.ts
46
+ /**
47
+ * Helper utility used to extract the domain from the request even if it's
48
+ * behind a proxy. This is useful for sitemaps and other things.
49
+ * @param request Request object
50
+ * @returns Current domain
51
+ */
52
+ const createDomain = (request) => {
53
+ const headers = request.headers;
54
+ const maybeProto = headers.get("x-forwarded-proto");
55
+ const maybeHost = headers.get("host");
56
+ const url = new URL(request.url);
57
+ if (maybeProto) return `${maybeProto}://${maybeHost ?? url.host}`;
58
+ if (url.hostname === "localhost") return `http://${url.host}`;
59
+ return `https://${url.host}`;
60
+ };
61
+
62
+ //#endregion
63
+ //#region src/http/is-route-active.ts
64
+ const ROOT_PATH = "/";
65
+ /**
66
+ * @name isRouteActive
67
+ * @description A function to check if a route is active. This is used to
68
+ * @param end
69
+ * @param path
70
+ * @param currentPath
71
+ */
72
+ function isRouteActive(path, currentPath, end) {
73
+ if (path === currentPath) return true;
74
+ if (typeof end === "function") return !end(currentPath);
75
+ return checkIfRouteIsActive(path, currentPath, end ?? true ? 1 : 3);
76
+ }
77
+ /**
78
+ * @name checkIfRouteIsActive
79
+ * @description A function to check if a route is active. This is used to
80
+ * highlight the active link in the navigation.
81
+ * @param targetLink - The link to check against
82
+ * @param currentRoute - the current route
83
+ * @param depth - how far down should segments be matched?
84
+ */
85
+ function checkIfRouteIsActive(targetLink, currentRoute, depth = 1) {
86
+ const currentRoutePath = currentRoute.split("?")[0] ?? "";
87
+ if (!isRoot(currentRoutePath) && isRoot(targetLink)) return false;
88
+ if (!currentRoutePath.includes(targetLink)) return false;
89
+ if (targetLink === currentRoutePath) return true;
90
+ return hasMatchingSegments(targetLink, currentRoutePath, depth);
91
+ }
92
+ function splitIntoSegments(href) {
93
+ return href.split("/").filter(Boolean);
94
+ }
95
+ function hasMatchingSegments(targetLink, currentRoute, depth) {
96
+ const segments = splitIntoSegments(targetLink);
97
+ const matchingSegments = numberOfMatchingSegments(currentRoute, segments);
98
+ if (targetLink === currentRoute) return true;
99
+ return matchingSegments > segments.length - (depth - 1);
100
+ }
101
+ function numberOfMatchingSegments(href, segments) {
102
+ let count = 0;
103
+ for (const segment of splitIntoSegments(href)) if (segments.includes(segment)) count += 1;
104
+ else return count;
105
+ return count;
106
+ }
107
+ function isRoot(path) {
108
+ return path === ROOT_PATH;
109
+ }
110
+
111
+ //#endregion
112
+ //#region src/http/request-helpers.ts
113
+ function getCleanUrl(request) {
114
+ const url = new URL(request.url);
115
+ url.searchParams.forEach((_, key) => {
116
+ url.searchParams.delete(key);
117
+ });
118
+ return url.toString();
119
+ }
120
+
121
+ //#endregion
122
+ export { checkIfRouteIsActive, createDomain, getCleanUrl, getCookieValue, isRouteActive, setCookieValue };
@@ -0,0 +1,92 @@
1
+ import { Cookie, SessionStorage } from "react-router";
2
+
3
+ //#region src/intl/language-detector.d.ts
4
+ interface LanguageDetectorOption {
5
+ /**
6
+ * Define the list of supported languages, this is used to determine if one of
7
+ * the languages requested by the user is supported by the application.
8
+ * This should be be same as the supportedLngs in the i18next options.
9
+ */
10
+ supportedLanguages: string[];
11
+ /**
12
+ * Define the fallback language that it's going to be used in the case user
13
+ * expected language is not supported.
14
+ * This should be be same as the fallbackLng in the i18next options.
15
+ */
16
+ fallbackLanguage: string;
17
+ /**
18
+ * If you want to use a cookie to store the user preferred language, you can
19
+ * pass the Cookie object here.
20
+ */
21
+ cookie?: Cookie;
22
+ /**
23
+ * If you want to use a session to store the user preferred language, you can
24
+ * pass the SessionStorage object here.
25
+ * When this is not defined, getting the locale will ignore the session.
26
+ */
27
+ sessionStorage?: SessionStorage;
28
+ /**
29
+ * If defined a sessionStorage and want to change the default key used to
30
+ * store the user preferred language, you can pass the key here.
31
+ * @default "lng"
32
+ */
33
+ sessionKey?: string;
34
+ /**
35
+ * If you want to use search parameters for language detection and want to
36
+ * change the default key used to for the parameter name,
37
+ * you can pass the key here.
38
+ * @default "lng"
39
+ */
40
+ searchParamKey?: string;
41
+ /**
42
+ * The order the library will use to detect the user preferred language.
43
+ * By default the order is
44
+ * - urlPath (first segment like /de/ or /en/)
45
+ * - cookie
46
+ * - session
47
+ * - searchParams
48
+ * - header
49
+ * And finally the fallback language.
50
+ */
51
+ order?: Array<"urlPath" | "searchParams" | "cookie" | "session" | "header">;
52
+ }
53
+ interface LanguageDetectorLinguiOptions {
54
+ detection: LanguageDetectorOption;
55
+ }
56
+ declare class LanguageDetectorLingui {
57
+ private detector;
58
+ private options;
59
+ constructor(options: LanguageDetectorLinguiOptions);
60
+ /**
61
+ * Detect the current locale by following the order defined in the
62
+ * `detection.order` option.
63
+ * By default the order is
64
+ * - urlPath (first segment like /de/ or /en/)
65
+ * - cookie
66
+ * - session
67
+ * - searchParams
68
+ * - header
69
+ * And finally the fallback language.
70
+ */
71
+ getLocale(request: Request): Promise<string>;
72
+ }
73
+ /**
74
+ * The LanguageDetector contains the logic to detect the user preferred language
75
+ * fully server-side by using a SessionStorage, Cookie, URLSearchParams, or
76
+ * Headers.
77
+ */
78
+ declare class LanguageDetector {
79
+ private options;
80
+ constructor(options: LanguageDetectorOption);
81
+ detect(request: Request): Promise<string>;
82
+ private isSessionOnly;
83
+ private isCookieOnly;
84
+ private fromUrlPath;
85
+ private fromSearchParams;
86
+ private fromCookie;
87
+ private fromSessionStorage;
88
+ private fromHeader;
89
+ private fromSupported;
90
+ }
91
+ //#endregion
92
+ export { LanguageDetector, LanguageDetectorLingui, type LanguageDetectorLinguiOptions, type LanguageDetectorOption };
@@ -0,0 +1,124 @@
1
+ import { parseAcceptLanguage } from "intl-parse-accept-language";
2
+
3
+ //#region src/intl/language-detector.ts
4
+ var LanguageDetectorLingui = class {
5
+ detector;
6
+ options;
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.detector = new LanguageDetector(this.options.detection);
10
+ }
11
+ /**
12
+ * Detect the current locale by following the order defined in the
13
+ * `detection.order` option.
14
+ * By default the order is
15
+ * - urlPath (first segment like /de/ or /en/)
16
+ * - cookie
17
+ * - session
18
+ * - searchParams
19
+ * - header
20
+ * And finally the fallback language.
21
+ */
22
+ async getLocale(request) {
23
+ return await this.detector.detect(request);
24
+ }
25
+ };
26
+ /**
27
+ * The LanguageDetector contains the logic to detect the user preferred language
28
+ * fully server-side by using a SessionStorage, Cookie, URLSearchParams, or
29
+ * Headers.
30
+ */
31
+ var LanguageDetector = class {
32
+ options;
33
+ constructor(options) {
34
+ this.options = options;
35
+ this.isSessionOnly(this.options);
36
+ this.isCookieOnly(this.options);
37
+ }
38
+ async detect(request) {
39
+ const order = this.options.order ?? [
40
+ "urlPath",
41
+ "cookie",
42
+ "session",
43
+ "searchParams",
44
+ "header"
45
+ ];
46
+ for (const method of order) {
47
+ let locale = null;
48
+ if (method === "urlPath") locale = this.fromUrlPath(request);
49
+ if (method === "searchParams") locale = this.fromSearchParams(request);
50
+ if (method === "cookie") locale = await this.fromCookie(request);
51
+ if (method === "session") locale = await this.fromSessionStorage(request);
52
+ if (method === "header") locale = this.fromHeader(request);
53
+ if (locale) return locale;
54
+ }
55
+ return this.options.fallbackLanguage;
56
+ }
57
+ isSessionOnly(options) {
58
+ if (options.order?.length === 1 && options.order[0] === "session" && !options.sessionStorage) throw new Error("You need a sessionStorage if you want to only get the locale from the session");
59
+ }
60
+ isCookieOnly(options) {
61
+ if (options.order?.length === 1 && options.order[0] === "cookie" && !options.cookie) throw new Error("You need a cookie if you want to only get the locale from the cookie");
62
+ }
63
+ fromUrlPath(request) {
64
+ const pathSegments = new URL(request.url).pathname.split("/").filter(Boolean);
65
+ if (pathSegments.length > 0) {
66
+ const firstSegment = pathSegments[0];
67
+ if (firstSegment) return this.fromSupported(firstSegment);
68
+ }
69
+ return null;
70
+ }
71
+ fromSearchParams(request) {
72
+ const url = new URL(request.url);
73
+ if (!url.searchParams.has(this.options.searchParamKey ?? "lng")) return null;
74
+ return this.fromSupported(url.searchParams.get(this.options.searchParamKey ?? "lng"));
75
+ }
76
+ async fromCookie(request) {
77
+ if (!this.options.cookie) return null;
78
+ const cookie = this.options.cookie;
79
+ try {
80
+ const lng = await cookie.parse(request.headers.get("Cookie"));
81
+ if (typeof lng !== "string" || !lng) return null;
82
+ return this.fromSupported(lng);
83
+ } catch (_error) {
84
+ return null;
85
+ }
86
+ }
87
+ async fromSessionStorage(request) {
88
+ if (!this.options.sessionStorage) return null;
89
+ const lng = (await this.options.sessionStorage.getSession(request.headers.get("Cookie"))).get(this.options.sessionKey ?? "lng");
90
+ if (!lng) return null;
91
+ return this.fromSupported(lng);
92
+ }
93
+ fromHeader(request) {
94
+ const locales = getClientLocales(request);
95
+ if (!locales) return null;
96
+ if (Array.isArray(locales)) return this.fromSupported(locales.join(","));
97
+ return this.fromSupported(locales);
98
+ }
99
+ fromSupported(language) {
100
+ if (!language) return this.options.fallbackLanguage;
101
+ return parseAcceptLanguage(Array.isArray(language) ? language.join(",") : language, {
102
+ ignoreWildcard: true,
103
+ validate: (locale) => this.options.supportedLanguages.includes(locale) ? locale : null
104
+ })[0] || this.options.fallbackLanguage;
105
+ }
106
+ };
107
+ function getClientLocales(requestOrHeaders) {
108
+ const acceptLanguage = getHeaders(requestOrHeaders).get("Accept-Language");
109
+ if (!acceptLanguage) return void 0;
110
+ const locales = parseAcceptLanguage(acceptLanguage, {
111
+ ignoreWildcard: true,
112
+ validate: Intl.DateTimeFormat.supportedLocalesOf
113
+ });
114
+ if (locales.length === 0) return void 0;
115
+ if (locales.length === 1) return locales[0];
116
+ return locales;
117
+ }
118
+ function getHeaders(requestOrHeaders) {
119
+ if (requestOrHeaders instanceof Request) return requestOrHeaders.headers;
120
+ return requestOrHeaders;
121
+ }
122
+
123
+ //#endregion
124
+ export { LanguageDetector, LanguageDetectorLingui };
@@ -0,0 +1,48 @@
1
+ //#region src/text/bytes.d.ts
2
+ declare function formatBytes(bytes: number, decimals?: number): string;
3
+ //#endregion
4
+ //#region src/text/text.d.ts
5
+ /**
6
+ * Replace straight quotes with typographically correct quotes for the given locale.
7
+ *
8
+ * @param text - The text containing straight quotes
9
+ * @param locale - The locale to use for quote style (e.g., 'en', 'de', 'fr')
10
+ * @returns Text with typographic quotes
11
+ *
12
+ * @example
13
+ * typographicQuotes('"Hello"', 'en') // → '"Hello"'
14
+ * typographicQuotes('"Hello"', 'de') // → '„Hello"'
15
+ * typographicQuotes('"Hello"', 'fr') // → '« Hello »'
16
+ */
17
+ declare function typographicQuotes(text: string, locale: string): string;
18
+ declare function toBoolean(value: string | boolean | null | undefined): boolean;
19
+ /**
20
+ * Replace &shy; HTML entity with Unicode soft hyphen
21
+ */
22
+ declare function replaceShyInString(input: string): string;
23
+ /**
24
+ * Split text into sentences
25
+ */
26
+ declare function splitIntoSentences(text: string): string[];
27
+ /**
28
+ * Split text into words
29
+ */
30
+ declare function splitIntoWords(text: string): string[];
31
+ /**
32
+ * Truncate text to a maximum length
33
+ */
34
+ declare function truncateText(text: string, maxLength: number, suffix?: string): string;
35
+ /**
36
+ * Author info parsed from a string like "Name <email> (url)"
37
+ */
38
+ type AuthorInfo = {
39
+ name?: string;
40
+ email?: string;
41
+ url?: string;
42
+ };
43
+ /**
44
+ * Parse an author string in the format "Name <email> (url)"
45
+ */
46
+ declare function parseAuthorString(input: string): AuthorInfo;
47
+ //#endregion
48
+ export { type AuthorInfo, formatBytes, parseAuthorString, replaceShyInString, splitIntoSentences, splitIntoWords, toBoolean, truncateText, typographicQuotes };