@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
package/src/date/index.ts DELETED
@@ -1,451 +0,0 @@
1
- /** 可转换为日期的输入;数字始终按 Unix 毫秒时间戳处理。 */
2
- export type DateInput = Date | number | string;
3
-
4
- /** {@link formatRelativeTime} 的语言与基准时间选项。 */
5
- export interface RelativeTimeOptions {
6
- /** `Intl.RelativeTimeFormat` 使用的语言;默认固定为 `zh-CN`。 */
7
- locale?: string | readonly string[];
8
- /** 比较基准,默认当前时间。 */
9
- now?: DateInput;
10
- /** 是否允许“昨天”“明天”等文本;默认 `auto`。 */
11
- numeric?: Intl.RelativeTimeFormatNumeric;
12
- /** 输出长度;默认 `long`。 */
13
- style?: Intl.RelativeTimeFormatStyle;
14
- }
15
-
16
- /**
17
- * 校验日期算术移动量。
18
- *
19
- * @param amount - 待校验的日、月或年移动量。
20
- * @throws `RangeError` 当值不是安全整数。
21
- */
22
- const assertIntegerAmount = (amount: number): void => {
23
- if (!Number.isSafeInteger(amount)) throw new RangeError("amount must be a safe integer.");
24
- };
25
-
26
- /**
27
- * 转换并克隆有效日期。
28
- *
29
- * @remarks 数字不进行秒/毫秒猜测;字符串遵循运行时 `Date` 解析规则,跨平台代码应传带显式时区的完整 ISO 8601。
30
- * @param value - Date、Unix 毫秒时间戳或运行时可解析字符串。
31
- * @returns 与输入不共享可变状态的新 Date。
32
- * @throws 输入无效时抛出 `TypeError`。
33
- */
34
- export function toDate(value: DateInput): Date {
35
- const date = value instanceof Date ? new Date(value.getTime()) : new Date(value);
36
- if (!Number.isFinite(date.getTime())) {
37
- throw new TypeError("The value is not a valid date.");
38
- }
39
- return date;
40
- }
41
-
42
- /**
43
- * 判断输入能否转换为有效日期。
44
- *
45
- * @param value - 任意待检查值。
46
- * @returns 仅 Date、数字或字符串且时间戳有限时返回 `true`。
47
- */
48
- export function isValidDate(value: unknown): value is DateInput {
49
- if (!(value instanceof Date || typeof value === "number" || typeof value === "string")) return false;
50
- return Number.isFinite(new Date(value).getTime());
51
- }
52
-
53
- /**
54
- * 返回输入日期所在本地时区日期的 `00:00:00.000`,不修改输入。
55
- *
56
- * @param value - 有效日期输入。
57
- * @returns 新建的本地日开始时间。
58
- * @throws 输入无效时抛出 `TypeError`。
59
- */
60
- export function startOfDay(value: DateInput): Date {
61
- const date = toDate(value);
62
- date.setHours(0, 0, 0, 0);
63
- return toDate(date);
64
- }
65
-
66
- /**
67
- * 返回输入日期所在本地时区日期的 `23:59:59.999`,不修改输入。
68
- *
69
- * @param value - 有效日期输入。
70
- * @returns 新建的本地日结束时间。
71
- * @throws 输入无效时抛出 `TypeError`。
72
- */
73
- export function endOfDay(value: DateInput): Date {
74
- const date = toDate(value);
75
- date.setHours(23, 59, 59, 999);
76
- return toDate(date);
77
- }
78
-
79
- /**
80
- * 按本地日历增加整数天,不修改输入。
81
- *
82
- * @param value - 基准日期。
83
- * @param amount - 可为负数的安全整数日数。
84
- * @returns 本地日历运算后的新 Date;夏令时变化可能使实际毫秒差不等于 24 小时。
85
- * @throws 日期无效时抛出 `TypeError`;数量或结果非法时抛出 `RangeError`。
86
- */
87
- export function addDays(value: DateInput, amount: number): Date {
88
- assertIntegerAmount(amount);
89
- const date = toDate(value);
90
- date.setDate(date.getDate() + amount);
91
- return toDate(date);
92
- }
93
-
94
- /**
95
- * 按本地日历增加整数月,并把不存在的日期夹到目标月末。
96
- *
97
- * @example 1 月 31 日增加一个月会落在 2 月最后一天。
98
- * @param value - 基准日期。
99
- * @param amount - 可为负数的安全整数月数。
100
- * @returns 月份运算后的新 Date。
101
- * @throws 日期无效时抛出 `TypeError`;数量或结果非法时抛出 `RangeError`。
102
- */
103
- export function addMonths(value: DateInput, amount: number): Date {
104
- assertIntegerAmount(amount);
105
- const date = toDate(value);
106
- const originalDay = date.getDate();
107
- date.setDate(1);
108
- date.setMonth(date.getMonth() + amount);
109
- const targetMonthEnd = new Date(date.getTime());
110
- // 避免 `new Date(year, ...)` 把 0 至 99 年解释为 1900 至 1999 年。
111
- targetMonthEnd.setMonth(targetMonthEnd.getMonth() + 1, 0);
112
- const lastDay = targetMonthEnd.getDate();
113
- date.setDate(Math.min(originalDay, lastDay));
114
- return toDate(date);
115
- }
116
-
117
- /**
118
- * 按本地日历增加整数年,并沿用 {@link addMonths} 的月末夹取规则。
119
- *
120
- * @param value - 基准日期。
121
- * @param amount - 可为负数的安全整数年数。
122
- * @returns 年份运算后的新 Date。
123
- * @throws 日期无效时抛出 `TypeError`;数量或结果非法时抛出 `RangeError`。
124
- */
125
- export function addYears(value: DateInput, amount: number): Date {
126
- assertIntegerAmount(amount);
127
- return addMonths(value, amount * 12);
128
- }
129
-
130
- /**
131
- * 判断两个输入是否属于同一本地日历日。
132
- *
133
- * @param left - 第一日期。
134
- * @param right - 第二日期。
135
- * @returns 本地年、月、日均相同时返回 `true`。
136
- * @throws 任一输入无效时抛出 `TypeError`。
137
- */
138
- export function isSameDay(left: DateInput, right: DateInput): boolean {
139
- const first = toDate(left);
140
- const second = toDate(right);
141
- return first.getFullYear() === second.getFullYear() && first.getMonth() === second.getMonth() && first.getDate() === second.getDate();
142
- }
143
-
144
- /**
145
- * 判断时间是否晚于基准时间。
146
- *
147
- * @param value - 待比较时间。
148
- * @param now - 比较基准,默认调用时的当前时刻。
149
- * @returns `value` 严格晚于基准时返回 `true`。
150
- * @throws 任一输入无效时抛出 `TypeError`。
151
- */
152
- export function isFuture(value: DateInput, now: DateInput = Date.now()): boolean {
153
- return toDate(value).getTime() > toDate(now).getTime();
154
- }
155
-
156
- /**
157
- * 返回指定基准所在本地日历日的完整闭区间。
158
- *
159
- * @param value - 日期基准,默认调用时当前日期。
160
- * @returns 新建的本地日开始和结束时间二元组。
161
- * @throws 输入无效时抛出 `TypeError`。
162
- */
163
- export function getLocalDayBounds(value: DateInput = Date.now()): [start: Date, end: Date] {
164
- return [startOfDay(value), endOfDay(value)];
165
- }
166
-
167
- /**
168
- * 判断日期是否位于包含首尾的时间区间。
169
- *
170
- * @param value - 待检查日期。
171
- * @param start - 包含的起点。
172
- * @param end - 包含的终点。
173
- * @returns 时间戳位于闭区间内时返回 `true`。
174
- * @throws 无效日期抛出 `TypeError`;首尾反向时抛出 `RangeError`。
175
- */
176
- export function isWithinInterval(value: DateInput, start: DateInput, end: DateInput): boolean {
177
- const timestamp = toDate(value).getTime();
178
- const startTimestamp = toDate(start).getTime();
179
- const endTimestamp = toDate(end).getTime();
180
- if (startTimestamp > endTimestamp) throw new RangeError("start cannot be later than end.");
181
- return timestamp >= startTimestamp && timestamp <= endTimestamp;
182
- }
183
-
184
- /**
185
- * 使用 `Intl.RelativeTimeFormat` 生成人类可读相对时间。
186
- *
187
- * @remarks 秒、分钟、小时、天、周、月和年按固定时长阈值选择;这适合展示,不适合计费或日历运算。
188
- * @param value - 目标时间。
189
- * @param options - 语言、样式与比较基准。
190
- * @returns 由 `Intl.RelativeTimeFormat` 生成的本地化文本。
191
- * @throws 日期无效时抛出 `TypeError`;Locale 或 Intl 选项非法时抛出 `RangeError`。
192
- */
193
- export function formatRelativeTime(value: DateInput, options: RelativeTimeOptions = {}): string {
194
- const differenceSeconds = (toDate(value).getTime() - toDate(options.now ?? Date.now()).getTime()) / 1000;
195
- const absolute = Math.abs(differenceSeconds);
196
- let divisor: number;
197
- let unit: Intl.RelativeTimeFormatUnit;
198
- if (absolute < 60) {
199
- divisor = 1;
200
- unit = "second";
201
- } else if (absolute < 3_600) {
202
- divisor = 60;
203
- unit = "minute";
204
- } else if (absolute < 86_400) {
205
- divisor = 3_600;
206
- unit = "hour";
207
- } else if (absolute < 604_800) {
208
- divisor = 86_400;
209
- unit = "day";
210
- } else if (absolute < 2_629_800) {
211
- divisor = 604_800;
212
- unit = "week";
213
- } else if (absolute < 31_557_600) {
214
- divisor = 2_629_800;
215
- unit = "month";
216
- } else {
217
- divisor = 31_557_600;
218
- unit = "year";
219
- }
220
- const formatter = new Intl.RelativeTimeFormat(options.locale ?? "zh-CN", {
221
- numeric: options.numeric ?? "auto",
222
- style: options.style ?? "long",
223
- });
224
- return formatter.format(Math.round(differenceSeconds / divisor), unit);
225
- }
226
-
227
- /** 日期选择器单日期快捷项。 */
228
- export interface DateShortcut {
229
- /** 面向中文日期选择器的显示文本;调用方可直接用于菜单标签。 */
230
- text: string;
231
- /**
232
- * 计算快捷项对应日期。
233
- * @returns 每次调用时基于当前本地时间创建的新 `Date`,调用方可安全修改。
234
- */
235
- value: () => Date;
236
- }
237
-
238
- /** 日期选择器范围快捷项。 */
239
- export interface DateRangeShortcut {
240
- /** 面向中文日期范围选择器的显示文本;调用方可直接用于菜单标签。 */
241
- text: string;
242
- /**
243
- * 计算快捷项对应的本地日期范围。
244
- * @returns 每次调用时创建的新元组;起点为 `00:00:00.000`,终点为 `23:59:59.999`。
245
- */
246
- value: () => [start: Date, end: Date];
247
- }
248
-
249
- /** 历史快捷项允许移动的本地日历单位。 */
250
- type CalendarUnit = "day" | "month" | "year";
251
-
252
- /**
253
- * 移动本地日历字段。
254
- *
255
- * @remarks 直接使用 Date Setter,以保留历史快捷项在月底和闰年的溢出语义。
256
- * @param date - 会被原地修改的日期。
257
- * @param amount - 对目标字段增加的整数。
258
- * @param unit - 要移动的日历字段。
259
- */
260
- const shiftCalendarFieldInPlace = (date: Date, amount: number, unit: CalendarUnit): void => {
261
- switch (unit) {
262
- case "day":
263
- date.setDate(date.getDate() + amount);
264
- break;
265
- case "month":
266
- date.setMonth(date.getMonth() + amount);
267
- break;
268
- case "year":
269
- date.setFullYear(date.getFullYear() + amount);
270
- break;
271
- }
272
- };
273
-
274
- /**
275
- * 创建动态单日期快捷项。
276
- *
277
- * @param text - 日期选择器显示文本。
278
- * @param amount - 相对当前时间的移动量。
279
- * @param unit - 移动使用的日历单位。
280
- * @returns 每次执行 `value` 都重新读取当前时间的快捷项。
281
- */
282
- const createDateShortcut = (text: string, amount: number, unit: CalendarUnit): DateShortcut => ({
283
- text,
284
- value: (): Date => {
285
- const date = new Date();
286
- shiftCalendarFieldInPlace(date, amount, unit);
287
- date.setHours(0, 0, 0, 0);
288
- return date;
289
- },
290
- });
291
-
292
- /**
293
- * 创建动态日期范围快捷项。
294
- *
295
- * @param text - 日期选择器显示文本。
296
- * @param amount - 范围边界相对当前时间的移动量。
297
- * @param unit - 移动使用的日历单位。
298
- * @param towardFuture - `true` 移动结束边界,`false` 移动开始边界。
299
- * @returns 每次求值都覆盖完整本地日边界的范围快捷项。
300
- */
301
- const createRangeShortcut = (text: string, amount: number, unit: CalendarUnit, towardFuture: boolean): DateRangeShortcut => ({
302
- text,
303
- value: (): [Date, Date] => {
304
- const start = new Date();
305
- const end = new Date();
306
- shiftCalendarFieldInPlace(towardFuture ? end : start, towardFuture ? amount : -amount, unit);
307
- start.setHours(0, 0, 0, 0);
308
- end.setHours(23, 59, 59, 999);
309
- return [start, end];
310
- },
311
- });
312
-
313
- /**
314
- * 把日期转换为固定中文相对时间文本。
315
- *
316
- * @remarks 10 位以内数字按 Unix 秒处理,其余数字按毫秒处理;月份与年份按本地日历月差计算。
317
- * @param value - Date、时间戳、可解析字符串或空值。
318
- * @returns 例如“3分钟前”“半年后”;非法或空输入返回空字符串。
319
- */
320
- export function formatChineseRelativeTime(value: Date | number | string | null | undefined): string {
321
- if (value === null || value === undefined) return "";
322
- let timestamp: number;
323
- if (typeof value === "string") timestamp = new Date(value).getTime();
324
- else if (typeof value === "number") timestamp = value.toString().length <= 10 ? value * 1000 : value;
325
- else timestamp = value.getTime();
326
- if (!Number.isFinite(timestamp)) return "";
327
-
328
- const minute = 60_000;
329
- const hour = minute * 60;
330
- const day = hour * 24;
331
- const currentTimestamp = Date.now();
332
- const difference = currentTimestamp - timestamp;
333
- const minuteDifference = Math.abs(difference) / minute;
334
- const hourDifference = Math.abs(difference) / hour;
335
- const dayDifference = Math.abs(difference) / day;
336
- const currentDate = new Date(currentTimestamp);
337
- const targetDate = new Date(timestamp);
338
- const monthDifference = (currentDate.getFullYear() - targetDate.getFullYear()) * 12 + currentDate.getMonth() - targetDate.getMonth();
339
- const suffix = difference < 0 ? "后" : "前";
340
- if (Math.abs(monthDifference) >= 12) return `${Math.floor(Math.abs(monthDifference) / 12)}年${suffix}`;
341
- if (Math.abs(monthDifference) >= 6) return `半年${suffix}`;
342
- if (Math.abs(monthDifference) >= 1) return `${Math.abs(monthDifference)}月${suffix}`;
343
- if (dayDifference >= 15) return `半月${suffix}`;
344
- if (dayDifference >= 7) return `${Math.floor(dayDifference / 7)}周${suffix}`;
345
- if (dayDifference >= 1) return `${Math.floor(dayDifference)}天${suffix}`;
346
- if (hourDifference >= 1) return `${Math.floor(hourDifference)}小时${suffix}`;
347
- if (minuteDifference >= 1) return `${Math.floor(minuteDifference)}分钟${suffix}`;
348
- return "刚刚";
349
- }
350
-
351
- /**
352
- * 创建从今天到前后一个月日期的完整本地日范围。
353
- *
354
- * @param towardFuture - `true` 返回今天至一个月后,默认返回一个月前至今天。
355
- * @returns 每次调用新建的本地日首尾边界。
356
- */
357
- export function createOneMonthRangeFromToday(towardFuture = false): [start: Date, end: Date] {
358
- const start = new Date();
359
- const end = new Date();
360
- shiftCalendarFieldInPlace(towardFuture ? end : start, towardFuture ? 1 : -1, "month");
361
- start.setHours(0, 0, 0, 0);
362
- end.setHours(23, 59, 59, 999);
363
- return [start, end];
364
- }
365
-
366
- /**
367
- * 判断日期是否晚于调用时的当前时刻。
368
- *
369
- * @param time - 待比较日期。
370
- * @returns 时间戳严格晚于 `Date.now()` 时返回 `true`。
371
- */
372
- export function isDateAfterNow(time: Date): boolean {
373
- return time.getTime() > Date.now();
374
- }
375
-
376
- /**
377
- * 根据浏览器本地小时返回固定中文问候语。
378
- *
379
- * @returns 与当前时段对应的中文欢迎文本。
380
- */
381
- export function getLocalTimeGreeting(): string {
382
- const hour = new Date().getHours();
383
- if (hour < 5) return "夜深了,注意身体哦!";
384
- if (hour < 9) return "早上好!欢迎回来!";
385
- if (hour < 12) return "上午好!欢迎回来!";
386
- if (hour < 14) return "中午好!欢迎回来!";
387
- if (hour < 18) return "下午好!欢迎回来!";
388
- if (hour < 24) return "晚上好!欢迎回来!";
389
- return "您好!欢迎回来!";
390
- }
391
-
392
- /**
393
- * 创建面向过去或未来的常用完整日期范围快捷项。
394
- *
395
- * @param towardFuture - `true` 创建未来范围,默认创建历史范围。
396
- * @returns 每次求值都会重新读取当前时间的范围快捷项。
397
- */
398
- export function createDateRangeShortcuts(towardFuture = false): DateRangeShortcut[] {
399
- return towardFuture
400
- ? [
401
- createRangeShortcut("后1天", 1, "day", true),
402
- createRangeShortcut("后3天", 3, "day", true),
403
- createRangeShortcut("后1周", 7, "day", true),
404
- createRangeShortcut("后1月", 1, "month", true),
405
- createRangeShortcut("后3月", 3, "month", true),
406
- createRangeShortcut("后6月", 6, "month", true),
407
- createRangeShortcut("后1年", 1, "year", true),
408
- ]
409
- : [
410
- createRangeShortcut("近1天", 1, "day", false),
411
- createRangeShortcut("近3天", 3, "day", false),
412
- createRangeShortcut("近1周", 7, "day", false),
413
- createRangeShortcut("近1月", 1, "month", false),
414
- createRangeShortcut("近3月", 3, "month", false),
415
- createRangeShortcut("近6月", 6, "month", false),
416
- createRangeShortcut("近1年", 1, "year", false),
417
- ];
418
- }
419
-
420
- /**
421
- * 创建面向过去或未来的常用单日期快捷项。
422
- *
423
- * @param towardFuture - `true` 创建未来日期,默认创建历史日期。
424
- * @returns 每次求值都会重新读取当前时间的单日期快捷项。
425
- */
426
- export function createDateShortcuts(towardFuture = false): DateShortcut[] {
427
- return towardFuture
428
- ? [
429
- createDateShortcut("今天", 0, "day"),
430
- createDateShortcut("明天", 1, "day"),
431
- createDateShortcut("一周后", 7, "day"),
432
- createDateShortcut("一月后", 1, "month"),
433
- createDateShortcut("一年后", 1, "year"),
434
- ]
435
- : [
436
- createDateShortcut("今天", 0, "day"),
437
- createDateShortcut("昨天", -1, "day"),
438
- createDateShortcut("一周前", -7, "day"),
439
- createDateShortcut("一月前", -1, "month"),
440
- createDateShortcut("一年前", -1, "year"),
441
- ];
442
- }
443
-
444
- /**
445
- * 返回今天的本地零点。
446
- *
447
- * @returns 新建的 `00:00:00.000` Date。
448
- */
449
- export function getStartOfToday(): Date {
450
- return startOfDay(new Date());
451
- }
package/src/dom/index.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * DOM-adjacent CSS value helpers with no import-time browser access.
3
- *
4
- * @packageDocumentation
5
- */
6
- export * from "./style.js";
package/src/dom/style.ts DELETED
@@ -1,92 +0,0 @@
1
- /** 可序列化为内联 CSS 的单个值。 */
2
- export type StyleValue = number | string | null | undefined;
3
-
4
- /** camelCase、kebab-case 或 CSS 自定义属性组成的只读样式对象。 */
5
- export type StyleObject = Readonly<Record<string, StyleValue>>;
6
-
7
- /** 字符串、样式对象、嵌套数组或空值。 */
8
- export type StyleInput = string | StyleObject | readonly StyleInput[] | null | undefined;
9
-
10
- /**
11
- * 判断 StyleInput 是否为递归样式数组。
12
- *
13
- * @param value - 待缩小的样式输入。
14
- * @returns 是只读样式数组时返回 `true`。
15
- */
16
- const isStyleArray = (value: StyleInput): value is readonly StyleInput[] => Array.isArray(value);
17
-
18
- /**
19
- * 判断文本是否可按十进制数值追加 CSS 单位。
20
- *
21
- * @param value - 已去除外围空白的文本。
22
- * @returns 有限十进制数值返回 `true`;二、八、十六进制前缀返回 `false`。
23
- */
24
- const isNumericString = (value: string): boolean => {
25
- if (value.length === 0 || !Number.isFinite(Number(value))) return false;
26
- const unsigned = value.startsWith("+") || value.startsWith("-") ? value.slice(1) : value;
27
- return !/^0[box]/iu.test(unsigned);
28
- };
29
-
30
- /**
31
- * 把 JavaScript 样式属性名转换为 CSS 属性名。
32
- *
33
- * @param key - camelCase、kebab-case 或 CSS 自定义属性名。
34
- * @returns kebab-case 属性名;`--` 自定义属性保持原样。
35
- */
36
- const toCssPropertyName = (key: string): string => {
37
- if (key.startsWith("--")) return key;
38
- const normalized = key.startsWith("ms") ? `-${key}` : key;
39
- return normalized.replace(/([A-Z])/gu, "-$1").toLowerCase();
40
- };
41
-
42
- /**
43
- * 为数值或纯数字字符串添加 CSS 单位。
44
- *
45
- * @param value - 数字、数字字符串或已有单位的 CSS 值;空值返回空字符串。
46
- * @param unit - 非零数字使用的单位,默认 `px`。
47
- * @returns 零统一返回 `"0"`;非数字字符串保持原样。
48
- * @throws `RangeError` 当数字非有限或单位为空。
49
- */
50
- export function addCssUnit(value?: string | number | null, unit = "px"): string {
51
- if (value === null || value === undefined || value === "") return "";
52
- if (unit.length === 0) throw new RangeError("unit cannot be empty.");
53
- if (typeof value === "number") {
54
- if (!Number.isFinite(value)) throw new RangeError("value must be finite.");
55
- return value === 0 ? "0" : `${value}${unit}`;
56
- }
57
- const trimmed = value.trim();
58
- if (!isNumericString(trimmed)) return value;
59
- return Number(trimmed) === 0 ? "0" : `${trimmed}${unit}`;
60
- }
61
-
62
- /**
63
- * 将样式字符串、对象或嵌套数组序列化为内联 CSS。
64
- *
65
- * @remarks 本函数只负责结构转换,不是 CSS 安全清洗器。不可信值必须由调用方按照
66
- * 实际渲染上下文验证,尤其不能允许用户控制属性名、`url()` 或自定义属性内容。
67
- * 数字不会自动附加单位;需要长度单位时应先调用 {@link addCssUnit}。
68
- * @param styles - 可嵌套样式输入;后出现的声明由 CSS 层叠规则覆盖先前声明。
69
- * @returns 以分号结束、以空格分隔的 CSS 声明字符串。
70
- * @throws `RangeError` 当对象中包含 `NaN` 或无穷数字。
71
- */
72
- export function serializeStyle(styles: StyleInput): string {
73
- if (styles === null || styles === undefined || styles === "") return "";
74
- if (isStyleArray(styles)) {
75
- return styles
76
- .map((item) => serializeStyle(item))
77
- .filter((item) => item.length > 0)
78
- .join(" ");
79
- }
80
- if (typeof styles === "string") {
81
- const value = styles.trim();
82
- return value.length === 0 ? "" : value.endsWith(";") ? value : `${value};`;
83
- }
84
-
85
- return Object.entries(styles)
86
- .filter(([, value]) => value !== null && value !== undefined && value !== "")
87
- .map(([key, value]) => {
88
- if (typeof value === "number" && !Number.isFinite(value)) throw new RangeError(`Style property "${key}" must be finite.`);
89
- return `${toCssPropertyName(key)}:${String(value)};`;
90
- })
91
- .join(" ");
92
- }