@mpxjs/webpack-plugin 2.10.24 → 2.11.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 (151) hide show
  1. package/lib/index.js +4 -1
  2. package/lib/platform/style/wx/index.js +417 -120
  3. package/lib/platform/template/wx/component-config/picker.js +0 -6
  4. package/lib/react/style-helper.js +6 -6
  5. package/lib/resolver/AddEnvPlugin.js +1 -1
  6. package/lib/resolver/AddModePlugin.js +1 -1
  7. package/lib/resolver/ExtendComponentsPlugin.js +68 -0
  8. package/lib/runtime/components/extends/section-list.mpx +14 -0
  9. package/lib/runtime/components/react/animationHooks/index.ts +7 -3
  10. package/lib/runtime/components/react/dist/animationHooks/index.d.ts +0 -2
  11. package/lib/runtime/components/react/dist/animationHooks/index.js +6 -1
  12. package/lib/runtime/components/react/dist/event.config.d.ts +2 -6
  13. package/lib/runtime/components/react/dist/event.config.js +28 -24
  14. package/lib/runtime/components/react/dist/getInnerListeners.d.ts +1 -1
  15. package/lib/runtime/components/react/dist/getInnerListeners.js +74 -56
  16. package/lib/runtime/components/react/dist/mpx-button.d.ts +1 -2
  17. package/lib/runtime/components/react/dist/mpx-button.jsx +16 -9
  18. package/lib/runtime/components/react/dist/mpx-camera.d.ts +0 -2
  19. package/lib/runtime/components/react/dist/mpx-camera.jsx +36 -5
  20. package/lib/runtime/components/react/dist/mpx-canvas/index.d.ts +0 -2
  21. package/lib/runtime/components/react/dist/mpx-canvas/index.jsx +1 -3
  22. package/lib/runtime/components/react/dist/mpx-canvas/utils.d.ts +1 -1
  23. package/lib/runtime/components/react/dist/mpx-checkbox-group.d.ts +0 -2
  24. package/lib/runtime/components/react/dist/mpx-checkbox-group.jsx +3 -4
  25. package/lib/runtime/components/react/dist/mpx-checkbox.d.ts +1 -2
  26. package/lib/runtime/components/react/dist/mpx-checkbox.jsx +8 -9
  27. package/lib/runtime/components/react/dist/mpx-form.d.ts +1 -2
  28. package/lib/runtime/components/react/dist/mpx-form.jsx +5 -5
  29. package/lib/runtime/components/react/dist/mpx-icon/index.d.ts +0 -2
  30. package/lib/runtime/components/react/dist/mpx-icon/index.jsx +2 -2
  31. package/lib/runtime/components/react/dist/mpx-image.d.ts +0 -2
  32. package/lib/runtime/components/react/dist/mpx-image.jsx +59 -43
  33. package/lib/runtime/components/react/dist/mpx-inline-text.jsx +13 -4
  34. package/lib/runtime/components/react/dist/mpx-input.d.ts +0 -2
  35. package/lib/runtime/components/react/dist/mpx-input.jsx +16 -8
  36. package/lib/runtime/components/react/dist/mpx-label.d.ts +1 -2
  37. package/lib/runtime/components/react/dist/mpx-label.jsx +5 -6
  38. package/lib/runtime/components/react/dist/mpx-movable-area.d.ts +0 -2
  39. package/lib/runtime/components/react/dist/mpx-movable-area.jsx +3 -3
  40. package/lib/runtime/components/react/dist/mpx-movable-view.d.ts +1 -2
  41. package/lib/runtime/components/react/dist/mpx-movable-view.jsx +6 -6
  42. package/lib/runtime/components/react/dist/mpx-nav.jsx +1 -1
  43. package/lib/runtime/components/react/dist/mpx-picker/index.jsx +72 -23
  44. package/lib/runtime/components/react/dist/mpx-picker/multiSelector.jsx +2 -2
  45. package/lib/runtime/components/react/dist/mpx-picker/type.d.ts +4 -0
  46. package/lib/runtime/components/react/dist/mpx-picker-view/index.d.ts +1 -1
  47. package/lib/runtime/components/react/dist/mpx-picker-view/index.jsx +10 -14
  48. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.d.ts +0 -2
  49. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.js +0 -5
  50. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.d.ts +1 -1
  51. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.jsx +34 -14
  52. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.d.ts +0 -2
  53. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.jsx +4 -5
  54. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.d.ts +0 -2
  55. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.jsx +3 -5
  56. package/lib/runtime/components/react/dist/mpx-popup/index.d.ts +5 -4
  57. package/lib/runtime/components/react/dist/mpx-popup/index.jsx +2 -2
  58. package/lib/runtime/components/react/dist/mpx-popup/popupBase.jsx +3 -2
  59. package/lib/runtime/components/react/dist/mpx-progress.d.ts +0 -2
  60. package/lib/runtime/components/react/dist/mpx-progress.jsx +1 -3
  61. package/lib/runtime/components/react/dist/mpx-radio-group.d.ts +0 -2
  62. package/lib/runtime/components/react/dist/mpx-radio-group.jsx +3 -4
  63. package/lib/runtime/components/react/dist/mpx-radio.d.ts +1 -2
  64. package/lib/runtime/components/react/dist/mpx-radio.jsx +8 -9
  65. package/lib/runtime/components/react/dist/mpx-rich-text/index.d.ts +0 -2
  66. package/lib/runtime/components/react/dist/mpx-rich-text/index.jsx +1 -3
  67. package/lib/runtime/components/react/dist/mpx-scroll-view.d.ts +1 -2
  68. package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +6 -6
  69. package/lib/runtime/components/react/dist/mpx-section-list.d.ts +51 -0
  70. package/lib/runtime/components/react/dist/mpx-section-list.jsx +376 -0
  71. package/lib/runtime/components/react/dist/mpx-simple-text.jsx +37 -19
  72. package/lib/runtime/components/react/dist/mpx-simple-view.d.ts +4 -1
  73. package/lib/runtime/components/react/dist/mpx-simple-view.jsx +11 -4
  74. package/lib/runtime/components/react/dist/mpx-slider.d.ts +0 -2
  75. package/lib/runtime/components/react/dist/mpx-slider.jsx +6 -7
  76. package/lib/runtime/components/react/dist/mpx-sticky-header.d.ts +1 -2
  77. package/lib/runtime/components/react/dist/mpx-sticky-header.jsx +5 -5
  78. package/lib/runtime/components/react/dist/mpx-sticky-section.d.ts +1 -2
  79. package/lib/runtime/components/react/dist/mpx-sticky-section.jsx +6 -6
  80. package/lib/runtime/components/react/dist/mpx-swiper-item.d.ts +1 -2
  81. package/lib/runtime/components/react/dist/mpx-swiper-item.jsx +5 -5
  82. package/lib/runtime/components/react/dist/mpx-swiper.d.ts +1 -2
  83. package/lib/runtime/components/react/dist/mpx-swiper.jsx +4 -8
  84. package/lib/runtime/components/react/dist/mpx-switch.d.ts +0 -2
  85. package/lib/runtime/components/react/dist/mpx-switch.jsx +1 -3
  86. package/lib/runtime/components/react/dist/mpx-text.d.ts +0 -2
  87. package/lib/runtime/components/react/dist/mpx-text.jsx +43 -25
  88. package/lib/runtime/components/react/dist/mpx-video.d.ts +0 -2
  89. package/lib/runtime/components/react/dist/mpx-video.jsx +4 -5
  90. package/lib/runtime/components/react/dist/mpx-view.d.ts +2 -2
  91. package/lib/runtime/components/react/dist/mpx-view.jsx +214 -114
  92. package/lib/runtime/components/react/dist/mpx-web-view.d.ts +2 -1
  93. package/lib/runtime/components/react/dist/mpx-web-view.jsx +5 -1
  94. package/lib/runtime/components/react/dist/namedColorSet.d.ts +2 -0
  95. package/lib/runtime/components/react/dist/namedColorSet.js +149 -0
  96. package/lib/runtime/components/react/dist/utils.d.ts +71 -70
  97. package/lib/runtime/components/react/dist/utils.jsx +1295 -456
  98. package/lib/runtime/components/react/event.config.ts +32 -25
  99. package/lib/runtime/components/react/getInnerListeners.ts +84 -60
  100. package/lib/runtime/components/react/mpx-button.tsx +18 -18
  101. package/lib/runtime/components/react/mpx-camera.tsx +40 -8
  102. package/lib/runtime/components/react/mpx-canvas/index.tsx +5 -7
  103. package/lib/runtime/components/react/mpx-canvas/utils.tsx +3 -1
  104. package/lib/runtime/components/react/mpx-checkbox-group.tsx +2 -8
  105. package/lib/runtime/components/react/mpx-checkbox.tsx +9 -13
  106. package/lib/runtime/components/react/mpx-form.tsx +6 -8
  107. package/lib/runtime/components/react/mpx-icon/index.tsx +1 -5
  108. package/lib/runtime/components/react/mpx-image.tsx +66 -58
  109. package/lib/runtime/components/react/mpx-inline-text.tsx +13 -4
  110. package/lib/runtime/components/react/mpx-input.tsx +19 -12
  111. package/lib/runtime/components/react/mpx-label.tsx +6 -10
  112. package/lib/runtime/components/react/mpx-movable-area.tsx +3 -5
  113. package/lib/runtime/components/react/mpx-movable-view.tsx +7 -9
  114. package/lib/runtime/components/react/mpx-nav.tsx +1 -1
  115. package/lib/runtime/components/react/mpx-picker/index.tsx +100 -29
  116. package/lib/runtime/components/react/mpx-picker/multiSelector.tsx +3 -3
  117. package/lib/runtime/components/react/mpx-picker/selector.tsx +1 -1
  118. package/lib/runtime/components/react/mpx-picker/type.ts +4 -0
  119. package/lib/runtime/components/react/mpx-picker-view/index.tsx +17 -24
  120. package/lib/runtime/components/react/mpx-picker-view/pickerVIewContext.ts +0 -9
  121. package/lib/runtime/components/react/mpx-picker-view-column/index.tsx +44 -23
  122. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItem.tsx +1 -9
  123. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItemLite.tsx +0 -9
  124. package/lib/runtime/components/react/mpx-popup/index.tsx +7 -3
  125. package/lib/runtime/components/react/mpx-popup/popupBase.tsx +3 -2
  126. package/lib/runtime/components/react/mpx-progress.tsx +0 -6
  127. package/lib/runtime/components/react/mpx-radio-group.tsx +2 -8
  128. package/lib/runtime/components/react/mpx-radio.tsx +9 -13
  129. package/lib/runtime/components/react/mpx-rich-text/index.tsx +0 -6
  130. package/lib/runtime/components/react/mpx-scroll-view.tsx +7 -9
  131. package/lib/runtime/components/react/mpx-section-list.tsx +557 -0
  132. package/lib/runtime/components/react/mpx-simple-text.tsx +36 -25
  133. package/lib/runtime/components/react/mpx-simple-view.tsx +17 -5
  134. package/lib/runtime/components/react/mpx-slider.tsx +15 -13
  135. package/lib/runtime/components/react/mpx-sticky-header.tsx +6 -8
  136. package/lib/runtime/components/react/mpx-sticky-section.tsx +7 -9
  137. package/lib/runtime/components/react/mpx-swiper-item.tsx +6 -7
  138. package/lib/runtime/components/react/mpx-swiper.tsx +5 -11
  139. package/lib/runtime/components/react/mpx-switch.tsx +0 -6
  140. package/lib/runtime/components/react/mpx-text.tsx +45 -36
  141. package/lib/runtime/components/react/mpx-video.tsx +3 -8
  142. package/lib/runtime/components/react/mpx-view.tsx +235 -129
  143. package/lib/runtime/components/react/mpx-web-view.tsx +12 -6
  144. package/lib/runtime/components/react/namedColorSet.ts +150 -0
  145. package/lib/runtime/components/react/types/getInnerListeners.d.ts +21 -1
  146. package/lib/runtime/components/react/types/global.d.ts +1 -0
  147. package/lib/runtime/components/react/utils.tsx +1352 -550
  148. package/lib/runtime/i18n.wxs +15 -9
  149. package/lib/style-compiler/strip-conditional.js +19 -4
  150. package/lib/template-compiler/compiler.js +6 -0
  151. package/package.json +6 -6
@@ -1,52 +1,63 @@
1
1
  import { useEffect, useCallback, useMemo, useRef, ReactNode, ReactElement, isValidElement, useContext, useState, Dispatch, SetStateAction, createElement, MutableRefObject } from 'react'
2
2
  import { LayoutChangeEvent, TextStyle, ImageProps, Image } from 'react-native'
3
- import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, error, warn } from '@mpxjs/utils'
3
+ import { isObject, isFunction, isNumber, hasOwn, diffAndCloneA, shallowEqual, error, warn } from '@mpxjs/utils'
4
4
  import { VarContext, ScrollViewContext, RouteContext, TextPassThroughContext, TextPassThroughContextValue } from './context'
5
5
  import { ExpressionParser, parseFunc, ReplaceSource } from './parser'
6
6
  import { initialWindowMetrics } from 'react-native-safe-area-context'
7
7
  import type { AnyFunc, ExtendedFunctionComponent } from './types/common'
8
8
  import { Gesture } from 'react-native-gesture-handler'
9
9
 
10
- export const TEXT_STYLE_MAP: Record<string, boolean> = {
10
+ // ============================================================
11
+ // declare + constants
12
+ // ============================================================
13
+
14
+ declare const __mpx_mode__: 'ios' | 'android' | 'harmony'
15
+
16
+ export const percentRegExp = /^\s*-?\d+(\.\d+)?%\s*$/
17
+ export const svgRegExp = /\.svg(?:[?#].*)?$/i
18
+ export const hiddenStyle = {
19
+ opacity: 0
20
+ }
21
+ export const isIOS = __mpx_mode__ === 'ios'
22
+ export const isAndroid = __mpx_mode__ === 'android'
23
+ export const isHarmony = __mpx_mode__ === 'harmony'
24
+ export const extendObject = Object.assign
25
+
26
+ const textStyleMap: Record<string, boolean> = {
11
27
  color: true,
12
28
  letterSpacing: true,
13
29
  lineHeight: true,
14
30
  includeFontPadding: true,
15
31
  writingDirection: true
16
32
  }
17
- export const PERCENT_REGEX = /^\s*-?\d+(\.\d+)?%\s*$/
18
- export const URL_REGEX = /^\s*url\(["']?(.*?)["']?\)\s*$/
19
- export const SVG_REGEXP = /\.svg(?:[?#].*)?$/i
20
- export const BACKGROUND_STYLE_MAP: Record<string, boolean> = {
33
+ const urlRegExp = /^\s*url\(["']?(.*?)["']?\)\s*$/
34
+ const linearGradientRegExp = /^\s*linear-gradient\(.*\)\s*$/
35
+ const digitStartRegExp = /^\d/
36
+ const varDecRegExp = /^--/
37
+ const varUseRegExp = /var\(/
38
+ const unoVarDecRegExp = /^--un-/
39
+ const unoVarUseRegExp = /var\(--un-/
40
+ const lengthValueRegExp = /^(-?(?:\d+(?:\.\d+)?|\.\d+)(?:rpx|px|%|vw|vh)?|hairlineWidth)$/
41
+ const DEFAULT_FONT_SIZE = 16
42
+ // transform: 'rotateX(45deg) ...' 单段拆出 fn 名与括号内值
43
+ const transformFnRegExp = /([/\w]+)\((.+)\)/
44
+ // boxShadow 子值识别 rpx 单位(仅 rpx 需要换算为 px,其它单位保留原样)
45
+ const rpxSuffixRegExp = /\d+rpx$/
46
+ // font-family / font 简写解析时统一去掉单/双引号
47
+ const quoteCharRegExp = /["']/g
48
+ const defaultBoxSizingStyle = {
49
+ boxSizing: 'content-box'
50
+ }
51
+ const backgroundStyleMap: Record<string, boolean> = {
21
52
  backgroundImage: true,
22
53
  backgroundSize: true,
23
54
  backgroundRepeat: true,
24
55
  backgroundPosition: true
25
56
  }
26
- export const TEXT_PROPS_MAP: Record<string, boolean> = {
57
+ const textPropsMap: Record<string, boolean> = {
27
58
  ellipsizeMode: true,
28
59
  numberOfLines: true
29
60
  }
30
- export const DEFAULT_FONT_SIZE = 16
31
- export const HIDDEN_STYLE = {
32
- opacity: 0
33
- }
34
- export const DEFAULT_BOX_SIZING_STYLE = {
35
- boxSizing: 'content-box'
36
- }
37
-
38
- declare const __mpx_mode__: 'ios' | 'android' | 'harmony'
39
-
40
- export const isIOS = __mpx_mode__ === 'ios'
41
- export const isAndroid = __mpx_mode__ === 'android'
42
- export const isHarmony = __mpx_mode__ === 'harmony'
43
-
44
- const varDecRegExp = /^--/
45
- const varUseRegExp = /var\(/
46
- const unoVarDecRegExp = /^--un-/
47
- const unoVarUseRegExp = /var\(--un-/
48
- const calcUseRegExp = /calc\(/
49
- const envUseRegExp = /env\(/
50
61
  const boxSizingAffectingStyleMap: Record<string, boolean> = {
51
62
  padding: true,
52
63
  paddingTop: true,
@@ -57,47 +68,233 @@ const boxSizingAffectingStyleMap: Record<string, boolean> = {
57
68
  borderTopWidth: true,
58
69
  borderRightWidth: true,
59
70
  borderBottomWidth: true,
60
- borderLeftWidth: true
71
+ borderLeftWidth: true,
72
+ border: true,
73
+ borderTop: true,
74
+ borderRight: true,
75
+ borderBottom: true,
76
+ borderLeft: true
77
+ }
78
+ const runtimeAbbreviationMap: Record<string, string[]> = {
79
+ margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
80
+ padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
81
+ borderRadius: ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'],
82
+ borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
83
+ borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
84
+ border: ['borderWidth', 'borderStyle', 'borderColor'],
85
+ // RN 不支持单边 border-*-style,统一展开到 borderStyle
86
+ // 实测 RN 上当 borderStyle 不为 solid 时单边 border-*-color 不生效,
87
+ // 这里把单边 color 也统一展开到 borderColor 规避(width 不能这样做,否则会覆盖其它三边)
88
+ borderTop: ['borderTopWidth', 'borderStyle', 'borderColor'],
89
+ borderRight: ['borderRightWidth', 'borderStyle', 'borderColor'],
90
+ borderBottom: ['borderBottomWidth', 'borderStyle', 'borderColor'],
91
+ borderLeft: ['borderLeftWidth', 'borderStyle', 'borderColor'],
92
+ flexFlow: ['flexDirection', 'flexWrap'],
93
+ textShadow: ['textShadowOffset.width', 'textShadowOffset.height', 'textShadowRadius', 'textShadowColor'],
94
+ textDecoration: ['textDecorationLine', 'textDecorationStyle', 'textDecorationColor'],
95
+ // gap:CSS 2 槽位(row-gap / column-gap),单值复制行列。RN 原生支持但要求 number。
96
+ // 单值串在 __getStyle 的 transformStyleObj / 编译期 formatValue 已被 __formatValue 换算为 number;
97
+ // 真正以字符串形态进 transformShorthand 的是多值串(如 '10px 20px')—— unitRegExp 不命中、原样透传。
98
+ // 因此进入 runtimeForceExpandCompositeMap,避免「单值透传」捷径放过这类多值串残留。
99
+ // 百分比(`gap: 50%` / `gap: 10px 50%`)由 transformShorthand 写回阶段就地 resolvePercent 落成 number
100
+ gap: ['rowGap', 'columnGap'],
101
+ // inset:4 槽位等价 margin 四值语法;RN 0.74+ 原生支持单值 DimensionValue,单值走 runtimeCompositeStyleMap 短路透传
102
+ inset: ['top', 'right', 'bottom', 'left'],
103
+ // outline: <outline-width> || <outline-style> || <outline-color>,顺序不敏感
104
+ outline: ['outlineWidth', 'outlineStyle', 'outlineColor']
105
+ }
106
+ const runtimeCompositeStyleMap: Record<string, boolean> = {
107
+ margin: true,
108
+ padding: true,
109
+ borderRadius: true,
110
+ borderWidth: true,
111
+ borderColor: true,
112
+ gap: true,
113
+ inset: true
114
+ }
115
+ const runtimeUnorderedAbbreviationMap: Record<string, boolean> = {
116
+ border: true,
117
+ borderTop: true,
118
+ borderRight: true,
119
+ borderBottom: true,
120
+ borderLeft: true,
121
+ flexFlow: true,
122
+ textShadow: true,
123
+ textDecoration: true,
124
+ // outline 与 border 简写共享同一套缺省值处理
125
+ outline: true
126
+ }
127
+ // 单值简写在 __getStyle 阶段经 __formatValue 把带单位长度换算为 number 后,
128
+ // 需要在 transformShorthand 入口转回字符串 token 交给展开逻辑的 key 集合。
129
+ // margin / padding / borderRadius / borderWidth / borderColor / inset 不在此列:
130
+ // RN 原生支持这些 key 直接吃单值 number,无需展开。
131
+ const runtimeNumericExpandShorthandMap: Record<string, boolean> = {
132
+ border: true,
133
+ borderTop: true,
134
+ borderRight: true,
135
+ borderBottom: true,
136
+ borderLeft: true,
137
+ outline: true,
138
+ gap: true
139
+ }
140
+ // CSS border-width: medium 的实测值(各主流浏览器一致取 3px)
141
+ // 与编译期 wx/index.js 同名常量保持一致;调整需两侧一起改
142
+ const BORDER_MEDIUM_WIDTH = 3
143
+ // 运行时简写槽位缺省值表(与编译期 ShorthandDefaultMap 镜像,仅 CSS quote 形式不同)
144
+ // 值即槽位缺省时追加的补齐值;borderColor / textShadowRadius 因 RN 有内置缺省值不补,不进此表
145
+ // none 清除语义统一保留到 useTransformStyle 末尾处理
146
+ const runtimeShorthandDefaultMap: Record<string, Record<string, any>> = {
147
+ border: { borderWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
148
+ borderTop: { borderTopWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
149
+ borderRight: { borderRightWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
150
+ borderBottom: { borderBottomWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
151
+ borderLeft: { borderLeftWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
152
+ // outline 与 border 缺省值完全对齐:缺 width → BORDER_MEDIUM_WIDTH;
153
+ // 缺 style → outlineStyle: none,末尾统一转换为 outlineWidth: 0
154
+ outline: { outlineWidth: BORDER_MEDIUM_WIDTH, outlineStyle: 'none' },
155
+ textShadow: { textShadowColor: '#000' }
156
+ // textDecoration / flexFlow 暂不配置,与 RN 默认一致
61
157
  }
62
158
 
159
+ const borderStyleMap: Record<string, boolean> = {
160
+ // RN 实测仅支持 solid/dotted/dashed;none 作为 CSS 合法值保留到末尾统一转换为 borderWidth: 0
161
+ solid: true,
162
+ dotted: true,
163
+ dashed: true,
164
+ none: true
165
+ }
166
+ const textDecorationLineMap: Record<string, boolean> = {
167
+ none: true,
168
+ underline: true,
169
+ 'line-through': true
170
+ }
171
+ const textDecorationStyleMap: Record<string, boolean> = {
172
+ solid: true,
173
+ double: true,
174
+ dotted: true,
175
+ dashed: true
176
+ }
177
+ // font 简写 <font-weight> 槽位允许的关键字 / 数值(CSS 子集,与 RN 支持一致)
178
+ const fontWeightMap: Record<string, boolean> = {
179
+ bold: true,
180
+ normal: true,
181
+ 100: true,
182
+ 200: true,
183
+ 300: true,
184
+ 400: true,
185
+ 500: true,
186
+ 600: true,
187
+ 700: true,
188
+ 800: true,
189
+ 900: true
190
+ }
191
+ const flexDirectionMap: Record<string, boolean> = {
192
+ row: true,
193
+ 'row-reverse': true,
194
+ column: true,
195
+ 'column-reverse': true
196
+ }
197
+ const flexWrapMap: Record<string, boolean> = {
198
+ wrap: true,
199
+ nowrap: true,
200
+ 'wrap-reverse': true
201
+ }
63
202
  const safeAreaInsetMap: Record<string, 'top' | 'right' | 'bottom' | 'left'> = {
64
203
  'safe-area-inset-top': 'top',
65
204
  'safe-area-inset-right': 'right',
66
205
  'safe-area-inset-bottom': 'bottom',
67
206
  'safe-area-inset-left': 'left'
68
207
  }
208
+ const radiusPercentRule: Record<string, 'height' | 'width'> = {
209
+ borderTopLeftRadius: 'width',
210
+ borderBottomLeftRadius: 'width',
211
+ borderBottomRightRadius: 'width',
212
+ borderTopRightRadius: 'width',
213
+ borderRadius: 'width'
214
+ }
215
+ const selfPercentRule: Record<string, 'height' | 'width'> = extendObject({
216
+ translateX: 'width',
217
+ translateY: 'height'
218
+ }, radiusPercentRule)
219
+ const parentHeightPercentRule: Record<string, boolean> = {
220
+ height: true,
221
+ minHeight: true,
222
+ maxHeight: true,
223
+ top: true,
224
+ bottom: true,
225
+ // row-gap 百分比按 CSS 规范基于容器内容区高度;columnGap 不入此表,落默认 parentWidth 分支
226
+ rowGap: true
227
+ }
228
+ const bgRepeatMap: Record<string, boolean> = {
229
+ repeat: true,
230
+ 'repeat-x': true,
231
+ 'repeat-y': true,
232
+ 'no-repeat': true
233
+ }
234
+ const bgPositionMap: Record<string, boolean> = {
235
+ left: true,
236
+ right: true,
237
+ top: true,
238
+ bottom: true,
239
+ center: true
240
+ }
69
241
 
70
- export const extendObject = Object.assign
242
+ // ============================================================
243
+ // interfaces / types
244
+ // ============================================================
71
245
 
72
- export function getDefaultAllowFontScaling (): boolean {
73
- return global.__mpx?.config?.rnConfig?.allowFontScaling ?? false
246
+ interface PercentConfig {
247
+ fontSize?: number | string
248
+ width?: number
249
+ height?: number
250
+ parentWidth?: number
251
+ parentHeight?: number
74
252
  }
75
253
 
76
- export function transformBoxSizing (style: Record<string, any> = {}, hasBoxSizingAffectingStyle = false) {
77
- if (hasBoxSizingAffectingStyle && style.boxSizing === undefined) {
78
- style.boxSizing = global.__mpx?.config?.rnConfig?.defaultBoxSizing ?? DEFAULT_BOX_SIZING_STYLE.boxSizing
79
- }
80
- return style
254
+ interface TransformStyleConfig {
255
+ enableVar?: boolean
256
+ parentWidth?: number
257
+ parentHeight?: number
258
+ transformRadiusPercent?: boolean
259
+ defaultStyle?: Record<string, any>
81
260
  }
82
261
 
83
- export function isBoxSizingAffectingStyle (key: string) {
84
- return hasOwn(boxSizingAffectingStyleMap, key)
262
+ export interface VisitorArg {
263
+ target: Record<string, any>
264
+ key: string
265
+ value: any
266
+ keyPath: Array<string>
85
267
  }
86
268
 
87
- function isTextStyle (key: string) {
88
- return hasOwn(TEXT_STYLE_MAP, key) || key.startsWith('font') || key.startsWith('text')
269
+ interface LayoutConfig {
270
+ props: Record<string, any>
271
+ hasSelfPercent: boolean
272
+ setWidth?: Dispatch<SetStateAction<number>>
273
+ setHeight?: Dispatch<SetStateAction<number>>
274
+ onLayout?: (event: LayoutChangeEvent) => void
275
+ nodeRef: React.RefObject<any>
89
276
  }
90
277
 
91
- function getSafeAreaInset (name: string, navigation: Record<string, any> | undefined) {
92
- const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets)
93
- return insets[safeAreaInsetMap[name]]
278
+ export interface WrapChildrenConfig {
279
+ hasVarDec: boolean
280
+ varContext?: Record<string, any>
281
+ textPassThrough?: TextPassThroughContextValue | null
94
282
  }
95
283
 
96
- export function useNavigation (): Record<string, any> | undefined {
97
- const { navigation } = useContext(RouteContext) || {}
98
- return navigation
284
+ export interface TextPassThroughValueOptions {
285
+ enableTextPassThrough?: boolean
286
+ }
287
+
288
+ export interface GestureHandler {
289
+ nodeRefs?: Array<{ getNodeInstance: () => { nodeRef: unknown } }>
290
+ current?: unknown
291
+ handlerTag?: number
99
292
  }
100
293
 
294
+ // ============================================================
295
+ // generic utility functions
296
+ // ============================================================
297
+
101
298
  export function omit<T, K extends string> (obj: T, fields: K[]): Omit<T, K> {
102
299
  const shallowCopy: any = extendObject({}, obj)
103
300
  for (let i = 0; i < fields.length; i += 1) {
@@ -107,31 +304,9 @@ export function omit<T, K extends string> (obj: T, fields: K[]): Omit<T, K> {
107
304
  return shallowCopy
108
305
  }
109
306
 
110
- /**
111
- * 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行
112
- */
113
- export const useUpdateEffect = (effect: any, deps: any) => {
114
- const isMounted = useRef(false)
115
-
116
- // for react-refresh
117
- useEffect(() => {
118
- return () => {
119
- isMounted.current = false
120
- }
121
- }, [])
122
-
123
- useEffect(() => {
124
- if (!isMounted.current) {
125
- isMounted.current = true
126
- } else {
127
- return effect()
128
- }
129
- }, deps)
130
- }
131
-
132
307
  export const parseUrl = (cssUrl = '') => {
133
308
  if (!cssUrl) return
134
- const match = cssUrl.match(URL_REGEX)
309
+ const match = cssUrl.match(urlRegExp)
135
310
  return match?.[1]
136
311
  }
137
312
 
@@ -143,6 +318,10 @@ export const getRestProps = (transferProps: any = {}, originProps: any = {}, del
143
318
  )
144
319
  }
145
320
 
321
+ export function getDefaultAllowFontScaling (): boolean {
322
+ return global.__mpx?.config?.rnConfig?.allowFontScaling ?? false
323
+ }
324
+
146
325
  export function isText (ele: ReactNode): ele is ReactElement {
147
326
  if (isValidElement(ele)) {
148
327
  const displayName = (ele.type as ExtendedFunctionComponent)?.displayName
@@ -158,106 +337,168 @@ export function isStringChildren (children: ReactNode) {
158
337
  return children.every((child) => typeof child === 'string')
159
338
  }
160
339
 
161
- type GroupData<T> = Record<string, Partial<T>>
340
+ // 多value解析
341
+ export function parseValues (str: string, char = ' ') {
342
+ let stack = 0
343
+ let temp = ''
344
+ const result = []
345
+ for (let i = 0; i < str.length; i++) {
346
+ if (str[i] === '(') {
347
+ stack++
348
+ } else if (str[i] === ')') {
349
+ stack--
350
+ }
351
+ // 非括号内 或者 非分隔字符且非空
352
+ if (stack !== 0 || str[i] !== char) {
353
+ temp += str[i]
354
+ }
355
+ if ((stack === 0 && str[i] === char) || i === str.length - 1) {
356
+ result.push(temp.trim())
357
+ temp = ''
358
+ }
359
+ }
360
+ return result
361
+ }
162
362
 
163
- export function groupBy<T extends Record<string, any>> (
164
- obj: T,
165
- callback: (key: string, val: T[keyof T]) => string,
166
- group: GroupData<T> = {}
167
- ): GroupData<T> {
168
- Object.entries(obj).forEach(([key, val]) => {
169
- const groupKey = callback(key, val)
170
- group[groupKey] = group[groupKey] || {}
171
- group[groupKey][key as keyof T] = val
172
- })
173
- return group
363
+ export const debounce = <T extends AnyFunc> (
364
+ func: T,
365
+ delay: number
366
+ ): ((...args: Parameters<T>) => void) & { clear: () => void } => {
367
+ let timer: any
368
+ const wrapper = (...args: ReadonlyArray<any>) => {
369
+ timer && clearTimeout(timer)
370
+ timer = setTimeout(() => {
371
+ func(...args)
372
+ }, delay)
373
+ }
374
+ wrapper.clear = () => {
375
+ timer && clearTimeout(timer)
376
+ timer = null
377
+ }
378
+ return wrapper
379
+ }
380
+
381
+ // ============================================================
382
+ // style classification & splitting
383
+ // ============================================================
384
+
385
+ function isTextStyle (key: string) {
386
+ return hasOwn(textStyleMap, key) || key.startsWith('font') || key.startsWith('text')
387
+ }
388
+
389
+ function isColorValue (token: string): boolean {
390
+ if (token.startsWith('#') || token.startsWith('rgb(') || token.startsWith('rgba(') || token.startsWith('hsl(') || token.startsWith('hsla(')) return true
391
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
392
+ return hasOwn(require('./namedColorSet').default, token.toLowerCase())
393
+ }
394
+
395
+ function isLengthValue (token: string): boolean {
396
+ return lengthValueRegExp.test(token)
174
397
  }
175
398
 
176
- export function splitStyle<T extends Record<string, any>> (styleObj: T, sideEffect?: (key: string, val: T[keyof T]) => void): {
399
+ export function isBoxSizingAffectingStyle (key: string) {
400
+ return hasOwn(boxSizingAffectingStyleMap, key)
401
+ }
402
+
403
+ // 仅在调用侧已确认存在 padding / border 等 box-sizing 影响项时调用:
404
+ // 给样式补默认 boxSizing(用户未显式设置时)。是否需要进入由调用侧决定,
405
+ // useTransformStyle / mpx-simple-view / mpx-simple-text 都按 hasBoxSizingAffectingStyle 短路。
406
+ export function transformBoxSizing (style: Record<string, any> = {}) {
407
+ if (style.boxSizing === undefined) {
408
+ style.boxSizing = global.__mpx?.config?.rnConfig?.defaultBoxSizing ?? defaultBoxSizingStyle.boxSizing
409
+ }
410
+ }
411
+
412
+ export function splitStyle<T extends Record<string, any>> (styleObj: T = {} as T, sideEffect?: (key: string, val: T[keyof T]) => void): {
177
413
  textStyle?: Partial<T>
178
414
  backgroundStyle?: Partial<T>
179
415
  innerStyle?: Partial<T>
180
416
  } {
181
- return groupBy(styleObj, (key, val) => {
417
+ const keys = Object.keys(styleObj)
418
+ let textStyle: Partial<T> | undefined
419
+ let backgroundStyle: Partial<T> | undefined
420
+ let innerStyle: Partial<T> | undefined
421
+ let firstSpecialIdx = -1
422
+
423
+ for (let i = 0; i < keys.length; i++) {
424
+ const key = keys[i]
425
+ const val = styleObj[key]
182
426
  sideEffect && sideEffect(key, val)
427
+
183
428
  if (isTextStyle(key)) {
184
- return 'textStyle'
185
- } else if (hasOwn(BACKGROUND_STYLE_MAP, key)) {
186
- return 'backgroundStyle'
187
- } else {
188
- return 'innerStyle'
429
+ if (firstSpecialIdx < 0) firstSpecialIdx = i
430
+ textStyle = textStyle || {}
431
+ textStyle[key as keyof T] = val
432
+ } else if (hasOwn(backgroundStyleMap, key)) {
433
+ if (firstSpecialIdx < 0) firstSpecialIdx = i
434
+ backgroundStyle = backgroundStyle || {}
435
+ backgroundStyle[key as keyof T] = val
436
+ } else if (firstSpecialIdx >= 0) {
437
+ innerStyle = innerStyle || {}
438
+ innerStyle[key as keyof T] = val
189
439
  }
190
- })
191
- }
192
- const radiusPercentRule: Record<string, 'height' | 'width'> = {
193
- borderTopLeftRadius: 'width',
194
- borderBottomLeftRadius: 'width',
195
- borderBottomRightRadius: 'width',
196
- borderTopRightRadius: 'width',
197
- borderRadius: 'width'
198
- }
199
- const selfPercentRule: Record<string, 'height' | 'width'> = extendObject({
200
- translateX: 'width',
201
- translateY: 'height'
202
- }, radiusPercentRule)
203
-
204
- const parentHeightPercentRule: Record<string, boolean> = {
205
- height: true,
206
- minHeight: true,
207
- maxHeight: true,
208
- top: true,
209
- bottom: true
210
- }
440
+ }
211
441
 
212
- interface PercentConfig {
213
- fontSize?: number | string
214
- width?: number
215
- height?: number
216
- parentFontSize?: number
217
- parentWidth?: number
218
- parentHeight?: number
219
- }
442
+ if (firstSpecialIdx < 0) return { innerStyle: styleObj }
220
443
 
221
- interface PositionMeta {
222
- hasPositionFixed: boolean
444
+ if (firstSpecialIdx > 0) {
445
+ innerStyle = innerStyle || {}
446
+ for (let i = 0; i < firstSpecialIdx; i++) {
447
+ const key = keys[i]
448
+ innerStyle[key as keyof T] = styleObj[key]
449
+ }
450
+ }
451
+ return { textStyle, backgroundStyle, innerStyle }
223
452
  }
224
453
 
225
- function resolvePercent (value: string | number | undefined, key: string, percentConfig: PercentConfig): string | number | undefined {
226
- if (!(typeof value === 'string' && PERCENT_REGEX.test(value))) return value
227
- let base
228
- let reason
229
- if (key === 'fontSize') {
230
- base = percentConfig.parentFontSize
231
- reason = 'parent-font-size'
232
- } else if (key === 'lineHeight') {
233
- base = resolvePercent(percentConfig.fontSize, 'fontSize', percentConfig)
234
- reason = 'font-size'
235
- } else if (selfPercentRule[key]) {
236
- base = percentConfig[selfPercentRule[key]]
237
- reason = selfPercentRule[key]
238
- } else if (parentHeightPercentRule[key]) {
239
- base = percentConfig.parentHeight
240
- reason = 'parent-height'
241
- } else {
242
- base = percentConfig.parentWidth
243
- reason = 'parent-width'
454
+ export function splitProps<T extends Record<string, any>> (props: T = {} as T): {
455
+ textProps?: Partial<T>
456
+ innerProps?: Partial<T>
457
+ } {
458
+ const keys = Object.keys(props)
459
+ let textProps: Partial<T> | undefined
460
+ let innerProps: Partial<T> | undefined
461
+ let firstTextIdx = -1
462
+
463
+ for (let i = 0; i < keys.length; i++) {
464
+ const key = keys[i]
465
+ if (hasOwn(textPropsMap, key)) {
466
+ if (firstTextIdx < 0) firstTextIdx = i
467
+ textProps = textProps || {}
468
+ textProps[key as keyof T] = props[key]
469
+ } else if (firstTextIdx >= 0) {
470
+ innerProps = innerProps || {}
471
+ innerProps[key as keyof T] = props[key]
472
+ }
244
473
  }
245
- if (typeof base !== 'number') {
246
- error(`[${key}] can not contain % unit unless you set [${reason}] with a number for the percent calculation.`)
247
- return value
248
- } else {
249
- return parseFloat(value) / 100 * base
474
+
475
+ if (firstTextIdx < 0) return { innerProps: props }
476
+
477
+ if (firstTextIdx > 0) {
478
+ innerProps = innerProps || {}
479
+ for (let i = 0; i < firstTextIdx; i++) {
480
+ const key = keys[i]
481
+ innerProps[key as keyof T] = props[key]
482
+ }
250
483
  }
484
+ return { textProps, innerProps }
251
485
  }
252
486
 
253
- function transformPercent (styleObj: Record<string, any>, percentKeyPaths: Array<Array<string>>, percentConfig: PercentConfig) {
254
- percentKeyPaths.forEach((percentKeyPath) => {
255
- setStyle(styleObj, percentKeyPath, ({ target, key, value }) => {
256
- target[key] = resolvePercent(value, key, percentConfig)
257
- })
258
- })
487
+ export function pickStyle (styleObj: Record<string, any> = {}, pickedKeys: Array<string>, callback?: (key: string, val: number | string) => number | string) {
488
+ return pickedKeys.reduce<Record<string, any>>((acc, key) => {
489
+ if (key in styleObj) {
490
+ acc[key] = callback ? callback(key, styleObj[key]) : styleObj[key]
491
+ }
492
+ return acc
493
+ }, {})
259
494
  }
260
495
 
496
+ // ============================================================
497
+ // style transform pipeline
498
+ // ============================================================
499
+
500
+ // --- var ---
501
+
261
502
  function resolveVar (input: string, varContext: Record<string, any>) {
262
503
  const parsed = parseFunc(input, 'var')
263
504
  const replaced = new ReplaceSource(input)
@@ -281,40 +522,84 @@ function resolveVar (input: string, varContext: Record<string, any>) {
281
522
  if (varValue === undefined) return
282
523
  replaced.replace(start, end - 1, varValue)
283
524
  }
284
-
285
- return global.__formatValue(replaced.source())
525
+
526
+ return global.__formatValue(replaced.source())
527
+ }
528
+
529
+ function transformVar (styleObj: Record<string, any>, varKeyPaths: Array<Array<string>>, varContext: Record<string, any>, visitOther: (arg: VisitorArg) => void) {
530
+ varKeyPaths.forEach((varKeyPath) => {
531
+ setStyle(styleObj, varKeyPath, ({ target, key, value }) => {
532
+ const resolved = resolveVar(value, varContext)
533
+ if (resolved === undefined) {
534
+ delete target[key]
535
+ error(`Can not resolve css var at ${varKeyPath.join('.')}:${value}.`)
536
+ return
537
+ }
538
+ target[key] = resolved
539
+ visitOther({ target, key, value: target[key], keyPath: varKeyPath })
540
+ })
541
+ })
542
+ }
543
+
544
+ // --- env ---
545
+
546
+ function transformEnv (styleObj: Record<string, any>, envKeyPaths: Array<Array<string>>, navigation: Record<string, any> | undefined) {
547
+ const insets = extendObject({}, initialWindowMetrics?.insets, navigation?.insets)
548
+ envKeyPaths.forEach((envKeyPath) => {
549
+ setStyle(styleObj, envKeyPath, ({ target, key, value }) => {
550
+ const parsed = parseFunc(value, 'env')
551
+ const replaced = new ReplaceSource(value)
552
+ parsed.forEach(({ start, end, args }) => {
553
+ const name = args[0]
554
+ const fallback = args[1] || ''
555
+ const inset = insets[safeAreaInsetMap[name]]
556
+ const next = '' + (inset ?? global.__formatValue(fallback))
557
+ replaced.replace(start, end - 1, next)
558
+ })
559
+ target[key] = global.__formatValue(replaced.source())
560
+ })
561
+ })
562
+ }
563
+
564
+ // --- percent ---
565
+
566
+ function resolvePercent (value: string | number | undefined, key: string, percentConfig: PercentConfig): string | number | undefined {
567
+ if (!(typeof value === 'string' && percentRegExp.test(value))) return value
568
+ let base
569
+ let reason
570
+ if (key === 'fontSize') {
571
+ base = DEFAULT_FONT_SIZE
572
+ reason = 'default-font-size'
573
+ } else if (key === 'lineHeight') {
574
+ base = resolvePercent(percentConfig.fontSize, 'fontSize', percentConfig)
575
+ reason = 'font-size'
576
+ } else if (selfPercentRule[key]) {
577
+ base = percentConfig[selfPercentRule[key]]
578
+ reason = selfPercentRule[key]
579
+ } else if (parentHeightPercentRule[key]) {
580
+ base = percentConfig.parentHeight
581
+ reason = 'parent-height'
582
+ } else {
583
+ base = percentConfig.parentWidth
584
+ reason = 'parent-width'
585
+ }
586
+ if (typeof base !== 'number') {
587
+ error(`[${key}] can not contain % unit unless you set [${reason}] with a number for the percent calculation.`)
588
+ return value
589
+ } else {
590
+ return parseFloat(value) / 100 * base
591
+ }
286
592
  }
287
593
 
288
- function transformVar (styleObj: Record<string, any>, varKeyPaths: Array<Array<string>>, varContext: Record<string, any>, visitOther: (arg: VisitorArg) => void) {
289
- varKeyPaths.forEach((varKeyPath) => {
290
- setStyle(styleObj, varKeyPath, ({ target, key, value }) => {
291
- const resolved = resolveVar(value, varContext)
292
- if (resolved === undefined) {
293
- delete target[key]
294
- error(`Can not resolve css var at ${varKeyPath.join('.')}:${value}.`)
295
- return
296
- }
297
- target[key] = resolved
298
- visitOther({ target, key, value: target[key], keyPath: varKeyPath })
594
+ function transformPercent (styleObj: Record<string, any>, percentKeyPaths: Array<Array<string>>, percentConfig: PercentConfig) {
595
+ percentKeyPaths.forEach((percentKeyPath) => {
596
+ setStyle(styleObj, percentKeyPath, ({ target, key, value }) => {
597
+ target[key] = resolvePercent(value, key, percentConfig)
299
598
  })
300
599
  })
301
600
  }
302
601
 
303
- function transformEnv (styleObj: Record<string, any>, envKeyPaths: Array<Array<string>>, navigation: Record<string, any> | undefined) {
304
- envKeyPaths.forEach((envKeyPath) => {
305
- setStyle(styleObj, envKeyPath, ({ target, key, value }) => {
306
- const parsed = parseFunc(value, 'env')
307
- const replaced = new ReplaceSource(value)
308
- parsed.forEach(({ start, end, args }) => {
309
- const name = args[0]
310
- const fallback = args[1] || ''
311
- const value = '' + (getSafeAreaInset(name, navigation) ?? global.__formatValue(fallback))
312
- replaced.replace(start, end - 1, value)
313
- })
314
- target[key] = global.__formatValue(replaced.source())
315
- })
316
- })
317
- }
602
+ // --- calc ---
318
603
 
319
604
  function transformCalc (styleObj: Record<string, any>, calcKeyPaths: Array<Array<string>>, formatter: (value: string, key: string) => number) {
320
605
  calcKeyPaths.forEach((calcKeyPath) => {
@@ -337,6 +622,8 @@ function transformCalc (styleObj: Record<string, any>, calcKeyPaths: Array<Array
337
622
  })
338
623
  }
339
624
 
625
+ // --- misc ---
626
+
340
627
  function transformStringify (styleObj: Record<string, any>) {
341
628
  if (isNumber(styleObj.fontWeight)) {
342
629
  styleObj.fontWeight = '' + styleObj.fontWeight
@@ -347,34 +634,8 @@ function transformStringify (styleObj: Record<string, any>) {
347
634
  }
348
635
  }
349
636
 
350
- function transformPosition (styleObj: Record<string, any>, meta: PositionMeta) {
351
- if (styleObj.position === 'fixed') {
352
- styleObj.position = 'absolute'
353
- meta.hasPositionFixed = true
354
- }
355
- }
356
- // 多value解析
357
- export function parseValues (str: string, char = ' ') {
358
- let stack = 0
359
- let temp = ''
360
- const result = []
361
- for (let i = 0; i < str.length; i++) {
362
- if (str[i] === '(') {
363
- stack++
364
- } else if (str[i] === ')') {
365
- stack--
366
- }
367
- // 非括号内 或者 非分隔字符且非空
368
- if (stack !== 0 || str[i] !== char) {
369
- temp += str[i]
370
- }
371
- if ((stack === 0 && str[i] === char) || i === str.length - 1) {
372
- result.push(temp.trim())
373
- temp = ''
374
- }
375
- }
376
- return result
377
- }
637
+ // --- transform / shadow ---
638
+
378
639
  // parse string transform, eg: transform: 'rotateX(45deg) rotateZ(0.785398rad)'
379
640
  function parseTransform (transformStr: string) {
380
641
  const values = parseValues(transformStr)
@@ -382,55 +643,108 @@ function parseTransform (transformStr: string) {
382
643
  // Todo 1 transform 排序不一致时,transform动画会闪烁,故这里同样的排序输出 transform
383
644
  // values.sort()
384
645
  const transform: { [propName: string]: string | number | number[] }[] = []
646
+ // 与编译期 wx/index.js 同 transform formatter 口径对齐:
647
+ // 「整条 transform 声明」绝不会因单个子项失败而丢弃,被丢的只有那一个子项,其余 transform 项仍输出。
648
+ // 因此本 forEach 内所有「丢这一项」的提示一律用 warn(编译期同名分支注释:「仅丢这一 transform 项,其它项仍输出,按规范使用 warn」)。
385
649
  values.forEach(item => {
386
- const match = item.match(/([/\w]+)\((.+)\)/)
387
- if (match && match.length >= 3) {
388
- let key = match[1]
389
- const val = match[2]
390
- switch (key) {
391
- case 'translateX':
392
- case 'translateY':
393
- case 'scaleX':
394
- case 'scaleY':
395
- case 'rotateX':
396
- case 'rotateY':
397
- case 'rotateZ':
398
- case 'rotate':
399
- case 'skewX':
400
- case 'skewY':
401
- case 'perspective':
402
- // rotate 处理成 rotateZ
403
- key = key === 'rotate' ? 'rotateZ' : key
404
- // 单个值处理
405
- transform.push({ [key]: global.__formatValue(val) })
406
- break
407
- case 'matrix':
408
- transform.push({ [key]: parseValues(val, ',').map(val => +val) })
409
- break
410
- case 'translate':
411
- case 'scale':
412
- case 'skew':
413
- case 'translate3d': // x y 支持 z不支持
414
- case 'scale3d': // x y 支持 z不支持
415
- {
416
- // 2 个以上的值处理
417
- key = key.replace('3d', '')
418
- const vals = parseValues(val, ',').splice(0, 3)
419
- // scale(.5) === scaleX(.5) scaleY(.5)
420
- if (vals.length === 1 && key === 'scale') {
421
- vals.push(vals[0])
650
+ const match = item.match(transformFnRegExp)
651
+ if (!match || match.length < 3) {
652
+ warn(`Transform value [${item}] is not a valid fn(value) form, dropped.`)
653
+ return
654
+ }
655
+ let key = match[1]
656
+ const val = match[2]
657
+ switch (key) {
658
+ case 'rotateX':
659
+ case 'rotateY':
660
+ case 'rotateZ':
661
+ case 'rotate':
662
+ case 'skewX':
663
+ case 'skewY':
664
+ key = key === 'rotate' ? 'rotateZ' : key
665
+ transform.push({ [key]: val })
666
+ break
667
+ case 'translateX':
668
+ case 'translateY':
669
+ case 'scaleX':
670
+ case 'scaleY':
671
+ case 'perspective':
672
+ transform.push({ [key]: global.__formatValue(val) })
673
+ break
674
+ case 'matrix': {
675
+ // parseValues 内部已 trim
676
+ const matrixValues = parseValues(val, ',').map(v => +v)
677
+ if (matrixValues.length === 6) {
678
+ const [a, b, c, d, tx, ty] = matrixValues
679
+ transform.push({ matrix: [a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1] })
680
+ } else {
681
+ warn(`Transform matrix only supports 6 values in React Native, got ${matrixValues.length}`)
682
+ }
683
+ break
684
+ }
685
+ case 'matrix3d': {
686
+ // parseValues 内部已 trim
687
+ const matrixValues = parseValues(val, ',').map(v => +v)
688
+ if (matrixValues.length === 16) {
689
+ transform.push({ matrix: matrixValues })
690
+ } else {
691
+ warn(`Transform matrix only supports 16 values in React Native, got ${matrixValues.length}`)
692
+ }
693
+ break
694
+ }
695
+ case 'translate':
696
+ case 'scale':
697
+ case 'skew':
698
+ case 'translate3d': // x y 支持 z不支持
699
+ case 'scale3d': // x y 支持 z不支持
700
+ {
701
+ // 2 个以上的值处理
702
+ key = key.replace('3d', '')
703
+ const vals = parseValues(val, ',').splice(0, 3)
704
+ // scale(.5) === scaleX(.5) scaleY(.5)
705
+ if (vals.length === 1 && key === 'scale') {
706
+ vals.push(vals[0])
707
+ }
708
+ const xyz = ['X', 'Y', 'Z']
709
+ vals.forEach((v, index) => {
710
+ if (key !== 'rotate' && index > 1) {
711
+ warn(`Transform [${key}Z] is not supported in React Native, dropped.`)
712
+ return
422
713
  }
423
- const xyz = ['X', 'Y', 'Z']
424
- transform.push(...vals.map((v, index) => {
425
- return { [`${key}${xyz[index] || ''}`]: global.__formatValue(v.trim()) }
426
- }))
714
+ // parseValues 内部已 trim
715
+ transform.push({ [`${key}${xyz[index] || ''}`]: global.__formatValue(v) })
716
+ })
717
+ break
718
+ }
719
+ case 'rotate3d': {
720
+ const parts = parseValues(val, ',')
721
+ if (parts.length !== 4) {
722
+ warn(`Transform rotate3d only supports 4 values, got ${parts.length}`)
427
723
  break
428
724
  }
725
+ // parseValues 内部已 trim
726
+ const x = +parts[0]
727
+ const y = +parts[1]
728
+ const z = +parts[2]
729
+ const angle = parts[3]
730
+ if (x && !y && !z) transform.push({ rotateX: angle })
731
+ else if (!x && y && !z) transform.push({ rotateY: angle })
732
+ else if (!x && !y && z) transform.push({ rotateZ: angle })
733
+ else warn(`Transform rotate3d(${val}) only supports single-axis vector (1,0,0) / (0,1,0) / (0,0,1) in React Native, dropped.`)
734
+ break
429
735
  }
736
+ case 'translateZ':
737
+ case 'scaleZ':
738
+ // RN 不支持 Z 轴 translate/scale,丢该子项;编译期同分支用 warn(unsupportedPropError isError=false)
739
+ warn(`Transform [${key}] is not supported in React Native, dropped.`)
740
+ break
741
+ default:
742
+ warn(`Transform fn [${key}] is not supported in React Native, dropped.`)
430
743
  }
431
744
  })
432
745
  return transform
433
746
  }
747
+
434
748
  // format style transform
435
749
  function transformTransform (style: Record<string, any>) {
436
750
  if (!style.transform || Array.isArray(style.transform)) return
@@ -440,20 +754,471 @@ function transformTransform (style: Record<string, any>) {
440
754
  function transformBoxShadow (styleObj: Record<string, any>) {
441
755
  if (!styleObj.boxShadow) return
442
756
  styleObj.boxShadow = parseValues(styleObj.boxShadow).reduce((res, i, idx) => {
443
- return `${res}${idx === 0 ? '' : ' '}${global.__formatValue(i)}`
757
+ let formatted: string | number
758
+ // 需要保留 px 关键字,这里仅处理 rpx 转 px
759
+ if (rpxSuffixRegExp.test(i)) {
760
+ formatted = global.__formatValue(i) + 'px'
761
+ } else {
762
+ formatted = i
763
+ }
764
+ return `${res}${idx === 0 ? '' : ' '}${formatted}`
444
765
  }, '')
445
766
  }
446
767
 
447
- interface TransformStyleConfig {
448
- enableVar?: boolean
449
- externalVarContext?: Record<string, any>
450
- parentFontSize?: number
451
- parentWidth?: number
452
- parentHeight?: number
453
- transformRadiusPercent?: boolean
768
+ // --- shorthand ---
769
+
770
+ function expandCompositeValues (values: string[]): string[] {
771
+ const v = values.slice(0, 4)
772
+ switch (v.length) {
773
+ case 1:
774
+ return [v[0], v[0], v[0], v[0]]
775
+ case 2:
776
+ return [v[0], v[1], v[0], v[1]]
777
+ case 3:
778
+ return [v[0], v[1], v[2], v[1]]
779
+ default:
780
+ return v
781
+ }
782
+ }
783
+
784
+ function expandAbbreviation (values: string[], props: string[]): Array<[string, any]> {
785
+ const result: Array<[string, any]> = []
786
+ for (let i = 0; i < props.length && i < values.length; i++) {
787
+ pushExpandedPair(result, props[i], global.__formatValue(values[i]))
788
+ }
789
+ return result
790
+ }
791
+
792
+ function pushExpandedPair (result: Array<[string, any]>, prop: string, value: any) {
793
+ if (prop.includes('.')) {
794
+ const [main, sub] = prop.split('.')
795
+ let entry = result.find(item => item[0] === main)
796
+ if (!entry) {
797
+ entry = [main, {}]
798
+ result.push(entry)
799
+ }
800
+ entry[1][sub] = value
801
+ } else {
802
+ result.push([prop, value])
803
+ }
804
+ }
805
+
806
+ function getUnorderedShorthandProp (key: string, token: string, used: Record<string, boolean>): string | undefined {
807
+ const props = runtimeAbbreviationMap[key]
808
+ for (const prop of props) {
809
+ if (used[prop]) continue
810
+ if (matchRuntimeShorthandProp(prop, token)) return prop
811
+ }
812
+ }
813
+
814
+ function matchRuntimeShorthandProp (prop: string, token: string): boolean {
815
+ if (prop === 'textShadowOffset.width' || prop === 'textShadowOffset.height' || prop === 'textShadowRadius') return isLengthValue(token)
816
+ if (prop === 'textShadowColor') return isColorValue(token)
817
+ if (prop.endsWith('Width')) return isLengthValue(token)
818
+ if (prop === 'textDecorationStyle') return hasOwn(textDecorationStyleMap, token)
819
+ if (prop.endsWith('Style')) return hasOwn(borderStyleMap, token)
820
+ if (prop.endsWith('Color')) return isColorValue(token)
821
+ if (prop === 'flexDirection') return hasOwn(flexDirectionMap, token)
822
+ if (prop === 'flexWrap') return hasOwn(flexWrapMap, token)
823
+ return false
824
+ }
825
+
826
+ // 通用补齐:扫描完所有 token 后,将 runtimeShorthandDefaultMap 中未被占用(不在 used)的槽位追加到 pairs
827
+ // used 即主循环的占用记录,key 是完整目标 prop 名(含 textShadowOffset.width 这类 dot 路径)
828
+ function applyRuntimeShorthandDefaults (key: string, pairs: Array<[string, any]>, used: Record<string, boolean>): Array<[string, any]> {
829
+ const defaults = runtimeShorthandDefaultMap[key]
830
+ if (!defaults) return pairs
831
+ for (const target in defaults) {
832
+ if (!used[target]) {
833
+ pushExpandedPair(pairs, target, defaults[target])
834
+ }
835
+ }
836
+ return pairs
837
+ }
838
+
839
+ function expandUnorderedAbbreviation (key: string, values: string[]): Array<[string, any]> {
840
+ const result: Array<[string, any]> = []
841
+ const used: Record<string, boolean> = {}
842
+ let hasTextDecorationNone = false
843
+ let hasUnderline = false
844
+ let hasLineThrough = false
845
+ for (const value of values) {
846
+ if (key === 'textDecoration' && hasOwn(textDecorationLineMap, value)) {
847
+ switch (value) {
848
+ case 'underline':
849
+ hasUnderline = true
850
+ continue
851
+ case 'line-through':
852
+ hasLineThrough = true
853
+ continue
854
+ case 'none':
855
+ hasTextDecorationNone = true
856
+ continue
857
+ // textDecorationLineMap 命中但分支未处理的 line token:落到通用 getUnorderedShorthandProp 流程
858
+ }
859
+ }
860
+ const prop = getUnorderedShorthandProp(key, value, used)
861
+ if (!prop) {
862
+ // 该 token 与 key 下任何空闲槽位都不匹配(未知类型 / 槽位已占满),静默丢弃可能让用户难以察觉
863
+ warn(`Token [${value}] in [${key}: ${values.join(' ')}] is not a valid value or has no available slot, dropped.`)
864
+ continue
865
+ }
866
+ used[prop] = true
867
+ pushExpandedPair(result, prop, global.__formatValue(value))
868
+ }
869
+ if (hasUnderline || hasLineThrough) {
870
+ result.push(['textDecorationLine', hasUnderline && hasLineThrough ? 'underline line-through' : hasUnderline ? 'underline' : 'line-through'])
871
+ } else if (hasTextDecorationNone) {
872
+ result.push(['textDecorationLine', 'none'])
873
+ }
874
+ // text-shadow 至少需要 offset-x / offset-y;缺省 height 时按 CSS 默认补 0 并发出 warn
875
+ if (key === 'textShadow') {
876
+ const offsetEntry = result.find(item => item[0] === 'textShadowOffset')?.[1]
877
+ if (offsetEntry && offsetEntry.width !== undefined && offsetEntry.height === undefined) {
878
+ warn(`Value of [textShadow:${values.join(' ')}] is missing offset-y, fallback to 0, please check again!`)
879
+ offsetEntry.height = 0
880
+ }
881
+ }
882
+ return applyRuntimeShorthandDefaults(key, result, used)
883
+ }
884
+
885
+ function expandFlex (value: string): Array<[string, any]> | null {
886
+ const values = parseValues(value)
887
+ if (values.length === 0) return null
888
+ if (values.length === 1) {
889
+ if (values[0] === 'none') {
890
+ return [['flexGrow', 0], ['flexShrink', 0]]
891
+ }
892
+ if (values[0] === 'initial') {
893
+ return [['flexGrow', 0], ['flexShrink', 1]]
894
+ }
895
+ }
896
+ const result: Array<[string, any]> = []
897
+ let i = 0
898
+ const isNum = (v: string) => !isNaN(+v)
899
+ if (isNum(values[i])) {
900
+ result.push(['flexGrow', +values[i++]])
901
+ } else {
902
+ result.push(['flexGrow', 1])
903
+ }
904
+ if (i < values.length && isNum(values[i])) {
905
+ result.push(['flexShrink', +values[i++]])
906
+ } else {
907
+ result.push(['flexShrink', 1])
908
+ }
909
+ if (i < values.length) {
910
+ if (values[i] !== 'auto') {
911
+ result.push(['flexBasis', global.__formatValue(values[i])])
912
+ }
913
+ } else {
914
+ result.push(['flexBasis', 0])
915
+ }
916
+ return result
917
+ }
918
+
919
+ // font 简写专用 transform,仿 transformFlex 的标志位模式。
920
+ // RN 等效子集语法:font: [ <font-style> ] [ <font-variant-css2> ] [ <font-weight> ] <font-size> [ / <line-height> ] <font-family>
921
+ // - 必填项:font-size 与 font-family;缺其一整条 font 声明丢弃(error)
922
+ // - 非必填 token(font-stretch / 数字型 font-variant-numeric / system 关键字等):warn 提示并忽略,保留其余槽位
923
+ function transformFont (styleObj: Record<string, any>) {
924
+ const value = styleObj.font
925
+ if (typeof value !== 'string') return
926
+ const tokens = parseValues(value)
927
+ let sizeIdx = -1
928
+ let lineHeight: string | undefined
929
+ const result: Array<[string, any]> = []
930
+ // 1. 定位 font-size(第一个 length,可能带 /<line-height>)
931
+ // 注意:unit-less 数字也命中 length 正则,需要先排除 font-weight 数字(100..900 / bold / normal),
932
+ // 否则 `font: 500 16px Arial` 会把 500 误判为 fontSize。
933
+ // fontSize 自身可能是 %(如 `font: 50% Arial`),保留给文本透传阶段按继承 fontSize 解析;
934
+ // fontSize % 校验通过的是 `isLengthValue`(length 正则含 %),主流程已用 length 分支接住,无需特判
935
+ for (let i = 0; i < tokens.length; i++) {
936
+ let t = tokens[i]
937
+ if (t.endsWith('/') && tokens[i + 1]) {
938
+ t += tokens[i + 1]
939
+ tokens.splice(i + 1, 1)
940
+ } else if (tokens[i + 1] === '/' && tokens[i + 2]) {
941
+ t += `/${tokens[i + 2]}`
942
+ tokens.splice(i + 1, 2)
943
+ } else if (tokens[i + 1]?.startsWith('/') && tokens[i + 1].length > 1) {
944
+ t += tokens[i + 1]
945
+ tokens.splice(i + 1, 1)
946
+ }
947
+ const [sizePart, lhPart] = parseValues(t, '/')
948
+ if (hasOwn(fontWeightMap, sizePart)) continue
949
+ if (isLengthValue(sizePart)) {
950
+ sizeIdx = i
951
+ result.push(['fontSize', global.__formatValue(sizePart)])
952
+ if (lhPart) lineHeight = lhPart
953
+ break
954
+ }
955
+ }
956
+ if (sizeIdx === -1) {
957
+ // 缺必填 font-size 整体丢弃;与 transformFlex「Flex shorthand value [...] ..., dropped.」同口径
958
+ error(`Font shorthand value [${value}] is missing required <font-size>, dropped.`)
959
+ delete styleObj.font
960
+ return
961
+ }
962
+ // 2. 前导段 font-style / font-variant(small-caps) / font-weight,顺序不敏感
963
+ for (let i = 0; i < sizeIdx; i++) {
964
+ const t = tokens[i]
965
+ if (t === 'normal') continue
966
+ if (t === 'italic') {
967
+ result.push(['fontStyle', t])
968
+ } else if (t === 'small-caps') {
969
+ // RN processFontVariant 接受字符串,内部 split 归一为数组,与 font-variant 长属性同口径
970
+ result.push(['fontVariant', t])
971
+ } else if (hasOwn(fontWeightMap, t)) {
972
+ result.push(['fontWeight', t])
973
+ } else {
974
+ // 其余(font-stretch / 数字型 font-variant-numeric / system 关键字等)→ 非必填:
975
+ // 与 transformBackground「Token [...] in [background: ...] ..., dropped.」同口径,warn + 忽略该 token、保留其余
976
+ warn(`Token [${t}] in [font: ${value}] is not supported (only font-style / small-caps / font-weight are valid before <font-size>), dropped.`)
977
+ }
978
+ }
979
+ // 3. line-height:文本透传阶段会基于当前 / 继承 fontSize 解析百分比。
980
+ // - unit-less 数字(1.5)→ '150%' 中间态,避免和已格式化 RN 绝对 number 混淆
981
+ // - 其它值(120% / 40px / 40rpx)→ __formatValue
982
+ if (lineHeight !== undefined) {
983
+ const lh = !isNaN(+lineHeight)
984
+ ? `${+lineHeight * 100}%`
985
+ : global.__formatValue(lineHeight)
986
+ if (lh !== undefined) {
987
+ result.push(['lineHeight', lh])
988
+ } else {
989
+ warn(`Line-height [${lineHeight}] in [font: ${value}] could not be resolved, dropped.`)
990
+ }
991
+ }
992
+ // 4. font-family(font-size 之后剩余部分;多字体取首值、去引号)
993
+ const familyStr = tokens.slice(sizeIdx + 1).join(' ').trim()
994
+ if (!familyStr) {
995
+ error(`Font shorthand value [${value}] is missing required <font-family>, dropped.`)
996
+ delete styleObj.font
997
+ return
998
+ }
999
+ // parseValues 内部已 trim
1000
+ const family = parseValues(familyStr.replace(quoteCharRegExp, ''), ',')[0]
1001
+ if (family) result.push(['fontFamily', family])
1002
+ delete styleObj.font
1003
+ for (const [prop, val] of result) {
1004
+ if (!hasOwn(styleObj, prop)) styleObj[prop] = val
1005
+ }
1006
+ }
1007
+
1008
+ function transformFlex (styleObj: Record<string, any>) {
1009
+ const value = styleObj.flex
1010
+ if (typeof value !== 'string') return
1011
+ delete styleObj.flex
1012
+ const flexResult = expandFlex(value)
1013
+ if (!flexResult) {
1014
+ // expandFlex 仅在 parseValues 出空数组时返回 null(空字符串 / 纯空白),属于明确非法,整段丢弃
1015
+ error(`Flex shorthand value [${value}] is not a valid CSS flex value, dropped.`)
1016
+ return
1017
+ }
1018
+ for (const [prop, val] of flexResult) {
1019
+ if (!hasOwn(styleObj, prop)) styleObj[prop] = val
1020
+ }
1021
+ }
1022
+
1023
+ export function transformShorthand (styleObj: Record<string, any>, shorthandKeys: string[], percentConfig: PercentConfig) {
1024
+ for (const key of shorthandKeys) {
1025
+ const value = styleObj[key]
1026
+
1027
+ // —— 以下为公共链路(与 textDecoration / flexFlow / textShadow / 四值简写共用)——
1028
+ let values: string[]
1029
+ if (typeof value === 'string') {
1030
+ values = parseValues(value)
1031
+ } else if (typeof value === 'number' && hasOwn(runtimeNumericExpandShorthandMap, key)) {
1032
+ // runtimeNumericExpandShorthandMap 中的简写(border / outline / gap)在 __getStyle 的 transformStyleObj
1033
+ // 阶段已被 __formatValue 把带单位长度换算为 number;这里转回字符串 token 交给后续展开,
1034
+ // 否则会原样残留为 RN 不认识或行为不一致的简写 key。
1035
+ values = ['' + value]
1036
+ } else {
1037
+ continue
1038
+ }
1039
+ const props = runtimeAbbreviationMap[key]
1040
+ if (!props) continue
1041
+ // 单值短路:composite 且单值通常透传(RN 原生支持 margin / padding / inset / border-* 单值 DimensionValue);
1042
+ // 但 gap 必须展开 —— RN gap / rowGap / columnGap 严格要求 number,单值串虽已在 __getStyle 阶段被 __formatValue 换算,
1043
+ // 多值串(如 '10px 20px')仍会原样透传到这里,需展开后逐 token 再过 __formatValue 才能落成 number;
1044
+ // `%` 在写回阶段由 transformShorthand 调 resolvePercent 落成 number(rowGap 基 parentHeight、columnGap 基 parentWidth)
1045
+ if (
1046
+ hasOwn(runtimeCompositeStyleMap, key) &&
1047
+ values.length === 1 &&
1048
+ key !== 'gap'
1049
+ ) continue
1050
+ let expandedValues = values
1051
+ let pairs: Array<[string, any]>
1052
+ if (hasOwn(runtimeCompositeStyleMap, key)) {
1053
+ expandedValues = expandCompositeValues(values)
1054
+ }
1055
+ if (hasOwn(runtimeUnorderedAbbreviationMap, key)) {
1056
+ // expandUnorderedAbbreviation 末尾已内部调用 applyRuntimeShorthandDefaults 补齐
1057
+ pairs = expandUnorderedAbbreviation(key, values)
1058
+ } else {
1059
+ pairs = expandAbbreviation(expandedValues, props)
1060
+ }
1061
+
1062
+ delete styleObj[key]
1063
+ for (const [prop, val] of pairs) {
1064
+ if (hasOwn(styleObj, prop)) continue
1065
+ // gap 简写展开后的 rowGap / columnGap 仍可能是 `%` 字符串(如 `gap: 50%` / `gap: 10px 50%`),
1066
+ // RN 类型严格要求 number,这里就地用 resolvePercent 落成数字(rowGap 基 parentHeight、columnGap 基 parentWidth)
1067
+ if ((prop === 'rowGap' || prop === 'columnGap') && typeof val === 'string' && percentRegExp.test(val)) {
1068
+ styleObj[prop] = resolvePercent(val, prop, percentConfig)
1069
+ } else {
1070
+ styleObj[prop] = val
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ // --- runtime alignment ---
1077
+
1078
+ function transformFontFamily (styleObj: Record<string, any>) {
1079
+ const value = styleObj.fontFamily
1080
+ if (typeof value !== 'string') return
1081
+ const stripped = value.replace(quoteCharRegExp, '').trim()
1082
+ if (!stripped) return
1083
+ const values = parseValues(stripped, ',')
1084
+ // parseValues 内部已 trim
1085
+ styleObj.fontFamily = values[0]
1086
+ }
1087
+
1088
+ function transformBorderStyleNone (styleObj: Record<string, any>) {
1089
+ if (styleObj.borderStyle === 'none') {
1090
+ delete styleObj.borderStyle
1091
+ delete styleObj.borderTopWidth
1092
+ delete styleObj.borderRightWidth
1093
+ delete styleObj.borderBottomWidth
1094
+ delete styleObj.borderLeftWidth
1095
+ styleObj.borderWidth = 0
1096
+ }
1097
+ if (styleObj.outlineStyle === 'none') {
1098
+ delete styleObj.outlineStyle
1099
+ styleObj.outlineWidth = 0
1100
+ }
1101
+ }
1102
+
1103
+ function transformPosition (styleObj: Record<string, any>) {
1104
+ if (styleObj.position === 'fixed') {
1105
+ styleObj.position = 'absolute'
1106
+ return true
1107
+ }
1108
+ return false
1109
+ }
1110
+
1111
+ function transOrderXY (values: string[]) {
1112
+ if (values.length === 2 && ['top', 'bottom'].includes(values[0])) {
1113
+ [values[0], values[1]] = [values[1], values[0]]
1114
+ }
1115
+ return values
1116
+ }
1117
+ function transformBackground (styleObj: Record<string, any>) {
1118
+ if (typeof styleObj.backgroundSize === 'string') {
1119
+ styleObj.backgroundSize = parseValues(styleObj.backgroundSize)
1120
+ }
1121
+ if (typeof styleObj.backgroundPosition === 'string') {
1122
+ const parts = parseValues(styleObj.backgroundPosition)
1123
+ styleObj.backgroundPosition = transOrderXY(parts.map(v => v === 'center' ? '50%' : v))
1124
+ }
1125
+ const value = styleObj.background
1126
+ if (typeof value !== 'string') return
1127
+ delete styleObj.background
1128
+ if (value === 'none') {
1129
+ styleObj.backgroundImage = 'none'
1130
+ styleObj.backgroundColor = 'transparent'
1131
+ return
1132
+ }
1133
+ const tokens = parseValues(value)
1134
+ const positionValues: string[] = []
1135
+ const sizeValues: string[] = []
1136
+ let isSize = false
1137
+ for (const token of tokens) {
1138
+ if (urlRegExp.test(token)) {
1139
+ styleObj.backgroundImage = token
1140
+ } else if (linearGradientRegExp.test(token)) {
1141
+ styleObj.backgroundImage = token
1142
+ } else if (hasOwn(bgRepeatMap, token)) {
1143
+ styleObj.backgroundRepeat = token
1144
+ } else if (isColorValue(token)) {
1145
+ styleObj.backgroundColor = token
1146
+ } else if (token === '/') {
1147
+ isSize = true
1148
+ } else {
1149
+ const slashParts = parseValues(token, '/')
1150
+ if (slashParts.length > 1) {
1151
+ const posPart = slashParts[0]
1152
+ if (posPart) positionValues.push(posPart === 'center' ? '50%' : posPart)
1153
+ isSize = true
1154
+ for (let i = 1; i < slashParts.length; i++) {
1155
+ if (slashParts[i]) sizeValues.push(slashParts[i])
1156
+ }
1157
+ } else if (isSize) {
1158
+ sizeValues.push(token)
1159
+ } else if (hasOwn(bgPositionMap, token) || percentRegExp.test(token) || digitStartRegExp.test(token)) {
1160
+ positionValues.push(token === 'center' ? '50%' : token)
1161
+ } else {
1162
+ // background-attachment / background-origin / background-clip 及拼写错误等:RN 均不支持,提示用户避免静默丢值
1163
+ warn(`Token [${token}] in [background: ${value}] is not a recognized background sub-value, dropped.`)
1164
+ }
1165
+ }
1166
+ }
1167
+ if (positionValues.length) styleObj.backgroundPosition = transOrderXY(positionValues)
1168
+ if (sizeValues.length) styleObj.backgroundSize = sizeValues
1169
+ }
1170
+
1171
+ // ============================================================
1172
+ // style traversal
1173
+ // ============================================================
1174
+
1175
+ export function traverseStyle (styleObj: Record<string, any>, visitor: (arg: VisitorArg) => void) {
1176
+ const keyPath: Array<string> = []
1177
+ function traverse<T extends Record<string, any>> (target: T) {
1178
+ if (Array.isArray(target)) {
1179
+ for (let i = 0; i < target.length; i++) {
1180
+ const key = String(i)
1181
+ const value = target[i]
1182
+ keyPath.push(key)
1183
+ visitor({ target, key, value, keyPath })
1184
+ traverse(value)
1185
+ keyPath.pop()
1186
+ }
1187
+ } else if (isObject(target)) {
1188
+ const keys = Object.keys(target)
1189
+ for (let i = 0; i < keys.length; i++) {
1190
+ const key = keys[i]
1191
+ const value = (target as Record<string, any>)[key]
1192
+ keyPath.push(key)
1193
+ visitor({ target, key, value, keyPath })
1194
+ traverse(value)
1195
+ keyPath.pop()
1196
+ }
1197
+ }
1198
+ }
1199
+ traverse(styleObj)
1200
+ }
1201
+
1202
+ export function setStyle (styleObj: Record<string, any>, keyPath: Array<string>, setter: (arg: VisitorArg) => void) {
1203
+ let target = styleObj
1204
+ const lastKey = keyPath[keyPath.length - 1]
1205
+ for (let i = 0; i < keyPath.length - 1; i++) {
1206
+ target = target[keyPath[i]]
1207
+ if (!target) return
1208
+ }
1209
+ setter({
1210
+ target,
1211
+ key: lastKey,
1212
+ value: target[lastKey],
1213
+ keyPath
1214
+ })
454
1215
  }
455
1216
 
456
- export function useTransformStyle (styleObj: Record<string, any> = {}, { enableVar, transformRadiusPercent, externalVarContext, parentFontSize, parentWidth, parentHeight }: TransformStyleConfig) {
1217
+ // ============================================================
1218
+ // core style hook
1219
+ // ============================================================
1220
+
1221
+ export function useTransformStyle (styleObj: Record<string, any> = {}, { enableVar, transformRadiusPercent, parentWidth, parentHeight, defaultStyle }: TransformStyleConfig) {
457
1222
  const varStyle: Record<string, any> = {}
458
1223
  const unoVarStyle: Record<string, any> = {}
459
1224
  const normalStyle: Record<string, any> = {}
@@ -461,16 +1226,84 @@ export function useTransformStyle (styleObj: Record<string, any> = {}, { enableV
461
1226
  let hasVarUse = false
462
1227
  let hasSelfPercent = false
463
1228
  let hasBoxSizingAffectingStyle = false
1229
+ // 顶层 transform* 标志位:在 styleVisitor 阶段从原始 styleObj 收集,
1230
+ // 调用侧据此决定是否进入对应 transform 函数。
1231
+ let hasTransform = false
1232
+ let hasBoxShadow = false
1233
+ let hasFontFamily = false
1234
+ let hasFlex = false
1235
+ let hasFont = false
1236
+ let needTransformBackground = false
1237
+ let needStringify = false
464
1238
  const varKeyPaths: Array<Array<string>> = []
465
1239
  const unoVarKeyPaths: Array<Array<string>> = []
466
1240
  const percentKeyPaths: Array<Array<string>> = []
467
1241
  const calcKeyPaths: Array<Array<string>> = []
468
1242
  const envKeyPaths: Array<Array<string>> = []
1243
+ const shorthandKeys: string[] = []
469
1244
  const [width, setWidth] = useState(0)
470
1245
  const [height, setHeight] = useState(0)
471
1246
  const navigation = useNavigation()
472
1247
 
473
- function varVisitor ({ target, key, value, keyPath }: VisitorArg) {
1248
+ function collectTopLevelFlags (key: string, value: any) {
1249
+ switch (key) {
1250
+ case 'transform':
1251
+ hasTransform = true
1252
+ break
1253
+ case 'boxShadow':
1254
+ hasBoxShadow = true
1255
+ break
1256
+ case 'fontFamily':
1257
+ hasFontFamily = true
1258
+ break
1259
+ case 'flex':
1260
+ hasFlex = true
1261
+ break
1262
+ case 'font':
1263
+ hasFont = true
1264
+ break
1265
+ case 'background':
1266
+ case 'backgroundSize':
1267
+ case 'backgroundPosition':
1268
+ needTransformBackground = true
1269
+ break
1270
+ case 'fontWeight':
1271
+ case 'transformOrigin':
1272
+ needStringify = true
1273
+ break
1274
+ }
1275
+ }
1276
+
1277
+ function visitOther ({ key, value, keyPath }: VisitorArg) {
1278
+ if (typeof value !== 'string') return
1279
+ const hasPercent = value.includes('%')
1280
+ const hasCalc = value.includes('calc(')
1281
+ const hasEnv = value.includes('env(')
1282
+ if (!(hasPercent || hasCalc || hasEnv)) return
1283
+ let resolvedKeyPath: Array<string> | undefined
1284
+ if (hasEnv) {
1285
+ resolvedKeyPath = keyPath.slice()
1286
+ envKeyPaths.push(resolvedKeyPath)
1287
+ }
1288
+ if (hasPercent) {
1289
+ // fixme 去掉 translate & border-radius 的百分比计算
1290
+ // fixme Image 组件 borderRadius 仅支持 number
1291
+ const needRadiusPercent = transformRadiusPercent && hasOwn(radiusPercentRule, key)
1292
+ // RN gap / rowGap / columnGap 不支持 %,需要运行时换算为 number 喂给 RN
1293
+ const needGapPercent = key === 'rowGap' || key === 'columnGap'
1294
+ if ((needRadiusPercent || needGapPercent) && percentRegExp.test(value)) {
1295
+ if (needRadiusPercent) hasSelfPercent = true
1296
+ resolvedKeyPath = resolvedKeyPath ?? keyPath.slice()
1297
+ percentKeyPaths.push(resolvedKeyPath)
1298
+ }
1299
+ }
1300
+ if (hasCalc) {
1301
+ resolvedKeyPath = resolvedKeyPath ?? keyPath.slice()
1302
+ calcKeyPaths.push(resolvedKeyPath)
1303
+ }
1304
+ }
1305
+
1306
+ function styleVisitor ({ target, key, value, keyPath }: VisitorArg) {
474
1307
  if (keyPath.length === 1) {
475
1308
  if (unoVarDecRegExp.test(key)) {
476
1309
  unoVarStyle[key] = value
@@ -480,128 +1313,116 @@ export function useTransformStyle (styleObj: Record<string, any> = {}, { enableV
480
1313
  } else {
481
1314
  // clone对象避免set值时改写到props
482
1315
  normalStyle[key] = isObject(value) ? diffAndCloneA(value).clone : value
1316
+ if (!hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(key)) {
1317
+ hasBoxSizingAffectingStyle = true
1318
+ }
1319
+ if (hasOwn(runtimeAbbreviationMap, key)) {
1320
+ shorthandKeys.push(key)
1321
+ }
1322
+ collectTopLevelFlags(key, value)
483
1323
  }
484
1324
  }
485
- // 对于var定义中使用的var无需替换值,可以通过resolveVar递归解析出值
486
- if (!varDecRegExp.test(key)) {
487
- // 一般情况下一个样式属性中不会混用unocss var和普通css var,可分开进行互斥处理
488
- if (unoVarUseRegExp.test(value)) {
489
- unoVarKeyPaths.push(keyPath.slice())
490
- } else if (varUseRegExp.test(value)) {
491
- hasVarUse = true
492
- varKeyPaths.push(keyPath.slice())
493
- } else {
494
- visitOther({ target, key, value, keyPath })
495
- }
496
- }
497
- }
498
-
499
- function boxSizingVisitor ({ key, keyPath }: VisitorArg) {
500
- if (keyPath.length === 1 && !hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(key)) {
501
- hasBoxSizingAffectingStyle = true
502
- }
503
- }
504
-
505
- function envVisitor ({ value, keyPath }: VisitorArg) {
506
- if (envUseRegExp.test(value)) {
507
- envKeyPaths.push(keyPath.slice())
508
- }
509
- }
510
-
511
- function calcVisitor ({ value, keyPath }: VisitorArg) {
512
- if (calcUseRegExp.test(value)) {
513
- calcKeyPaths.push(keyPath.slice())
514
- }
515
- }
516
-
517
- function percentVisitor ({ key, value, keyPath }: VisitorArg) {
518
- // fixme 去掉 translate & border-radius 的百分比计算
519
- // fixme Image 组件 borderRadius 仅支持 number
520
- if (transformRadiusPercent && hasOwn(radiusPercentRule, key) && PERCENT_REGEX.test(value)) {
521
- hasSelfPercent = true
522
- percentKeyPaths.push(keyPath.slice())
523
- } else if ((key === 'fontSize' || key === 'lineHeight') && PERCENT_REGEX.test(value)) {
524
- percentKeyPaths.push(keyPath.slice())
525
- }
526
- }
527
-
528
- function visitOther ({ target, key, value, keyPath }: VisitorArg) {
529
- if (typeof value === 'string' && (value.includes('%') || value.includes('calc(') || value.includes('env('))) {
530
- [envVisitor, percentVisitor, calcVisitor].forEach(visitor => visitor({ target, key, value, keyPath }))
1325
+ // var 定义中使用的 var 无需替换值,可以通过 resolveVar 递归解析出值
1326
+ if (varDecRegExp.test(key) || typeof value !== 'string') return
1327
+ // 一般情况下一个样式属性中不会混用 unocss var 和普通 css var,可分开互斥处理
1328
+ if (unoVarUseRegExp.test(value)) {
1329
+ unoVarKeyPaths.push(keyPath.slice())
1330
+ } else if (varUseRegExp.test(value)) {
1331
+ hasVarUse = true
1332
+ varKeyPaths.push(keyPath.slice())
1333
+ } else {
1334
+ visitOther({ target, key, value, keyPath })
531
1335
  }
532
1336
  }
533
1337
 
534
1338
  // traverse var & generate normalStyle
535
- traverseStyle(styleObj, [varVisitor, boxSizingVisitor])
536
- hasVarDec = hasVarDec || !!externalVarContext
1339
+ traverseStyle(styleObj, styleVisitor)
537
1340
  enableVar = enableVar || hasVarDec || hasVarUse
538
1341
  const enableVarRef = useRef(enableVar)
539
1342
  if (enableVarRef.current !== enableVar) {
540
1343
  error('css variable use/declare should be stable in the component lifecycle, or you can set [enable-var] with true.')
541
1344
  }
542
1345
  // apply css var
543
- const varContextRef = useRef({})
1346
+ const varContextRef = useRef<Record<string, any>>({})
544
1347
  if (enableVarRef.current) {
545
1348
  // eslint-disable-next-line react-hooks/rules-of-hooks
546
1349
  const varContext = useContext(VarContext)
547
- const newVarContext = extendObject({}, varContext, externalVarContext, varStyle)
548
- // 缓存比较newVarContext是否发生变化
549
- if (diffAndCloneA(varContextRef.current, newVarContext).diff) {
550
- varContextRef.current = newVarContext
1350
+ // 无声明节点:直接用父 varContext 解析,跳过 merge / 比对 / ref 维护。
1351
+ // 有声明节点:浅合并出 newVarContext,仅在内容变化时替换 ref 维持引用稳定,
1352
+ // 供 wrapChildren 的 <VarContext.Provider> 使用。
1353
+ let resolvedVarContext = varContext
1354
+ if (hasVarDec) {
1355
+ const newVarContext = extendObject({}, varContext, varStyle)
1356
+ if (!shallowEqual(varContextRef.current, newVarContext)) {
1357
+ varContextRef.current = newVarContext
1358
+ }
1359
+ resolvedVarContext = varContextRef.current
551
1360
  }
552
- transformVar(normalStyle, varKeyPaths, varContextRef.current, visitOther)
1361
+ if (varKeyPaths.length) transformVar(normalStyle, varKeyPaths, resolvedVarContext, visitOther)
553
1362
  }
554
1363
 
555
1364
  // apply unocss var
556
- if (unoVarKeyPaths.length) {
557
- transformVar(normalStyle, unoVarKeyPaths, unoVarStyle, visitOther)
558
- }
559
-
560
- const percentConfig = {
1365
+ if (unoVarKeyPaths.length) transformVar(normalStyle, unoVarKeyPaths, unoVarStyle, visitOther)
1366
+ // apply env
1367
+ if (envKeyPaths.length) transformEnv(normalStyle, envKeyPaths, navigation)
1368
+ // apply percent / calc
1369
+ const percentConfig: PercentConfig = {
561
1370
  width,
562
1371
  height,
563
1372
  fontSize: normalStyle.fontSize,
564
1373
  parentWidth,
565
- parentHeight,
566
- parentFontSize
567
- }
568
-
569
- const positionMeta = {
570
- hasPositionFixed: false
1374
+ parentHeight
571
1375
  }
572
-
573
- // apply env
574
- transformEnv(normalStyle, envKeyPaths, navigation)
575
- // apply percent
576
- transformPercent(normalStyle, percentKeyPaths, percentConfig)
577
- // apply calc
578
- transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => {
579
- if (PERCENT_REGEX.test(value)) {
580
- if (hasOwn(selfPercentRule, key)) {
581
- hasSelfPercent = true
582
- }
583
- const resolved = resolvePercent(value, key, percentConfig)
584
- return typeof resolved === 'number' ? resolved : 0
585
- } else {
586
- const formatted = global.__formatValue(value)
587
- if (typeof formatted === 'number') {
588
- return formatted
1376
+ if (hasFont) transformFont(normalStyle)
1377
+ if (percentKeyPaths.length) transformPercent(normalStyle, percentKeyPaths, percentConfig)
1378
+ if (calcKeyPaths.length) {
1379
+ transformCalc(normalStyle, calcKeyPaths, (value: string, key: string) => {
1380
+ if (percentRegExp.test(value)) {
1381
+ if (hasOwn(selfPercentRule, key)) {
1382
+ hasSelfPercent = true
1383
+ }
1384
+ const resolved = resolvePercent(value, key, percentConfig)
1385
+ return typeof resolved === 'number' ? resolved : 0
589
1386
  } else {
590
- warn('calc() only support number, px, rpx, % temporarily.')
591
- return 0
1387
+ const formatted = global.__formatValue(value)
1388
+ if (typeof formatted === 'number') {
1389
+ return formatted
1390
+ } else {
1391
+ warn('calc() only support number, px, rpx, % temporarily.')
1392
+ return 0
1393
+ }
592
1394
  }
593
- }
594
- })
1395
+ })
1396
+ }
595
1397
 
596
- // apply position
597
- transformPosition(normalStyle, positionMeta)
598
1398
  // transform number enum stringify
599
- transformStringify(normalStyle)
600
- // transform rpx to px
601
- transformBoxShadow(normalStyle)
602
- // transform 字符串格式转化数组格式(先转数组再处理css var)
603
- transformTransform(normalStyle)
604
- transformBoxSizing(normalStyle, hasBoxSizingAffectingStyle)
1399
+ if (needStringify) transformStringify(normalStyle)
1400
+ // transform unit
1401
+ if (hasBoxShadow) transformBoxShadow(normalStyle)
1402
+ // transform 字符串格式转化数组格式
1403
+ if (hasTransform) transformTransform(normalStyle)
1404
+ // apply runtime style processing alignment
1405
+ if (hasFontFamily) transformFontFamily(normalStyle)
1406
+ if (hasFlex) transformFlex(normalStyle)
1407
+ if (shorthandKeys.length) transformShorthand(normalStyle, shorthandKeys, percentConfig)
1408
+ if (needTransformBackground) transformBackground(normalStyle)
1409
+ // borderStyle / outlineStyle: 'none' 的清除语义放到变量解析、简写展开之后统一处理。
1410
+ transformBorderStyleNone(normalStyle)
1411
+ // transform position: fixed
1412
+ const hasPositionFixed = transformPosition(normalStyle)
1413
+ // 合并组件默认样式:默认样式在 user transform 之后兜底写入,需完全符合 RN style 规范
1414
+ // 命中 user 已存在 key(含简写展开后的长属性)则跳过。
1415
+ // 复用同一次循环顺带补 hasBoxSizingAffectingStyle,避免独立扫一遍 default。
1416
+ if (defaultStyle) {
1417
+ for (const k in defaultStyle) {
1418
+ if (!hasOwn(defaultStyle, k)) continue
1419
+ if (!hasOwn(normalStyle, k)) normalStyle[k] = defaultStyle[k]
1420
+ if (!hasBoxSizingAffectingStyle && isBoxSizingAffectingStyle(k)) {
1421
+ hasBoxSizingAffectingStyle = true
1422
+ }
1423
+ }
1424
+ }
1425
+ if (hasBoxSizingAffectingStyle) transformBoxSizing(normalStyle)
605
1426
 
606
1427
  return {
607
1428
  hasVarDec,
@@ -610,83 +1431,82 @@ export function useTransformStyle (styleObj: Record<string, any> = {}, { enableV
610
1431
  setHeight,
611
1432
  normalStyle,
612
1433
  hasSelfPercent,
613
- hasPositionFixed: positionMeta.hasPositionFixed
1434
+ hasPositionFixed
614
1435
  }
615
1436
  }
616
1437
 
617
- export interface VisitorArg {
618
- target: Record<string, any>
619
- key: string
620
- value: any
621
- keyPath: Array<string>
1438
+ // ============================================================
1439
+ // other React hooks
1440
+ // ============================================================
1441
+
1442
+ export function useNavigation (): Record<string, any> | undefined {
1443
+ const { navigation } = useContext(RouteContext) || {}
1444
+ return navigation
622
1445
  }
623
1446
 
624
- export function traverseStyle (styleObj: Record<string, any>, visitors: Array<(arg: VisitorArg) => void>) {
625
- const keyPath: Array<string> = []
626
- function traverse<T extends Record<string, any>> (target: T) {
627
- if (Array.isArray(target)) {
628
- target.forEach((value, index) => {
629
- const key = String(index)
630
- keyPath.push(key)
631
- visitors.forEach(visitor => visitor({ target, key, value, keyPath }))
632
- traverse(value)
633
- keyPath.pop()
634
- })
635
- } else if (isObject(target)) {
636
- Object.entries(target).forEach(([key, value]) => {
637
- keyPath.push(key)
638
- visitors.forEach(visitor => visitor({ target, key, value, keyPath }))
639
- traverse(value)
640
- keyPath.pop()
641
- })
642
- }
1447
+ function getTextPercentBase (currentFontSize?: string | number, parentTextStyle?: TextStyle) {
1448
+ return typeof currentFontSize === 'number'
1449
+ ? currentFontSize
1450
+ : typeof parentTextStyle?.fontSize === 'number'
1451
+ ? parentTextStyle.fontSize
1452
+ : DEFAULT_FONT_SIZE
1453
+ }
1454
+
1455
+ export function resolveTextFontSizePercentStyle<T extends TextStyle | undefined> (
1456
+ textStyle: T,
1457
+ parentTextStyle?: TextStyle
1458
+ ): T {
1459
+ if (!textStyle) return textStyle
1460
+
1461
+ if (typeof textStyle.fontSize === 'string' && percentRegExp.test(textStyle.fontSize)) {
1462
+ const base = getTextPercentBase(undefined, parentTextStyle)
1463
+ textStyle.fontSize = parseFloat(textStyle.fontSize) / 100 * base
643
1464
  }
644
- traverse(styleObj)
1465
+
1466
+ return textStyle
645
1467
  }
646
1468
 
647
- export function setStyle (styleObj: Record<string, any>, keyPath: Array<string>, setter: (arg: VisitorArg) => void) {
648
- let target = styleObj
649
- const lastKey = keyPath[keyPath.length - 1]
650
- for (let i = 0; i < keyPath.length - 1; i++) {
651
- target = target[keyPath[i]]
652
- if (!target) return
1469
+ export function resolveTextLineHeightPercentStyle<T extends TextStyle | undefined> (
1470
+ textStyle: T,
1471
+ parentTextStyle?: TextStyle
1472
+ ): T {
1473
+ if (!textStyle) return textStyle
1474
+
1475
+ if (typeof textStyle.lineHeight === 'string' && percentRegExp.test(textStyle.lineHeight)) {
1476
+ const base = getTextPercentBase(textStyle.fontSize, parentTextStyle)
1477
+ textStyle.lineHeight = parseFloat(textStyle.lineHeight) / 100 * base
653
1478
  }
654
- setter({
655
- target,
656
- key: lastKey,
657
- value: target[lastKey],
658
- keyPath
659
- })
1479
+
1480
+ return textStyle
660
1481
  }
661
1482
 
662
- export function splitProps<T extends Record<string, any>> (props: T): {
663
- textProps?: Partial<T>
664
- innerProps?: Partial<T>
665
- } {
666
- return groupBy(props, (key) => {
667
- if (hasOwn(TEXT_PROPS_MAP, key)) {
668
- return 'textProps'
1483
+ /**
1484
+ * 用法等同于 useEffect,但是会忽略首次执行,只在依赖更新时执行
1485
+ */
1486
+ export const useUpdateEffect = (effect: any, deps: any) => {
1487
+ const isMounted = useRef(false)
1488
+
1489
+ // for react-refresh
1490
+ useEffect(() => {
1491
+ return () => {
1492
+ isMounted.current = false
1493
+ }
1494
+ }, [])
1495
+
1496
+ useEffect(() => {
1497
+ if (!isMounted.current) {
1498
+ isMounted.current = true
669
1499
  } else {
670
- return 'innerProps'
1500
+ return effect()
671
1501
  }
672
- }) as {
673
- textProps: Partial<T>
674
- innerProps: Partial<T>
675
- }
1502
+ }, deps)
676
1503
  }
677
1504
 
678
- interface LayoutConfig {
679
- props: Record<string, any>
680
- hasSelfPercent: boolean
681
- setWidth?: Dispatch<SetStateAction<number>>
682
- setHeight?: Dispatch<SetStateAction<number>>
683
- onLayout?: (event?: LayoutChangeEvent) => void
684
- nodeRef: React.RefObject<any>
685
- }
686
1505
  export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout, nodeRef }: LayoutConfig) => {
687
1506
  const layoutRef = useRef({})
688
1507
  const hasLayoutRef = useRef(false)
689
- const layoutStyle = useMemo(() => { return !hasLayoutRef.current && hasSelfPercent ? HIDDEN_STYLE : {} }, [hasLayoutRef.current])
1508
+ // 固定首次 layout 前是否需要隐藏,避免 hasSelfPercent 后续变化时重新隐藏组件
1509
+ const layoutStyle = useMemo(() => { return !hasLayoutRef.current && hasSelfPercent ? hiddenStyle : undefined }, [hasLayoutRef.current])
690
1510
  const layoutProps: Record<string, any> = {}
691
1511
  const navigation = useNavigation()
692
1512
  const enableOffset = props['enable-offset']
@@ -715,37 +1535,36 @@ export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout
715
1535
  }
716
1536
  }
717
1537
 
718
- export interface WrapChildrenConfig {
719
- hasVarDec: boolean
720
- varContext?: Record<string, any>
721
- textPassThrough?: TextPassThroughContextValue | null
722
- }
723
-
724
- export interface TextPassThroughValueOptions {
725
- inheritTextProps?: boolean
726
- disabled?: boolean
727
- }
728
-
729
- export function useTextPassThroughValue (
1538
+ export function useTextPassThrough (
730
1539
  textStyle?: TextStyle,
731
1540
  textProps?: Record<string, any>,
732
- { inheritTextProps = true, disabled = false }: TextPassThroughValueOptions = {}
1541
+ { enableTextPassThrough = false }: TextPassThroughValueOptions = {}
733
1542
  ) {
734
- const parent = useContext(TextPassThroughContext)
735
- const valueRef = useRef<TextPassThroughContextValue | null>(null)
1543
+ const shouldEnableTextPassThrough = (
1544
+ enableTextPassThrough ||
1545
+ !!textStyle ||
1546
+ !!textProps
1547
+ )
1548
+ const enableTextPassThroughRef = useRef(shouldEnableTextPassThrough)
1549
+
1550
+ if (enableTextPassThroughRef.current !== shouldEnableTextPassThrough) {
1551
+ error('[Mpx runtime error]: text style/props use should be stable in the component lifecycle, or you can set [enable-text-pass-through] with true.')
1552
+ }
736
1553
 
737
- if (disabled) return null
1554
+ if (!enableTextPassThroughRef.current) return null
738
1555
 
739
- if (!textStyle && !textProps && (inheritTextProps || !parent?.pendingTextProps)) return null
1556
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1557
+ const parent = useContext(TextPassThroughContext)
1558
+ // eslint-disable-next-line react-hooks/rules-of-hooks
1559
+ const valueRef = useRef<TextPassThroughContextValue | null>(null)
740
1560
 
741
- const nextTextStyle = textStyle
742
- ? extendObject({}, parent?.textStyle, textStyle)
1561
+ const resolvedTextStyle = resolveTextFontSizePercentStyle(textStyle, parent?.textStyle)
1562
+ const nextTextStyle = resolvedTextStyle
1563
+ ? extendObject({}, parent?.textStyle, resolvedTextStyle)
743
1564
  : parent?.textStyle
744
- const nextTextProps = inheritTextProps
745
- ? textProps
746
- ? extendObject({}, parent?.pendingTextProps, textProps)
747
- : parent?.pendingTextProps
748
- : textProps
1565
+ const nextTextProps = textProps
1566
+ ? extendObject({}, parent?.pendingTextProps, textProps)
1567
+ : parent?.pendingTextProps
749
1568
  const nextValue = {
750
1569
  textStyle: nextTextStyle,
751
1570
  pendingTextProps: nextTextProps
@@ -758,104 +1577,6 @@ export function useTextPassThroughValue (
758
1577
  return valueRef.current
759
1578
  }
760
1579
 
761
- export function wrapChildren (props: Record<string, any> = {}, { hasVarDec, varContext, textPassThrough }: WrapChildrenConfig) {
762
- let { children } = props
763
- if (textPassThrough) {
764
- children = <TextPassThroughContext.Provider value={textPassThrough} key='textPassThroughWrap'>{children}</TextPassThroughContext.Provider>
765
- }
766
- if (hasVarDec && varContext) {
767
- children = <VarContext.Provider value={varContext} key='varContextWrap'>{children}</VarContext.Provider>
768
- }
769
- return children
770
- }
771
-
772
- export const debounce = <T extends AnyFunc> (
773
- func: T,
774
- delay: number
775
- ): ((...args: Parameters<T>) => void) & { clear: () => void } => {
776
- let timer: any
777
- const wrapper = (...args: ReadonlyArray<any>) => {
778
- timer && clearTimeout(timer)
779
- timer = setTimeout(() => {
780
- func(...args)
781
- }, delay)
782
- }
783
- wrapper.clear = () => {
784
- timer && clearTimeout(timer)
785
- timer = null
786
- }
787
- return wrapper
788
- }
789
-
790
- export const useDebounceCallback = <T extends AnyFunc> (
791
- func: T,
792
- delay: number
793
- ): ((...args: Parameters<T>) => void) & { clear: () => void } => {
794
- const debounced = useMemo(() => debounce(func, delay), [func])
795
- return debounced
796
- }
797
-
798
- export const useStableCallback = <T extends AnyFunc | null | undefined> (
799
- callback: T
800
- ): T extends AnyFunc ? T : () => void => {
801
- const ref = useRef<T>(callback)
802
- ref.current = callback
803
- return useCallback<any>(
804
- (...args: any[]) => ref.current?.(...args),
805
- []
806
- )
807
- }
808
-
809
- export function usePrevious<T> (value: T): T | undefined {
810
- const ref = useRef<T | undefined>()
811
- const prev = ref.current
812
- ref.current = value
813
- return prev
814
- }
815
-
816
- export interface GestureHandler {
817
- nodeRefs?: Array<{ getNodeInstance: () => { nodeRef: unknown } }>
818
- current?: unknown
819
- }
820
-
821
- export function flatGesture (gestures: Array<GestureHandler> = []) {
822
- return (gestures && gestures.flatMap((gesture: GestureHandler) => {
823
- if (gesture && gesture.nodeRefs) {
824
- return gesture.nodeRefs
825
- .map((item: { getNodeInstance: () => any }) => item.getNodeInstance()?.instance?.gestureRef || {})
826
- }
827
- return gesture?.current ? [gesture] : []
828
- })) || []
829
- }
830
-
831
- export function getCurrentPage (pageId: number | null | undefined) {
832
- if (!global.getCurrentPages) return
833
- const pages = global.getCurrentPages()
834
- return pages.find((page: any) => isFunction(page.getPageId) && page.getPageId() === pageId)
835
- }
836
-
837
- export function renderImage (
838
- imageProps: ImageProps,
839
- enableFastImage = true
840
- ) {
841
- let Component = Image
842
- if (enableFastImage) {
843
- // eslint-disable-next-line @typescript-eslint/no-var-requires
844
- const fastImageModule = require('@d11/react-native-fast-image')
845
- Component = fastImageModule.default || fastImageModule
846
- }
847
- return createElement(Component, imageProps)
848
- }
849
-
850
- export function pickStyle (styleObj: Record<string, any> = {}, pickedKeys: Array<string>, callback?: (key: string, val: number | string) => number | string) {
851
- return pickedKeys.reduce<Record<string, any>>((acc, key) => {
852
- if (key in styleObj) {
853
- acc[key] = callback ? callback(key, styleObj[key]) : styleObj[key]
854
- }
855
- return acc
856
- }, {})
857
- }
858
-
859
1580
  export function useHover ({ enableHover, hoverStartTime, hoverStayTime, disabled }: { enableHover: boolean, hoverStartTime: number, hoverStayTime: number, disabled?: boolean }) {
860
1581
  const enableHoverRef = useRef(enableHover)
861
1582
  if (enableHoverRef.current !== enableHover) {
@@ -868,37 +1589,46 @@ export function useHover ({ enableHover, hoverStartTime, hoverStayTime, disabled
868
1589
  // eslint-disable-next-line react-hooks/rules-of-hooks
869
1590
  const [isHover, setIsHover] = useState(false)
870
1591
  // eslint-disable-next-line react-hooks/rules-of-hooks
871
- const dataRef = useRef<{
1592
+ const hoverRef = useRef<{
872
1593
  startTimer?: ReturnType<typeof setTimeout>
873
1594
  stayTimer?: ReturnType<typeof setTimeout>
874
- }>({})
1595
+ hoverStartTime: number
1596
+ hoverStayTime: number
1597
+ disabled?: boolean
1598
+ }>({ hoverStartTime, hoverStayTime, disabled })
1599
+ hoverRef.current.hoverStartTime = hoverStartTime
1600
+ hoverRef.current.hoverStayTime = hoverStayTime
1601
+ hoverRef.current.disabled = disabled
875
1602
 
876
1603
  // eslint-disable-next-line react-hooks/rules-of-hooks
877
1604
  useEffect(() => {
878
1605
  return () => {
879
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer)
880
- dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer)
1606
+ hoverRef.current.startTimer && clearTimeout(hoverRef.current.startTimer)
1607
+ hoverRef.current.stayTimer && clearTimeout(hoverRef.current.stayTimer)
881
1608
  }
882
1609
  }, [])
883
1610
 
884
- const setStartTimer = () => {
885
- if (disabled) return
886
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer)
887
- dataRef.current.startTimer = setTimeout(() => {
888
- setIsHover(true)
889
- }, +hoverStartTime)
890
- }
891
-
892
- const setStayTimer = () => {
893
- if (disabled) return
894
- dataRef.current.stayTimer && clearTimeout(dataRef.current.stayTimer)
895
- dataRef.current.startTimer && clearTimeout(dataRef.current.startTimer)
896
- dataRef.current.stayTimer = setTimeout(() => {
897
- setIsHover(false)
898
- }, +hoverStayTime)
899
- }
900
1611
  // eslint-disable-next-line react-hooks/rules-of-hooks
901
1612
  const gesture = useMemo(() => {
1613
+ const setStartTimer = () => {
1614
+ const data = hoverRef.current
1615
+ if (data.disabled) return
1616
+ data.startTimer && clearTimeout(data.startTimer)
1617
+ data.startTimer = setTimeout(() => {
1618
+ setIsHover(true)
1619
+ }, +data.hoverStartTime)
1620
+ }
1621
+
1622
+ const setStayTimer = () => {
1623
+ const data = hoverRef.current
1624
+ if (data.disabled) return
1625
+ data.stayTimer && clearTimeout(data.stayTimer)
1626
+ data.startTimer && clearTimeout(data.startTimer)
1627
+ data.stayTimer = setTimeout(() => {
1628
+ setIsHover(false)
1629
+ }, +data.hoverStayTime)
1630
+ }
1631
+
902
1632
  return Gesture.Pan()
903
1633
  .onTouchesDown(() => {
904
1634
  setStartTimer()
@@ -933,3 +1663,75 @@ export function useRunOnJSCallback (callbackMapRef: MutableRefObject<Record<stri
933
1663
 
934
1664
  return invokeCallback
935
1665
  }
1666
+
1667
+ export const useDebounceCallback = <T extends AnyFunc> (
1668
+ func: T,
1669
+ delay: number
1670
+ ): ((...args: Parameters<T>) => void) & { clear: () => void } => {
1671
+ const debounced = useMemo(() => debounce(func, delay), [func])
1672
+ return debounced
1673
+ }
1674
+
1675
+ export const useStableCallback = <T extends AnyFunc | null | undefined> (
1676
+ callback: T
1677
+ ): T extends AnyFunc ? T : () => void => {
1678
+ const ref = useRef<T>(callback)
1679
+ ref.current = callback
1680
+ return useCallback<any>(
1681
+ (...args: any[]) => ref.current?.(...args),
1682
+ []
1683
+ )
1684
+ }
1685
+
1686
+ export function usePrevious<T> (value: T): T | undefined {
1687
+ const ref = useRef<T | undefined>()
1688
+ const prev = ref.current
1689
+ ref.current = value
1690
+ return prev
1691
+ }
1692
+
1693
+ // ============================================================
1694
+ // component helpers
1695
+ // ============================================================
1696
+
1697
+ export function wrapChildren (children: ReactNode, { hasVarDec, varContext, textPassThrough }: WrapChildrenConfig) {
1698
+ if (textPassThrough) {
1699
+ children = <TextPassThroughContext.Provider value={textPassThrough} key='textPassThroughWrap'>{children}</TextPassThroughContext.Provider>
1700
+ }
1701
+ if (hasVarDec && varContext) {
1702
+ children = <VarContext.Provider value={varContext} key='varContextWrap'>{children}</VarContext.Provider>
1703
+ }
1704
+ return children
1705
+ }
1706
+
1707
+ export function renderImage (
1708
+ imageProps: ImageProps,
1709
+ enableFastImage = true
1710
+ ) {
1711
+ let Component = Image
1712
+ if (enableFastImage) {
1713
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1714
+ const fastImageModule = require('@d11/react-native-fast-image')
1715
+ Component = fastImageModule.default || fastImageModule
1716
+ }
1717
+ return createElement(Component, imageProps)
1718
+ }
1719
+
1720
+ export function flatGesture (gestures: Array<GestureHandler> = []) {
1721
+ return (gestures && gestures.flatMap((gesture: GestureHandler) => {
1722
+ if (gesture && gesture.nodeRefs) {
1723
+ return gesture.nodeRefs
1724
+ .map((item: { getNodeInstance: () => any }) => item.getNodeInstance()?.instance?.gestureRef || {})
1725
+ }
1726
+ if (gesture && (gesture.current || gesture.handlerTag !== undefined)) {
1727
+ return [gesture]
1728
+ }
1729
+ return []
1730
+ })) || []
1731
+ }
1732
+
1733
+ export function getCurrentPage (pageId: number | null | undefined) {
1734
+ if (!global.getCurrentPages) return
1735
+ const pages = global.getCurrentPages()
1736
+ return pages.find((page: any) => isFunction(page.getPageId) && page.getPageId() === pageId)
1737
+ }