@fast-china/utils 2.0.0 → 2.0.2

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/Fast.png +0 -0
  3. package/README.md +27 -29
  4. package/README.zh.md +27 -29
  5. package/dist/base64/index.mjs.map +1 -1
  6. package/dist/crypto/index.d.mts +265 -96
  7. package/dist/crypto/index.mjs +563 -261
  8. package/dist/crypto/index.mjs.map +1 -1
  9. package/dist/identity/index.d.mts +5 -5
  10. package/dist/identity/index.mjs +2 -2
  11. package/dist/identity/index.mjs.map +1 -1
  12. package/dist/index.d.mts +3 -3
  13. package/dist/index.global.min.js +3 -0
  14. package/dist/index.global.min.js.map +1 -0
  15. package/dist/index.mjs +2 -2
  16. package/dist/storage/index.d.mts +11 -8
  17. package/dist/storage/index.mjs +21 -18
  18. package/dist/storage/index.mjs.map +1 -1
  19. package/dist/vue/emits.mjs.map +1 -1
  20. package/dist/vue/index.d.mts +2 -2
  21. package/dist/vue/install.d.mts +11 -29
  22. package/dist/vue/install.mjs +25 -25
  23. package/dist/vue/install.mjs.map +1 -1
  24. package/dist/vue/props.mjs.map +1 -1
  25. package/dist/vue/render.d.mts +2 -3
  26. package/dist/vue/render.mjs +3 -9
  27. package/dist/vue/render.mjs.map +1 -1
  28. package/docs/API.md +30 -14
  29. package/docs/API.zh-CN.md +30 -14
  30. package/docs/DEVELOPMENT_RELEASE.zh-CN.md +4 -4
  31. package/docs/RUNTIME_CONTRACT.md +10 -10
  32. package/package.json +9 -11
  33. package/dist/array/index.d.mts.map +0 -1
  34. package/dist/async/index.d.mts.map +0 -1
  35. package/dist/base64/index.d.mts.map +0 -1
  36. package/dist/color/index.d.mts.map +0 -1
  37. package/dist/crypto/index.d.mts.map +0 -1
  38. package/dist/date/index.d.mts.map +0 -1
  39. package/dist/dom/style.d.mts.map +0 -1
  40. package/dist/env/index.d.mts.map +0 -1
  41. package/dist/identity/index.d.mts.map +0 -1
  42. package/dist/logger/index.d.mts.map +0 -1
  43. package/dist/number/index.d.mts.map +0 -1
  44. package/dist/object/index.d.mts.map +0 -1
  45. package/dist/storage/index.d.mts.map +0 -1
  46. package/dist/string/index.d.mts.map +0 -1
  47. package/dist/vue/emits.d.mts.map +0 -1
  48. package/dist/vue/expose.d.mts.map +0 -1
  49. package/dist/vue/func.d.mts.map +0 -1
  50. package/dist/vue/install.d.mts.map +0 -1
  51. package/dist/vue/props.d.mts.map +0 -1
  52. package/dist/vue/render.d.mts.map +0 -1
  53. package/dist/vue/slots.d.mts.map +0 -1
  54. package/dist/vue/with.d.mts.map +0 -1
  55. package/src/array/index.ts +0 -173
  56. package/src/async/index.ts +0 -475
  57. package/src/base64/index.ts +0 -374
  58. package/src/color/index.ts +0 -208
  59. package/src/crypto/index.ts +0 -670
  60. package/src/date/index.ts +0 -451
  61. package/src/dom/index.ts +0 -6
  62. package/src/dom/style.ts +0 -92
  63. package/src/env/index.ts +0 -169
  64. package/src/identity/index.ts +0 -144
  65. package/src/index.ts +0 -20
  66. package/src/internal/text.ts +0 -46
  67. package/src/logger/index.ts +0 -219
  68. package/src/number/index.ts +0 -235
  69. package/src/object/index.ts +0 -160
  70. package/src/storage/index.ts +0 -524
  71. package/src/string/index.ts +0 -328
  72. package/src/vue/emits.ts +0 -71
  73. package/src/vue/expose.ts +0 -11
  74. package/src/vue/func.ts +0 -17
  75. package/src/vue/index.ts +0 -13
  76. package/src/vue/install.ts +0 -185
  77. package/src/vue/props.ts +0 -39
  78. package/src/vue/render.ts +0 -41
  79. package/src/vue/slots.ts +0 -23
  80. package/src/vue/with.ts +0 -10
@@ -1,374 +0,0 @@
1
- import { encodeUtf8, getTextDecoder } from "../internal/text.js";
2
- import { secureRandomInt } from "../number/index.js";
3
-
4
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5
-
6
- /** 内部共享解码器支持的两种字符表规则。 */
7
- type Base64Variant = "standard" | "url";
8
-
9
- /**
10
- * 创建统一的 Base64 输入错误。
11
- *
12
- * @param variant - 当前解析的标准 Base64 或 Base64URL 变体。
13
- * @returns 带有具体变体名称的 `TypeError`。
14
- */
15
- const invalidBase64 = (variant: Base64Variant): TypeError => {
16
- return new TypeError(`The value is not valid ${variant === "url" ? "Base64URL" : "Base64"}.`);
17
- };
18
-
19
- /**
20
- * 校验并规范化 Base64 文本。
21
- *
22
- * @remarks 标准 Base64 允许 ASCII 空白,Base64URL 不允许;两者最终都会转换为标准字符表和完整填充。
23
- * @param value - 原始编码文本。
24
- * @param variant - 需要应用的字符表与空白规则。
25
- * @returns 使用标准字符表且长度为 4 倍数的文本。
26
- * @throws `TypeError` 当字符、填充位置或编码长度非法。
27
- */
28
- const normalizeBase64 = (value: string, variant: Base64Variant): string => {
29
- const withoutWhitespace = variant === "standard" ? value.replace(/[\t\n\f\r ]+/gu, "") : value;
30
- const pattern = variant === "url" ? /^[\w-]*={0,2}$/u : /^[A-Za-z\d+/]*={0,2}$/u;
31
- if (!pattern.test(withoutWhitespace) || withoutWhitespace.length % 4 === 1) throw invalidBase64(variant);
32
- if (withoutWhitespace.includes("=") && withoutWhitespace.length % 4 !== 0) throw invalidBase64(variant);
33
-
34
- const standard = (variant === "url" ? withoutWhitespace.replace(/-/gu, "+").replace(/_/gu, "/") : withoutWhitespace).replace(/=+$/u, "");
35
- return standard.padEnd(Math.ceil(standard.length / 4) * 4, "=");
36
- };
37
-
38
- /**
39
- * 使用固定字符表编码任意字节。
40
- *
41
- * @param bytes - 不会被修改的源字节。
42
- * @returns 带标准 `=` 填充的 Base64 文本。
43
- */
44
- const encodeBytes = (bytes: Uint8Array): string => {
45
- let result = "";
46
- for (let index = 0; index < bytes.length; index += 3) {
47
- const first = bytes[index] ?? 0;
48
- const second = bytes[index + 1];
49
- const third = bytes[index + 2];
50
- const bitmap = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
51
- result += alphabet[(bitmap >> 18) & 63] ?? "";
52
- result += alphabet[(bitmap >> 12) & 63] ?? "";
53
- result += second === undefined ? "=" : (alphabet[(bitmap >> 6) & 63] ?? "");
54
- result += third === undefined ? "=" : (alphabet[bitmap & 63] ?? "");
55
- }
56
- return result;
57
- };
58
-
59
- /**
60
- * 把 Base64 或 Base64URL 文本解码为字节。
61
- *
62
- * @remarks 最后一组未使用位必须为零,以拒绝同一字节序列的非规范编码。
63
- * @param value - 待解码文本。
64
- * @param variant - 输入使用的编码变体。
65
- * @returns 新建的字节数组。
66
- * @throws `TypeError` 当输入格式或尾部位非法。
67
- */
68
- const decodeBytes = (value: string, variant: Base64Variant): Uint8Array => {
69
- const normalized = normalizeBase64(value, variant);
70
- if (normalized.length === 0) return new Uint8Array();
71
-
72
- const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
73
- const result = new Uint8Array((normalized.length / 4) * 3 - padding);
74
- let outputIndex = 0;
75
-
76
- for (let index = 0; index < normalized.length; index += 4) {
77
- const first = alphabet.indexOf(normalized[index] ?? "");
78
- const second = alphabet.indexOf(normalized[index + 1] ?? "");
79
- const thirdCharacter = normalized[index + 2] ?? "=";
80
- const fourthCharacter = normalized[index + 3] ?? "=";
81
- const third = thirdCharacter === "=" ? 0 : alphabet.indexOf(thirdCharacter);
82
- const fourth = fourthCharacter === "=" ? 0 : alphabet.indexOf(fourthCharacter);
83
- if (first < 0 || second < 0 || third < 0 || fourth < 0) throw invalidBase64(variant);
84
-
85
- const isLast = index + 4 === normalized.length;
86
- // 填充字符对应的未使用低位必须为零,否则不同文本可以解码为同一字节序列。
87
- if (isLast && ((padding === 2 && (second & 15) !== 0) || (padding === 1 && (third & 3) !== 0))) {
88
- throw invalidBase64(variant);
89
- }
90
-
91
- const bitmap = (first << 18) | (second << 12) | (third << 6) | fourth;
92
- if (outputIndex < result.length) result[outputIndex++] = (bitmap >> 16) & 255;
93
- if (outputIndex < result.length) result[outputIndex++] = (bitmap >> 8) & 255;
94
- if (outputIndex < result.length) result[outputIndex++] = bitmap & 255;
95
- }
96
- return result;
97
- };
98
-
99
- /**
100
- * 以 Fatal 模式解码 UTF-8。
101
- *
102
- * @param bytes - 待解码字节。
103
- * @returns 解码后的 JavaScript 字符串。
104
- * @throws `TypeError` 当字节不是有效 UTF-8;原始平台异常保存在 `cause` 中。
105
- */
106
- const decodeUtf8 = (bytes: Uint8Array): string => {
107
- const textDecoder = getTextDecoder();
108
- try {
109
- return textDecoder.decode(bytes);
110
- } catch (cause) {
111
- throw new TypeError("The decoded bytes are not valid UTF-8.", { cause });
112
- }
113
- };
114
-
115
- /**
116
- * 将任意字节编码为标准 Base64。
117
- *
118
- * @param bytes - 不会被修改的字节序列。
119
- * @returns 带标准 `=` 填充的 Base64 文本。
120
- */
121
- export function encodeBase64Bytes(bytes: Uint8Array): string {
122
- return encodeBytes(bytes);
123
- }
124
-
125
- /**
126
- * 解码标准 Base64。
127
- *
128
- * @remarks 允许省略填充和包含 ASCII 空白,但拒绝非规范尾部位。
129
- * @param value - Base64 文本。
130
- * @returns 新建的字节数组。
131
- * @throws 输入非法时抛出 `TypeError`。
132
- */
133
- export function decodeBase64Bytes(value: string): Uint8Array {
134
- return decodeBytes(value, "standard");
135
- }
136
-
137
- /**
138
- * 将 UTF-8 文本编码为标准 Base64。
139
- *
140
- * @param value - 任意 Unicode 字符串。
141
- * @returns 带标准填充的 Base64 文本。
142
- * @throws 缺少 Encoding API 时抛出 `Error`。
143
- */
144
- export function encodeBase64(value: string): string {
145
- return encodeBytes(encodeUtf8(value));
146
- }
147
-
148
- /**
149
- * 将标准 Base64 解码为 UTF-8 文本。
150
- *
151
- * @param value - Base64 文本。
152
- * @returns 解码后的 Unicode 字符串。
153
- * @throws Base64 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。
154
- */
155
- export function decodeBase64(value: string): string {
156
- return decodeUtf8(decodeBase64Bytes(value));
157
- }
158
-
159
- /**
160
- * 将任意字节编码为无填充 Base64URL。
161
- *
162
- * @param bytes - 不会被修改的字节序列。
163
- * @returns 仅使用 URL 安全字母表的文本。
164
- */
165
- export function encodeBase64UrlBytes(bytes: Uint8Array): string {
166
- return encodeBytes(bytes).replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
167
- }
168
-
169
- /**
170
- * 解码 Base64URL 字节。
171
- *
172
- * @remarks 接受带填充和无填充形式,不接受空白或标准 Base64 的 `+`、`/`。
173
- * @param value - Base64URL 文本。
174
- * @returns 新建的字节数组。
175
- * @throws 输入非法时抛出 `TypeError`。
176
- */
177
- export function decodeBase64UrlBytes(value: string): Uint8Array {
178
- return decodeBytes(value, "url");
179
- }
180
-
181
- /**
182
- * 将 UTF-8 文本编码为无填充 Base64URL。
183
- *
184
- * @param value - 任意 Unicode 字符串。
185
- * @returns 仅使用 URL 安全字母表的文本。
186
- * @throws 缺少 Encoding API 时抛出 `Error`。
187
- */
188
- export function encodeBase64Url(value: string): string {
189
- return encodeBase64UrlBytes(encodeUtf8(value));
190
- }
191
-
192
- /**
193
- * 将 Base64URL 解码为 UTF-8 文本。
194
- *
195
- * @param value - 带填充或无填充的 Base64URL 文本。
196
- * @returns 解码后的 Unicode 字符串。
197
- * @throws Base64URL 或 UTF-8 非法时抛出 `TypeError`;缺少 Encoding API 时抛出 `Error`。
198
- */
199
- export function decodeBase64Url(value: string): string {
200
- return decodeUtf8(decodeBase64UrlBytes(value));
201
- }
202
-
203
- /**
204
- * 把 Latin-1 文本编码为标准 Base64。
205
- *
206
- * @param value - 每个 UTF-16 码元都必须位于 0–255 的文本。
207
- * @returns 带标准填充的 Base64 文本。
208
- * @throws `TypeError` 当文本包含 Latin-1 范围外的码元。
209
- */
210
- export function encodeLatin1Base64(value: string): string {
211
- const bytes = new Uint8Array(value.length);
212
- for (let index = 0; index < value.length; index += 1) {
213
- const code = value.charCodeAt(index);
214
- if (code > 255) throw new TypeError("The string to be encoded contains characters outside of the Latin-1 range.");
215
- bytes[index] = code;
216
- }
217
- return encodeBase64Bytes(bytes);
218
- }
219
-
220
- /**
221
- * 把标准 Base64 解码为 Latin-1 文本。
222
- *
223
- * @param value - 标准 Base64 文本;允许 ASCII 空白和省略尾部填充。
224
- * @returns 每个字节直接映射为同值 UTF-16 码元的文本。
225
- * @throws `TypeError` 当 Base64 格式或尾部位非法。
226
- */
227
- export function decodeLatin1Base64(value: string): string {
228
- const bytes = decodeBase64Bytes(value);
229
- let result = "";
230
- for (const byte of bytes) result += String.fromCharCode(byte);
231
- return result;
232
- }
233
-
234
- const randomPrefixAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
235
- const defaultRandomPrefixLength = 6;
236
-
237
- /** SecureBase64 兼容格式中一组不可变的字符插入位置。 */
238
- interface Base64PasswordDictionaryEntry {
239
- /** 原始载荷中的固定字符索引;超出短载荷长度时忽略当前条目。 */
240
- index: number;
241
- /** 从原始载荷复制字符的固定索引;数值属于持久化协议,不能重新计算。 */
242
- randomIndex: number;
243
- }
244
-
245
- /** 固定 SecureBase64 字典;索引、顺序与映射属于持久化格式,禁止修改。 */
246
- const base64PasswordDictionary: readonly Readonly<Base64PasswordDictionaryEntry>[] = Object.freeze([
247
- { index: 977, randomIndex: 188 },
248
- { index: 926, randomIndex: 201 },
249
- { index: 851, randomIndex: 225 },
250
- { index: 700, randomIndex: 255 },
251
- { index: 600, randomIndex: 268 },
252
- { index: 500, randomIndex: 277 },
253
- { index: 400, randomIndex: 288 },
254
- { index: 330, randomIndex: 327 },
255
- { index: 300, randomIndex: 180 },
256
- { index: 200, randomIndex: 178 },
257
- { index: 100, randomIndex: 124 },
258
- { index: 98, randomIndex: 95 },
259
- { index: 92, randomIndex: 90 },
260
- { index: 91, randomIndex: 87 },
261
- { index: 88, randomIndex: 84 },
262
- { index: 82, randomIndex: 79 },
263
- { index: 78, randomIndex: 71 },
264
- { index: 72, randomIndex: 69 },
265
- { index: 68, randomIndex: 66 },
266
- { index: 59, randomIndex: 55 },
267
- { index: 48, randomIndex: 43 },
268
- { index: 42, randomIndex: 37 },
269
- { index: 36, randomIndex: 30 },
270
- { index: 33, randomIndex: 27 },
271
- { index: 24, randomIndex: 20 },
272
- { index: 23, randomIndex: 18 },
273
- { index: 21, randomIndex: 16 },
274
- { index: 17, randomIndex: 14 },
275
- { index: 13, randomIndex: 9 },
276
- { index: 7, randomIndex: 4 },
277
- { index: 5, randomIndex: 3 },
278
- { index: 2, randomIndex: 1 },
279
- ]);
280
-
281
- /**
282
- * 校验 SecureBase64 兼容格式的前缀长度。
283
- *
284
- * @param length - 待校验的前缀字符数。
285
- * @throws `RangeError` 当长度不是非负安全整数。
286
- */
287
- const assertPrefixLength = (length: number): void => {
288
- if (!Number.isSafeInteger(length) || length < 0) throw new RangeError("prefixStrLength must be a non-negative safe integer.");
289
- };
290
-
291
- /**
292
- * 生成 SecureBase64 兼容格式使用的安全随机前缀。
293
- *
294
- * @remarks 前缀字符使用 Web Crypto 均匀生成,用于降低相同明文产生相同载荷的概率;它本身不构成加密。
295
- * @param length - 需要生成的字符数,调用前必须完成校验。
296
- * @returns 由历史字母表组成的文本。
297
- */
298
- const createRandomPrefix = (length: number): string => {
299
- let result = "";
300
- for (let index = 0; index < length; index += 1) {
301
- result += randomPrefixAlphabet[secureRandomInt(0, randomPrefixAlphabet.length)] ?? "";
302
- }
303
- return result;
304
- };
305
-
306
- /**
307
- * 按固定字典向 Base64 文本插入冗余字符。
308
- *
309
- * @remarks 字典按高索引到低索引排列,确保插入不会改变尚未处理的位置。
310
- * @param base64Value - 尚未插入兼容字典字符的 Base64 文本。
311
- * @returns 与旧持久化格式兼容的 SecureBase64 载荷。
312
- */
313
- const insertDictionaryCharacters = (base64Value: string): string => {
314
- let result = base64Value;
315
- for (const item of base64PasswordDictionary) {
316
- if (item.index >= base64Value.length) continue;
317
- // 旧字典的 index=100 项会在 101–124 字符载荷中越界;退回末字符可保持单字符插入协议,旧解码器仍能移除。
318
- const character = base64Value[item.randomIndex] ?? base64Value.at(-1) ?? "";
319
- result = result.slice(0, item.index) + character + result.slice(item.index);
320
- }
321
- return result;
322
- };
323
-
324
- /**
325
- * 移除历史字典插入的冗余字符。
326
- *
327
- * @param base64Value - 已移除安全随机前缀的 SecureBase64 载荷。
328
- * @returns 可交给标准 Base64 解码器的文本。
329
- */
330
- const removeDictionaryCharacters = (base64Value: string): string => {
331
- let result = base64Value;
332
- for (let index = base64PasswordDictionary.length - 1; index >= 0; index -= 1) {
333
- const item = base64PasswordDictionary[index];
334
- if (item !== undefined && item.index < base64Value.length) result = result.slice(0, item.index) + result.slice(item.index + 1);
335
- }
336
- return result;
337
- };
338
-
339
- /**
340
- * 使用固定字典和安全随机前缀编码文本。
341
- *
342
- * @remarks 给定相同的 6 字符前缀时,默认输出与旧有效载荷逐字符兼容。旧字典在 101–124 字符载荷中引用越界;这里复制末字符作为单字符回退,
343
- * 使旧删除字典流程仍能解码。传入 `0` 会同时关闭随机前缀与字典插入。
344
- * 该格式只是可逆编码,不提供加密、完整性或身份认证。
345
- * @param value - 任意可由 `encodeURIComponent` 处理的 Unicode 文本。
346
- * @param prefixLength - 随机字母前缀长度;默认 `6`。
347
- * @returns 带随机前缀和兼容字典字符的 Base64 文本;空输入返回空字符串。
348
- * @throws `RangeError` 当前缀长度不是非负安全整数;缺少 Web Crypto 或输入包含孤立代理项时保留平台错误。
349
- */
350
- export function encodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): string {
351
- if (value.length === 0) return "";
352
- assertPrefixLength(prefixLength);
353
- const prefix = createRandomPrefix(prefixLength);
354
- let encoded = encodeLatin1Base64(encodeURIComponent(value));
355
- if (prefixLength !== 0) encoded = insertDictionaryCharacters(encoded);
356
- return prefix + encoded;
357
- }
358
-
359
- /**
360
- * 解码 {@link encodeSecureBase64} 生成的 SecureBase64 兼容格式。
361
- *
362
- * @param value - SecureBase64 文本;必须使用与编码时相同的前缀长度。
363
- * @param prefixLength - 需要移除的前缀长度;默认 `6`,传入 `0` 时不移除字典字符。
364
- * @returns 解码后的 Unicode 文本;空输入返回空字符串。
365
- * @throws `RangeError` 当前缀长度不是非负安全整数;载荷、Base64 或 URI 编码非法时抛出 `TypeError` 或 `URIError`。
366
- */
367
- export function decodeSecureBase64(value: string, prefixLength: number = defaultRandomPrefixLength): string {
368
- if (value.length === 0) return "";
369
- assertPrefixLength(prefixLength);
370
- if (prefixLength > value.length) throw new TypeError("The Base64 value is shorter than its configured prefix.");
371
- let encoded = value.slice(prefixLength);
372
- if (prefixLength !== 0) encoded = removeDictionaryCharacters(encoded);
373
- return decodeURIComponent(decodeLatin1Base64(encoded));
374
- }
@@ -1,208 +0,0 @@
1
- /** 0 至 255 范围的 RGB 颜色。 */
2
- export interface RgbColor {
3
- /** 蓝色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */
4
- blue: number;
5
- /** 绿色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */
6
- green: number;
7
- /** 红色通道;必须是闭区间 `[0, 255]` 内的有限数,格式化时舍入到整数。 */
8
- red: number;
9
- }
10
-
11
- /** 带 0 至 1 Alpha 通道的 RGB 颜色。 */
12
- export interface RgbaColor extends RgbColor {
13
- /** 不透明度;必须是闭区间 `[0, 1]` 内的有限数,`0` 完全透明,`1` 完全不透明。 */
14
- alpha: number;
15
- }
16
-
17
- /**
18
- * 校验 RGB 颜色通道。
19
- *
20
- * @param value - 待校验通道值。
21
- * @param channel - 用于错误消息的通道名称。
22
- * @throws `RangeError` 当值非有限或超出 0 至 255。
23
- */
24
- const assertRgbChannel = (value: number, channel: string): void => {
25
- if (!Number.isFinite(value) || value < 0 || value > 255) {
26
- throw new RangeError(`${channel} must be a finite number from 0 through 255.`);
27
- }
28
- };
29
-
30
- /**
31
- * 校验透明度通道。
32
- *
33
- * @param value - 待校验 Alpha 值。
34
- * @throws `RangeError` 当值非有限或超出闭区间 `[0, 1]`。
35
- */
36
- const assertAlpha = (value: number): void => {
37
- if (!Number.isFinite(value) || value < 0 || value > 1) {
38
- throw new RangeError("alpha must be a finite number from 0 through 1.");
39
- }
40
- };
41
-
42
- /**
43
- * 规范化十六进制颜色文本。
44
- *
45
- * @param value - 可带 `#` 的 3、4、6 或 8 位颜色文本。
46
- * @returns 不带 `#` 的 6 或 8 位文本。
47
- * @throws `TypeError` 当长度或字符不符合十六进制颜色格式。
48
- */
49
- const normalizeHexColor = (value: string): string => {
50
- const normalized = value.startsWith("#") ? value.slice(1) : value;
51
- if (![3, 4, 6, 8].includes(normalized.length) || !/^[\dA-F]+$/iu.test(normalized)) {
52
- throw new TypeError("Expected a 3, 4, 6, or 8 digit hexadecimal color.");
53
- }
54
- return normalized.length <= 4 ? Array.from(normalized, (character) => `${character}${character}`).join("") : normalized;
55
- };
56
-
57
- /**
58
- * 格式化单个颜色通道。
59
- *
60
- * @param value - 已校验的 0 至 255 通道值。
61
- * @returns 舍入后的两位小写十六进制文本。
62
- */
63
- const formatHexChannel = (value: number): string => Math.round(value).toString(16).padStart(2, "0");
64
-
65
- /**
66
- * 解析可带可不带 `#` 的 `rgb`、`rgba`、`rrggbb` 或 `rrggbbaa`。
67
- *
68
- * @param value - 十六进制颜色文本。
69
- * @returns 标准化的 RGBA 对象;省略 Alpha 时为 1。
70
- * @throws 输入非法时抛出 `TypeError`。
71
- */
72
- export function parseHexColor(value: string): RgbaColor {
73
- const normalized = normalizeHexColor(value);
74
- return {
75
- red: Number.parseInt(normalized.slice(0, 2), 16),
76
- green: Number.parseInt(normalized.slice(2, 4), 16),
77
- blue: Number.parseInt(normalized.slice(4, 6), 16),
78
- alpha: normalized.length === 8 ? Number.parseInt(normalized.slice(6, 8), 16) / 255 : 1,
79
- };
80
- }
81
-
82
- /**
83
- * 把 RGB 或 RGBA 对象格式化为小写十六进制颜色。
84
- *
85
- * @param color - 颜色通道;RGB 会四舍五入到最近整数。
86
- * @param includeAlpha - 是否输出 Alpha;默认只在传入 Alpha 且小于 1 时输出。
87
- * @returns 小写 `#rrggbb` 或 `#rrggbbaa` 文本。
88
- * @throws 通道或 Alpha 非法时抛出 `RangeError`。
89
- */
90
- export function formatHexColor(color: RgbColor | RgbaColor, includeAlpha: boolean = "alpha" in color && color.alpha < 1): string {
91
- assertRgbChannel(color.red, "red");
92
- assertRgbChannel(color.green, "green");
93
- assertRgbChannel(color.blue, "blue");
94
- const alpha = "alpha" in color ? color.alpha : 1;
95
- assertAlpha(alpha);
96
- const rgb = `${formatHexChannel(color.red)}${formatHexChannel(color.green)}${formatHexChannel(color.blue)}`;
97
- return `#${rgb}${includeAlpha ? formatHexChannel(alpha * 255) : ""}`;
98
- }
99
-
100
- /**
101
- * 线性混合两种十六进制颜色,包括 Alpha 通道。
102
- *
103
- * @param first - `amount = 0` 时的颜色。
104
- * @param second - `amount = 1` 时的颜色。
105
- * @param amount - 0 至 1 的混合比例。
106
- * @returns 小写十六进制颜色;任一输入含透明度时保留 Alpha。
107
- * @throws 颜色非法时抛出 `TypeError`;比例非法时抛出 `RangeError`。
108
- */
109
- export function mixHexColors(first: string, second: string, amount: number): string {
110
- if (!Number.isFinite(amount) || amount < 0 || amount > 1) {
111
- throw new RangeError("amount must be a finite number from 0 through 1.");
112
- }
113
- const left = parseHexColor(first);
114
- const right = parseHexColor(second);
115
- /**
116
- * 在单个 RGBA 通道上执行与外层相同权重的线性混合。
117
- *
118
- * @param start - 第一个颜色的通道值。
119
- * @param end - 第二个颜色的通道值。
120
- * @returns 按外层 `amount` 线性混合后的通道值。
121
- */
122
- const mix = (start: number, end: number): number => start + (end - start) * amount;
123
- return formatHexColor(
124
- {
125
- red: mix(left.red, right.red),
126
- green: mix(left.green, right.green),
127
- blue: mix(left.blue, right.blue),
128
- alpha: mix(left.alpha, right.alpha),
129
- },
130
- left.alpha < 1 || right.alpha < 1
131
- );
132
- }
133
-
134
- /**
135
- * 按比例向黑色混合。
136
- *
137
- * @param color - 合法十六进制颜色。
138
- * @param amount - 0 至 1 的混合比例。
139
- * @returns 混入黑色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。
140
- */
141
- export function mixHexColorWithBlack(color: string, amount: number): string {
142
- return mixHexColors(color, "#000000", amount);
143
- }
144
-
145
- /**
146
- * 按比例向白色混合。
147
- *
148
- * @param color - 合法十六进制颜色。
149
- * @param amount - 0 至 1 的混合比例。
150
- * @returns 混入白色后的十六进制颜色;参数与异常语义见 {@link mixHexColors}。
151
- */
152
- export function mixHexColorWithWhite(color: string, amount: number): string {
153
- return mixHexColors(color, "#ffffff", amount);
154
- }
155
-
156
- /**
157
- * 按 WCAG sRGB 转换曲线线性化颜色通道。
158
- *
159
- * @param channel - 已校验的 0 至 255 通道值。
160
- * @returns 0 至 1 的线性光值。
161
- */
162
- const linearizeSrgbChannel = (channel: number): number => {
163
- const value = channel / 255;
164
- return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
165
- };
166
-
167
- /**
168
- * 计算 WCAG sRGB 相对亮度。
169
- *
170
- * @remarks Alpha 通道不会参与计算;半透明颜色应先与实际背景混合。
171
- * @param color - 合法十六进制颜色。
172
- * @returns 0 至 1 的相对亮度。
173
- * @throws 输入非法时抛出 `TypeError`。
174
- */
175
- export function relativeLuminance(color: string): number {
176
- const { red, green, blue } = parseHexColor(color);
177
- return 0.2126 * linearizeSrgbChannel(red) + 0.7152 * linearizeSrgbChannel(green) + 0.0722 * linearizeSrgbChannel(blue);
178
- }
179
-
180
- /**
181
- * 计算两种不透明颜色的 WCAG 对比度,范围 1 至 21。
182
- *
183
- * @remarks 返回比值本身,不代表特定字号或 WCAG 等级必然通过。
184
- * @param first - 第一种十六进制颜色。
185
- * @param second - 第二种十六进制颜色。
186
- * @returns 较亮颜色与较暗颜色的对比度。
187
- * @throws 输入非法时抛出 `TypeError`。
188
- */
189
- export function contrastRatio(first: string, second: string): number {
190
- const firstLuminance = relativeLuminance(first);
191
- const secondLuminance = relativeLuminance(second);
192
- const lighter = Math.max(firstLuminance, secondLuminance);
193
- const darker = Math.min(firstLuminance, secondLuminance);
194
- return (lighter + 0.05) / (darker + 0.05);
195
- }
196
-
197
- /**
198
- * 从两个候选颜色中选择与背景对比度更高的一项。
199
- *
200
- * @param background - 实际不透明背景色。
201
- * @param first - 第一候选,默认黑色。
202
- * @param second - 第二候选,默认白色。
203
- * @returns 对比度较高的原始候选字符串;相同时返回 `first`。
204
- * @throws 任一颜色非法时抛出 `TypeError`。
205
- */
206
- export function pickHigherContrastColor(background: string, first = "#000000", second = "#ffffff"): string {
207
- return contrastRatio(background, first) >= contrastRatio(background, second) ? first : second;
208
- }