@mpxjs/webpack-plugin 2.10.24-beta.1 → 2.10.24-test.1

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