@mpxjs/webpack-plugin 2.10.22 → 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 (123) hide show
  1. package/lib/loader.js +2 -0
  2. package/lib/platform/style/wx/index.js +395 -104
  3. package/lib/react/index.js +3 -1
  4. package/lib/react/processTemplate.js +3 -1
  5. package/lib/react/style-helper.js +6 -6
  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 -1
  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 +36 -31
  53. package/lib/runtime/components/react/dist/mpx-simple-view.d.ts +4 -2
  54. package/lib/runtime/components/react/dist/mpx-simple-view.jsx +24 -17
  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 +44 -40
  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 +229 -128
  73. package/lib/runtime/components/react/dist/mpx-web-view.jsx +5 -1
  74. package/lib/runtime/components/react/dist/namedColorSet.d.ts +2 -0
  75. package/lib/runtime/components/react/dist/namedColorSet.js +149 -0
  76. package/lib/runtime/components/react/dist/utils.d.ts +73 -69
  77. package/lib/runtime/components/react/dist/utils.jsx +1295 -442
  78. package/lib/runtime/components/react/event.config.ts +32 -25
  79. package/lib/runtime/components/react/getInnerListeners.ts +84 -60
  80. package/lib/runtime/components/react/mpx-button.tsx +18 -16
  81. package/lib/runtime/components/react/mpx-camera.tsx +0 -3
  82. package/lib/runtime/components/react/mpx-canvas/index.tsx +1 -3
  83. package/lib/runtime/components/react/mpx-canvas/utils.tsx +3 -1
  84. package/lib/runtime/components/react/mpx-checkbox-group.tsx +2 -6
  85. package/lib/runtime/components/react/mpx-checkbox.tsx +9 -11
  86. package/lib/runtime/components/react/mpx-form.tsx +6 -6
  87. package/lib/runtime/components/react/mpx-icon/index.tsx +1 -3
  88. package/lib/runtime/components/react/mpx-image.tsx +65 -54
  89. package/lib/runtime/components/react/mpx-input.tsx +1 -3
  90. package/lib/runtime/components/react/mpx-label.tsx +6 -8
  91. package/lib/runtime/components/react/mpx-movable-area.tsx +3 -4
  92. package/lib/runtime/components/react/mpx-movable-view.tsx +7 -7
  93. package/lib/runtime/components/react/mpx-picker-view/index.tsx +6 -8
  94. package/lib/runtime/components/react/mpx-picker-view-column/index.tsx +2 -4
  95. package/lib/runtime/components/react/mpx-progress.tsx +0 -3
  96. package/lib/runtime/components/react/mpx-radio-group.tsx +2 -6
  97. package/lib/runtime/components/react/mpx-radio.tsx +9 -11
  98. package/lib/runtime/components/react/mpx-rich-text/index.tsx +0 -3
  99. package/lib/runtime/components/react/mpx-scroll-view.tsx +7 -7
  100. package/lib/runtime/components/react/mpx-simple-text.tsx +36 -38
  101. package/lib/runtime/components/react/mpx-simple-view.tsx +29 -17
  102. package/lib/runtime/components/react/mpx-slider.tsx +15 -10
  103. package/lib/runtime/components/react/mpx-sticky-header.tsx +6 -6
  104. package/lib/runtime/components/react/mpx-sticky-section.tsx +7 -7
  105. package/lib/runtime/components/react/mpx-swiper-item.tsx +6 -6
  106. package/lib/runtime/components/react/mpx-swiper.tsx +5 -8
  107. package/lib/runtime/components/react/mpx-switch.tsx +0 -3
  108. package/lib/runtime/components/react/mpx-text.tsx +46 -49
  109. package/lib/runtime/components/react/mpx-video.tsx +3 -5
  110. package/lib/runtime/components/react/mpx-view.tsx +250 -141
  111. package/lib/runtime/components/react/mpx-web-view.tsx +8 -3
  112. package/lib/runtime/components/react/namedColorSet.ts +150 -0
  113. package/lib/runtime/components/react/types/getInnerListeners.d.ts +21 -1
  114. package/lib/runtime/components/react/types/global.d.ts +1 -0
  115. package/lib/runtime/components/react/utils.tsx +1347 -539
  116. package/lib/runtime/i18n.wxs +15 -9
  117. package/lib/style-compiler/strip-conditional.js +19 -4
  118. package/lib/template-compiler/compiler.js +5 -1
  119. package/lib/utils/source-location.js +9 -13
  120. package/lib/web/index.js +3 -1
  121. package/lib/web/processTemplate.js +3 -1
  122. package/package.json +5 -4
  123. package/LICENSE +0 -433
@@ -1,9 +1,10 @@
1
1
  const { hump2dash } = require('../../../utils/hump-dash')
2
2
  const { parseValues } = require('../../../utils/string')
3
3
 
4
- module.exports = function getSpec({ warn, error }) {
4
+ module.exports = function getSpec ({ warn, error }) {
5
5
  // React Native 双端都不支持的 CSS property
6
- const unsupportedPropExp = /^(white-space|text-overflow|animation|font-variant-caps|font-variant-numeric|font-variant-east-asian|font-variant-alternates|font-variant-ligatures|caret-color|float|clear)$/
6
+ // border-*-style RN 双端都不支持,仅支持统一的 border-style;shorthand 路径会展开到 border-style,长属性写法直接拦截
7
+ const unsupportedPropExp = /^(white-space|text-overflow|animation|font-variant-caps|font-variant-numeric|font-variant-east-asian|font-variant-alternates|font-variant-ligatures|caret-color|float|clear|border-(top|right|bottom|left)-style)$/
7
8
  const unsupportedPropMode = {
8
9
  // React Native ios 不支持的 CSS property
9
10
  ios: /^(vertical-align)$/,
@@ -12,12 +13,22 @@ module.exports = function getSpec({ warn, error }) {
12
13
  // TODO: rnoh 文档暂未找到 css 属性支持说明,暂时同步 android,同时需要注意此处校验是否有缺失,类似 will-change 之类属性
13
14
  harmony: /^(text-decoration-style|text-decoration-color|shadow-offset|shadow-opacity|shadow-radius)$/
14
15
  }
16
+ const isNum = (v) => !isNaN(+v)
15
17
  // var(xx)
16
18
  const cssVariableExp = /var\(/
17
19
  // calc(xx)
18
20
  const calcExp = /calc\(/
19
21
  const envExp = /env\(/
20
22
  const silentVerify = 'silent'
23
+ const namedColorSet = new Set(['transparent', 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen'])
24
+ const hexColorExp = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
25
+ const colorFnExp = /^(rgb|rgba|hsl|hsla|hwb)\(.+\)$/
26
+ const valueExp = {
27
+ integer: { test: isNum },
28
+ length: /^((-?(\d+(\.\d+)?|\.\d+))(rpx|px|%|vw|vh)?|hairlineWidth)$/,
29
+ color: { test: (v) => namedColorSet.has(v) || hexColorExp.test(v) || colorFnExp.test(v) }
30
+ }
31
+
21
32
  // 不支持的属性提示
22
33
  const unsupportedPropError = ({ prop, value, selector }, { mode }, isError = true) => {
23
34
  const tips = isError ? error : warn
@@ -39,12 +50,18 @@ module.exports = function getSpec({ warn, error }) {
39
50
  color: 'color',
40
51
  enum: 'enum'
41
52
  }
53
+ // 校验失败提示中的示例值(按类型)。enum 依赖具体 prop 的白名单,在 tipsType 内动态拼。
54
+ const valueTypeExample = {
55
+ [ValueType.length]: '2rpx,10%,30rpx',
56
+ [ValueType.color]: 'rgb,rgba,hsl,hsla,hwb,named color,#000000'
57
+ }
42
58
  // React 属性支持的枚举值
43
59
  const SUPPORTED_PROP_VAL_ARR = {
44
60
  'box-sizing': ['border-box', 'content-box'],
45
61
  'backface-visibility': ['visible', 'hidden'],
46
62
  overflow: ['visible', 'hidden', 'scroll'],
47
- 'border-style': ['solid', 'dotted', 'dashed'],
63
+ // RN 实测仅支持 solid/dotted/dashed;none 作为 CSS 合法值保留到运行时统一转换为 borderWidth: 0
64
+ 'border-style': ['solid', 'dotted', 'dashed', 'none'],
48
65
  'object-fit': ['cover', 'contain', 'fill', 'scale-down'],
49
66
  direction: ['inherit', 'ltr', 'rtl'],
50
67
  display: ['flex', 'none'],
@@ -77,7 +94,9 @@ module.exports = function getSpec({ warn, error }) {
77
94
  'margin-bottom': ['auto', ValueType.length],
78
95
  'margin-right': ['auto', ValueType.length],
79
96
  'margin-horizontal': ['auto', ValueType.length],
80
- 'margin-vertical': ['auto', ValueType.length]
97
+ 'margin-vertical': ['auto', ValueType.length],
98
+ // outline-style 走 enum 校验;与 border-style 对齐,不支持 double / groove / ridge
99
+ 'outline-style': ['solid', 'dotted', 'dashed', 'none']
81
100
  }
82
101
  // 获取值类型
83
102
  const getValueType = (prop) => {
@@ -119,7 +138,7 @@ module.exports = function getSpec({ warn, error }) {
119
138
  const verifyValues = ({ prop, value, selector }, isError = true) => {
120
139
  prop = prop.trim()
121
140
  const rawValue = value.trim()
122
- const tips = isError === silentVerify ? () => {} : isError ? error : warn
141
+ const tips = isError === silentVerify ? () => { } : isError ? error : warn
123
142
 
124
143
  // CSS 自定义属性(--xxx)是变量定义,不属于 RN 样式属性:
125
144
  // 不能按 `-height/-color` 等后缀推断类型去校验,否则会把变量定义错误过滤,导致运行时 var() 取值失败
@@ -145,20 +164,12 @@ module.exports = function getSpec({ warn, error }) {
145
164
 
146
165
  // calc() / env() 跳过值校验,但保留 rawValue 输出
147
166
  if (calcExp.test(valueForVerify) || envExp.test(valueForVerify)) return true
148
- const namedColor = ['transparent', 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']
149
- const valueExp = {
150
- integer: /^(-?(\d+(\.\d+)?|\.\d+))$/,
151
- length: /^((-?(\d+(\.\d+)?|\.\d+))(rpx|px|%|vw|vh)?|hairlineWidth)$/,
152
- color: new RegExp(('^(' + namedColor.join('|') + ')$') + '|(^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$)|^(rgb|rgba|hsl|hsla|hwb)\\(.+\\)$')
153
- }
154
167
  const type = getValueType(prop)
155
168
  const tipsType = (type) => {
156
- const info = {
157
- [ValueType.length]: '2rpx,10%,30rpx',
158
- [ValueType.color]: 'rgb,rgba,hsl,hsla,hwb,named color,#000000',
159
- [ValueType.enum]: `${SUPPORTED_PROP_VAL_ARR[prop]?.join(',')}`
160
- }
161
- tips(`Value of ${prop} in ${selector} should be ${type}${info[type] ? `, eg ${info[type]}` : ''}, received [${rawValue}], please check again!`)
169
+ const example = type === ValueType.enum
170
+ ? SUPPORTED_PROP_VAL_ARR[prop]?.join(',')
171
+ : valueTypeExample[type]
172
+ tips(`Value of ${prop} in ${selector} should be ${type}${example ? `, eg ${example}` : ''}, received [${rawValue}], please check again!`)
162
173
  }
163
174
  switch (type) {
164
175
  case ValueType.length: {
@@ -183,6 +194,17 @@ module.exports = function getSpec({ warn, error }) {
183
194
  return true
184
195
  }
185
196
  case ValueType.enum: {
197
+ // font-variant 长属性允许空格分隔的多值(如 `small-caps tabular-nums`),
198
+ // 走整串比对会误杀多值场景;拆 token 后逐个命中枚举白名单即放行。
199
+ // 输出仍保留 raw value,运行时由 RN `processFontVariant` 自行 `split(' ')` 归一为数组。
200
+ if (prop === 'font-variant') {
201
+ const tokens = parseValues(valueForVerify)
202
+ if (!tokens.length || !tokens.every(t => SUPPORTED_PROP_VAL_ARR[prop].includes(t))) {
203
+ tipsType(type)
204
+ return false
205
+ }
206
+ return true
207
+ }
186
208
  const isIn = SUPPORTED_PROP_VAL_ARR[prop].includes(valueForVerify)
187
209
  const isType = Object.keys(valueExp).some(item => valueExp[item].test(valueForVerify) && SUPPORTED_PROP_VAL_ARR[prop].includes(ValueType[item]))
188
210
  if (!isIn && !isType) {
@@ -201,22 +223,15 @@ module.exports = function getSpec({ warn, error }) {
201
223
 
202
224
  // 简写转换规则
203
225
  const AbbreviationMap = {
204
- // 仅支持 offset-x | offset-y | blur-radius | color 排序
205
226
  'text-shadow': ['textShadowOffset.width', 'textShadowOffset.height', 'textShadowRadius', 'textShadowColor'],
206
- // 仅支持 width | style | color 这种排序
207
227
  border: ['borderWidth', 'borderStyle', 'borderColor'],
208
- // 仅支持 width | style | color 这种排序
209
- 'border-left': ['borderLeftWidth', 'borderLeftStyle', 'borderLeftColor'],
210
- // 仅支持 width | style | color 这种排序
211
- 'border-right': ['borderRightWidth', 'borderRightStyle', 'borderRightColor'],
212
- // 仅支持 width | style | color 这种排序
213
- 'border-top': ['borderTopWidth', 'borderTopStyle', 'borderTopColor'],
214
- // 仅支持 width | style | color 这种排序
215
- 'border-bottom': ['borderBottomWidth', 'borderBottomStyle', 'borderBottomColor'],
216
- // 0.76 及以上版本RN支持 box-shadow,实测0.77版本drn红米note12pro Android12 不支持内阴影,其他表现和web一致
217
- // 仅支持 offset-x | offset-y | blur-radius | color 排序
218
- // 'box-shadow': ['shadowOffset.width', 'shadowOffset.height', 'shadowRadius', 'shadowColor'],
219
- // 仅支持 text-decoration-line text-decoration-style text-decoration-color 这种格式
228
+ // RN 不支持单边 border-*-style,统一展开到 borderStyle
229
+ // 实测 RN 上当 borderStyle 不为 solid 时单边 border-*-color 不生效,
230
+ // 这里把单边 color 也统一展开到 borderColor 规避(width 不能这样做,否则会覆盖其它三边)
231
+ 'border-left': ['borderLeftWidth', 'borderStyle', 'borderColor'],
232
+ 'border-right': ['borderRightWidth', 'borderStyle', 'borderColor'],
233
+ 'border-top': ['borderTopWidth', 'borderStyle', 'borderColor'],
234
+ 'border-bottom': ['borderBottomWidth', 'borderStyle', 'borderColor'],
220
235
  'text-decoration': ['textDecorationLine', 'textDecorationStyle', 'textDecorationColor'],
221
236
  // flex-grow | flex-shrink | flex-basis
222
237
  flex: ['flexGrow', 'flexShrink', 'flexBasis'],
@@ -226,17 +241,166 @@ module.exports = function getSpec({ warn, error }) {
226
241
  'border-width': ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
227
242
  'border-color': ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
228
243
  margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
229
- padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
244
+ padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
245
+ // gap:CSS 2 槽位(row-gap / column-gap),单值复制行列
246
+ gap: ['rowGap', 'columnGap'],
247
+ // inset:4 槽位等价 margin 四值语法;RN inset 长属性不稳定,单值也强制展开
248
+ inset: ['top', 'right', 'bottom', 'left'],
249
+ // outline:RN 0.74 不支持、0.76+ 生效;走无序展开,与 border 共享缺省补齐
250
+ outline: ['outlineWidth', 'outlineStyle', 'outlineColor']
230
251
  }
252
+
253
+ // 这些简写按 CSS 规范允许 token 顺序自由排列,按值类型识别归位
254
+ const UnorderedAbbreviationMap = {
255
+ 'text-shadow': true,
256
+ 'text-decoration': true,
257
+ 'flex-flow': true,
258
+ border: true,
259
+ 'border-left': true,
260
+ 'border-right': true,
261
+ 'border-top': true,
262
+ 'border-bottom': true,
263
+ // outline: <outline-width> || <outline-style> || <outline-color>,顺序不敏感
264
+ outline: true
265
+ }
266
+
267
+ // CSS border-width: medium 的实测值(各主流浏览器一致取 3px)
268
+ // CSS 规范允许 medium 实现自定,这里取业界事实标准;调整此值需与运行时同名常量一起改
269
+ const BORDER_MEDIUM_WIDTH = 3
270
+ // 简写槽位缺省值表(数据驱动;新增简写或调整缺省值只改这里)
271
+ // 值即槽位缺省时追加的补齐值;none 清除语义统一保留到运行时处理
272
+ // 注意:borderColor / textShadowRadius 因 RN 有内置缺省值,无需补齐,不进此表
273
+ const ShorthandDefaultMap = {
274
+ border: { borderWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
275
+ 'border-top': { borderTopWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
276
+ 'border-right': { borderRightWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
277
+ 'border-bottom': { borderBottomWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
278
+ 'border-left': { borderLeftWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
279
+ // outline 与 border 缺省值完全对齐:缺 width → BORDER_MEDIUM_WIDTH;
280
+ // 缺 style → outlineStyle: none,运行时统一转换为 outlineWidth: 0
281
+ outline: { outlineWidth: BORDER_MEDIUM_WIDTH, outlineStyle: 'none' },
282
+ 'text-shadow': {
283
+ // textShadowOffset.height 的「width 存在才补 0」由 formatUnorderedAbbreviation 内既有 fallback 处理
284
+ // 值不带 quote,由后续 style-helper 的 formatValue 统一 JSON.stringify
285
+ textShadowColor: '#000'
286
+ }
287
+ // text-decoration / flex-flow 暂不配置,与 RN 默认一致
288
+ }
289
+
290
+ // 通用补齐:扫描完所有 token 后,将 ShorthandDefaultMap 中未被占用(不在 used)的槽位追加到 cssMap
291
+ // used 即主循环的占用记录,key 是完整目标 prop 名(含 textShadowOffset.width 这类 dot 路径)
292
+ const applyShorthandDefaults = (cssMap, prop, used) => {
293
+ const defaults = ShorthandDefaultMap[prop]
294
+ if (!defaults) return cssMap
295
+ for (const target in defaults) {
296
+ if (!used[target]) {
297
+ pushAbbreviationValue(cssMap, target, defaults[target])
298
+ }
299
+ }
300
+ return cssMap
301
+ }
302
+
303
+ const pushAbbreviationValue = (cssMap, prop, value) => {
304
+ if (prop.includes('.')) {
305
+ // 多个属性值的prop
306
+ const [main, sub] = prop.split('.')
307
+ const cssData = cssMap.find(item => item.prop === main)
308
+ if (cssData) { // 设置过
309
+ cssData.value[sub] = value
310
+ } else { // 第一次设置
311
+ cssMap.push({
312
+ prop: main,
313
+ value: {
314
+ [sub]: value
315
+ }
316
+ })
317
+ }
318
+ } else {
319
+ // 单个值的属性
320
+ cssMap.push({
321
+ prop,
322
+ value
323
+ })
324
+ }
325
+ }
326
+
327
+ const getVerifiedProp = (props, value, selector, mode, used) => {
328
+ return props.find(prop => {
329
+ if (used[prop]) return false
330
+ const newProp = hump2dash(prop.replace(/\..+/, ''))
331
+ return verifyValues({ prop: newProp, value, selector }, silentVerify) &&
332
+ verifyProps({ prop: newProp, value, selector }, { mode }, false)
333
+ })
334
+ }
335
+
336
+ const formatUnorderedAbbreviation = ({ prop, value, selector }, { mode }) => {
337
+ const originalValue = value
338
+ const values = Array.isArray(value) ? value : parseValues(value)
339
+ const original = `${prop}:${originalValue}`
340
+ const props = AbbreviationMap[prop]
341
+ const cssMap = []
342
+ const used = {}
343
+ let hasTextDecorationNone = false
344
+ let hasUnderline = false
345
+ let hasLineThrough = false
346
+ // values[0] 而非 originalValue:避免 originalValue 是数组时 toString() 误命中
347
+ if (values.length === 1 && cssVariableExp.test(values[0])) {
348
+ return { prop, value: values[0] }
349
+ }
350
+ values.forEach(value => {
351
+ if (prop === 'text-decoration' && verifyValues({ prop: 'text-decoration-line', value, selector }, silentVerify)) {
352
+ switch (value) {
353
+ case 'underline':
354
+ hasUnderline = true
355
+ return
356
+ case 'line-through':
357
+ hasLineThrough = true
358
+ return
359
+ case 'none':
360
+ hasTextDecorationNone = true
361
+ return
362
+ // verifyValues 通过但分支未处理的 line token:落到通用 getVerifiedProp 流程
363
+ }
364
+ }
365
+ const matchedProp = getVerifiedProp(props, value, selector, mode, used)
366
+ if (!matchedProp) {
367
+ warn(`Value of [${original}] in ${selector} is invalid, received [${value}], please check again!`)
368
+ return
369
+ }
370
+ used[matchedProp] = true
371
+ pushAbbreviationValue(cssMap, matchedProp, value)
372
+ })
373
+ if (prop === 'text-decoration') {
374
+ if (hasUnderline || hasLineThrough) {
375
+ pushAbbreviationValue(cssMap, 'textDecorationLine', hasUnderline && hasLineThrough ? 'underline line-through' : hasUnderline ? 'underline' : 'line-through')
376
+ } else if (hasTextDecorationNone) {
377
+ pushAbbreviationValue(cssMap, 'textDecorationLine', 'none')
378
+ }
379
+ }
380
+ if (prop === 'text-shadow') {
381
+ // text-shadow 至少需要 offset-x 与 offset-y;缺省 height 时按 CSS 默认补 0,避免 RN 上 textShadowOffset.height 缺失
382
+ const shadowOffsetEntry = cssMap.find(item => item.prop === 'textShadowOffset')
383
+ if (shadowOffsetEntry) {
384
+ const offsetVal = shadowOffsetEntry.value
385
+ if (offsetVal && offsetVal.width !== undefined && offsetVal.height === undefined) {
386
+ warn(`Value of [${original}] in ${selector} is missing offset-y, fallback to 0, please check again!`)
387
+ offsetVal.height = 0
388
+ }
389
+ }
390
+ }
391
+ return applyShorthandDefaults(cssMap, prop, used)
392
+ }
393
+
231
394
  const formatAbbreviation = ({ prop, value, selector }, { mode }) => {
395
+ if (UnorderedAbbreviationMap[prop]) {
396
+ return formatUnorderedAbbreviation({ prop, value, selector }, { mode })
397
+ }
232
398
  const original = `${prop}:${value}`
233
399
  const props = AbbreviationMap[prop]
234
400
  const values = Array.isArray(value) ? value : parseValues(value)
235
401
  const cssMap = []
236
- // 复合属性不支持单个css var(css var可以接收单个值可以是复合值,复合值运行时不处理,这里前置提示一下)
237
- if (values.length === 1 && cssVariableExp.test(value)) {
238
- error(`Property ${prop} in ${selector} is abbreviated property and does not support a single CSS var`)
239
- return cssMap
402
+ if (values.length === 1 && cssVariableExp.test(values[0])) {
403
+ return { prop, value: values[0] }
240
404
  }
241
405
  let idx = 0
242
406
  let propsIdx = 0
@@ -267,28 +431,8 @@ module.exports = function getSpec({ warn, error }) {
267
431
  } else {
268
432
  idx++
269
433
  }
270
- } else if (prop.includes('.')) {
271
- // 多个属性值的prop
272
- const [main, sub] = prop.split('.')
273
- const cssData = cssMap.find(item => item.prop === main)
274
- if (cssData) { // 设置过
275
- cssData.value[sub] = value
276
- } else { // 第一次设置
277
- cssMap.push({
278
- prop: main,
279
- value: {
280
- [sub]: value
281
- }
282
- })
283
- }
284
- idx += 1
285
- propsIdx += 1
286
434
  } else {
287
- // 单个值的属性
288
- cssMap.push({
289
- prop,
290
- value
291
- })
435
+ pushAbbreviationValue(cssMap, prop, value)
292
436
  idx += 1
293
437
  propsIdx += 1
294
438
  }
@@ -296,18 +440,31 @@ module.exports = function getSpec({ warn, error }) {
296
440
  return cssMap
297
441
  }
298
442
 
443
+ // 单值直接返回(RN 原生支持单值 DimensionValue)的属性:margin / padding / border-* 四值简写
444
+ // 不含 gap / inset:
445
+ // - inset:RN inset 长属性非稳定,单值透传会留下不可靠的 inset key,必须展开到 top/right/bottom/left
446
+ // - gap:RN gap / rowGap / columnGap 只接受 number;单值串虽然后续 style-helper formatValue 也会处理,
447
+ // 但展开到 rowGap / columnGap 与运行时 runtimeForceExpandCompositeMap 行为保持对齐,更可读
448
+ const compositeSingleValuePassthrough = (prop) => prop !== 'gap' && prop !== 'inset'
449
+
299
450
  const formatCompositeVal = ({ prop, value, selector }, { mode }) => {
300
- const values = parseValues(value).splice(0, 4)
301
- switch (values.length) {
302
- case 1:
303
- verifyValues({ prop, value, selector }, false)
304
- return { prop, value }
305
- case 2:
306
- values.push(...values)
307
- break
308
- case 3:
309
- values.push(values[1])
310
- break
451
+ // 槽位数由 AbbreviationMap[prop].length 决定(gap=2,inset/margin/padding/border-*=4
452
+ const count = AbbreviationMap[prop].length
453
+ const values = parseValues(value).splice(0, count)
454
+ // 单值短路:margin / padding RN 原生支持单值,原样透传;gap / inset 单值也需展开,不走此捷径
455
+ if (values.length === 1 && compositeSingleValuePassthrough(prop)) {
456
+ verifyValues({ prop, value, selector }, false)
457
+ return { prop, value }
458
+ }
459
+ if (count === 2) {
460
+ // gap:单值复制到行列两槽;双值原样
461
+ if (values.length === 1) values.push(values[0])
462
+ } else {
463
+ switch (values.length) {
464
+ case 1: values.push(values[0], values[0], values[0]); break // 仅 inset 命中(margin/padding 已被上面短路)
465
+ case 2: values.push(...values); break
466
+ case 3: values.push(values[1]); break
467
+ }
311
468
  }
312
469
  return formatAbbreviation({ prop, value: values, selector }, { mode })
313
470
  }
@@ -318,19 +475,19 @@ module.exports = function getSpec({ warn, error }) {
318
475
  if (+value === 0) {
319
476
  return {
320
477
  prop,
321
- value
478
+ value: 0
322
479
  }
323
480
  }
324
481
  return verifyValues({ prop, value, selector }) && ({
325
482
  prop,
326
- value: /^\s*(-?(\d+(\.\d+)?|\.\d+))\s*$/.test(value) ? `${Math.round(value * 100)}%` : value
483
+ value: isNum(value) ? `${Math.round(value * 100)}%` : value
327
484
  })
328
485
  }
329
486
 
330
487
  // background 相关属性的转换 Todo
331
488
  // 仅支持以下属性,不支持其他背景相关的属性
332
489
  // /^((?!(-color)).)*background((?!(-color)).)*$/ 包含background且不包含background-color
333
- const checkBackgroundImage = ({ prop, value, selector }, { mode }) => {
490
+ const formatBackground = ({ prop, value, selector }, { mode }) => {
334
491
  const bgPropMap = {
335
492
  image: 'background-image',
336
493
  color: 'background-color',
@@ -360,15 +517,21 @@ module.exports = function getSpec({ warn, error }) {
360
517
  return values.length === 0 ? false : { prop: bgPropMap.size, value: values }
361
518
  }
362
519
  const formatBackgroundPosition = (value) => {
520
+ const yAxisKeywords = ['top', 'bottom']
363
521
  const values = []
364
522
  parseValues(value).forEach(item => {
365
523
  if (verifyValues({ prop: bgPropMap.position, value: item, selector })) {
366
524
  // 支持 number 值 / 枚举, center与50%等价
367
525
  values.push(item === 'center' ? '50%' : item)
368
526
  } else {
369
- error(`Value of [${bgPropMap.size}] in ${selector} does not support commas, received [${value}], please check again!`)
527
+ // 仅丢这一 token,其它 push 的位置仍输出,按规范使用 warn
528
+ warn(`Value of [${bgPropMap.position}] in ${selector} does not support value [${item}], please check again!`)
370
529
  }
371
530
  })
531
+ // CSS 允许 y x 顺序的关键字(如 top left),但输出需要 [x, y] 顺序
532
+ if (values.length === 2 && yAxisKeywords.includes(values[0])) {
533
+ ;[values[0], values[1]] = [values[1], values[0]]
534
+ }
372
535
  return { prop: bgPropMap.position, value: values }
373
536
  }
374
537
  switch (prop) {
@@ -391,8 +554,7 @@ module.exports = function getSpec({ warn, error }) {
391
554
  case bgPropMap.all: {
392
555
  // background: 支持 image/color/repeat 与 position/size
393
556
  if (cssVariableExp.test(value)) {
394
- error(`Property [${bgPropMap.all}] in ${selector} is abbreviated property and does not support CSS var`)
395
- return false
557
+ return { prop, value }
396
558
  }
397
559
  // background: none
398
560
  if (value === 'none') {
@@ -409,9 +571,13 @@ module.exports = function getSpec({ warn, error }) {
409
571
  if (isSize) {
410
572
  if (verifyValues({ prop: bgPropMap.size, value: item, selector }, silentVerify)) {
411
573
  sizeValues.push(item)
574
+ } else {
575
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support background-size token [${item}], please check again!`)
412
576
  }
413
577
  } else if (verifyValues({ prop: bgPropMap.position, value: item, selector }, silentVerify)) {
414
578
  positionValues.push(item)
579
+ } else {
580
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support background-position token [${item}], please check again!`)
415
581
  }
416
582
  }
417
583
  const handlePositionSize = (item) => {
@@ -445,8 +611,10 @@ module.exports = function getSpec({ warn, error }) {
445
611
  bgMap.push({ prop: bgPropMap.color, value: item })
446
612
  } else if (verifyValues({ prop: bgPropMap.repeat, value: item, selector }, silentVerify)) {
447
613
  bgMap.push({ prop: bgPropMap.repeat, value: item })
448
- } else {
449
- handlePositionSize(item)
614
+ } else if (!handlePositionSize(item)) {
615
+ // 既不是 url / linear-gradient / color / repeat / position-size,也不是合法 position 起头的 token
616
+ // 典型场景:background-attachment / background-origin / background-clip 及拼写错误,RN 均不支持
617
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support token [${item}], please check again!`)
450
618
  }
451
619
  })
452
620
  if (positionValues.length) {
@@ -495,9 +663,6 @@ module.exports = function getSpec({ warn, error }) {
495
663
  key = key === 'rotate' ? 'rotateZ' : key
496
664
  transform.push({ [key]: val })
497
665
  break
498
- case 'matrix':
499
- transform.push({ [key]: parseValues(val, ',').map(val => +val) })
500
- break
501
666
  case 'translate':
502
667
  case 'scale':
503
668
  case 'skew':
@@ -512,25 +677,69 @@ module.exports = function getSpec({ warn, error }) {
512
677
  vals.push(vals[0])
513
678
  }
514
679
  const xyz = ['X', 'Y', 'Z']
515
- transform.push(...vals.map((v, index) => {
680
+ vals.forEach((v, index) => {
516
681
  if (key !== 'rotate' && index > 1) {
517
- unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode })
682
+ // 局部丢弃:仅这一维 (Z) 对应的子项被推出去也是错的,但 X/Y 仍正常输出,按规范使用 warn
683
+ unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode }, false)
684
+ return
518
685
  }
519
- return { [`${key}${xyz[index] || ''}`]: v.trim() }
520
- }))
686
+ // parseValues 内部已 trim,这里不再重复
687
+ transform.push({ [`${key}${xyz[index] || ''}`]: v })
688
+ })
689
+ break
690
+ }
691
+ case 'rotate3d': {
692
+ const parts = parseValues(val, ',')
693
+ if (parts.length === 4) {
694
+ // parseValues 内部已 trim
695
+ const x = +parts[0]
696
+ const y = +parts[1]
697
+ const z = +parts[2]
698
+ const angle = parts[3]
699
+ if (x && !y && !z) transform.push({ rotateX: angle })
700
+ else if (!x && y && !z) transform.push({ rotateY: angle })
701
+ else if (!x && !y && z) transform.push({ rotateZ: angle })
702
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
703
+ else unsupportedPropError({ prop, value, selector }, { mode }, false)
704
+ } else {
705
+ warn(`Value of [transform] in ${selector} does not support rotate3d with ${parts.length} values, only 4 values are supported, please check again!`)
706
+ }
707
+ break
708
+ }
709
+ case 'matrix':
710
+ {
711
+ const matrixValues = parseValues(val, ',').map(val => +val)
712
+ if (matrixValues.length === 6) {
713
+ const [a, b, c, d, tx, ty] = matrixValues
714
+ transform.push({ matrix: [a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1] })
715
+ } else {
716
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
717
+ warn(`Value of [transform] in ${selector} does not support matrix with ${matrixValues.length} values, only 6 values are supported in ${mode} environment, please check again!`)
718
+ }
521
719
  break
522
720
  }
721
+ case 'matrix3d':
722
+ {
723
+ const matrixValues = parseValues(val, ',').map(val => +val)
724
+ if (matrixValues.length === 16) {
725
+ transform.push({ matrix: matrixValues })
726
+ } else {
727
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
728
+ warn(`Value of [transform] in ${selector} does not support matrix3d with ${matrixValues.length} values, only 16 values are supported in ${mode} environment, please check again!`)
729
+ }
730
+ break
731
+ }
732
+ // 不支持的属性处理
523
733
  case 'translateZ':
524
734
  case 'scaleZ':
525
- case 'rotate3d': // x y z angle
526
- case 'matrix3d':
527
735
  default:
528
- // 不支持的属性处理
529
- unsupportedPropError({ prop, value, selector }, { mode })
736
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
737
+ unsupportedPropError({ prop, value, selector }, { mode }, false)
530
738
  break
531
739
  }
532
740
  } else {
533
- error(`Property [${prop}] is invalid in ${selector}, received [${value}], please check again!`)
741
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
742
+ warn(`Property [${prop}] is invalid in ${selector}, received [${value}], please check again!`)
534
743
  }
535
744
  })
536
745
  return {
@@ -547,7 +756,8 @@ module.exports = function getSpec({ warn, error }) {
547
756
  if ((isNumber(value) && value >= 0) || cssVariableExp.test(value)) {
548
757
  return { prop, value }
549
758
  } else {
550
- error(`Value of [${prop}] in ${selector} accepts any floating point value >= 0, received [${value}], please check again!`)
759
+ // 仅这一槽位被丢,调用侧 cssMap 仍会输出其它槽位,按规范使用 warn
760
+ warn(`Value of [${prop}] in ${selector} accepts any floating point value >= 0, received [${value}], please check again!`)
551
761
  return false
552
762
  }
553
763
  }
@@ -616,7 +826,8 @@ module.exports = function getSpec({ warn, error }) {
616
826
  } else if (values.length > 1) {
617
827
  warn(`Value of [${prop}] only supports one in ${selector}, received [${value}], and the first one is used by default.`)
618
828
  }
619
- return { prop, value: values[0].trim() }
829
+ // parseValues 内部已 trim,这里无需再 trim
830
+ return { prop, value: values[0] }
620
831
  }
621
832
 
622
833
  // const formatBoxShadow = ({ prop, value, selector }, { mode }) => {
@@ -633,16 +844,90 @@ module.exports = function getSpec({ warn, error }) {
633
844
  // })
634
845
  // return cssMap
635
846
  // }
636
- const formatBorder = ({ prop, value, selector }, { mode }) => {
847
+
848
+ // font 简写专用 formatter。RN 等效子集语法:
849
+ // font: [ <font-style> ] [ <font-variant-css2> ] [ <font-weight> ] <font-size> [ / <line-height> ] <font-family>
850
+ // - 必填项:font-size 与 font-family;缺其一整条声明丢弃(error)
851
+ // - 非必填 token(font-stretch / 数字型 font-variant-numeric / system 关键字等):warn 提示并忽略,保留其余槽位
852
+ // - font-variant 仅支持 small-caps(CSS font 简写规范 <font-variant-css2>),与 RN processFontVariant 同口径(字符串透传,内部 split 为数组)
853
+ const formatFont = ({ prop, value, selector }, { mode }) => {
637
854
  value = value.trim()
638
- if (value === 'none') {
639
- return {
640
- prop: 'borderWidth',
641
- value: 0
855
+ // 单 var() 兜底:编译期无法判断内部 token,原样返回交给运行时解析
856
+ if (cssVariableExp.test(value) && parseValues(value).length === 1) {
857
+ return { prop, value }
858
+ }
859
+ const tokens = parseValues(value)
860
+ const cssMap = []
861
+ let sizeIdx = -1
862
+ let lineHeight
863
+ // 1. 定位 font-size(第一个 length 类型 token,可能带 /<line-height>)
864
+ // 注意:unit-less 数字也命中 length 正则,需要先排除 font-weight 数字(100..900 / bold / normal),
865
+ // 否则 `font: small-caps 500 28rpx Arial` 会把 500 误判为 fontSize。
866
+ for (let i = 0; i < tokens.length; i++) {
867
+ let t = tokens[i]
868
+ if (t.endsWith('/') && tokens[i + 1]) {
869
+ t += tokens[i + 1]
870
+ tokens.splice(i + 1, 1)
871
+ } else if (tokens[i + 1] === '/' && tokens[i + 2]) {
872
+ t += `/${tokens[i + 2]}`
873
+ tokens.splice(i + 1, 2)
874
+ } else if (tokens[i + 1]?.startsWith('/') && tokens[i + 1].length > 1) {
875
+ t += tokens[i + 1]
876
+ tokens.splice(i + 1, 1)
642
877
  }
643
- } else {
644
- return formatAbbreviation({ prop, value, selector }, { mode })
878
+ const [sizePart, lhPart] = parseValues(t, '/')
879
+ if (verifyValues({ prop: 'font-weight', value: sizePart, selector }, silentVerify)) continue
880
+ if (verifyValues({ prop: 'font-size', value: sizePart, selector }, silentVerify)) {
881
+ sizeIdx = i
882
+ cssMap.push({ prop: 'fontSize', value: sizePart })
883
+ if (lhPart) lineHeight = lhPart
884
+ break
885
+ }
886
+ }
887
+ if (sizeIdx === -1) {
888
+ // 缺必填 font-size → 整条声明丢弃
889
+ error(`Value of [${prop}:${value}] in ${selector} is missing required <font-size>, please check again!`)
890
+ return false
891
+ }
892
+ // 2. 前导段(font-size 之前):识别 font-style / font-variant(small-caps) / font-weight,顺序不敏感
893
+ for (let i = 0; i < sizeIdx; i++) {
894
+ const t = tokens[i]
895
+ if (t === 'normal') continue // 默认值跳过
896
+ if (verifyValues({ prop: 'font-style', value: t, selector }, silentVerify)) {
897
+ cssMap.push({ prop: 'fontStyle', value: t })
898
+ } else if (t === 'small-caps') {
899
+ // CSS font 简写的 variant 槽位仅 <font-variant-css2>(normal | small-caps);
900
+ // 字符串透传,RN processFontVariant 会 split 归一为数组,与 font-variant 长属性同口径
901
+ cssMap.push({ prop: 'fontVariant', value: t })
902
+ } else if (verifyValues({ prop: 'font-weight', value: t, selector }, silentVerify)) {
903
+ cssMap.push({ prop: 'fontWeight', value: t })
904
+ } else {
905
+ // font-stretch / 数字型 font-variant-numeric / system 关键字等 → 非必填槽位,warn 并忽略该 token、保留其余
906
+ warn(`Value of [${prop}:${value}] in ${selector}: token [${t}] is not supported (only font-style / small-caps / font-weight are valid before <font-size>), ignored.`)
907
+ }
908
+ }
909
+ // 3. line-height(数值复用 formatLineHeight 口径换算为百分比)
910
+ if (lineHeight !== undefined) {
911
+ const lh = formatLineHeight({ prop: 'line-height', value: lineHeight, selector })
912
+ if (lh) cssMap.push(lh)
913
+ }
914
+ // 4. font-family(font-size 之后剩余部分)
915
+ const familyStr = tokens.slice(sizeIdx + 1).join(' ').trim()
916
+ if (!familyStr) {
917
+ // 缺必填 font-family → 整条声明丢弃
918
+ error(`Value of [${prop}:${value}] in ${selector} is missing required <font-family>, please check again!`)
919
+ return false
645
920
  }
921
+ const family = formatFontFamily({ prop: 'font-family', value: familyStr, selector })
922
+ if (family) cssMap.push({ prop: 'fontFamily', value: family.value })
923
+ return cssMap
924
+ }
925
+
926
+ // border / outline 简写只负责按值类型展开和补齐缺省值;
927
+ // none 清除语义不在编译期折叠,统一保留到运行时最终处理。
928
+ const formatBorder = ({ prop, value, selector }, { mode }) => {
929
+ value = value.trim()
930
+ return formatUnorderedAbbreviation({ prop, value, selector }, { mode })
646
931
  }
647
932
 
648
933
  return {
@@ -650,12 +935,12 @@ module.exports = function getSpec({ warn, error }) {
650
935
  rules: [
651
936
  { // 背景相关属性的处理
652
937
  test: /^(background|background-image|background-size|background-position)$/,
653
- ios: checkBackgroundImage,
654
- android: checkBackgroundImage,
655
- harmony: checkBackgroundImage
938
+ ios: formatBackground,
939
+ android: formatBackground,
940
+ harmony: formatBackground
656
941
  },
657
- { // margin padding 内外边距的处理
658
- test: /^(margin|padding|border-radius|border-width|border-color)$/,
942
+ { // margin padding 内外边距 / gap / inset 的处理(统一走复合值展开)
943
+ test: /^(margin|padding|border-radius|border-width|border-color|gap|inset)$/,
659
944
  ios: formatCompositeVal,
660
945
  android: formatCompositeVal,
661
946
  harmony: formatCompositeVal
@@ -684,14 +969,20 @@ module.exports = function getSpec({ warn, error }) {
684
969
  android: formatFontFamily,
685
970
  harmony: formatFontFamily
686
971
  },
972
+ { // font 简写专用处理:必须置于通用 AbbreviationMap 匹配规则之前
973
+ test: 'font',
974
+ ios: formatFont,
975
+ android: formatFont,
976
+ harmony: formatFont
977
+ },
687
978
  // {
688
979
  // test: 'box-shadow',
689
980
  // ios: formatBoxShadow,
690
981
  // android: formatBoxShadow,
691
982
  // harmony: formatBoxShadow
692
983
  // },
693
- {
694
- test: 'border',
984
+ { // border / outline 简写共享 formatBorder(按值类型展开 + 缺省补齐;none 清除语义留到运行时处理)
985
+ test: /^(border|border-left|border-right|border-top|border-bottom|outline)$/,
695
986
  ios: formatBorder,
696
987
  android: formatBorder,
697
988
  harmony: formatBorder