Suite of AT Protocol TypeScript libraries built on web standards
1export const utf8Len = (str: string): number => {
2 return new TextEncoder().encode(str).byteLength;
3};
4
5export const graphemeLen = (str: string): number => {
6 if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
7 const segmenter = new Intl.Segmenter(undefined, {
8 granularity: "grapheme",
9 });
10 return Array.from(segmenter.segment(str)).length;
11 }
12
13 return Array.from(str).length;
14};
15
16export const utf8ToB64Url = (utf8: string): string => {
17 const encoder = new TextEncoder();
18 const bytes = encoder.encode(utf8);
19 return btoa(String.fromCharCode(...bytes))
20 .replace(/\+/g, "-")
21 .replace(/\//g, "_")
22 .replace(/=/g, "");
23};
24
25export const b64UrlToUtf8 = (b64: string): string => {
26 const base64 = b64.replace(/-/g, "+").replace(/_/g, "/");
27 const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
28
29 const binaryString = atob(padded);
30 const bytes = new Uint8Array(binaryString.length);
31 for (let i = 0; i < binaryString.length; i++) {
32 bytes[i] = binaryString.charCodeAt(i);
33 }
34
35 const decoder = new TextDecoder();
36 return decoder.decode(bytes);
37};
38
39export const parseLanguage = (langTag: string): LanguageTag | null => {
40 const parsed = langTag.match(bcp47Regexp);
41 if (!parsed?.groups) return null;
42 const parts = parsed.groups;
43 const result: LanguageTag = {};
44
45 if (parts.grandfathered) result.grandfathered = parts.grandfathered;
46 if (parts.language) result.language = parts.language;
47 if (parts.extlang) result.extlang = parts.extlang;
48 if (parts.script) result.script = parts.script;
49 if (parts.region) result.region = parts.region;
50 if (parts.variant) result.variant = parts.variant;
51 if (parts.extension) result.extension = parts.extension;
52 if (parts.privateUseA || parts.privateUseB) {
53 result.privateUse = parts.privateUseA || parts.privateUseB;
54 }
55
56 return result;
57};
58
59export const validateLanguage = (langTag: string): boolean => {
60 return bcp47Regexp.test(langTag);
61};
62
63export type LanguageTag = {
64 grandfathered?: string;
65 language?: string;
66 extlang?: string;
67 script?: string;
68 region?: string;
69 variant?: string;
70 extension?: string;
71 privateUse?: string;
72};
73
74const bcp47Regexp =
75 /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/;