@mpxjs/webpack-plugin 2.10.24 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/lib/index.js +4 -1
  2. package/lib/platform/style/wx/index.js +417 -120
  3. package/lib/platform/template/wx/component-config/picker.js +0 -6
  4. package/lib/react/style-helper.js +6 -6
  5. package/lib/resolver/AddEnvPlugin.js +1 -1
  6. package/lib/resolver/AddModePlugin.js +1 -1
  7. package/lib/resolver/ExtendComponentsPlugin.js +68 -0
  8. package/lib/runtime/components/extends/section-list.mpx +14 -0
  9. package/lib/runtime/components/react/animationHooks/index.ts +7 -3
  10. package/lib/runtime/components/react/dist/animationHooks/index.d.ts +0 -2
  11. package/lib/runtime/components/react/dist/animationHooks/index.js +6 -1
  12. package/lib/runtime/components/react/dist/event.config.d.ts +2 -6
  13. package/lib/runtime/components/react/dist/event.config.js +28 -24
  14. package/lib/runtime/components/react/dist/getInnerListeners.d.ts +1 -1
  15. package/lib/runtime/components/react/dist/getInnerListeners.js +74 -56
  16. package/lib/runtime/components/react/dist/mpx-button.d.ts +1 -2
  17. package/lib/runtime/components/react/dist/mpx-button.jsx +16 -9
  18. package/lib/runtime/components/react/dist/mpx-camera.d.ts +0 -2
  19. package/lib/runtime/components/react/dist/mpx-camera.jsx +36 -5
  20. package/lib/runtime/components/react/dist/mpx-canvas/index.d.ts +0 -2
  21. package/lib/runtime/components/react/dist/mpx-canvas/index.jsx +1 -3
  22. package/lib/runtime/components/react/dist/mpx-canvas/utils.d.ts +1 -1
  23. package/lib/runtime/components/react/dist/mpx-checkbox-group.d.ts +0 -2
  24. package/lib/runtime/components/react/dist/mpx-checkbox-group.jsx +3 -4
  25. package/lib/runtime/components/react/dist/mpx-checkbox.d.ts +1 -2
  26. package/lib/runtime/components/react/dist/mpx-checkbox.jsx +8 -9
  27. package/lib/runtime/components/react/dist/mpx-form.d.ts +1 -2
  28. package/lib/runtime/components/react/dist/mpx-form.jsx +5 -5
  29. package/lib/runtime/components/react/dist/mpx-icon/index.d.ts +0 -2
  30. package/lib/runtime/components/react/dist/mpx-icon/index.jsx +2 -2
  31. package/lib/runtime/components/react/dist/mpx-image.d.ts +0 -2
  32. package/lib/runtime/components/react/dist/mpx-image.jsx +59 -43
  33. package/lib/runtime/components/react/dist/mpx-inline-text.jsx +13 -4
  34. package/lib/runtime/components/react/dist/mpx-input.d.ts +0 -2
  35. package/lib/runtime/components/react/dist/mpx-input.jsx +16 -8
  36. package/lib/runtime/components/react/dist/mpx-label.d.ts +1 -2
  37. package/lib/runtime/components/react/dist/mpx-label.jsx +5 -6
  38. package/lib/runtime/components/react/dist/mpx-movable-area.d.ts +0 -2
  39. package/lib/runtime/components/react/dist/mpx-movable-area.jsx +3 -3
  40. package/lib/runtime/components/react/dist/mpx-movable-view.d.ts +1 -2
  41. package/lib/runtime/components/react/dist/mpx-movable-view.jsx +6 -6
  42. package/lib/runtime/components/react/dist/mpx-nav.jsx +1 -1
  43. package/lib/runtime/components/react/dist/mpx-picker/index.jsx +72 -23
  44. package/lib/runtime/components/react/dist/mpx-picker/multiSelector.jsx +2 -2
  45. package/lib/runtime/components/react/dist/mpx-picker/type.d.ts +4 -0
  46. package/lib/runtime/components/react/dist/mpx-picker-view/index.d.ts +1 -1
  47. package/lib/runtime/components/react/dist/mpx-picker-view/index.jsx +10 -14
  48. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.d.ts +0 -2
  49. package/lib/runtime/components/react/dist/mpx-picker-view/pickerVIewContext.js +0 -5
  50. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.d.ts +1 -1
  51. package/lib/runtime/components/react/dist/mpx-picker-view-column/index.jsx +34 -14
  52. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.d.ts +0 -2
  53. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItem.jsx +4 -5
  54. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.d.ts +0 -2
  55. package/lib/runtime/components/react/dist/mpx-picker-view-column/pickerViewColumnItemLite.jsx +3 -5
  56. package/lib/runtime/components/react/dist/mpx-popup/index.d.ts +5 -4
  57. package/lib/runtime/components/react/dist/mpx-popup/index.jsx +2 -2
  58. package/lib/runtime/components/react/dist/mpx-popup/popupBase.jsx +3 -2
  59. package/lib/runtime/components/react/dist/mpx-progress.d.ts +0 -2
  60. package/lib/runtime/components/react/dist/mpx-progress.jsx +1 -3
  61. package/lib/runtime/components/react/dist/mpx-radio-group.d.ts +0 -2
  62. package/lib/runtime/components/react/dist/mpx-radio-group.jsx +3 -4
  63. package/lib/runtime/components/react/dist/mpx-radio.d.ts +1 -2
  64. package/lib/runtime/components/react/dist/mpx-radio.jsx +8 -9
  65. package/lib/runtime/components/react/dist/mpx-rich-text/index.d.ts +0 -2
  66. package/lib/runtime/components/react/dist/mpx-rich-text/index.jsx +1 -3
  67. package/lib/runtime/components/react/dist/mpx-scroll-view.d.ts +1 -2
  68. package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +6 -6
  69. package/lib/runtime/components/react/dist/mpx-section-list.d.ts +51 -0
  70. package/lib/runtime/components/react/dist/mpx-section-list.jsx +376 -0
  71. package/lib/runtime/components/react/dist/mpx-simple-text.jsx +37 -19
  72. package/lib/runtime/components/react/dist/mpx-simple-view.d.ts +4 -1
  73. package/lib/runtime/components/react/dist/mpx-simple-view.jsx +11 -4
  74. package/lib/runtime/components/react/dist/mpx-slider.d.ts +0 -2
  75. package/lib/runtime/components/react/dist/mpx-slider.jsx +6 -7
  76. package/lib/runtime/components/react/dist/mpx-sticky-header.d.ts +1 -2
  77. package/lib/runtime/components/react/dist/mpx-sticky-header.jsx +5 -5
  78. package/lib/runtime/components/react/dist/mpx-sticky-section.d.ts +1 -2
  79. package/lib/runtime/components/react/dist/mpx-sticky-section.jsx +6 -6
  80. package/lib/runtime/components/react/dist/mpx-swiper-item.d.ts +1 -2
  81. package/lib/runtime/components/react/dist/mpx-swiper-item.jsx +5 -5
  82. package/lib/runtime/components/react/dist/mpx-swiper.d.ts +1 -2
  83. package/lib/runtime/components/react/dist/mpx-swiper.jsx +4 -8
  84. package/lib/runtime/components/react/dist/mpx-switch.d.ts +0 -2
  85. package/lib/runtime/components/react/dist/mpx-switch.jsx +1 -3
  86. package/lib/runtime/components/react/dist/mpx-text.d.ts +0 -2
  87. package/lib/runtime/components/react/dist/mpx-text.jsx +43 -25
  88. package/lib/runtime/components/react/dist/mpx-video.d.ts +0 -2
  89. package/lib/runtime/components/react/dist/mpx-video.jsx +4 -5
  90. package/lib/runtime/components/react/dist/mpx-view.d.ts +2 -2
  91. package/lib/runtime/components/react/dist/mpx-view.jsx +214 -114
  92. package/lib/runtime/components/react/dist/mpx-web-view.d.ts +2 -1
  93. package/lib/runtime/components/react/dist/mpx-web-view.jsx +5 -1
  94. package/lib/runtime/components/react/dist/namedColorSet.d.ts +2 -0
  95. package/lib/runtime/components/react/dist/namedColorSet.js +149 -0
  96. package/lib/runtime/components/react/dist/utils.d.ts +71 -70
  97. package/lib/runtime/components/react/dist/utils.jsx +1295 -456
  98. package/lib/runtime/components/react/event.config.ts +32 -25
  99. package/lib/runtime/components/react/getInnerListeners.ts +84 -60
  100. package/lib/runtime/components/react/mpx-button.tsx +18 -18
  101. package/lib/runtime/components/react/mpx-camera.tsx +40 -8
  102. package/lib/runtime/components/react/mpx-canvas/index.tsx +5 -7
  103. package/lib/runtime/components/react/mpx-canvas/utils.tsx +3 -1
  104. package/lib/runtime/components/react/mpx-checkbox-group.tsx +2 -8
  105. package/lib/runtime/components/react/mpx-checkbox.tsx +9 -13
  106. package/lib/runtime/components/react/mpx-form.tsx +6 -8
  107. package/lib/runtime/components/react/mpx-icon/index.tsx +1 -5
  108. package/lib/runtime/components/react/mpx-image.tsx +66 -58
  109. package/lib/runtime/components/react/mpx-inline-text.tsx +13 -4
  110. package/lib/runtime/components/react/mpx-input.tsx +19 -12
  111. package/lib/runtime/components/react/mpx-label.tsx +6 -10
  112. package/lib/runtime/components/react/mpx-movable-area.tsx +3 -5
  113. package/lib/runtime/components/react/mpx-movable-view.tsx +7 -9
  114. package/lib/runtime/components/react/mpx-nav.tsx +1 -1
  115. package/lib/runtime/components/react/mpx-picker/index.tsx +100 -29
  116. package/lib/runtime/components/react/mpx-picker/multiSelector.tsx +3 -3
  117. package/lib/runtime/components/react/mpx-picker/selector.tsx +1 -1
  118. package/lib/runtime/components/react/mpx-picker/type.ts +4 -0
  119. package/lib/runtime/components/react/mpx-picker-view/index.tsx +17 -24
  120. package/lib/runtime/components/react/mpx-picker-view/pickerVIewContext.ts +0 -9
  121. package/lib/runtime/components/react/mpx-picker-view-column/index.tsx +44 -23
  122. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItem.tsx +1 -9
  123. package/lib/runtime/components/react/mpx-picker-view-column/pickerViewColumnItemLite.tsx +0 -9
  124. package/lib/runtime/components/react/mpx-popup/index.tsx +7 -3
  125. package/lib/runtime/components/react/mpx-popup/popupBase.tsx +3 -2
  126. package/lib/runtime/components/react/mpx-progress.tsx +0 -6
  127. package/lib/runtime/components/react/mpx-radio-group.tsx +2 -8
  128. package/lib/runtime/components/react/mpx-radio.tsx +9 -13
  129. package/lib/runtime/components/react/mpx-rich-text/index.tsx +0 -6
  130. package/lib/runtime/components/react/mpx-scroll-view.tsx +7 -9
  131. package/lib/runtime/components/react/mpx-section-list.tsx +557 -0
  132. package/lib/runtime/components/react/mpx-simple-text.tsx +36 -25
  133. package/lib/runtime/components/react/mpx-simple-view.tsx +17 -5
  134. package/lib/runtime/components/react/mpx-slider.tsx +15 -13
  135. package/lib/runtime/components/react/mpx-sticky-header.tsx +6 -8
  136. package/lib/runtime/components/react/mpx-sticky-section.tsx +7 -9
  137. package/lib/runtime/components/react/mpx-swiper-item.tsx +6 -7
  138. package/lib/runtime/components/react/mpx-swiper.tsx +5 -11
  139. package/lib/runtime/components/react/mpx-switch.tsx +0 -6
  140. package/lib/runtime/components/react/mpx-text.tsx +45 -36
  141. package/lib/runtime/components/react/mpx-video.tsx +3 -8
  142. package/lib/runtime/components/react/mpx-view.tsx +235 -129
  143. package/lib/runtime/components/react/mpx-web-view.tsx +12 -6
  144. package/lib/runtime/components/react/namedColorSet.ts +150 -0
  145. package/lib/runtime/components/react/types/getInnerListeners.d.ts +21 -1
  146. package/lib/runtime/components/react/types/global.d.ts +1 -0
  147. package/lib/runtime/components/react/utils.tsx +1352 -550
  148. package/lib/runtime/i18n.wxs +15 -9
  149. package/lib/style-compiler/strip-conditional.js +19 -4
  150. package/lib/template-compiler/compiler.js +6 -0
  151. package/package.json +6 -6
@@ -1,30 +1,42 @@
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)$/,
10
11
  // React Native android 不支持的 CSS property
11
- android: /^(text-decoration-style|text-decoration-color|shadow-offset|shadow-opacity|shadow-radius)$/,
12
+ android: /^(text-decoration-color|shadow-offset|shadow-opacity|shadow-radius)$/,
12
13
  // TODO: rnoh 文档暂未找到 css 属性支持说明,暂时同步 android,同时需要注意此处校验是否有缺失,类似 will-change 之类属性
13
- harmony: /^(text-decoration-style|text-decoration-color|shadow-offset|shadow-opacity|shadow-radius)$/
14
+ harmony: /^(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) => {
34
+ if (isError === silentVerify) return
23
35
  const tips = isError ? error : warn
24
36
  tips(`Property [${prop}] on ${selector} is not supported in ${mode} environment!`)
25
37
  }
26
38
  // prop 校验
27
- const verifyProps = ({ prop, value, selector }, { mode }, isError = true) => {
39
+ const verifyProps = ({ prop, value, selector }, { mode, isError = true }) => {
28
40
  prop = prop.trim()
29
41
  if (unsupportedPropExp.test(prop) || unsupportedPropMode[mode].test(prop)) {
30
42
  unsupportedPropError({ prop, value, selector }, { mode }, isError)
@@ -39,12 +51,18 @@ module.exports = function getSpec({ warn, error }) {
39
51
  color: 'color',
40
52
  enum: 'enum'
41
53
  }
54
+ // 校验失败提示中的示例值(按类型)。enum 依赖具体 prop 的白名单,在 tipsType 内动态拼。
55
+ const valueTypeExample = {
56
+ [ValueType.length]: '2rpx,10%,30rpx',
57
+ [ValueType.color]: 'rgb,rgba,hsl,hsla,hwb,named color,#000000'
58
+ }
42
59
  // React 属性支持的枚举值
43
60
  const SUPPORTED_PROP_VAL_ARR = {
44
61
  'box-sizing': ['border-box', 'content-box'],
45
62
  'backface-visibility': ['visible', 'hidden'],
46
63
  overflow: ['visible', 'hidden', 'scroll'],
47
- 'border-style': ['solid', 'dotted', 'dashed'],
64
+ // RN 实测仅支持 solid/dotted/dashed;none 作为 CSS 合法值保留到运行时统一转换为 borderWidth: 0
65
+ 'border-style': ['solid', 'dotted', 'dashed', 'none'],
48
66
  'object-fit': ['cover', 'contain', 'fill', 'scale-down'],
49
67
  direction: ['inherit', 'ltr', 'rtl'],
50
68
  display: ['flex', 'none'],
@@ -70,6 +88,11 @@ module.exports = function getSpec({ warn, error }) {
70
88
  'background-repeat': ['no-repeat'],
71
89
  width: ['auto', ValueType.length],
72
90
  height: ['auto', ValueType.length],
91
+ left: ['auto', ValueType.length],
92
+ right: ['auto', ValueType.length],
93
+ top: ['auto', ValueType.length],
94
+ bottom: ['auto', ValueType.length],
95
+ inset: ['auto', ValueType.length],
73
96
  'flex-basis': ['auto', ValueType.length],
74
97
  margin: ['auto', ValueType.length],
75
98
  'margin-top': ['auto', ValueType.length],
@@ -77,14 +100,20 @@ module.exports = function getSpec({ warn, error }) {
77
100
  'margin-bottom': ['auto', ValueType.length],
78
101
  'margin-right': ['auto', ValueType.length],
79
102
  'margin-horizontal': ['auto', ValueType.length],
80
- 'margin-vertical': ['auto', ValueType.length]
103
+ 'margin-vertical': ['auto', ValueType.length],
104
+ // outline-style 走 enum 校验;与 border-style 对齐,不支持 double / groove / ridge
105
+ 'outline-style': ['solid', 'dotted', 'dashed', 'none']
106
+ }
107
+ const getSupportedPropValArr = (prop, mode) => {
108
+ if ((mode === 'android' || mode === 'harmony') && prop === 'text-decoration-style') return ['solid']
109
+ return SUPPORTED_PROP_VAL_ARR[prop]
81
110
  }
82
111
  // 获取值类型
83
112
  const getValueType = (prop) => {
84
113
  const propValueTypeRules = [
85
114
  // 重要!!优先判断是不是枚举类型
86
115
  [ValueType.enum, new RegExp('^(' + Object.keys(SUPPORTED_PROP_VAL_ARR).join('|') + ')$')],
87
- [ValueType.length, /^((gap|left|right|top|bottom)|(.+-(width|height|left|right|top|bottom|radius|spacing|size|gap|offset)))$/],
116
+ [ValueType.length, /^(gap|(.+-(width|height|left|right|top|bottom|radius|spacing|size|gap|offset)))$/],
88
117
  [ValueType.integer, /^((opacity|flex-grow|flex-shrink|z-index)|(.+-(index|opacity)))$/],
89
118
  [ValueType.color, /^(color|(.+-color))$/]
90
119
  ]
@@ -92,6 +121,7 @@ module.exports = function getSpec({ warn, error }) {
92
121
  if (rule[1].test(prop)) return rule[0]
93
122
  }
94
123
  }
124
+ const getDashProp = (prop) => hump2dash(prop.replace(/\..+/, ''))
95
125
 
96
126
  // 从 CSS 变量中提取 fallback 值进行验证
97
127
  // 返回值:fallback 值 | null(没有 fallback)| undefined(循环引用)
@@ -116,10 +146,10 @@ module.exports = function getSpec({ warn, error }) {
116
146
  // 返回值:
117
147
  // - 通过:返回 true
118
148
  // - 失败:返回 false
119
- const verifyValues = ({ prop, value, selector }, isError = true) => {
149
+ const verifyValues = ({ prop, value, selector }, { isError = true, mode } = {}) => {
120
150
  prop = prop.trim()
121
151
  const rawValue = value.trim()
122
- const tips = isError === silentVerify ? () => {} : isError ? error : warn
152
+ const tips = isError === silentVerify ? () => { } : isError ? error : warn
123
153
 
124
154
  // CSS 自定义属性(--xxx)是变量定义,不属于 RN 样式属性:
125
155
  // 不能按 `-height/-color` 等后缀推断类型去校验,否则会把变量定义错误过滤,导致运行时 var() 取值失败
@@ -145,20 +175,13 @@ module.exports = function getSpec({ warn, error }) {
145
175
 
146
176
  // calc() / env() 跳过值校验,但保留 rawValue 输出
147
177
  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
178
  const type = getValueType(prop)
179
+ const supportedPropValArr = getSupportedPropValArr(prop, mode)
155
180
  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!`)
181
+ const example = type === ValueType.enum
182
+ ? supportedPropValArr?.join(',')
183
+ : valueTypeExample[type]
184
+ tips(`Value of ${prop} in ${selector} should be ${type}${example ? `, eg ${example}` : ''}, received [${rawValue}], please check again!`)
162
185
  }
163
186
  switch (type) {
164
187
  case ValueType.length: {
@@ -183,8 +206,19 @@ module.exports = function getSpec({ warn, error }) {
183
206
  return true
184
207
  }
185
208
  case ValueType.enum: {
186
- const isIn = SUPPORTED_PROP_VAL_ARR[prop].includes(valueForVerify)
187
- const isType = Object.keys(valueExp).some(item => valueExp[item].test(valueForVerify) && SUPPORTED_PROP_VAL_ARR[prop].includes(ValueType[item]))
209
+ // font-variant 长属性允许空格分隔的多值(如 `small-caps tabular-nums`),
210
+ // 走整串比对会误杀多值场景;拆 token 后逐个命中枚举白名单即放行。
211
+ // 输出仍保留 raw value,运行时由 RN `processFontVariant` 自行 `split(' ')` 归一为数组。
212
+ if (prop === 'font-variant') {
213
+ const tokens = parseValues(valueForVerify)
214
+ if (!tokens.length || !tokens.every(t => supportedPropValArr.includes(t))) {
215
+ tipsType(type)
216
+ return false
217
+ }
218
+ return true
219
+ }
220
+ const isIn = supportedPropValArr.includes(valueForVerify)
221
+ const isType = Object.keys(valueExp).some(item => supportedPropValArr.includes(ValueType[item]) && valueExp[item].test(valueForVerify))
188
222
  if (!isIn && !isType) {
189
223
  tipsType(type)
190
224
  return false
@@ -196,27 +230,20 @@ module.exports = function getSpec({ warn, error }) {
196
230
  }
197
231
  // prop & value 校验:过滤的不合法的属性和属性值
198
232
  const verification = ({ prop, value, selector }, { mode }) => {
199
- return verifyProps({ prop, value, selector }, { mode }) && verifyValues({ prop, value, selector }) && ({ prop, value })
233
+ return verifyProps({ prop, value, selector }, { mode }) && verifyValues({ prop, value, selector }, { mode }) && ({ prop, value })
200
234
  }
201
235
 
202
236
  // 简写转换规则
203
237
  const AbbreviationMap = {
204
- // 仅支持 offset-x | offset-y | blur-radius | color 排序
205
238
  'text-shadow': ['textShadowOffset.width', 'textShadowOffset.height', 'textShadowRadius', 'textShadowColor'],
206
- // 仅支持 width | style | color 这种排序
207
239
  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 这种格式
240
+ // RN 不支持单边 border-*-style,统一展开到 borderStyle
241
+ // 实测 RN 上当 borderStyle 不为 solid 时单边 border-*-color 不生效,
242
+ // 这里把单边 color 也统一展开到 borderColor 规避(width 不能这样做,否则会覆盖其它三边)
243
+ 'border-left': ['borderLeftWidth', 'borderStyle', 'borderColor'],
244
+ 'border-right': ['borderRightWidth', 'borderStyle', 'borderColor'],
245
+ 'border-top': ['borderTopWidth', 'borderStyle', 'borderColor'],
246
+ 'border-bottom': ['borderBottomWidth', 'borderStyle', 'borderColor'],
220
247
  'text-decoration': ['textDecorationLine', 'textDecorationStyle', 'textDecorationColor'],
221
248
  // flex-grow | flex-shrink | flex-basis
222
249
  flex: ['flexGrow', 'flexShrink', 'flexBasis'],
@@ -226,17 +253,166 @@ module.exports = function getSpec({ warn, error }) {
226
253
  'border-width': ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
227
254
  'border-color': ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
228
255
  margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
229
- padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
256
+ padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
257
+ // gap:CSS 2 槽位(row-gap / column-gap),单值复制行列
258
+ gap: ['rowGap', 'columnGap'],
259
+ // inset:4 槽位等价 margin 四值语法;RN inset 长属性不稳定,单值也强制展开
260
+ inset: ['top', 'right', 'bottom', 'left'],
261
+ // outline:RN 0.74 不支持、0.76+ 生效;走无序展开,与 border 共享缺省补齐
262
+ outline: ['outlineWidth', 'outlineStyle', 'outlineColor']
263
+ }
264
+
265
+ // 这些简写按 CSS 规范允许 token 顺序自由排列,按值类型识别归位
266
+ const UnorderedAbbreviationMap = {
267
+ 'text-shadow': true,
268
+ 'text-decoration': true,
269
+ 'flex-flow': true,
270
+ border: true,
271
+ 'border-left': true,
272
+ 'border-right': true,
273
+ 'border-top': true,
274
+ 'border-bottom': true,
275
+ // outline: <outline-width> || <outline-style> || <outline-color>,顺序不敏感
276
+ outline: true
277
+ }
278
+
279
+ // CSS border-width: medium 的实测值(各主流浏览器一致取 3px)
280
+ // CSS 规范允许 medium 实现自定,这里取业界事实标准;调整此值需与运行时同名常量一起改
281
+ const BORDER_MEDIUM_WIDTH = 3
282
+ // 简写槽位缺省值表(数据驱动;新增简写或调整缺省值只改这里)
283
+ // 值即槽位缺省时追加的补齐值;none 清除语义统一保留到运行时处理
284
+ // 注意:borderColor / textShadowRadius 因 RN 有内置缺省值,无需补齐,不进此表
285
+ const ShorthandDefaultMap = {
286
+ border: { borderWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
287
+ 'border-top': { borderTopWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
288
+ 'border-right': { borderRightWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
289
+ 'border-bottom': { borderBottomWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
290
+ 'border-left': { borderLeftWidth: BORDER_MEDIUM_WIDTH, borderStyle: 'none' },
291
+ // outline 与 border 缺省值完全对齐:缺 width → BORDER_MEDIUM_WIDTH;
292
+ // 缺 style → outlineStyle: none,运行时统一转换为 outlineWidth: 0
293
+ outline: { outlineWidth: BORDER_MEDIUM_WIDTH, outlineStyle: 'none' },
294
+ 'text-shadow': {
295
+ // textShadowOffset.height 的「width 存在才补 0」由 formatUnorderedAbbreviation 内既有 fallback 处理
296
+ // 值不带 quote,由后续 style-helper 的 formatValue 统一 JSON.stringify
297
+ textShadowColor: '#000'
298
+ }
299
+ // text-decoration / flex-flow 暂不配置,与 RN 默认一致
300
+ }
301
+
302
+ // 通用补齐:扫描完所有 token 后,将 ShorthandDefaultMap 中未被占用(不在 used)的槽位追加到 cssMap
303
+ // used 即主循环的占用记录,key 是完整目标 prop 名(含 textShadowOffset.width 这类 dot 路径)
304
+ const applyShorthandDefaults = (cssMap, prop, used) => {
305
+ const defaults = ShorthandDefaultMap[prop]
306
+ if (!defaults) return cssMap
307
+ for (const target in defaults) {
308
+ if (!used[target]) {
309
+ pushAbbreviationValue(cssMap, target, defaults[target])
310
+ }
311
+ }
312
+ return cssMap
313
+ }
314
+
315
+ const pushAbbreviationValue = (cssMap, prop, value) => {
316
+ if (prop.includes('.')) {
317
+ // 多个属性值的prop
318
+ const [main, sub] = prop.split('.')
319
+ const cssData = cssMap.find(item => item.prop === main)
320
+ if (cssData) { // 设置过
321
+ cssData.value[sub] = value
322
+ } else { // 第一次设置
323
+ cssMap.push({
324
+ prop: main,
325
+ value: {
326
+ [sub]: value
327
+ }
328
+ })
329
+ }
330
+ } else {
331
+ // 单个值的属性
332
+ cssMap.push({
333
+ prop,
334
+ value
335
+ })
336
+ }
230
337
  }
338
+
339
+ const getVerifiedProp = (props, value, selector, mode, used) => {
340
+ return props.find(prop => {
341
+ if (used[prop]) return false
342
+ const newProp = getDashProp(prop)
343
+ return verifyValues({ prop: newProp, value, selector }, { mode, isError: silentVerify }) &&
344
+ verifyProps({ prop: newProp, value, selector }, { mode, isError: silentVerify })
345
+ })
346
+ }
347
+
348
+ const formatUnorderedAbbreviation = ({ prop, value, selector }, { mode }) => {
349
+ const originalValue = value
350
+ const values = Array.isArray(value) ? value : parseValues(value)
351
+ const original = `${prop}:${originalValue}`
352
+ const props = AbbreviationMap[prop]
353
+ const cssMap = []
354
+ const used = {}
355
+ let hasTextDecorationNone = false
356
+ let hasUnderline = false
357
+ let hasLineThrough = false
358
+ // values[0] 而非 originalValue:避免 originalValue 是数组时 toString() 误命中
359
+ if (values.length === 1 && cssVariableExp.test(values[0])) {
360
+ return { prop, value: values[0] }
361
+ }
362
+ values.forEach(value => {
363
+ if (prop === 'text-decoration' && verifyValues({ prop: 'text-decoration-line', value, selector }, { isError: silentVerify, mode })) {
364
+ switch (value) {
365
+ case 'underline':
366
+ hasUnderline = true
367
+ return
368
+ case 'line-through':
369
+ hasLineThrough = true
370
+ return
371
+ case 'none':
372
+ hasTextDecorationNone = true
373
+ return
374
+ // verifyValues 通过但分支未处理的 line token:落到通用 getVerifiedProp 流程
375
+ }
376
+ }
377
+ const matchedProp = getVerifiedProp(props, value, selector, mode, used)
378
+ if (!matchedProp) {
379
+ warn(`Value of [${original}] in ${selector} contains unsupported value [${value}], please check again!`)
380
+ return
381
+ }
382
+ used[matchedProp] = true
383
+ pushAbbreviationValue(cssMap, matchedProp, value)
384
+ })
385
+ if (prop === 'text-decoration') {
386
+ if (hasUnderline || hasLineThrough) {
387
+ pushAbbreviationValue(cssMap, 'textDecorationLine', hasUnderline && hasLineThrough ? 'underline line-through' : hasUnderline ? 'underline' : 'line-through')
388
+ } else if (hasTextDecorationNone) {
389
+ pushAbbreviationValue(cssMap, 'textDecorationLine', 'none')
390
+ }
391
+ }
392
+ if (prop === 'text-shadow') {
393
+ // text-shadow 至少需要 offset-x 与 offset-y;缺省 height 时按 CSS 默认补 0,避免 RN 上 textShadowOffset.height 缺失
394
+ const shadowOffsetEntry = cssMap.find(item => item.prop === 'textShadowOffset')
395
+ if (shadowOffsetEntry) {
396
+ const offsetVal = shadowOffsetEntry.value
397
+ if (offsetVal && offsetVal.width !== undefined && offsetVal.height === undefined) {
398
+ warn(`Value of [${original}] in ${selector} is missing offset-y, fallback to 0, please check again!`)
399
+ offsetVal.height = 0
400
+ }
401
+ }
402
+ }
403
+ return applyShorthandDefaults(cssMap, prop, used)
404
+ }
405
+
231
406
  const formatAbbreviation = ({ prop, value, selector }, { mode }) => {
407
+ if (UnorderedAbbreviationMap[prop]) {
408
+ return formatUnorderedAbbreviation({ prop, value, selector }, { mode })
409
+ }
232
410
  const original = `${prop}:${value}`
233
411
  const props = AbbreviationMap[prop]
234
412
  const values = Array.isArray(value) ? value : parseValues(value)
235
413
  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
414
+ if (values.length === 1 && cssVariableExp.test(values[0])) {
415
+ return { prop, value: values[0] }
240
416
  }
241
417
  let idx = 0
242
418
  let propsIdx = 0
@@ -248,8 +424,8 @@ module.exports = function getSpec({ warn, error }) {
248
424
  break
249
425
  }
250
426
  const value = values[idx]
251
- const newProp = hump2dash(prop.replace(/\..+/, ''))
252
- if (!verifyProps({ prop: newProp, value, selector }, { mode }, diff === 0)) {
427
+ const newProp = getDashProp(prop)
428
+ if (!verifyProps({ prop: newProp, value, selector }, { mode, isError: diff === 0 })) {
253
429
  // 有 ios or android 不支持的 prop,跳过 prop
254
430
  if (diff === 0) {
255
431
  propsIdx++
@@ -257,7 +433,7 @@ module.exports = function getSpec({ warn, error }) {
257
433
  } else {
258
434
  propsIdx++
259
435
  }
260
- } else if (!verifyValues({ prop: newProp, value, selector }, diff === 0)) {
436
+ } else if (!verifyValues({ prop: newProp, value, selector }, { isError: diff === 0, mode })) {
261
437
  // 值不合法 跳过 value
262
438
  if (diff === 0) {
263
439
  propsIdx++
@@ -267,28 +443,8 @@ module.exports = function getSpec({ warn, error }) {
267
443
  } else {
268
444
  idx++
269
445
  }
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
446
  } else {
287
- // 单个值的属性
288
- cssMap.push({
289
- prop,
290
- value
291
- })
447
+ pushAbbreviationValue(cssMap, prop, value)
292
448
  idx += 1
293
449
  propsIdx += 1
294
450
  }
@@ -297,17 +453,24 @@ module.exports = function getSpec({ warn, error }) {
297
453
  }
298
454
 
299
455
  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
456
+ // 槽位数由 AbbreviationMap[prop].length 决定(gap=2,inset/margin/padding/border-*=4
457
+ const count = AbbreviationMap[prop].length
458
+ const values = parseValues(value).splice(0, count)
459
+ // 单值短路:margin / padding / inset / border-* 等 RN 原生支持单值 DimensionValue,原样透传;
460
+ // gap 单值仍需展开(RN gap / rowGap / columnGap 只接受 number,展开后与运行时 runtimeForceExpandCompositeMap 对齐)
461
+ if (values.length === 1 && prop !== 'gap') {
462
+ verifyValues({ prop, value, selector }, { isError: false })
463
+ return { prop, value }
464
+ }
465
+ if (count === 2) {
466
+ // gap:单值复制到行列两槽;双值原样
467
+ if (values.length === 1) values.push(values[0])
468
+ } else {
469
+ switch (values.length) {
470
+ // case 1 已被上面单值短路兜走(margin / padding / inset / border-* 四值简写均原样透传单值)
471
+ case 2: values.push(...values); break
472
+ case 3: values.push(values[1]); break
473
+ }
311
474
  }
312
475
  return formatAbbreviation({ prop, value: values, selector }, { mode })
313
476
  }
@@ -318,19 +481,19 @@ module.exports = function getSpec({ warn, error }) {
318
481
  if (+value === 0) {
319
482
  return {
320
483
  prop,
321
- value
484
+ value: 0
322
485
  }
323
486
  }
324
487
  return verifyValues({ prop, value, selector }) && ({
325
488
  prop,
326
- value: /^\s*(-?(\d+(\.\d+)?|\.\d+))\s*$/.test(value) ? `${Math.round(value * 100)}%` : value
489
+ value: isNum(value) ? `${Math.round(value * 100)}%` : value
327
490
  })
328
491
  }
329
492
 
330
493
  // background 相关属性的转换 Todo
331
494
  // 仅支持以下属性,不支持其他背景相关的属性
332
495
  // /^((?!(-color)).)*background((?!(-color)).)*$/ 包含background且不包含background-color
333
- const checkBackgroundImage = ({ prop, value, selector }, { mode }) => {
496
+ const formatBackground = ({ prop, value, selector }, { mode }) => {
334
497
  const bgPropMap = {
335
498
  image: 'background-image',
336
499
  color: 'background-color',
@@ -360,15 +523,21 @@ module.exports = function getSpec({ warn, error }) {
360
523
  return values.length === 0 ? false : { prop: bgPropMap.size, value: values }
361
524
  }
362
525
  const formatBackgroundPosition = (value) => {
526
+ const yAxisKeywords = ['top', 'bottom']
363
527
  const values = []
364
528
  parseValues(value).forEach(item => {
365
529
  if (verifyValues({ prop: bgPropMap.position, value: item, selector })) {
366
530
  // 支持 number 值 / 枚举, center与50%等价
367
531
  values.push(item === 'center' ? '50%' : item)
368
532
  } else {
369
- error(`Value of [${bgPropMap.size}] in ${selector} does not support commas, received [${value}], please check again!`)
533
+ // 仅丢这一 token,其它 push 的位置仍输出,按规范使用 warn
534
+ warn(`Value of [${bgPropMap.position}] in ${selector} does not support value [${item}], please check again!`)
370
535
  }
371
536
  })
537
+ // CSS 允许 y x 顺序的关键字(如 top left),但输出需要 [x, y] 顺序
538
+ if (values.length === 2 && yAxisKeywords.includes(values[0])) {
539
+ ;[values[0], values[1]] = [values[1], values[0]]
540
+ }
372
541
  return { prop: bgPropMap.position, value: values }
373
542
  }
374
543
  switch (prop) {
@@ -391,8 +560,7 @@ module.exports = function getSpec({ warn, error }) {
391
560
  case bgPropMap.all: {
392
561
  // background: 支持 image/color/repeat 与 position/size
393
562
  if (cssVariableExp.test(value)) {
394
- error(`Property [${bgPropMap.all}] in ${selector} is abbreviated property and does not support CSS var`)
395
- return false
563
+ return { prop, value }
396
564
  }
397
565
  // background: none
398
566
  if (value === 'none') {
@@ -407,11 +575,15 @@ module.exports = function getSpec({ warn, error }) {
407
575
  let isSize = false
408
576
  const pushPositionOrSize = (item) => {
409
577
  if (isSize) {
410
- if (verifyValues({ prop: bgPropMap.size, value: item, selector }, silentVerify)) {
578
+ if (verifyValues({ prop: bgPropMap.size, value: item, selector }, { isError: silentVerify, mode })) {
411
579
  sizeValues.push(item)
580
+ } else {
581
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support background-size token [${item}], please check again!`)
412
582
  }
413
- } else if (verifyValues({ prop: bgPropMap.position, value: item, selector }, silentVerify)) {
583
+ } else if (verifyValues({ prop: bgPropMap.position, value: item, selector }, { isError: silentVerify, mode })) {
414
584
  positionValues.push(item)
585
+ } else {
586
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support background-position token [${item}], please check again!`)
415
587
  }
416
588
  }
417
589
  const handlePositionSize = (item) => {
@@ -427,7 +599,7 @@ module.exports = function getSpec({ warn, error }) {
427
599
  })
428
600
  return true
429
601
  }
430
- if (isSize || verifyValues({ prop: bgPropMap.position, value: item, selector }, silentVerify)) {
602
+ if (isSize || verifyValues({ prop: bgPropMap.position, value: item, selector }, { isError: silentVerify, mode })) {
431
603
  pushPositionOrSize(item)
432
604
  return true
433
605
  }
@@ -441,12 +613,14 @@ module.exports = function getSpec({ warn, error }) {
441
613
  bgMap.push({ prop: bgPropMap.image, value: url })
442
614
  } else if (linerVal) {
443
615
  bgMap.push({ prop: bgPropMap.image, value: linerVal })
444
- } else if (verifyValues({ prop: bgPropMap.color, value: item, selector }, silentVerify)) {
616
+ } else if (verifyValues({ prop: bgPropMap.color, value: item, selector }, { isError: silentVerify, mode })) {
445
617
  bgMap.push({ prop: bgPropMap.color, value: item })
446
- } else if (verifyValues({ prop: bgPropMap.repeat, value: item, selector }, silentVerify)) {
618
+ } else if (verifyValues({ prop: bgPropMap.repeat, value: item, selector }, { isError: silentVerify, mode })) {
447
619
  bgMap.push({ prop: bgPropMap.repeat, value: item })
448
- } else {
449
- handlePositionSize(item)
620
+ } else if (!handlePositionSize(item)) {
621
+ // 既不是 url / linear-gradient / color / repeat / position-size,也不是合法 position 起头的 token
622
+ // 典型场景:background-attachment / background-origin / background-clip 及拼写错误,RN 均不支持
623
+ warn(`Value of [${bgPropMap.all}:${value}] in ${selector} does not support token [${item}], please check again!`)
450
624
  }
451
625
  })
452
626
  if (positionValues.length) {
@@ -495,9 +669,6 @@ module.exports = function getSpec({ warn, error }) {
495
669
  key = key === 'rotate' ? 'rotateZ' : key
496
670
  transform.push({ [key]: val })
497
671
  break
498
- case 'matrix':
499
- transform.push({ [key]: parseValues(val, ',').map(val => +val) })
500
- break
501
672
  case 'translate':
502
673
  case 'scale':
503
674
  case 'skew':
@@ -512,25 +683,69 @@ module.exports = function getSpec({ warn, error }) {
512
683
  vals.push(vals[0])
513
684
  }
514
685
  const xyz = ['X', 'Y', 'Z']
515
- transform.push(...vals.map((v, index) => {
686
+ vals.forEach((v, index) => {
516
687
  if (key !== 'rotate' && index > 1) {
517
- unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode })
688
+ // 局部丢弃:仅这一维 (Z) 对应的子项被推出去也是错的,但 X/Y 仍正常输出,按规范使用 warn
689
+ unsupportedPropError({ prop: `${key}Z`, value, selector }, { mode }, false)
690
+ return
518
691
  }
519
- return { [`${key}${xyz[index] || ''}`]: v.trim() }
520
- }))
692
+ // parseValues 内部已 trim,这里不再重复
693
+ transform.push({ [`${key}${xyz[index] || ''}`]: v })
694
+ })
695
+ break
696
+ }
697
+ case 'rotate3d': {
698
+ const parts = parseValues(val, ',')
699
+ if (parts.length === 4) {
700
+ // parseValues 内部已 trim
701
+ const x = +parts[0]
702
+ const y = +parts[1]
703
+ const z = +parts[2]
704
+ const angle = parts[3]
705
+ if (x && !y && !z) transform.push({ rotateX: angle })
706
+ else if (!x && y && !z) transform.push({ rotateY: angle })
707
+ else if (!x && !y && z) transform.push({ rotateZ: angle })
708
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
709
+ else unsupportedPropError({ prop, value, selector }, { mode }, false)
710
+ } else {
711
+ warn(`Value of [transform] in ${selector} does not support rotate3d with ${parts.length} values, only 4 values are supported, please check again!`)
712
+ }
713
+ break
714
+ }
715
+ case 'matrix':
716
+ {
717
+ const matrixValues = parseValues(val, ',').map(val => +val)
718
+ if (matrixValues.length === 6) {
719
+ const [a, b, c, d, tx, ty] = matrixValues
720
+ transform.push({ matrix: [a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, tx, ty, 0, 1] })
721
+ } else {
722
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
723
+ 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!`)
724
+ }
725
+ break
726
+ }
727
+ case 'matrix3d':
728
+ {
729
+ const matrixValues = parseValues(val, ',').map(val => +val)
730
+ if (matrixValues.length === 16) {
731
+ transform.push({ matrix: matrixValues })
732
+ } else {
733
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
734
+ 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!`)
735
+ }
521
736
  break
522
737
  }
738
+ // 不支持的属性处理
523
739
  case 'translateZ':
524
740
  case 'scaleZ':
525
- case 'rotate3d': // x y z angle
526
- case 'matrix3d':
527
741
  default:
528
- // 不支持的属性处理
529
- unsupportedPropError({ prop, value, selector }, { mode })
742
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
743
+ unsupportedPropError({ prop, value, selector }, { mode }, false)
530
744
  break
531
745
  }
532
746
  } else {
533
- error(`Property [${prop}] is invalid in ${selector}, received [${value}], please check again!`)
747
+ // 仅丢这一 transform 项,其它项仍输出,按规范使用 warn
748
+ warn(`Property [${prop}] is invalid in ${selector}, received [${value}], please check again!`)
534
749
  }
535
750
  })
536
751
  return {
@@ -547,7 +762,8 @@ module.exports = function getSpec({ warn, error }) {
547
762
  if ((isNumber(value) && value >= 0) || cssVariableExp.test(value)) {
548
763
  return { prop, value }
549
764
  } else {
550
- error(`Value of [${prop}] in ${selector} accepts any floating point value >= 0, received [${value}], please check again!`)
765
+ // 仅这一槽位被丢,调用侧 cssMap 仍会输出其它槽位,按规范使用 warn
766
+ warn(`Value of [${prop}] in ${selector} accepts any floating point value >= 0, received [${value}], please check again!`)
551
767
  return false
552
768
  }
553
769
  }
@@ -616,7 +832,8 @@ module.exports = function getSpec({ warn, error }) {
616
832
  } else if (values.length > 1) {
617
833
  warn(`Value of [${prop}] only supports one in ${selector}, received [${value}], and the first one is used by default.`)
618
834
  }
619
- return { prop, value: values[0].trim() }
835
+ // parseValues 内部已 trim,这里无需再 trim
836
+ return { prop, value: values[0] }
620
837
  }
621
838
 
622
839
  // const formatBoxShadow = ({ prop, value, selector }, { mode }) => {
@@ -633,16 +850,90 @@ module.exports = function getSpec({ warn, error }) {
633
850
  // })
634
851
  // return cssMap
635
852
  // }
636
- const formatBorder = ({ prop, value, selector }, { mode }) => {
853
+
854
+ // font 简写专用 formatter。RN 等效子集语法:
855
+ // font: [ <font-style> ] [ <font-variant-css2> ] [ <font-weight> ] <font-size> [ / <line-height> ] <font-family>
856
+ // - 必填项:font-size 与 font-family;缺其一整条声明丢弃(error)
857
+ // - 非必填 token(font-stretch / 数字型 font-variant-numeric / system 关键字等):warn 提示并忽略,保留其余槽位
858
+ // - font-variant 仅支持 small-caps(CSS font 简写规范 <font-variant-css2>),与 RN processFontVariant 同口径(字符串透传,内部 split 为数组)
859
+ const formatFont = ({ prop, value, selector }, { mode }) => {
637
860
  value = value.trim()
638
- if (value === 'none') {
639
- return {
640
- prop: 'borderWidth',
641
- value: 0
861
+ // 单 var() 兜底:编译期无法判断内部 token,原样返回交给运行时解析
862
+ if (cssVariableExp.test(value) && parseValues(value).length === 1) {
863
+ return { prop, value }
864
+ }
865
+ const tokens = parseValues(value)
866
+ const cssMap = []
867
+ let sizeIdx = -1
868
+ let lineHeight
869
+ // 1. 定位 font-size(第一个 length 类型 token,可能带 /<line-height>)
870
+ // 注意:unit-less 数字也命中 length 正则,需要先排除 font-weight 数字(100..900 / bold / normal),
871
+ // 否则 `font: small-caps 500 28rpx Arial` 会把 500 误判为 fontSize。
872
+ for (let i = 0; i < tokens.length; i++) {
873
+ let t = tokens[i]
874
+ if (t.endsWith('/') && tokens[i + 1]) {
875
+ t += tokens[i + 1]
876
+ tokens.splice(i + 1, 1)
877
+ } else if (tokens[i + 1] === '/' && tokens[i + 2]) {
878
+ t += `/${tokens[i + 2]}`
879
+ tokens.splice(i + 1, 2)
880
+ } else if (tokens[i + 1]?.startsWith('/') && tokens[i + 1].length > 1) {
881
+ t += tokens[i + 1]
882
+ tokens.splice(i + 1, 1)
642
883
  }
643
- } else {
644
- return formatAbbreviation({ prop, value, selector }, { mode })
884
+ const [sizePart, lhPart] = parseValues(t, '/')
885
+ if (verifyValues({ prop: 'font-weight', value: sizePart, selector }, { isError: silentVerify, mode })) continue
886
+ if (verifyValues({ prop: 'font-size', value: sizePart, selector }, { isError: silentVerify, mode })) {
887
+ sizeIdx = i
888
+ cssMap.push({ prop: 'fontSize', value: sizePart })
889
+ if (lhPart) lineHeight = lhPart
890
+ break
891
+ }
892
+ }
893
+ if (sizeIdx === -1) {
894
+ // 缺必填 font-size → 整条声明丢弃
895
+ error(`Value of [${prop}:${value}] in ${selector} is missing required <font-size>, please check again!`)
896
+ return false
897
+ }
898
+ // 2. 前导段(font-size 之前):识别 font-style / font-variant(small-caps) / font-weight,顺序不敏感
899
+ for (let i = 0; i < sizeIdx; i++) {
900
+ const t = tokens[i]
901
+ if (t === 'normal') continue // 默认值跳过
902
+ if (verifyValues({ prop: 'font-style', value: t, selector }, { isError: silentVerify, mode })) {
903
+ cssMap.push({ prop: 'fontStyle', value: t })
904
+ } else if (t === 'small-caps') {
905
+ // CSS font 简写的 variant 槽位仅 <font-variant-css2>(normal | small-caps);
906
+ // 字符串透传,RN processFontVariant 会 split 归一为数组,与 font-variant 长属性同口径
907
+ cssMap.push({ prop: 'fontVariant', value: t })
908
+ } else if (verifyValues({ prop: 'font-weight', value: t, selector }, { isError: silentVerify, mode })) {
909
+ cssMap.push({ prop: 'fontWeight', value: t })
910
+ } else {
911
+ // font-stretch / 数字型 font-variant-numeric / system 关键字等 → 非必填槽位,warn 并忽略该 token、保留其余
912
+ 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.`)
913
+ }
914
+ }
915
+ // 3. line-height(数值复用 formatLineHeight 口径换算为百分比)
916
+ if (lineHeight !== undefined) {
917
+ const lh = formatLineHeight({ prop: 'line-height', value: lineHeight, selector })
918
+ if (lh) cssMap.push(lh)
919
+ }
920
+ // 4. font-family(font-size 之后剩余部分)
921
+ const familyStr = tokens.slice(sizeIdx + 1).join(' ').trim()
922
+ if (!familyStr) {
923
+ // 缺必填 font-family → 整条声明丢弃
924
+ error(`Value of [${prop}:${value}] in ${selector} is missing required <font-family>, please check again!`)
925
+ return false
645
926
  }
927
+ const family = formatFontFamily({ prop: 'font-family', value: familyStr, selector })
928
+ if (family) cssMap.push({ prop: 'fontFamily', value: family.value })
929
+ return cssMap
930
+ }
931
+
932
+ // border / outline 简写只负责按值类型展开和补齐缺省值;
933
+ // none 清除语义不在编译期折叠,统一保留到运行时最终处理。
934
+ const formatBorder = ({ prop, value, selector }, { mode }) => {
935
+ value = value.trim()
936
+ return formatUnorderedAbbreviation({ prop, value, selector }, { mode })
646
937
  }
647
938
 
648
939
  return {
@@ -650,12 +941,12 @@ module.exports = function getSpec({ warn, error }) {
650
941
  rules: [
651
942
  { // 背景相关属性的处理
652
943
  test: /^(background|background-image|background-size|background-position)$/,
653
- ios: checkBackgroundImage,
654
- android: checkBackgroundImage,
655
- harmony: checkBackgroundImage
944
+ ios: formatBackground,
945
+ android: formatBackground,
946
+ harmony: formatBackground
656
947
  },
657
- { // margin padding 内外边距的处理
658
- test: /^(margin|padding|border-radius|border-width|border-color)$/,
948
+ { // margin padding 内外边距 / gap / inset 的处理(统一走复合值展开)
949
+ test: /^(margin|padding|border-radius|border-width|border-color|gap|inset)$/,
659
950
  ios: formatCompositeVal,
660
951
  android: formatCompositeVal,
661
952
  harmony: formatCompositeVal
@@ -684,14 +975,20 @@ module.exports = function getSpec({ warn, error }) {
684
975
  android: formatFontFamily,
685
976
  harmony: formatFontFamily
686
977
  },
978
+ { // font 简写专用处理:必须置于通用 AbbreviationMap 匹配规则之前
979
+ test: 'font',
980
+ ios: formatFont,
981
+ android: formatFont,
982
+ harmony: formatFont
983
+ },
687
984
  // {
688
985
  // test: 'box-shadow',
689
986
  // ios: formatBoxShadow,
690
987
  // android: formatBoxShadow,
691
988
  // harmony: formatBoxShadow
692
989
  // },
693
- {
694
- test: 'border',
990
+ { // border / outline 简写共享 formatBorder(按值类型展开 + 缺省补齐;none 清除语义留到运行时处理)
991
+ test: /^(border|border-left|border-right|border-top|border-bottom|outline)$/,
695
992
  ios: formatBorder,
696
993
  android: formatBorder,
697
994
  harmony: formatBorder