@atproto/common-web 0.1.0 β 0.2.1
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/build.js +0 -8
- package/dist/arrays.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +9766 -197
- package/dist/index.js.map +4 -4
- package/dist/ipld.d.ts +1 -0
- package/dist/strings.d.ts +14 -0
- package/dist/tid.d.ts +3 -3
- package/dist/times.d.ts +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/util.d.ts +6 -2
- package/package.json +21 -18
- package/src/arrays.ts +13 -0
- package/src/index.ts +1 -0
- package/src/ipld.ts +29 -0
- package/src/strings.ts +48 -1
- package/src/tid.ts +25 -21
- package/src/times.ts +4 -0
- package/src/types.ts +2 -0
- package/src/util.ts +23 -4
- package/tests/check.test.ts +89 -0
- package/tests/strings.test.ts +86 -2
- package/tests/tid.test.ts +124 -0
- package/tests/util.test.ts +107 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.build.tsbuildinfo +0 -1
- package/update-pkg.js +0 -14
package/dist/ipld.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export declare type IpldValue = JsonValue | CID | Uint8Array | Array<IpldValue>
|
|
|
7
7
|
};
|
|
8
8
|
export declare const jsonToIpld: (val: JsonValue) => IpldValue;
|
|
9
9
|
export declare const ipldToJson: (val: IpldValue) => JsonValue;
|
|
10
|
+
export declare const ipldEquals: (a: IpldValue, b: IpldValue) => boolean;
|
package/dist/strings.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
export declare const utf8Len: (str: string) => number;
|
|
2
2
|
export declare const graphemeLen: (str: string) => number;
|
|
3
|
+
export declare const utf8ToB64Url: (utf8: string) => string;
|
|
4
|
+
export declare const b64UrlToUtf8: (b64: string) => string;
|
|
5
|
+
export declare const parseLanguage: (langTag: string) => LanguageTag | null;
|
|
6
|
+
export declare const validateLanguage: (langTag: string) => boolean;
|
|
7
|
+
export declare type LanguageTag = {
|
|
8
|
+
grandfathered?: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
extlang?: string;
|
|
11
|
+
script?: string;
|
|
12
|
+
region?: string;
|
|
13
|
+
variant?: string;
|
|
14
|
+
extension?: string;
|
|
15
|
+
privateUse?: string;
|
|
16
|
+
};
|
package/dist/tid.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export declare class TID {
|
|
2
2
|
str: string;
|
|
3
3
|
constructor(str: string);
|
|
4
|
-
static next(): TID;
|
|
5
|
-
static nextStr(): string;
|
|
4
|
+
static next(prev?: TID): TID;
|
|
5
|
+
static nextStr(prev?: string): string;
|
|
6
6
|
static fromTime(timestamp: number, clockid: number): TID;
|
|
7
7
|
static fromStr(str: string): TID;
|
|
8
|
-
static newestFirst(a: TID, b: TID): number;
|
|
9
8
|
static oldestFirst(a: TID, b: TID): number;
|
|
9
|
+
static newestFirst(a: TID, b: TID): number;
|
|
10
10
|
static is(str: string): boolean;
|
|
11
11
|
timestamp(): number;
|
|
12
12
|
clockid(): number;
|
package/dist/times.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
package/dist/util.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
export declare const noUndefinedVals: <T>(obj: Record<string, T>) => Record<string, T>;
|
|
3
|
+
export declare const jitter: (maxMs: number) => number;
|
|
3
4
|
export declare const wait: (ms: number) => Promise<unknown>;
|
|
4
|
-
export declare
|
|
5
|
+
export declare type BailableWait = {
|
|
5
6
|
bail: () => void;
|
|
6
7
|
wait: () => Promise<void>;
|
|
7
8
|
};
|
|
9
|
+
export declare const bailableWait: (ms: number) => BailableWait;
|
|
8
10
|
export declare const flattenUint8Arrays: (arrs: Uint8Array[]) => Uint8Array;
|
|
9
|
-
export declare const
|
|
11
|
+
export declare const streamToBuffer: (stream: AsyncIterable<Uint8Array>) => Promise<Uint8Array>;
|
|
10
12
|
export declare const s32encode: (i: number) => string;
|
|
11
13
|
export declare const s32decode: (s: string) => number;
|
|
12
14
|
export declare const asyncFilter: <T>(arr: T[], fn: (t: T) => Promise<boolean>) => Promise<T[]>;
|
|
@@ -14,3 +16,5 @@ export declare const isErrnoException: (err: unknown) => err is NodeJS.ErrnoExce
|
|
|
14
16
|
export declare const errHasMsg: (err: unknown, msg: string) => boolean;
|
|
15
17
|
export declare const chunkArray: <T>(arr: T[], chunkSize: number) => T[][];
|
|
16
18
|
export declare const range: (num: number) => number[];
|
|
19
|
+
export declare const dedupeStrs: (strs: string[]) => string[];
|
|
20
|
+
export declare const parseIntWithFallback: <T>(value: string | undefined, fallback: T) => number | T;
|
package/package.json
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/common-web",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "0.2.1",
|
|
5
4
|
"license": "MIT",
|
|
5
|
+
"description": "Shared web-platform-friendly code for atproto libraries",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://atproto.com",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
13
|
+
"directory": "packages/common-web"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"graphemer": "^1.4.0",
|
|
18
|
+
"multiformats": "^9.9.0",
|
|
19
|
+
"uint8arrays": "3.0.0",
|
|
20
|
+
"zod": "^3.21.4"
|
|
21
|
+
},
|
|
6
22
|
"scripts": {
|
|
7
23
|
"test": "jest",
|
|
8
|
-
"prettier": "prettier --check src/",
|
|
9
|
-
"prettier:fix": "prettier --write src/",
|
|
10
|
-
"lint": "eslint . --ext .ts,.tsx",
|
|
11
|
-
"lint:fix": "yarn lint --fix",
|
|
12
|
-
"verify": "run-p prettier lint",
|
|
13
|
-
"verify:fix": "yarn prettier:fix && yarn lint:fix",
|
|
14
24
|
"build": "node ./build.js",
|
|
15
25
|
"postbuild": "tsc --build tsconfig.build.json",
|
|
16
|
-
"update-main-to-dist": "node
|
|
17
|
-
"update-main-to-src": "node ./update-pkg.js --update-main-to-src",
|
|
18
|
-
"prepublish": "npm run update-main-to-dist",
|
|
19
|
-
"postpublish": "npm run update-main-to-src"
|
|
26
|
+
"update-main-to-dist": "node ../../update-main-to-dist.js packages/common-web"
|
|
20
27
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
"uint8arrays": "3.0.0",
|
|
24
|
-
"zod": "^3.14.2"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
28
|
+
"types": "dist/index.d.ts"
|
|
29
|
+
}
|
package/src/arrays.ts
ADDED
package/src/index.ts
CHANGED
package/src/ipld.ts
CHANGED
|
@@ -75,3 +75,32 @@ export const ipldToJson = (val: IpldValue): JsonValue => {
|
|
|
75
75
|
// pass through
|
|
76
76
|
return val as JsonValue
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => {
|
|
80
|
+
// walk arrays
|
|
81
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
82
|
+
if (a.length !== b.length) return false
|
|
83
|
+
for (let i = 0; i < a.length; i++) {
|
|
84
|
+
if (!ipldEquals(a[i], b[i])) return false
|
|
85
|
+
}
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
// objects
|
|
89
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
90
|
+
// check bytes
|
|
91
|
+
if (a instanceof Uint8Array && b instanceof Uint8Array) {
|
|
92
|
+
return ui8.equals(a, b)
|
|
93
|
+
}
|
|
94
|
+
// check cids
|
|
95
|
+
if (CID.asCID(a) && CID.asCID(b)) {
|
|
96
|
+
return CID.asCID(a)?.equals(CID.asCID(b))
|
|
97
|
+
}
|
|
98
|
+
// walk plain objects
|
|
99
|
+
if (Object.keys(a).length !== Object.keys(b).length) return false
|
|
100
|
+
for (const key of Object.keys(a)) {
|
|
101
|
+
if (!ipldEquals(a[key], b[key])) return false
|
|
102
|
+
}
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
return a === b
|
|
106
|
+
}
|
package/src/strings.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import Graphemer from 'graphemer'
|
|
2
|
+
import * as ui8 from 'uint8arrays'
|
|
3
|
+
|
|
1
4
|
// counts the number of bytes in a utf8 string
|
|
2
5
|
export const utf8Len = (str: string): number => {
|
|
3
6
|
return new TextEncoder().encode(str).byteLength
|
|
@@ -5,5 +8,49 @@ export const utf8Len = (str: string): number => {
|
|
|
5
8
|
|
|
6
9
|
// counts the number of graphemes (user-displayed characters) in a string
|
|
7
10
|
export const graphemeLen = (str: string): number => {
|
|
8
|
-
|
|
11
|
+
const splitter = new Graphemer()
|
|
12
|
+
return splitter.countGraphemes(str)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const utf8ToB64Url = (utf8: string): string => {
|
|
16
|
+
return ui8.toString(ui8.fromString(utf8, 'utf8'), 'base64url')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const b64UrlToUtf8 = (b64: string): string => {
|
|
20
|
+
return ui8.toString(ui8.fromString(b64, 'base64url'), 'utf8')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const parseLanguage = (langTag: string): LanguageTag | null => {
|
|
24
|
+
const parsed = langTag.match(bcp47Regexp)
|
|
25
|
+
if (!parsed?.groups) return null
|
|
26
|
+
const parts = parsed.groups
|
|
27
|
+
return {
|
|
28
|
+
grandfathered: parts.grandfathered,
|
|
29
|
+
language: parts.language,
|
|
30
|
+
extlang: parts.extlang,
|
|
31
|
+
script: parts.script,
|
|
32
|
+
region: parts.region,
|
|
33
|
+
variant: parts.variant,
|
|
34
|
+
extension: parts.extension,
|
|
35
|
+
privateUse: parts.privateUseA || parts.privateUseB,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const validateLanguage = (langTag: string): boolean => {
|
|
40
|
+
return bcp47Regexp.test(langTag)
|
|
9
41
|
}
|
|
42
|
+
|
|
43
|
+
export type LanguageTag = {
|
|
44
|
+
grandfathered?: string
|
|
45
|
+
language?: string
|
|
46
|
+
extlang?: string
|
|
47
|
+
script?: string
|
|
48
|
+
region?: string
|
|
49
|
+
variant?: string
|
|
50
|
+
extension?: string
|
|
51
|
+
privateUse?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Validates well-formed BCP 47 syntax: https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1
|
|
55
|
+
const bcp47Regexp =
|
|
56
|
+
/^((?<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})+))$/
|
package/src/tid.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { s32encode, s32decode } from './util'
|
|
2
|
+
|
|
3
|
+
const TID_LEN = 13
|
|
4
|
+
|
|
2
5
|
let lastTimestamp = 0
|
|
3
6
|
let timestampCount = 0
|
|
4
7
|
let clockid: number | null = null
|
|
5
8
|
|
|
9
|
+
function dedash(str: string): string {
|
|
10
|
+
return str.replaceAll('-', '')
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export class TID {
|
|
7
14
|
str: string
|
|
8
15
|
|
|
9
16
|
constructor(str: string) {
|
|
10
|
-
const noDashes = str
|
|
11
|
-
if (noDashes.length !==
|
|
17
|
+
const noDashes = dedash(str)
|
|
18
|
+
if (noDashes.length !== TID_LEN) {
|
|
12
19
|
throw new Error(`Poorly formatted TID: ${noDashes.length} length`)
|
|
13
20
|
}
|
|
14
21
|
this.str = noDashes
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
static next(): TID {
|
|
24
|
+
static next(prev?: TID): TID {
|
|
18
25
|
// javascript does not have microsecond precision
|
|
19
26
|
// instead, we append a counter to the timestamp to indicate if multiple timestamps were created within the same millisecond
|
|
20
27
|
// take max of current time & last timestamp to prevent tids moving backwards if system clock drifts backwards
|
|
@@ -29,11 +36,15 @@ export class TID {
|
|
|
29
36
|
if (clockid === null) {
|
|
30
37
|
clockid = Math.floor(Math.random() * 32)
|
|
31
38
|
}
|
|
32
|
-
|
|
39
|
+
const tid = TID.fromTime(timestamp, clockid)
|
|
40
|
+
if (!prev || tid.newerThan(prev)) {
|
|
41
|
+
return tid
|
|
42
|
+
}
|
|
43
|
+
return TID.fromTime(prev.timestamp() + 1, clockid)
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
static nextStr(): string {
|
|
36
|
-
return TID.next().toString()
|
|
46
|
+
static nextStr(prev?: string): string {
|
|
47
|
+
return TID.next(prev ? new TID(prev) : undefined).toString()
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
static fromTime(timestamp: number, clockid: number): TID {
|
|
@@ -46,31 +57,24 @@ export class TID {
|
|
|
46
57
|
return new TID(str)
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
static newestFirst(a: TID, b: TID): number {
|
|
50
|
-
return a.compareTo(b) * -1
|
|
51
|
-
}
|
|
52
|
-
|
|
53
60
|
static oldestFirst(a: TID, b: TID): number {
|
|
54
61
|
return a.compareTo(b)
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
static newestFirst(a: TID, b: TID): number {
|
|
65
|
+
return b.compareTo(a)
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
static is(str: string): boolean {
|
|
58
|
-
|
|
59
|
-
TID.fromStr(str)
|
|
60
|
-
return true
|
|
61
|
-
} catch (err) {
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
69
|
+
return dedash(str).length === TID_LEN
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
timestamp(): number {
|
|
67
|
-
|
|
68
|
-
return s32decode(substr)
|
|
73
|
+
return s32decode(this.str.slice(0, 11))
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
clockid(): number {
|
|
72
|
-
|
|
73
|
-
return s32decode(substr)
|
|
77
|
+
return s32decode(this.str.slice(11, 13))
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
formatted(): string {
|
|
@@ -93,7 +97,7 @@ export class TID {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
equals(other: TID): boolean {
|
|
96
|
-
return this.
|
|
100
|
+
return this.str === other.str
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
newerThan(other: TID): boolean {
|
package/src/times.ts
CHANGED
package/src/types.ts
CHANGED
package/src/util.ts
CHANGED
|
@@ -9,13 +9,20 @@ export const noUndefinedVals = <T>(
|
|
|
9
9
|
return obj
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export const jitter = (maxMs: number) => {
|
|
13
|
+
return Math.round((Math.random() - 0.5) * maxMs * 2)
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export const wait = (ms: number) => {
|
|
13
17
|
return new Promise((res) => setTimeout(res, ms))
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
export type BailableWait = {
|
|
21
|
+
bail: () => void
|
|
22
|
+
wait: () => Promise<void>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const bailableWait = (ms: number): BailableWait => {
|
|
19
26
|
let bail
|
|
20
27
|
const waitPromise = new Promise<void>((res) => {
|
|
21
28
|
const timeout = setTimeout(res, ms)
|
|
@@ -40,7 +47,7 @@ export const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => {
|
|
|
40
47
|
return flattened
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
export const
|
|
50
|
+
export const streamToBuffer = async (
|
|
44
51
|
stream: AsyncIterable<Uint8Array>,
|
|
45
52
|
): Promise<Uint8Array> => {
|
|
46
53
|
const arrays: Uint8Array[] = []
|
|
@@ -106,3 +113,15 @@ export const range = (num: number): number[] => {
|
|
|
106
113
|
}
|
|
107
114
|
return nums
|
|
108
115
|
}
|
|
116
|
+
|
|
117
|
+
export const dedupeStrs = (strs: string[]): string[] => {
|
|
118
|
+
return [...new Set(strs)]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const parseIntWithFallback = <T>(
|
|
122
|
+
value: string | undefined,
|
|
123
|
+
fallback: T,
|
|
124
|
+
): number | T => {
|
|
125
|
+
const parsed = parseInt(value || '', 10)
|
|
126
|
+
return isNaN(parsed) ? fallback : parsed
|
|
127
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { check } from '../src/index'
|
|
2
|
+
import { ZodError } from 'zod'
|
|
3
|
+
|
|
4
|
+
describe('check', () => {
|
|
5
|
+
describe('is', () => {
|
|
6
|
+
it('checks object against definition', () => {
|
|
7
|
+
const checkable: check.Checkable<boolean> = {
|
|
8
|
+
parse(obj) {
|
|
9
|
+
return Boolean(obj)
|
|
10
|
+
},
|
|
11
|
+
safeParse(obj) {
|
|
12
|
+
return {
|
|
13
|
+
success: true,
|
|
14
|
+
data: Boolean(obj),
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
expect(check.is(true, checkable)).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('handles failed checks', () => {
|
|
23
|
+
const checkable: check.Checkable<boolean> = {
|
|
24
|
+
parse(obj) {
|
|
25
|
+
return Boolean(obj)
|
|
26
|
+
},
|
|
27
|
+
safeParse() {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
error: new ZodError([]),
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
expect(check.is(true, checkable)).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('assure', () => {
|
|
40
|
+
it('returns value on success', () => {
|
|
41
|
+
const checkable: check.Checkable<boolean> = {
|
|
42
|
+
parse(obj) {
|
|
43
|
+
return Boolean(obj)
|
|
44
|
+
},
|
|
45
|
+
safeParse(obj) {
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
data: Boolean(obj),
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expect(check.assure(checkable, true)).toEqual(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws on failure', () => {
|
|
57
|
+
const err = new Error('foo')
|
|
58
|
+
const checkable: check.Checkable<boolean> = {
|
|
59
|
+
parse() {
|
|
60
|
+
throw err
|
|
61
|
+
},
|
|
62
|
+
safeParse() {
|
|
63
|
+
throw err
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
expect(() => check.assure(checkable, true)).toThrow(err)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('isObject', () => {
|
|
72
|
+
const falseTestValues: unknown[] = [null, undefined, 'foo', 123, true]
|
|
73
|
+
|
|
74
|
+
for (const obj of falseTestValues) {
|
|
75
|
+
it(`returns false for ${obj}`, () => {
|
|
76
|
+
expect(check.isObject(obj)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('returns true for objects', () => {
|
|
81
|
+
expect(check.isObject({})).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('returns true for instances of classes', () => {
|
|
85
|
+
const obj = new (class {})()
|
|
86
|
+
expect(check.isObject(obj)).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
})
|
package/tests/strings.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { graphemeLen, utf8Len } from '../src'
|
|
1
|
+
import { graphemeLen, parseLanguage, utf8Len, validateLanguage } from '../src'
|
|
2
2
|
|
|
3
3
|
describe('string', () => {
|
|
4
4
|
it('calculates utf8 string length', () => {
|
|
@@ -14,7 +14,7 @@ describe('string', () => {
|
|
|
14
14
|
expect(utf8Len('π¨βπ©βπ§βπ§')).toBe(25)
|
|
15
15
|
})
|
|
16
16
|
|
|
17
|
-
it('
|
|
17
|
+
it('calculates grapheme length', () => {
|
|
18
18
|
expect(graphemeLen('a')).toBe(1)
|
|
19
19
|
expect(graphemeLen('~')).toBe(1)
|
|
20
20
|
expect(graphemeLen('ΓΆ')).toBe(1)
|
|
@@ -27,4 +27,88 @@ describe('string', () => {
|
|
|
27
27
|
expect(graphemeLen('π¨βπ©βπ§βπ§')).toBe(1)
|
|
28
28
|
expect(graphemeLen('a~ΓΆΓ±Β©β½βπππ¨βπ©βπ§βπ§')).toBe(10)
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
describe('languages', () => {
|
|
32
|
+
it('validates BCP 47', () => {
|
|
33
|
+
// valid
|
|
34
|
+
expect(validateLanguage('de')).toEqual(true)
|
|
35
|
+
expect(validateLanguage('de-CH')).toEqual(true)
|
|
36
|
+
expect(validateLanguage('de-DE-1901')).toEqual(true)
|
|
37
|
+
expect(validateLanguage('es-419')).toEqual(true)
|
|
38
|
+
expect(validateLanguage('sl-IT-nedis')).toEqual(true)
|
|
39
|
+
expect(validateLanguage('mn-Cyrl-MN')).toEqual(true)
|
|
40
|
+
expect(validateLanguage('x-fr-CH')).toEqual(true)
|
|
41
|
+
expect(
|
|
42
|
+
validateLanguage('en-GB-boont-r-extended-sequence-x-private'),
|
|
43
|
+
).toEqual(true)
|
|
44
|
+
expect(validateLanguage('sr-Cyrl')).toEqual(true)
|
|
45
|
+
expect(validateLanguage('hy-Latn-IT-arevela')).toEqual(true)
|
|
46
|
+
expect(validateLanguage('i-klingon')).toEqual(true)
|
|
47
|
+
// invalid
|
|
48
|
+
expect(validateLanguage('')).toEqual(false)
|
|
49
|
+
expect(validateLanguage('x')).toEqual(false)
|
|
50
|
+
expect(validateLanguage('de-CH-')).toEqual(false)
|
|
51
|
+
expect(validateLanguage('i-bad-grandfathered')).toEqual(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('parses BCP 47', () => {
|
|
55
|
+
// valid
|
|
56
|
+
expect(parseLanguage('de')).toEqual({
|
|
57
|
+
language: 'de',
|
|
58
|
+
})
|
|
59
|
+
expect(parseLanguage('de-CH')).toEqual({
|
|
60
|
+
language: 'de',
|
|
61
|
+
region: 'CH',
|
|
62
|
+
})
|
|
63
|
+
expect(parseLanguage('de-DE-1901')).toEqual({
|
|
64
|
+
language: 'de',
|
|
65
|
+
region: 'DE',
|
|
66
|
+
variant: '1901',
|
|
67
|
+
})
|
|
68
|
+
expect(parseLanguage('es-419')).toEqual({
|
|
69
|
+
language: 'es',
|
|
70
|
+
region: '419',
|
|
71
|
+
})
|
|
72
|
+
expect(parseLanguage('sl-IT-nedis')).toEqual({
|
|
73
|
+
language: 'sl',
|
|
74
|
+
region: 'IT',
|
|
75
|
+
variant: 'nedis',
|
|
76
|
+
})
|
|
77
|
+
expect(parseLanguage('mn-Cyrl-MN')).toEqual({
|
|
78
|
+
language: 'mn',
|
|
79
|
+
script: 'Cyrl',
|
|
80
|
+
region: 'MN',
|
|
81
|
+
})
|
|
82
|
+
expect(parseLanguage('x-fr-CH')).toEqual({
|
|
83
|
+
privateUse: 'x-fr-CH',
|
|
84
|
+
})
|
|
85
|
+
expect(
|
|
86
|
+
parseLanguage('en-GB-boont-r-extended-sequence-x-private'),
|
|
87
|
+
).toEqual({
|
|
88
|
+
language: 'en',
|
|
89
|
+
region: 'GB',
|
|
90
|
+
variant: 'boont',
|
|
91
|
+
extension: 'r-extended-sequence',
|
|
92
|
+
privateUse: 'x-private',
|
|
93
|
+
})
|
|
94
|
+
expect(parseLanguage('sr-Cyrl')).toEqual({
|
|
95
|
+
language: 'sr',
|
|
96
|
+
script: 'Cyrl',
|
|
97
|
+
})
|
|
98
|
+
expect(parseLanguage('hy-Latn-IT-arevela')).toEqual({
|
|
99
|
+
language: 'hy',
|
|
100
|
+
script: 'Latn',
|
|
101
|
+
region: 'IT',
|
|
102
|
+
variant: 'arevela',
|
|
103
|
+
})
|
|
104
|
+
expect(parseLanguage('i-klingon')).toEqual({
|
|
105
|
+
grandfathered: 'i-klingon',
|
|
106
|
+
})
|
|
107
|
+
// invalid
|
|
108
|
+
expect(parseLanguage('')).toEqual(null)
|
|
109
|
+
expect(parseLanguage('x')).toEqual(null)
|
|
110
|
+
expect(parseLanguage('de-CH-')).toEqual(null)
|
|
111
|
+
expect(parseLanguage('i-bad-grandfathered')).toEqual(null)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
30
114
|
})
|