@planet-matrix/mobius-model 0.5.0 → 0.6.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.
Files changed (175) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +123 -36
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +183 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -118
  12. package/src/basic/function.ts +81 -62
  13. package/src/basic/is.ts +152 -71
  14. package/src/basic/promise.ts +29 -8
  15. package/src/basic/string.ts +2 -33
  16. package/src/color/README.md +105 -0
  17. package/src/color/index.ts +3 -0
  18. package/src/color/internal.ts +42 -0
  19. package/src/color/rgb/analyze.ts +236 -0
  20. package/src/color/rgb/construct.ts +130 -0
  21. package/src/color/rgb/convert.ts +227 -0
  22. package/src/color/rgb/derive.ts +303 -0
  23. package/src/color/rgb/index.ts +6 -0
  24. package/src/color/rgb/internal.ts +208 -0
  25. package/src/color/rgb/parse.ts +302 -0
  26. package/src/color/rgb/serialize.ts +144 -0
  27. package/src/color/types.ts +57 -0
  28. package/src/color/xyz/analyze.ts +80 -0
  29. package/src/color/xyz/construct.ts +19 -0
  30. package/src/color/xyz/convert.ts +71 -0
  31. package/src/color/xyz/index.ts +3 -0
  32. package/src/color/xyz/internal.ts +23 -0
  33. package/src/css/README.md +93 -0
  34. package/src/css/class.ts +559 -0
  35. package/src/css/index.ts +1 -0
  36. package/src/encoding/README.md +66 -79
  37. package/src/encoding/base64.ts +13 -4
  38. package/src/environment/README.md +97 -0
  39. package/src/environment/basic.ts +26 -0
  40. package/src/environment/device.ts +311 -0
  41. package/src/environment/feature.ts +285 -0
  42. package/src/environment/geo.ts +337 -0
  43. package/src/environment/index.ts +7 -0
  44. package/src/environment/runtime.ts +400 -0
  45. package/src/environment/snapshot.ts +60 -0
  46. package/src/environment/variable.ts +239 -0
  47. package/src/event/README.md +90 -0
  48. package/src/event/class-event-proxy.ts +228 -0
  49. package/src/event/common.ts +19 -0
  50. package/src/event/event-manager.ts +203 -0
  51. package/src/event/index.ts +4 -0
  52. package/src/event/instance-event-proxy.ts +186 -0
  53. package/src/event/internal.ts +24 -0
  54. package/src/exception/README.md +96 -0
  55. package/src/exception/browser.ts +219 -0
  56. package/src/exception/index.ts +4 -0
  57. package/src/exception/nodejs.ts +169 -0
  58. package/src/exception/normalize.ts +106 -0
  59. package/src/exception/types.ts +99 -0
  60. package/src/identifier/README.md +92 -0
  61. package/src/identifier/id.ts +119 -0
  62. package/src/identifier/index.ts +2 -0
  63. package/src/identifier/uuid.ts +187 -0
  64. package/src/index.ts +16 -1
  65. package/src/log/README.md +79 -0
  66. package/src/log/index.ts +5 -0
  67. package/src/log/log-emitter.ts +72 -0
  68. package/src/log/log-record.ts +10 -0
  69. package/src/log/log-scheduler.ts +74 -0
  70. package/src/log/log-type.ts +8 -0
  71. package/src/log/logger.ts +543 -0
  72. package/src/orchestration/README.md +89 -0
  73. package/src/orchestration/coordination/barrier.ts +214 -0
  74. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  75. package/src/orchestration/coordination/errors.ts +98 -0
  76. package/src/orchestration/coordination/index.ts +16 -0
  77. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  78. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  79. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  80. package/src/orchestration/coordination/mutex.ts +257 -0
  81. package/src/orchestration/coordination/permit.ts +127 -0
  82. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  83. package/src/orchestration/coordination/semaphore.ts +280 -0
  84. package/src/orchestration/index.ts +1 -0
  85. package/src/random/README.md +55 -86
  86. package/src/random/index.ts +1 -1
  87. package/src/random/string.ts +35 -0
  88. package/src/reactor/README.md +4 -0
  89. package/src/reactor/reactor-core/primitive.ts +9 -9
  90. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  91. package/src/singleton/README.md +79 -0
  92. package/src/singleton/factory.ts +55 -0
  93. package/src/singleton/index.ts +2 -0
  94. package/src/singleton/manager.ts +204 -0
  95. package/src/storage/README.md +107 -0
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/table.ts +449 -0
  98. package/src/timer/README.md +86 -0
  99. package/src/timer/expiration/expiration-manager.ts +594 -0
  100. package/src/timer/expiration/index.ts +3 -0
  101. package/src/timer/expiration/min-heap.ts +208 -0
  102. package/src/timer/expiration/remaining-manager.ts +241 -0
  103. package/src/timer/index.ts +1 -0
  104. package/src/type/README.md +54 -307
  105. package/src/type/class.ts +2 -2
  106. package/src/type/index.ts +14 -14
  107. package/src/type/is.ts +265 -2
  108. package/src/type/object.ts +37 -0
  109. package/src/type/string.ts +7 -2
  110. package/src/type/tuple.ts +6 -6
  111. package/src/type/union.ts +16 -0
  112. package/src/web/README.md +77 -0
  113. package/src/web/capture.ts +35 -0
  114. package/src/web/clipboard.ts +97 -0
  115. package/src/web/dom.ts +117 -0
  116. package/src/web/download.ts +16 -0
  117. package/src/web/event.ts +46 -0
  118. package/src/web/index.ts +10 -0
  119. package/src/web/local-storage.ts +113 -0
  120. package/src/web/location.ts +28 -0
  121. package/src/web/permission.ts +172 -0
  122. package/src/web/script-loader.ts +432 -0
  123. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  124. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  125. package/tests/unit/basic/array.spec.ts +1 -1
  126. package/tests/unit/basic/stream.spec.ts +1 -1
  127. package/tests/unit/basic/string.spec.ts +0 -9
  128. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  129. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  130. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  131. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  132. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  133. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  134. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  135. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  136. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  137. package/tests/unit/css/class.spec.ts +157 -0
  138. package/tests/unit/environment/basic.spec.ts +20 -0
  139. package/tests/unit/environment/device.spec.ts +146 -0
  140. package/tests/unit/environment/feature.spec.ts +388 -0
  141. package/tests/unit/environment/geo.spec.ts +111 -0
  142. package/tests/unit/environment/runtime.spec.ts +364 -0
  143. package/tests/unit/environment/snapshot.spec.ts +4 -0
  144. package/tests/unit/environment/variable.spec.ts +190 -0
  145. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  146. package/tests/unit/event/event-manager.spec.ts +246 -0
  147. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  148. package/tests/unit/exception/browser.spec.ts +213 -0
  149. package/tests/unit/exception/nodejs.spec.ts +144 -0
  150. package/tests/unit/exception/normalize.spec.ts +57 -0
  151. package/tests/unit/identifier/id.spec.ts +71 -0
  152. package/tests/unit/identifier/uuid.spec.ts +85 -0
  153. package/tests/unit/log/log-emitter.spec.ts +33 -0
  154. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  155. package/tests/unit/log/log-type.spec.ts +7 -0
  156. package/tests/unit/log/logger.spec.ts +222 -0
  157. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  158. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  159. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  160. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  161. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  162. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  163. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  164. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  165. package/tests/unit/random/string.spec.ts +11 -0
  166. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  167. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  168. package/tests/unit/singleton/singleton.spec.ts +49 -0
  169. package/tests/unit/storage/table.spec.ts +620 -0
  170. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  171. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  172. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  173. package/.oxlintrc.json +0 -5
  174. package/src/random/uuid.ts +0 -103
  175. package/tests/unit/random/uuid.spec.ts +0 -37
@@ -0,0 +1,23 @@
1
+ import {
2
+ internalAssertAlpha,
3
+ internalAssertNonNegativeFinite,
4
+ } from "../internal.ts"
5
+
6
+ import type { XyzColorValue } from "../types.ts"
7
+
8
+ /**
9
+ * 规范化 XYZ 颜色值结构。
10
+ */
11
+ export const internalNormalizeXyzColorValue = (color: XyzColorValue): XyzColorValue => {
12
+ internalAssertNonNegativeFinite(color.x, "Color channel x")
13
+ internalAssertNonNegativeFinite(color.y, "Color channel y")
14
+ internalAssertNonNegativeFinite(color.z, "Color channel z")
15
+ internalAssertAlpha(color.alpha)
16
+
17
+ return {
18
+ x: color.x,
19
+ y: color.y,
20
+ z: color.z,
21
+ alpha: color.alpha,
22
+ }
23
+ }
@@ -0,0 +1,93 @@
1
+ # CSS
2
+
3
+ ## Description
4
+
5
+ CSS 模块提供围绕 CSS 类名集合(class set)的表达、转换和组合能力,用于在字符串、数组、对象三种常见表示之间建立稳定、可解释且可组合的公共语义。
6
+
7
+ 它关注的不是样式体系本身,而是“类名集合如何被表达和变换”。对象表示除了能表达已启用类名,还可以显式保留值为 `false` 的键,以承载“已知但未启用”的状态。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 CSS 模块时,应把它看成“类名集合模型”,而不是“字符串拼接工具”。这个模块要解决的是不同输入形态之间的语义对齐问题:同一组类名可能来自 `.foo.bar`、`foo bar`、`["foo", "bar"]` 或 `{ foo: true, bar: false }`,进入系统后,应尽快被整理成清楚、稳定、可继续变换的集合语义。
12
+
13
+ 三种表示的语义并不完全相同:
14
+
15
+ - 字符串与数组主要表达“当前启用中的类名集合”。
16
+ - 对象既能表达启用状态,也能保留值为 `false` 的键,从而表达“已知但未启用”的状态信息。
17
+ - 空类名本身不承载有效样式语义,应在公共转换路径中尽早过滤。
18
+
19
+ 因此,这个模块追求的不是机械的完全可逆,而是转换结果为什么会这样始终容易解释。如果某个转换会丢失对象中 `false` 键这类附加状态,那是表示层语义差异,而不是实现缺陷。
20
+
21
+ ## For Using
22
+
23
+ 当应用需要在组件层、渲染层、样式工具层之间传递类名集合时,可以使用这个模块统一管理类名表达,而不是在调用点反复手写拆分、拼接、去重和真假值过滤。
24
+
25
+ 当前公共能力大致可以按以下几类理解:
26
+
27
+ - 类名表示类型:用于区分 `ClassString`、`ClassArray`、`ClassObject` 以及三者联合的输入形态。
28
+ - 表示间转换:用于在字符串、数组、对象之间显式切换,并根据目标表示保留其应有语义。
29
+ - 统一归一化:用于把混合输入整理成某一种稳定目标表示,减少下游接口摩擦。
30
+ - 前缀处理:用于给类名补前缀、移除前缀,适合组件库或命名空间约定场景。
31
+ - 集合操作:用于添加、移除、切换、替换、判断是否包含某组类名。
32
+
33
+ 这个模块不适合承载样式命名规范、主题系统、选择器优先级策略或任何特定框架的样式约定。它只负责类名集合语义,不负责更高层的样式设计决策。
34
+
35
+ ## For Contributing
36
+
37
+ 贡献 CSS 模块时,应优先判断新增能力是否真正表达了稳定的类名集合语义,而不是某个框架、某个模板语法或某种局部命名习惯的便捷包装。只有前者才适合作为长期公共 API。
38
+
39
+ 在扩展时,应优先遵守以下边界:
40
+
41
+ - 公共能力应围绕类名集合的表达、转换、组合和集合操作展开。
42
+ - 不要把具体框架的模板语法、渲染约定或样式系统规则直接写入模块公共语义。
43
+ - 表示之间的转换应优先保证结果可解释,而不是追求表面上的完全可逆。
44
+ - 对象表示中的 `false` 键是有意义的显式状态;字符串与数组表示则只表达启用中的类名。
45
+
46
+ ### JSDoc 注释格式要求
47
+
48
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
49
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
50
+ - 如果描述后还有其他内容,应在描述后加一个空行。
51
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
52
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
53
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
54
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
55
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
56
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
57
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
58
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
59
+
60
+ ### 实现规范要求
61
+
62
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
63
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
64
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
65
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
66
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
67
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
68
+ - 辅助元素永远不要公开导出。
69
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/css/internal.ts`;若当前没有必要,不必为了形式强行拆分。
70
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
71
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
72
+ - 子模块不需要有自己的 `README.md`。
73
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
74
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
75
+ - 涉及字符串解析、空白规整、点号分隔、真假值过滤等规则时,应让行为保持直白、可预测,而不要让调用方猜测隐含规范。
76
+
77
+ ### 导出策略要求
78
+
79
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
80
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
81
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
82
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的类名集合语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
83
+
84
+ ### 测试要求
85
+
86
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
87
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
88
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
89
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
90
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
91
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
92
+ - 模块的单元测试文件目录是 `./tests/unit/css`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/css/<sub-module-name>`。
93
+ - 测试应重点覆盖三种表示之间的语义差异,尤其是空类名过滤、对象 `false` 键保留与集合操作结果是否可解释。
@@ -0,0 +1,559 @@
1
+ /**
2
+ * 表示以空格分隔的 CSS 类名字串。
3
+ */
4
+ export type ClassString = string
5
+
6
+ /**
7
+ * 表示按顺序保存的 CSS 类名数组。
8
+ */
9
+ export type ClassArray = string[]
10
+
11
+ /**
12
+ * 表示以类名为键、以启用状态为值的 CSS 类名对象。
13
+ *
14
+ * 该表示允许保留值为 `false` 的键,以表达已知但当前未启用的类名状态。
15
+ */
16
+ export type ClassObject = Record<string, boolean>
17
+
18
+ /**
19
+ * 表示 CSS 类名的三种公共表达形式:字符串、数组或对象。
20
+ */
21
+ export type ClassUnion = ClassString | ClassArray | ClassObject
22
+
23
+ /**
24
+ * 规范化类名字串,使其适合作为统一的字符串表示。
25
+ *
26
+ * 该函数会将 `.` 视为分隔符,将连续空白折叠为一个空格,并移除首尾空白。
27
+ *
28
+ * @example
29
+ * ```
30
+ * // Expect: 'mobius-base mobius-theme--light'
31
+ * const example1 = neatenClassString('mobius-base mobius-theme--light')
32
+ *
33
+ * // Expect: 'mobius-base mobius-theme--light'
34
+ * const example2 = neatenClassString('.mobius-base.mobius-theme--light')
35
+ *
36
+ * // Expect: 'mobius-base mobius-theme--light'
37
+ * const example3 = neatenClassString(' .mobius-base mobius-theme--light ')
38
+ * ```
39
+ */
40
+ export const neatenClassString = (str: string): ClassString => {
41
+ const classString = str.replaceAll('.', ' ').replaceAll(/\s+/g, ' ').trim()
42
+ return classString
43
+ }
44
+
45
+ /**
46
+ * 将类名字串转换为类名数组。
47
+ *
48
+ * 输入会先经过 `neatenClassString` 规范化,再按空格拆分,并移除空项。
49
+ *
50
+ * @example
51
+ * ```
52
+ * // Expect: ['mobius-base', 'mobius-theme--light']
53
+ * const example1 = classStringToClassArray('mobius-base mobius-theme--light')
54
+ *
55
+ * // Expect: ['mobius-base', 'mobius-theme--light']
56
+ * const example2 = classStringToClassArray('.mobius-base.mobius-theme--light')
57
+ *
58
+ * // Expect: ['mobius-base', 'mobius-theme--light']
59
+ * const example3 = classStringToClassArray(' .mobius-base mobius-theme--light ')
60
+ * ```
61
+ */
62
+ export const classStringToClassArray = (str: ClassString): ClassArray => {
63
+ const classArray = neatenClassString(str).split(' ').filter(s => s.length !== 0)
64
+ return classArray
65
+ }
66
+
67
+ /**
68
+ * 将类名数组转换为布尔对象表示。
69
+ *
70
+ * 数组中的每个非空类名都会映射为值为 `true` 的对象键。
71
+ *
72
+ * @example
73
+ * ```
74
+ * // Expect: { 'mobius-base': true, 'mobius-theme--light': true }
75
+ * const example1 = classArrayToClassObject(['mobius-base', 'mobius-theme--light'])
76
+ *
77
+ * // Expect: { 'mobius-base': true }
78
+ * const example2 = classArrayToClassObject(['mobius-base', ''])
79
+ *
80
+ * // Expect: {}
81
+ * const example3 = classArrayToClassObject([])
82
+ * ```
83
+ */
84
+ export const classArrayToClassObject = (arr: ClassArray): ClassObject => {
85
+ const classObject: ClassObject = {}
86
+ arr.filter(s => s.length !== 0).forEach(s => {
87
+ classObject[s] = true
88
+ })
89
+ return classObject
90
+ }
91
+
92
+ /**
93
+ * 将类名字串直接转换为布尔对象表示。
94
+ *
95
+ * 该函数会先把字符串拆分为类名数组,再将每个类名映射为值为 `true` 的对象键。
96
+ *
97
+ * @example
98
+ * ```
99
+ * // Expect: { 'mobius-base': true, 'mobius-theme--light': true }
100
+ * const example1 = classStringToClassObject('mobius-base mobius-theme--light')
101
+ *
102
+ * // Expect: { 'mobius-base': true, 'mobius-theme--light': true }
103
+ * const example2 = classStringToClassObject('.mobius-base.mobius-theme--light')
104
+ * ```
105
+ */
106
+ export const classStringToClassObject = (str: ClassString): ClassObject => {
107
+ const classArray = classStringToClassArray(str)
108
+ const classObject = classArrayToClassObject(classArray)
109
+ return classObject
110
+ }
111
+
112
+ /**
113
+ * 将布尔对象表示转换为类名数组。
114
+ *
115
+ * 值为 `false` 的类名和空类名会被忽略。
116
+ *
117
+ * @example
118
+ * ```
119
+ * // Expect: ['active', 'primary']
120
+ * const example1 = classObjectToClassArray({ active: true, disabled: false, primary: true })
121
+ *
122
+ * // Expect: ['active']
123
+ * const example2 = classObjectToClassArray({ '': true, active: true, disabled: false })
124
+ * ```
125
+ */
126
+ export const classObjectToClassArray = (obj: ClassObject): ClassArray => {
127
+ const classArray: string[] = []
128
+ Object.entries(obj)
129
+ .filter(([key, value]) => key.length !== 0 && value)
130
+ .forEach(([key, _]) => {
131
+ classArray.push(key)
132
+ })
133
+ return classArray
134
+ }
135
+
136
+ /**
137
+ * 将类名数组按空格连接为类名字串。
138
+ *
139
+ * 空字符串项会被过滤,以避免产生多余空格。
140
+ *
141
+ * @example
142
+ * ```
143
+ * // Expect: 'mobius-base mobius-theme--light'
144
+ * const example1 = classArrayToClassString(['mobius-base', 'mobius-theme--light'])
145
+ *
146
+ * // Expect: 'mobius-base mobius-theme--light'
147
+ * const example2 = classArrayToClassString(['mobius-base', '', 'mobius-theme--light'])
148
+ * ```
149
+ */
150
+ export const classArrayToClassString = (arr: ClassArray): ClassString => {
151
+ const classString = arr.filter(s => s.length !== 0).join(' ')
152
+ return classString
153
+ }
154
+
155
+ /**
156
+ * 将布尔对象表示转换为类名字串。
157
+ *
158
+ * 该函数会忽略值为 `false` 的类名和空类名,并保留其余类名的迭代顺序。
159
+ *
160
+ * @example
161
+ * ```
162
+ * // Expect: 'active primary'
163
+ * const example1 = classObjectToClassString({ active: true, disabled: false, primary: true })
164
+ *
165
+ * // Expect: 'active'
166
+ * const example2 = classObjectToClassString({ '': true, active: true, disabled: false })
167
+ * ```
168
+ */
169
+ export const classObjectToClassString = (obj: ClassObject): ClassString => {
170
+ const classArray = classObjectToClassArray(obj)
171
+ const classString = classArrayToClassString(classArray)
172
+ return classString
173
+ }
174
+
175
+ /**
176
+ * 将任意公共类名表示转换为字符串表示。
177
+ *
178
+ * 如果输入本身已经是字符串,则原样返回;数组和对象会分别经过对应的序列化流程。
179
+ *
180
+ * @example
181
+ * ```
182
+ * // Expect: ' .button active '
183
+ * const example1 = toClassString(' .button active ')
184
+ *
185
+ * // Expect: 'button active'
186
+ * const example2 = toClassString(['button', 'active'])
187
+ *
188
+ * // Expect: 'button active'
189
+ * const example3 = toClassString({ button: true, active: true, disabled: false })
190
+ * ```
191
+ */
192
+ export const toClassString = (tar: ClassUnion): ClassString => {
193
+ if (typeof tar === 'string') {
194
+ return tar
195
+ } else if (Array.isArray(tar)) {
196
+ return classArrayToClassString(tar)
197
+ } else {
198
+ return classObjectToClassString(tar)
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 将任意公共类名表示转换为数组表示。
204
+ *
205
+ * 如果输入本身已经是数组,则会在过滤空类名后返回一个浅拷贝,以避免调用方共享同一数组实例。
206
+ *
207
+ * @example
208
+ * ```
209
+ * // Expect: ['button', 'active']
210
+ * const example1 = toClassArray('button active')
211
+ *
212
+ * // Expect: ['button', 'active']
213
+ * const example2 = toClassArray(['button', 'active'])
214
+ *
215
+ * // Expect: ['button', 'active']
216
+ * const example3 = toClassArray({ button: true, disabled: false, active: true })
217
+ * ```
218
+ */
219
+ export const toClassArray = (tar: ClassUnion): ClassArray => {
220
+ if (typeof tar === 'string') {
221
+ return classStringToClassArray(tar)
222
+ } else if (Array.isArray(tar)) {
223
+ return tar.filter(s => s.length !== 0)
224
+ } else {
225
+ return classObjectToClassArray(tar)
226
+ }
227
+ }
228
+
229
+ /**
230
+ * 将任意公共类名表示转换为对象表示。
231
+ *
232
+ * 如果输入本身已经是对象,则会在过滤空类名后返回一个浅拷贝,以避免外部直接共享内部结果。
233
+ *
234
+ * @example
235
+ * ```
236
+ * // Expect: { button: true, active: true }
237
+ * const example1 = toClassObject('button active')
238
+ *
239
+ * // Expect: { button: true, active: true }
240
+ * const example2 = toClassObject(['button', 'active'])
241
+ *
242
+ * // Expect: { button: true, active: false }
243
+ * const example3 = toClassObject({ button: true, active: false, '': true })
244
+ * ```
245
+ */
246
+ export const toClassObject = (tar: ClassUnion): ClassObject => {
247
+ if (typeof tar === 'string') {
248
+ return classStringToClassObject(tar)
249
+ } else if (Array.isArray(tar)) {
250
+ return classArrayToClassObject(tar)
251
+ } else {
252
+ const classObject: ClassObject = {}
253
+ Object.entries(tar).forEach(([key, value]) => {
254
+ if (key.length !== 0) {
255
+ classObject[key] = value
256
+ }
257
+ })
258
+ return classObject
259
+ }
260
+ }
261
+
262
+ /**
263
+ * 按目标值的外部表示,将类名集合格式化为相同形态。
264
+ *
265
+ * 该函数适合在内部统一按对象进行计算后,再把结果还原为调用方原本使用的表示。
266
+ *
267
+ * @example
268
+ * ```
269
+ * // Expect: 'button active'
270
+ * const example1 = formatClassToTarget('', ['button', 'active'])
271
+ *
272
+ * // Expect: ['button', 'active']
273
+ * const example2 = formatClassToTarget([], 'button active')
274
+ *
275
+ * // Expect: { button: true, active: true }
276
+ * const example3 = formatClassToTarget({}, ['button', 'active'])
277
+ * ```
278
+ */
279
+ export const formatClassToTarget = <T extends ClassUnion>(target: T, cls: ClassUnion): T => {
280
+ if (typeof target === 'string') {
281
+ // oxlint-disable-next-line no-unsafe-type-assertion
282
+ return toClassString(cls) as T
283
+ } else if (Array.isArray(target)) {
284
+ // oxlint-disable-next-line no-unsafe-type-assertion
285
+ return toClassArray(cls) as T
286
+ } else {
287
+ // oxlint-disable-next-line no-unsafe-type-assertion
288
+ return toClassObject(cls) as T
289
+ }
290
+ }
291
+
292
+ /**
293
+ * 为类名集合中的每一项补上指定前缀。
294
+ *
295
+ * 已经带有该前缀的类名会保持不变。
296
+ *
297
+ * @example
298
+ * ```
299
+ * // Expect: 'pm-button pm-active'
300
+ * const example1 = prefixClassWith('pm-', 'button active')
301
+ *
302
+ * // Expect: ['pm-button', 'pm-active']
303
+ * const example2 = prefixClassWith('pm-', ['button', 'pm-active'])
304
+ *
305
+ * // Expect: { 'pm-button': true, 'pm-active': true }
306
+ * const example3 = prefixClassWith('pm-', { button: true, 'pm-active': true })
307
+ * ```
308
+ */
309
+ export const prefixClassWith = <T extends ClassUnion>(prefix: string, cls: T): T => {
310
+ if (typeof cls === 'string') {
311
+ const classArray = classStringToClassArray(cls).map(item =>
312
+ item.startsWith(prefix) ? item : `${prefix}${item}`
313
+ )
314
+ const classString = classArrayToClassString(classArray)
315
+ // oxlint-disable-next-line no-unsafe-type-assertion
316
+ return classString as T
317
+ } else if (Array.isArray(cls)) {
318
+ const classArray = cls.filter(item => item.length !== 0).map(item =>
319
+ item.startsWith(prefix) ? item : `${prefix}${item}`
320
+ )
321
+ // oxlint-disable-next-line no-unsafe-type-assertion
322
+ return classArray as T
323
+ } else {
324
+ const classObject: ClassObject = {}
325
+ Object.entries(cls).forEach(([key, value]) => {
326
+ if (key.length === 0) {
327
+ return
328
+ }
329
+ const _key = key.startsWith(prefix) ? key : `${prefix}${key}`
330
+ classObject[_key] = value === true
331
+ })
332
+ // oxlint-disable-next-line no-unsafe-type-assertion
333
+ return classObject as T
334
+ }
335
+ }
336
+
337
+ /**
338
+ * 从类名集合中的每一项移除指定前缀。
339
+ *
340
+ * 不带该前缀的类名会保持原样。
341
+ *
342
+ * @example
343
+ * ```
344
+ * // Expect: 'button active plain'
345
+ * const example1 = removePrefixOfClass('pm-', 'pm-button pm-active plain')
346
+ *
347
+ * // Expect: ['button', 'plain']
348
+ * const example2 = removePrefixOfClass('pm-', ['pm-button', 'plain'])
349
+ *
350
+ * // Expect: { button: true, plain: false }
351
+ * const example3 = removePrefixOfClass('pm-', { 'pm-button': true, plain: false, 'pm-': true })
352
+ * ```
353
+ */
354
+ export const removePrefixOfClass = <T extends ClassUnion>(prefix: string, cls: T): T => {
355
+ if (typeof cls === 'string') {
356
+ const classArray = classStringToClassArray(cls).map(item => {
357
+ if (item.startsWith(prefix)) {
358
+ return item.slice(prefix.length)
359
+ }
360
+ return item
361
+ })
362
+ const classString = classArrayToClassString(classArray)
363
+ // oxlint-disable-next-line no-unsafe-type-assertion
364
+ return classString as T
365
+ } else if (Array.isArray(cls)) {
366
+ const classArray = cls.filter(item => item.length !== 0).map(item => {
367
+ if (item.startsWith(prefix)) {
368
+ return item.slice(prefix.length)
369
+ }
370
+ return item
371
+ })
372
+ const filteredClassArray = classArray.filter(item => item.length !== 0)
373
+ // oxlint-disable-next-line no-unsafe-type-assertion
374
+ return filteredClassArray as T
375
+ } else {
376
+ const classObject: ClassObject = {}
377
+ Object.entries(cls).forEach(([key, value]) => {
378
+ if (key.length === 0) {
379
+ return
380
+ }
381
+ const _key = key.startsWith(prefix) ? key.slice(prefix.length) : key
382
+ if (_key.length !== 0) {
383
+ classObject[_key] = value === true
384
+ }
385
+ })
386
+ // oxlint-disable-next-line no-unsafe-type-assertion
387
+ return classObject as T
388
+ }
389
+ }
390
+
391
+ /**
392
+ * 向目标类名集合中加入新的类名。
393
+ *
394
+ * 当目标与新增项都包含同名类时,以新增项转换后的对象表示为准。
395
+ *
396
+ * @example
397
+ * ```
398
+ * // Expect: 'button active primary'
399
+ * const example1 = addClass('button', 'active primary')
400
+ *
401
+ * // Expect: ['button', 'active', 'primary']
402
+ * const example2 = addClass(['button'], { active: true, primary: true })
403
+ * ```
404
+ */
405
+ export const addClass = <T extends ClassUnion>(target: T, added: ClassUnion): T => {
406
+ const targetClassObj = toClassObject(target)
407
+ const addedClassObj = toClassObject(added)
408
+ const resClassObj = { ...targetClassObj, ...addedClassObj }
409
+ const result = formatClassToTarget(target, resClassObj)
410
+ return result
411
+ }
412
+
413
+ /**
414
+ * 从目标类名集合中移除指定类名。
415
+ *
416
+ * 该函数会把待移除项标记为 `false`,再按目标形态输出结果。
417
+ *
418
+ * @example
419
+ * ```
420
+ * // Expect: 'button primary'
421
+ * const example1 = removeClass('button active primary', 'active')
422
+ *
423
+ * // Expect: { button: true, active: false }
424
+ * const example2 = removeClass({ button: true, active: true }, ['active'])
425
+ * ```
426
+ */
427
+ export const removeClass = <T extends ClassUnion>(target: T, removed: ClassUnion): T => {
428
+ const targetClassObj = toClassObject(target)
429
+ const removedClassObj = toClassObject(removed)
430
+ Object.keys(removedClassObj).forEach(key => {
431
+ removedClassObj[key] = false
432
+ })
433
+ const resClassObj = { ...targetClassObj, ...removedClassObj }
434
+ const result = formatClassToTarget(target, resClassObj)
435
+ return result
436
+ }
437
+
438
+ /**
439
+ * 切换目标类名集合中指定类名的启用状态。
440
+ *
441
+ * 已存在的类名会被关闭,不存在的类名会被开启。
442
+ *
443
+ * @example
444
+ * ```
445
+ * // Expect: 'button primary'
446
+ * const example1 = toggleClass('button active', 'active primary')
447
+ *
448
+ * // Expect: { button: true, primary: true }
449
+ * const example2 = toggleClass({ button: true, active: true }, ['active', 'primary'])
450
+ * ```
451
+ */
452
+ export const toggleClass = <T extends ClassUnion>(target: T, toggled: ClassUnion): T => {
453
+ const targetClassObj = toClassObject(target)
454
+ const toggledClassArr = toClassArray(toggled)
455
+ toggledClassArr.forEach((cls) => {
456
+ // oxlint-disable-next-line strict-boolean-expressions
457
+ targetClassObj[cls] = !targetClassObj[cls]
458
+ })
459
+ const result = formatClassToTarget(target, targetClassObj)
460
+ return result
461
+ }
462
+
463
+ /**
464
+ * 替换目标类名集合中的类名。
465
+ *
466
+ * `replaced` 支持三种形式:
467
+ * - 字符串:仅移除对应类名。
468
+ * - 字符串数组:逐项移除对应类名。
469
+ * - 元组数组:按 `[from, to]` 的形式逐项替换类名。
470
+ * - 对象:按键值对执行替换,值为空字符串时表示删除。
471
+ *
472
+ * @example
473
+ * ```
474
+ * // Expect: 'button selected'
475
+ * const example1 = replaceClass('button active', [['active', 'selected']])
476
+ *
477
+ * // Expect: ['button', 'selected']
478
+ * const example2 = replaceClass(['button', 'active'], { active: 'selected' })
479
+ *
480
+ * // Expect: { button: true, active: false }
481
+ * const example3 = replaceClass({ button: true, active: true }, 'active')
482
+ * ```
483
+ */
484
+ export const replaceClass = <T extends ClassUnion>(
485
+ target: T,
486
+ replaced: Record<string, string> | string | string[] | Array<[string, string]>,
487
+ ): T => {
488
+ if (typeof replaced === 'string') {
489
+ return removeClass(target, replaced)
490
+ }
491
+
492
+ const targetClassObj = toClassObject(target)
493
+
494
+ if (Array.isArray(replaced)) {
495
+ for (const item of replaced) {
496
+ if (typeof item === 'string') {
497
+ const fromValue = targetClassObj[item]
498
+
499
+ if (fromValue !== undefined) {
500
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
501
+ delete targetClassObj[item]
502
+ }
503
+
504
+ continue
505
+ }
506
+
507
+ const [from, to] = item
508
+ const fromValue = targetClassObj[from]
509
+
510
+ if (fromValue !== undefined) {
511
+ if (to !== '') {
512
+ targetClassObj[to] = fromValue
513
+ }
514
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
515
+ delete targetClassObj[from]
516
+ }
517
+ }
518
+
519
+ return formatClassToTarget(target, targetClassObj)
520
+ }
521
+
522
+ for (const [from, rawTo] of Object.entries(replaced)) {
523
+ if (typeof rawTo !== 'string') {
524
+ continue
525
+ }
526
+
527
+ const fromValue = targetClassObj[from]
528
+
529
+ if (fromValue !== undefined) {
530
+ if (rawTo !== '') {
531
+ targetClassObj[rawTo] = fromValue
532
+ }
533
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
534
+ delete targetClassObj[from]
535
+ }
536
+ }
537
+
538
+ return formatClassToTarget(target, targetClassObj)
539
+ }
540
+
541
+ /**
542
+ * 判断目标类名集合是否完整包含另一组类名。
543
+ *
544
+ * 只有当 `contained` 中的每个类名都存在于 `target` 中时,才会返回 `true`。
545
+ *
546
+ * @example
547
+ * ```
548
+ * // Expect: true
549
+ * const example1 = containClass('button active', 'button active primary')
550
+ *
551
+ * // Expect: false
552
+ * const example2 = containClass(['button', 'missing'], { button: true, active: true })
553
+ * ```
554
+ */
555
+ export const containClass = (contained: ClassUnion, target: ClassUnion): boolean => {
556
+ const containedClassArr = toClassArray(contained)
557
+ const targetClassArr = toClassArray(target)
558
+ return containedClassArr.every(item => targetClassArr.includes(item))
559
+ }
@@ -0,0 +1 @@
1
+ export * from "./class.ts"