@dongmingzhixiu/vuexir-compiler 0.5.7 → 0.7.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.
- package/dist/codegen/bindings.d.ts +11 -0
- package/dist/codegen/bindings.js +32 -0
- package/dist/codegen/compose.d.ts +10 -1
- package/dist/codegen/compose.js +297 -65
- package/dist/codegen/dynamic-vector.d.ts +3 -0
- package/dist/codegen/dynamic-vector.js +81 -0
- package/dist/codegen/page-js-android.d.ts +2 -0
- package/dist/codegen/page-js-android.js +59 -0
- package/dist/codegen/project-assets.d.ts +5 -0
- package/dist/codegen/project-assets.js +31 -0
- package/dist/codegen/project-styles.d.ts +2 -0
- package/dist/codegen/project-styles.js +41 -0
- package/dist/codegen/swiftui.d.ts +4 -25
- package/dist/codegen/swiftui.js +251 -71
- package/dist/codegen/vector.d.ts +3 -0
- package/dist/codegen/vector.js +165 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/dist/ir/schema.d.ts +41 -1
- package/dist/ir/schema.js +39 -20
- package/dist/transform/css.d.ts +21 -1
- package/dist/transform/css.js +217 -14
- package/dist/transform/dynamic-svg.d.ts +3 -0
- package/dist/transform/dynamic-svg.js +53 -0
- package/dist/transform/svg.d.ts +16 -0
- package/dist/transform/svg.js +96 -0
- package/dist/transform/tab-bar.d.ts +17 -0
- package/dist/transform/tab-bar.js +44 -0
- package/dist/transform/visuals.d.ts +17 -0
- package/dist/transform/visuals.js +91 -0
- package/dist/transform/vue2ir.d.ts +12 -1
- package/dist/transform/vue2ir.js +581 -188
- package/package.json +4 -2
package/dist/transform/css.js
CHANGED
|
@@ -10,8 +10,73 @@
|
|
|
10
10
|
* 不在子集内的属性 → 收集到 warnings(编译期提示,不静默忽略)。
|
|
11
11
|
*/
|
|
12
12
|
import postcss from 'postcss';
|
|
13
|
+
import { parseLinearGradient, splitCssArgs, splitCssWords } from './visuals.js';
|
|
14
|
+
/** 支持标签、类、ID、通配符及按顺序匹配的后代/直接子代选择器。 */
|
|
15
|
+
function parseSelector(selector) {
|
|
16
|
+
if (!selector.trim() || /[+~:[\]]/.test(selector))
|
|
17
|
+
return null;
|
|
18
|
+
const tokens = selector.trim().replace(/\s*>\s*/g, ' > ').split(/\s+/);
|
|
19
|
+
const out = [];
|
|
20
|
+
let direct = false;
|
|
21
|
+
for (const token of tokens) {
|
|
22
|
+
if (token === '>') {
|
|
23
|
+
if (!out.length || direct)
|
|
24
|
+
return null;
|
|
25
|
+
direct = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!/^(?:[A-Za-z][\w-]*|\*)?(?:[.#][A-Za-z_][\w-]*)*$/.test(token))
|
|
29
|
+
return null;
|
|
30
|
+
out.push({ compound: token, direct });
|
|
31
|
+
direct = false;
|
|
32
|
+
}
|
|
33
|
+
return direct ? null : out;
|
|
34
|
+
}
|
|
35
|
+
function matchesCompound(compound, element) {
|
|
36
|
+
const tag = compound.match(/^[A-Za-z][\w-]*/)?.[0];
|
|
37
|
+
if (tag && tag.toLowerCase() !== element.tag.toLowerCase())
|
|
38
|
+
return false;
|
|
39
|
+
return [...compound.matchAll(/([.#])([A-Za-z_][\w-]*)/g)].every((m) => m[1] === '#' ? element.id === m[2] : element.classes.includes(m[2]));
|
|
40
|
+
}
|
|
41
|
+
/** ancestors 按根到父排列;回溯匹配,避免多层后代选择器被扁平化而误命中。 */
|
|
42
|
+
export function matchesCssSelector(selector, element, ancestors) {
|
|
43
|
+
const chain = [...ancestors, element];
|
|
44
|
+
const match = (part, at) => {
|
|
45
|
+
if (at < 0 || !matchesCompound(selector[part].compound, chain[at]))
|
|
46
|
+
return false;
|
|
47
|
+
if (part === 0)
|
|
48
|
+
return true;
|
|
49
|
+
if (selector[part].direct)
|
|
50
|
+
return match(part - 1, at - 1);
|
|
51
|
+
for (let i = at - 1; i >= 0; i--)
|
|
52
|
+
if (match(part - 1, i))
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
return selector.length > 0 && match(selector.length - 1, chain.length - 1);
|
|
57
|
+
}
|
|
58
|
+
export function resolveCssRules(rules, element, ancestors, inline, warnings) {
|
|
59
|
+
const matched = rules.filter((r) => matchesCssSelector(r.selector, element, ancestors));
|
|
60
|
+
if (inline.trim()) {
|
|
61
|
+
const root = postcss.parse(`x{${inline}}`);
|
|
62
|
+
root.walkDecls((decl) => {
|
|
63
|
+
const props = {};
|
|
64
|
+
for (const [k, val] of mapDecl(decl.prop, decl.value, warnings) ?? [])
|
|
65
|
+
mergeIrProps(props, { [k]: val });
|
|
66
|
+
matched.push({ selector: [], specificity: 1_000_000, important: decl.important === true, props });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
matched.sort((a, b) => Number(a.important) - Number(b.important) || a.specificity - b.specificity);
|
|
70
|
+
const props = {};
|
|
71
|
+
for (const rule of matched)
|
|
72
|
+
mergeIrProps(props, rule.props);
|
|
73
|
+
return props;
|
|
74
|
+
}
|
|
13
75
|
const px = (v) => {
|
|
14
76
|
const s = v.trim();
|
|
77
|
+
const calc = s.replace(/\s+/g, '').match(/^calc\((-?[\d.]+)px([+-])([\d.]+)px\)$/);
|
|
78
|
+
if (calc)
|
|
79
|
+
return Number(calc[1]) + (calc[2] === '+' ? 1 : -1) * Number(calc[3]);
|
|
15
80
|
const m = s.match(/^([\d.]+)px$/);
|
|
16
81
|
if (m)
|
|
17
82
|
return Number(m[1]);
|
|
@@ -50,6 +115,8 @@ function mapDecl(prop, value, warnings) {
|
|
|
50
115
|
}
|
|
51
116
|
function mapDeclInner(prop, value, warnings) {
|
|
52
117
|
const v = value.trim();
|
|
118
|
+
if (prop.startsWith('--'))
|
|
119
|
+
return []; // 变量声明由 token 解析器处理,不是未支持的视觉属性。
|
|
53
120
|
switch (prop) {
|
|
54
121
|
case 'grid-template-columns': {
|
|
55
122
|
// R136:CSS grid 的最小支持——`repeat(N, 1fr)` / `1fr 1fr 1fr` → 列数
|
|
@@ -64,6 +131,8 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
64
131
|
// R135:CSS 的 display:flex 默认 main axis 是 row(此前默认 column →
|
|
65
132
|
// 横向工具栏/星期行被渲染成纵向堆叠,日历标题栏就是这么坏掉的)
|
|
66
133
|
return v === 'flex' ? [['flexDirection', 'row']] : null;
|
|
134
|
+
case 'flex-wrap':
|
|
135
|
+
return v === 'wrap' ? [['flexWrap', true]] : v === 'nowrap' ? [] : null;
|
|
67
136
|
case 'flex-direction':
|
|
68
137
|
return [['flexDirection', v]];
|
|
69
138
|
case 'flex': {
|
|
@@ -73,6 +142,10 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
73
142
|
return null;
|
|
74
143
|
return n > 0 ? [['flex', n]] : [];
|
|
75
144
|
}
|
|
145
|
+
case 'align-self': {
|
|
146
|
+
const value = v === 'flex-start' ? 'start' : v === 'flex-end' ? 'end' : v;
|
|
147
|
+
return ['start', 'center', 'end', 'stretch'].includes(value) ? [['alignSelf', value]] : null;
|
|
148
|
+
}
|
|
76
149
|
case 'flex-grow':
|
|
77
150
|
// flex-grow:N → IR flex(弹性占比);CSS 子项在剩余空间分配中按 grow 比例伸展
|
|
78
151
|
return /^[0-9]+$/.test(v) && Number(v) > 0 ? [['flex', Number(v)]] : null;
|
|
@@ -80,6 +153,7 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
80
153
|
// flex-shrink:0 = 不压缩:iOS 端 flex:1 生成 maxHeight/.infinity 天然不压缩,
|
|
81
154
|
// 默认内容自适应亦不压缩 → 语义已满足,无需 IR 字段(仅消除「不在子集」误报)
|
|
82
155
|
return /^[0-9]+$/.test(v) ? [] : null;
|
|
156
|
+
case 'overflow-y':
|
|
83
157
|
case 'overflow':
|
|
84
158
|
// overflow:auto → 标记转 scroll 节点(vue2ir 消费后删除)
|
|
85
159
|
// R71 overflow:hidden → IR clip(裁剪超出容器内容,对位 Web overflow 语义)
|
|
@@ -91,19 +165,21 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
91
165
|
case 'width': {
|
|
92
166
|
if (v === '100%')
|
|
93
167
|
return [['width', 'fill']];
|
|
168
|
+
if (/^[\d.]+%$/.test(v))
|
|
169
|
+
return [['widthFraction', Number(v.slice(0, -1)) / 100]];
|
|
94
170
|
const n = px(v);
|
|
95
171
|
return n !== null ? [['width', n]] : null;
|
|
96
172
|
}
|
|
97
173
|
case 'height': {
|
|
98
174
|
// height:100% → 撑满父高(转 flex:1;父高度确定链上占满,root 撑满屏场景等价)
|
|
99
175
|
if (v === '100%')
|
|
100
|
-
return [['
|
|
176
|
+
return [['height', 'fill']];
|
|
101
177
|
const n = px(v);
|
|
102
178
|
return n !== null ? [['height', n]] : null;
|
|
103
179
|
}
|
|
104
180
|
case 'padding': {
|
|
105
181
|
// 支持 1~4 值简写:1=四边相等;2=上下/左右;3=上/左右/下;4=上右下左
|
|
106
|
-
const parts = v
|
|
182
|
+
const parts = splitCssWords(v).map(px);
|
|
107
183
|
if (parts.length === 1 && parts[0] !== null)
|
|
108
184
|
return [['padding', parts[0]]];
|
|
109
185
|
if (parts.length >= 2 && parts.every((n) => n !== null)) {
|
|
@@ -123,6 +199,16 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
123
199
|
const side = prop.slice('padding-'.length);
|
|
124
200
|
return [['padding', { [side]: n }]];
|
|
125
201
|
}
|
|
202
|
+
case 'margin': {
|
|
203
|
+
const parts = splitCssWords(v);
|
|
204
|
+
if (parts.length < 1 || parts.length > 4)
|
|
205
|
+
return null;
|
|
206
|
+
const [t, r = t, b = t, l = r] = parts;
|
|
207
|
+
const values = [t, r, b, l].map(px);
|
|
208
|
+
if (values.some((n) => n === null))
|
|
209
|
+
return null;
|
|
210
|
+
return [['marginTop', values[0]], ['marginRight', values[1]], ['marginBottom', values[2]], ['marginLeft', values[3]]];
|
|
211
|
+
}
|
|
126
212
|
case 'margin-bottom':
|
|
127
213
|
case 'margin-right':
|
|
128
214
|
case 'margin-left': {
|
|
@@ -175,8 +261,48 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
175
261
|
const ir = map[v];
|
|
176
262
|
return ir ? [['alignItems', ir]] : null;
|
|
177
263
|
}
|
|
264
|
+
case 'fill':
|
|
265
|
+
case 'stroke':
|
|
266
|
+
case 'stroke-linecap':
|
|
267
|
+
return [[prop, v]];
|
|
268
|
+
case 'stroke-width':
|
|
269
|
+
return [['stroke-width', Number(v.replace(/px$/, ''))]];
|
|
270
|
+
case 'min-width':
|
|
271
|
+
case 'min-height': {
|
|
272
|
+
const n = px(v);
|
|
273
|
+
return n !== null ? [[prop === 'min-width' ? 'minWidth' : 'minHeight', n]] : null;
|
|
274
|
+
}
|
|
275
|
+
case 'font-family':
|
|
276
|
+
return [['fontFamily', /(?:^|[,\s])(?:monospace|ui-monospace)(?:$|[,\s])/i.test(v) ? 'monospace' : /(?:^|[,\s])serif(?:$|[,\s])/i.test(v) ? 'serif' : 'sans']];
|
|
277
|
+
case 'letter-spacing': {
|
|
278
|
+
const m = v.match(/^(-?[\d.]+)px$/);
|
|
279
|
+
return m ? [['letterSpacing', Number(m[1])]] : null;
|
|
280
|
+
}
|
|
281
|
+
case 'box-shadow': {
|
|
282
|
+
if (v === 'none')
|
|
283
|
+
return [['shadow', undefined]];
|
|
284
|
+
const layers = splitCssArgs(v);
|
|
285
|
+
const layer = layers.find((p) => !/\binset\b/.test(p));
|
|
286
|
+
if (layers.some((p) => /\binset\b/.test(p)))
|
|
287
|
+
warnings.push('box-shadow inset 暂不支持,已保留外阴影');
|
|
288
|
+
if (!layer)
|
|
289
|
+
return [];
|
|
290
|
+
const m = layer.match(/^(-?[\d.]+)(?:px)?\s+(-?[\d.]+)(?:px)?(?:\s+([\d.]+)(?:px)?)?(?:\s+(-?[\d.]+)(?:px)?)?\s+(.+)$/);
|
|
291
|
+
if (!m || !isCssColor(m[5]))
|
|
292
|
+
return null;
|
|
293
|
+
if (m[4] && Number(m[4]) !== 0)
|
|
294
|
+
warnings.push('box-shadow spread 暂未映射,保留偏移和模糊');
|
|
295
|
+
return [['shadow', { x: Number(m[1]), y: Number(m[2]), blur: Number(m[3] ?? 0), color: m[5] }]];
|
|
296
|
+
}
|
|
297
|
+
case 'background-image':
|
|
178
298
|
case 'background':
|
|
179
299
|
case 'background-color':
|
|
300
|
+
if (/^(linear|radial)-gradient\(/.test(v)) {
|
|
301
|
+
const gradient = parseLinearGradient(v);
|
|
302
|
+
return gradient ? [['gradient', gradient]] : null;
|
|
303
|
+
}
|
|
304
|
+
if (v === 'transparent')
|
|
305
|
+
return [['background', 'rgba(0,0,0,0)']];
|
|
180
306
|
// 颜色子集校验(named/var() 原生端不可解析)——拒绝并告警,不生成非法原生代码
|
|
181
307
|
if (!isCssColor(v)) {
|
|
182
308
|
warnings.push(`CSS 颜色值「${v}」仅支持 #rgb/#rrggbb/rgb()/hsl(),已忽略(named/var 暂不支持,R15)`);
|
|
@@ -185,8 +311,10 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
185
311
|
return [['background', v]];
|
|
186
312
|
case 'border-radius': {
|
|
187
313
|
const n = px(v);
|
|
314
|
+
if (v === '50%')
|
|
315
|
+
return [['radius', 9999]];
|
|
188
316
|
if (n === null && v.endsWith('%')) {
|
|
189
|
-
warnings.push(`border-radius
|
|
317
|
+
warnings.push(`border-radius ${v} 暂不支持(支持 50% 圆形)`);
|
|
190
318
|
return [];
|
|
191
319
|
}
|
|
192
320
|
return n !== null ? [['radius', n]] : null;
|
|
@@ -250,7 +378,7 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
250
378
|
return [];
|
|
251
379
|
case 'border': {
|
|
252
380
|
// 仅支持 1px solid <color>(rgba(...) / #hex)
|
|
253
|
-
const m = v.match(/^(\d+)px\s+solid\s+(.+)$/i);
|
|
381
|
+
const m = v.match(/^(\d*\.?\d+)px\s+solid\s+(.+)$/i);
|
|
254
382
|
if (!m) {
|
|
255
383
|
warnings.push(`border 仅支持「Npx solid <color>」简写,已忽略(可用 border-width / border-color)`);
|
|
256
384
|
return [];
|
|
@@ -307,13 +435,11 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
307
435
|
return null;
|
|
308
436
|
}
|
|
309
437
|
case 'line-height': {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
return n !== null ? [['lineHeight', n]] : null;
|
|
438
|
+
const absolute = px(v);
|
|
439
|
+
if (absolute !== null)
|
|
440
|
+
return [['lineHeight', absolute]];
|
|
441
|
+
const ratio = num(v);
|
|
442
|
+
return ratio !== null && ratio > 0 ? [['lineHeightMultiplier', ratio]] : null;
|
|
317
443
|
}
|
|
318
444
|
case 'text-align':
|
|
319
445
|
return [['textAlign', v]];
|
|
@@ -324,6 +450,17 @@ function mapDeclInner(prop, value, warnings) {
|
|
|
324
450
|
case 'position':
|
|
325
451
|
// M5:position:absolute → 脱离文档流(ZStack 定位 + offset);其余值不映射
|
|
326
452
|
return v === 'absolute' ? [['position', 'absolute']] : null;
|
|
453
|
+
case 'inset': {
|
|
454
|
+
const values = splitCssWords(v).map(px);
|
|
455
|
+
if (values.length < 1 || values.length > 4 || values.some((v) => v === null))
|
|
456
|
+
return null;
|
|
457
|
+
const [t, r = t, b = t, l = r] = values;
|
|
458
|
+
return [['absTop', t], ['absRight', r], ['absBottom', b], ['absLeft', l]];
|
|
459
|
+
}
|
|
460
|
+
case 'z-index': {
|
|
461
|
+
const n = Number(v);
|
|
462
|
+
return Number.isInteger(n) ? [['zIndex', n]] : null;
|
|
463
|
+
}
|
|
327
464
|
case 'top':
|
|
328
465
|
case 'left':
|
|
329
466
|
case 'right':
|
|
@@ -411,13 +548,35 @@ export function mergeIrProps(target, src) {
|
|
|
411
548
|
t.borderSides = out;
|
|
412
549
|
continue;
|
|
413
550
|
}
|
|
551
|
+
if (key === 'background')
|
|
552
|
+
delete t.gradient;
|
|
553
|
+
if (key === 'gradient')
|
|
554
|
+
delete t.background;
|
|
555
|
+
if (key === 'lineHeight')
|
|
556
|
+
delete t.lineHeightMultiplier;
|
|
557
|
+
if (key === 'lineHeightMultiplier')
|
|
558
|
+
delete t.lineHeight;
|
|
414
559
|
t[key] = val;
|
|
415
560
|
}
|
|
416
561
|
return target;
|
|
417
562
|
}
|
|
563
|
+
/** 只采集默认 :root,避免最后一个未启用皮肤覆盖当前默认值。 */
|
|
564
|
+
export function extractCssVariables(styles) {
|
|
565
|
+
const vars = {};
|
|
566
|
+
for (const source of styles)
|
|
567
|
+
postcss.parse(source).walkRules((rule) => {
|
|
568
|
+
if (rule.selectors.some((s) => s.trim() === ':root'))
|
|
569
|
+
rule.walkDecls((d) => {
|
|
570
|
+
if (d.prop.startsWith('--'))
|
|
571
|
+
vars[d.prop] = d.value;
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
return vars;
|
|
575
|
+
}
|
|
418
576
|
/** 解析 `<style>` 内容 → 类规则表 */
|
|
419
|
-
export function parseCss(source, warnings) {
|
|
577
|
+
export function parseCss(source, warnings, variables = {}) {
|
|
420
578
|
const classProps = new Map();
|
|
579
|
+
const styleRules = [];
|
|
421
580
|
const comboRules = [];
|
|
422
581
|
const scopedRules = [];
|
|
423
582
|
const pseudoRules = [];
|
|
@@ -429,6 +588,29 @@ export function parseCss(source, warnings) {
|
|
|
429
588
|
// R15:语法错误必须显式失败(vue2ir 转为 VueCompileError),不能产出半份样式表
|
|
430
589
|
throw new CssParseError(`CSS 解析失败:${e instanceof Error ? e.message : String(e)}`);
|
|
431
590
|
}
|
|
591
|
+
const vars = { ...variables };
|
|
592
|
+
root.walkRules((rule) => {
|
|
593
|
+
if (!rule.selectors.some((s) => s.trim() === ':root'))
|
|
594
|
+
return;
|
|
595
|
+
rule.walkDecls((d) => { if (d.prop.startsWith('--'))
|
|
596
|
+
vars[d.prop] = d.value; });
|
|
597
|
+
});
|
|
598
|
+
const resolveValue = (value) => {
|
|
599
|
+
let out = value.replace(/env\(safe-area-inset-(?:top|bottom|left|right)(?:,\s*[^)]+)?\)/g, '0px');
|
|
600
|
+
for (let n = 0; n < 12; n++) {
|
|
601
|
+
const next = out.replace(/env\(safe-area-inset-(?:top|bottom|left|right)(?:,\s*[^)]+)?\)/g, '0px').replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]+))?\)/g, (all, key, fallback) => vars[key] ?? fallback ?? all);
|
|
602
|
+
if (next === out)
|
|
603
|
+
break;
|
|
604
|
+
out = next;
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
};
|
|
608
|
+
root.walkDecls((decl) => {
|
|
609
|
+
if (!decl.prop.startsWith('--') && !['color', 'background-color', 'border-color', 'background'].includes(decl.prop))
|
|
610
|
+
decl.value = resolveValue(decl.value);
|
|
611
|
+
if (decl.prop === 'background' && /gradient\(/.test(decl.value))
|
|
612
|
+
decl.value = resolveValue(decl.value);
|
|
613
|
+
});
|
|
432
614
|
/** 规则声明块 → IR props */
|
|
433
615
|
const collectProps = (rule, warnings) => {
|
|
434
616
|
const props = {};
|
|
@@ -460,11 +642,29 @@ export function parseCss(source, warnings) {
|
|
|
460
642
|
warnings.push(`@media (prefers-color-scheme: light) 不支持(App 强制深色),已忽略:${sel}`);
|
|
461
643
|
return;
|
|
462
644
|
}
|
|
645
|
+
else if (/^\(?max-width:\s*(\d+)px\)?$/.test(params) && Number(params.match(/(\d+)px/)[1]) >= 390) {
|
|
646
|
+
// 编译基准为手机视口 390px,命中移动端覆盖样式。
|
|
647
|
+
}
|
|
463
648
|
else {
|
|
464
649
|
warnings.push(`媒体查询「${params}」不在子集内(仅 prefers-color-scheme: dark),已忽略:${sel}`);
|
|
465
650
|
return;
|
|
466
651
|
}
|
|
467
652
|
}
|
|
653
|
+
const selectors = rule.selectors.map((s) => ({ raw: s.trim(), parsed: parseSelector(s.replace(/::before\b/g, ' > .__vrnPseudoBefore').replace(/::after\b/g, ' > .__vrnPseudoAfter')) }));
|
|
654
|
+
for (const { parsed } of selectors) {
|
|
655
|
+
if (!parsed)
|
|
656
|
+
continue;
|
|
657
|
+
const raw = parsed.map((p) => p.compound).join(' ');
|
|
658
|
+
const specificity = (raw.match(/#/g)?.length ?? 0) * 10000 + (raw.match(/\./g)?.length ?? 0) * 100 +
|
|
659
|
+
parsed.filter((p) => /^[A-Za-z]/.test(p.compound)).length;
|
|
660
|
+
rule.walkDecls((decl) => {
|
|
661
|
+
// 兼容旧规则表;映射告警由 collectProps 统一收集,避免重复。
|
|
662
|
+
const props = {};
|
|
663
|
+
for (const [k, val] of mapDecl(decl.prop, decl.value, []) ?? [])
|
|
664
|
+
mergeIrProps(props, { [k]: val });
|
|
665
|
+
styleRules.push({ selector: parsed, specificity, important: decl.important === true, props });
|
|
666
|
+
});
|
|
667
|
+
}
|
|
468
668
|
// M5 伪类:.cls:active / .cls:hover(仅 background 取作按压态)
|
|
469
669
|
const pm = sel.match(/^\.([A-Za-z_][\w-]*):(active|hover)$/);
|
|
470
670
|
if (pm) {
|
|
@@ -497,7 +697,10 @@ export function parseCss(source, warnings) {
|
|
|
497
697
|
// 仅支持类选择器:.xxx 或 .a.b(多类叠加)
|
|
498
698
|
const m = sel.match(/^\.([A-Za-z_][\w-]*)(?:\.[A-Za-z_][\w-]*)*$/);
|
|
499
699
|
if (!m) {
|
|
500
|
-
|
|
700
|
+
if (selectors.every((s) => s.parsed))
|
|
701
|
+
collectProps(rule, warnings);
|
|
702
|
+
else
|
|
703
|
+
warnings.push(`选择器「${rule.selector}」含不支持的选择器,已跳过不支持部分(CSS 子集限制)`);
|
|
501
704
|
return;
|
|
502
705
|
}
|
|
503
706
|
const classes = compound(sel);
|
|
@@ -511,7 +714,7 @@ export function parseCss(source, warnings) {
|
|
|
511
714
|
comboRules.push({ classes, props });
|
|
512
715
|
}
|
|
513
716
|
});
|
|
514
|
-
return { classProps, comboRules, scopedRules, pseudoRules, warnings };
|
|
717
|
+
return { classProps, comboRules, scopedRules, pseudoRules, styleRules, warnings };
|
|
515
718
|
}
|
|
516
719
|
/** 解析内联 style="..."(分号分隔声明) */
|
|
517
720
|
export function parseInlineStyle(styleText, warnings) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type ElementNode } from '@vue/compiler-core';
|
|
2
|
+
/** SVG 动态路径、坐标、循环与文字保留为轻量绘图记录,运行时不依赖 DOM。 */
|
|
3
|
+
export declare function dynamicSvgExpression(root: ElementNode, style: (el: ElementNode, parents: ElementNode[]) => Record<string, unknown>, gradients: Record<string, string>, gradientOpacities?: Record<string, number>): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NodeTypes } from '@vue/compiler-core';
|
|
2
|
+
/** SVG 动态路径、坐标、循环与文字保留为轻量绘图记录,运行时不依赖 DOM。 */
|
|
3
|
+
export function dynamicSvgExpression(root, style, gradients, gradientOpacities = {}) {
|
|
4
|
+
const q = JSON.stringify;
|
|
5
|
+
const value = (el, key, fallback) => {
|
|
6
|
+
const dynamic = el.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === key);
|
|
7
|
+
if (dynamic && 'exp' in dynamic && dynamic.exp?.type === NodeTypes.SIMPLE_EXPRESSION)
|
|
8
|
+
return '(' + dynamic.exp.content + ')';
|
|
9
|
+
const attr = el.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === key);
|
|
10
|
+
return attr && 'value' in attr && attr.value ? q(attr.value.content) : fallback;
|
|
11
|
+
};
|
|
12
|
+
const walk = (el, parents, inherited) => {
|
|
13
|
+
const css = style(el, parents);
|
|
14
|
+
const own = { ...inherited };
|
|
15
|
+
for (const k of ['fill', 'stroke', 'stroke-width', 'opacity', 'fill-opacity', 'stroke-opacity'])
|
|
16
|
+
own[k] = value(el, k, css[k] !== undefined ? q(String(css[k])) : own[k] ?? (k === 'fill' ? q('#000000') : k === 'stroke' ? q('none') : '1'));
|
|
17
|
+
const dynamicStyle = el.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === 'style');
|
|
18
|
+
if (dynamicStyle && 'exp' in dynamicStyle && dynamicStyle.exp?.type === NodeTypes.SIMPLE_EXPRESSION)
|
|
19
|
+
for (const [key, camel] of [['fill', 'fill'], ['stroke', 'stroke'], ['opacity', 'opacity'], ['fill-opacity', 'fillOpacity'], ['stroke-opacity', 'strokeOpacity'], ['stroke-width', 'strokeWidth']])
|
|
20
|
+
own[key] = `((${dynamicStyle.exp.content})[${q(camel)}] ?? ${own[key]})`;
|
|
21
|
+
const n = (key, fallback = '0') => 'Number(' + value(el, key, fallback) + ')';
|
|
22
|
+
let path = '';
|
|
23
|
+
if (el.tag === 'path')
|
|
24
|
+
path = value(el, 'd', q(''));
|
|
25
|
+
if (el.tag === 'line')
|
|
26
|
+
path = `'M'+${n('x1')}+','+${n('y1')}+' L'+${n('x2')}+','+${n('y2')}`;
|
|
27
|
+
if (el.tag === 'circle' || el.tag === 'ellipse') {
|
|
28
|
+
const x = n('cx'), y = n('cy'), rx = n(el.tag === 'circle' ? 'r' : 'rx'), ry = n(el.tag === 'circle' ? 'r' : 'ry');
|
|
29
|
+
path = `ellipsePath(${x},${y},${rx},${ry})`;
|
|
30
|
+
}
|
|
31
|
+
if (el.tag === 'rect')
|
|
32
|
+
path = `'M'+${n('x')}+','+${n('y')}+' h'+${n('width')}+' v'+${n('height')}+' h'+(-${n('width')})+' Z'`;
|
|
33
|
+
const paint = (v) => `(function(c){if(typeof c!=='string')return 'none';return c.replace(/url\\(#([^)]*)\\)/g,function(_,id){return (${q(gradients)})[id]||'none';});})(${v})`;
|
|
34
|
+
let body = path ? `out.push({d:(${path}),fill:${paint(own.fill)},stroke:${paint(own.stroke)},width:Number(${own['stroke-width']}),opacity:Number(${own.opacity}),fillOpacity:Number(${own['fill-opacity']})*(function(c){var m=String(c).match(/url\\(#([^)]*)\\)/);return m ? (${q(gradientOpacities)})[m[1]] ?? 1 : 1;})(${own.fill}),strokeOpacity:Number(${own['stroke-opacity']})});` : '';
|
|
35
|
+
if (el.tag === 'text') {
|
|
36
|
+
const text = el.children.map((child) => child.type === NodeTypes.TEXT ? q(child.content) : child.type === NodeTypes.INTERPOLATION && child.content.type === NodeTypes.SIMPLE_EXPRESSION ? 'String(' + child.content.content + ')' : q('')).join('+') || q('');
|
|
37
|
+
body += `out.push({text:(${text}),x:${n('x')},y:${n('y')},fill:${paint(own.fill)},size:${Number(css.fontSize ?? 10)},anchor:${value(el, 'text-anchor', q('start'))}});`;
|
|
38
|
+
}
|
|
39
|
+
if (!['defs', 'linearGradient', 'radialGradient', 'stop'].includes(el.tag))
|
|
40
|
+
body += el.children.filter((c) => c.type === NodeTypes.ELEMENT).map((child) => walk(child, [...parents, el], own)).join('\n');
|
|
41
|
+
const cond = el.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'if');
|
|
42
|
+
if (cond && 'exp' in cond && cond.exp?.type === NodeTypes.SIMPLE_EXPRESSION)
|
|
43
|
+
body = `if(${cond.exp.content}){${body}}`;
|
|
44
|
+
const loop = el.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'for');
|
|
45
|
+
if (loop && 'exp' in loop && loop.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
46
|
+
const m = loop.exp.content.match(/^\s*(?:\(([^)]+)\)|([\w$]+))\s+(?:in|of)\s+([\s\S]+)$/);
|
|
47
|
+
if (m)
|
|
48
|
+
body = `(${m[3]} || []).forEach(function(${m[1] ?? m[2]}){${body}});`;
|
|
49
|
+
}
|
|
50
|
+
return body;
|
|
51
|
+
};
|
|
52
|
+
return `JSON.stringify((function(){var out=[];function ellipsePath(x,y,rx,ry){var k=.5522847498;return 'M'+(x+rx)+','+y+' C'+(x+rx)+','+(y+k*ry)+' '+(x+k*rx)+','+(y+ry)+' '+x+','+(y+ry)+' C'+(x-k*rx)+','+(y+ry)+' '+(x-rx)+','+(y+k*ry)+' '+(x-rx)+','+y+' C'+(x-rx)+','+(y-k*ry)+' '+(x-k*rx)+','+(y-ry)+' '+x+','+(y-ry)+' C'+(x+k*rx)+','+(y-ry)+' '+(x+rx)+','+(y-k*ry)+' '+(x+rx)+','+y+' Z';}${walk(root, [], {})};return out;})())`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ElementNode } from '@vue/compiler-core';
|
|
2
|
+
export interface VectorData {
|
|
3
|
+
viewBox: number[];
|
|
4
|
+
dynamic?: string;
|
|
5
|
+
colors?: string[];
|
|
6
|
+
shapes: {
|
|
7
|
+
commands: (string | number)[][];
|
|
8
|
+
fill?: string;
|
|
9
|
+
stroke?: string;
|
|
10
|
+
strokeWidth: number;
|
|
11
|
+
lineCap?: string;
|
|
12
|
+
trim?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
/** SVG 路径/圆/矩形转平台无关路径;弧线、相对坐标、平滑曲线在 IR 前归一化。 */
|
|
16
|
+
export declare function parseSvg(node: ElementNode, style: (el: ElementNode, parents: ElementNode[]) => Record<string, unknown>, warnings: string[], registerExpression?: (expression: string) => string): VectorData;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { dynamicSvgExpression } from './dynamic-svg.js';
|
|
2
|
+
import svgpath from 'svgpath';
|
|
3
|
+
import { NodeTypes } from '@vue/compiler-core';
|
|
4
|
+
/** SVG 路径/圆/矩形转平台无关路径;弧线、相对坐标、平滑曲线在 IR 前归一化。 */
|
|
5
|
+
export function parseSvg(node, style, warnings, registerExpression) {
|
|
6
|
+
const attrs = (el) => {
|
|
7
|
+
const values = Object.fromEntries(el.props.filter((p) => p.type === NodeTypes.ATTRIBUTE).map((p) => [p.name, 'value' in p ? p.value?.content ?? '' : '']));
|
|
8
|
+
for (const p of el.props)
|
|
9
|
+
if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.exp?.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
10
|
+
const value = p.exp.content.trim().replace(/^\(+|\)+$/g, '');
|
|
11
|
+
if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value))
|
|
12
|
+
values[p.arg.content] = value;
|
|
13
|
+
}
|
|
14
|
+
return values;
|
|
15
|
+
};
|
|
16
|
+
const gradientColors = {};
|
|
17
|
+
const gradientOpacities = {};
|
|
18
|
+
const findGradient = (el) => {
|
|
19
|
+
if (el.tag === 'linearGradient' || el.tag === 'radialGradient') {
|
|
20
|
+
const id = attrs(el).id;
|
|
21
|
+
const stop = el.children.find((c) => c.type === NodeTypes.ELEMENT && c.tag === 'stop');
|
|
22
|
+
if (id && stop) {
|
|
23
|
+
const dynamic = stop.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === 'style');
|
|
24
|
+
const expression = dynamic && 'exp' in dynamic && dynamic.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? dynamic.exp.content : '';
|
|
25
|
+
gradientOpacities[id] = Number(attrs(stop)['stop-opacity'] ?? expression.match(/stopOpacity\s*:\s*([\d.]+)/)?.[1] ?? 1);
|
|
26
|
+
gradientColors[id] = attrs(stop)['stop-color'] ?? expression.match(/stopColor\s*:\s*\(?['"]([^'"]+)['"]/)?.[1] ?? 'currentColor';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
el.children.forEach((c) => { if (c.type === NodeTypes.ELEMENT)
|
|
30
|
+
findGradient(c); });
|
|
31
|
+
};
|
|
32
|
+
findGradient(node);
|
|
33
|
+
const root = attrs(node);
|
|
34
|
+
const viewBox = (root.viewBox ?? `0 0 ${root.width ?? 24} ${root.height ?? 24}`).split(/[ ,]+/).map(Number);
|
|
35
|
+
const data = { viewBox: viewBox.length === 4 && viewBox.every(Number.isFinite) && viewBox[2] > 0 && viewBox[3] > 0 ? viewBox : [0, 0, 24, 24], shapes: [] };
|
|
36
|
+
const hasDynamic = (el) => el.props.some((p) => p.type === NodeTypes.DIRECTIVE && ((p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && ['d', 'viewBox'].includes(p.arg.content)) || p.name === 'for')) || el.children.some((c) => c.type === NodeTypes.ELEMENT && hasDynamic(c));
|
|
37
|
+
if (registerExpression && hasDynamic(node)) {
|
|
38
|
+
const expression = dynamicSvgExpression(node, style, gradientColors, gradientOpacities);
|
|
39
|
+
data.colors = [...new Set(expression.match(/var\(--[\w-]+\)|rgba?\([^)]+\)|#[\da-fA-F]{3,8}/g) ?? [])];
|
|
40
|
+
data.dynamic = registerExpression(expression);
|
|
41
|
+
const width = root.width ? Number(root.width) : 320, height = root.height ? Number(root.height) : 190;
|
|
42
|
+
if (!root.viewBox)
|
|
43
|
+
data.viewBox = [0, 0, width, height];
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
const walk = (el, inherited, parents) => {
|
|
47
|
+
const a = { ...inherited, ...attrs(el) };
|
|
48
|
+
for (const [k, v] of Object.entries(style(el, parents)))
|
|
49
|
+
if (['fill', 'stroke', 'stroke-width', 'stroke-linecap'].includes(k))
|
|
50
|
+
a[k] = String(v);
|
|
51
|
+
if (['defs', 'linearGradient', 'radialGradient', 'clipPath', 'mask'].includes(el.tag))
|
|
52
|
+
return;
|
|
53
|
+
let d = '';
|
|
54
|
+
const n = (key, fallback = 0) => Number(a[key] ?? fallback);
|
|
55
|
+
if (el.tag === 'path')
|
|
56
|
+
d = a.d ?? '';
|
|
57
|
+
else if (el.tag === 'circle' || el.tag === 'ellipse') {
|
|
58
|
+
const x = n('cx'), y = n('cy'), rx = n('rx', n('r')), ry = n('ry', n('r'));
|
|
59
|
+
d = `M ${x + rx} ${y} A ${rx} ${ry} 0 1 0 ${x - rx} ${y} A ${rx} ${ry} 0 1 0 ${x + rx} ${y} Z`;
|
|
60
|
+
}
|
|
61
|
+
else if (el.tag === 'line')
|
|
62
|
+
d = `M ${n('x1')} ${n('y1')} L ${n('x2')} ${n('y2')}`;
|
|
63
|
+
else if (el.tag === 'polyline' || el.tag === 'polygon')
|
|
64
|
+
d = `M ${a.points ?? ''}${el.tag === 'polygon' ? ' Z' : ''}`;
|
|
65
|
+
else if (el.tag === 'rect') {
|
|
66
|
+
const x = n('x'), y = n('y'), w = n('width'), h = n('height'), r = Math.min(n('rx'), w / 2, h / 2);
|
|
67
|
+
d = r ? `M ${x + r} ${y} H ${x + w - r} Q ${x + w} ${y} ${x + w} ${y + r} V ${y + h - r} Q ${x + w} ${y + h} ${x + w - r} ${y + h} H ${x + r} Q ${x} ${y + h} ${x} ${y + h - r} V ${y + r} Q ${x} ${y} ${x + r} ${y} Z`
|
|
68
|
+
: `M ${x} ${y} h ${w} v ${h} h ${-w} Z`;
|
|
69
|
+
}
|
|
70
|
+
if (d) {
|
|
71
|
+
const path = svgpath(d).abs().unshort().unarc();
|
|
72
|
+
if (a.transform)
|
|
73
|
+
path.transform(a.transform);
|
|
74
|
+
path.abs();
|
|
75
|
+
const commands = [];
|
|
76
|
+
path.iterate((segment) => { commands.push([...segment]); });
|
|
77
|
+
if (!commands.length)
|
|
78
|
+
warnings.push('SVG path 为空或无法解析');
|
|
79
|
+
else {
|
|
80
|
+
const color = (v) => v === 'none' ? undefined : /^url\(#/.test(v) ? gradientColors[v.slice(5, -1)] ?? 'currentColor' : v;
|
|
81
|
+
const dynamicAttr = (name) => {
|
|
82
|
+
const p = el.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.arg.content === name);
|
|
83
|
+
return p && 'exp' in p && p.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? p.exp.content : a[name];
|
|
84
|
+
};
|
|
85
|
+
const offset = dynamicAttr('stroke-dashoffset'), length = dynamicAttr('stroke-dasharray');
|
|
86
|
+
const trim = registerExpression && offset && length ? registerExpression(`String(Math.max(0,Math.min(1,1-(${offset})/(${length}))))`) : undefined;
|
|
87
|
+
data.shapes.push({ commands, trim, fill: color(a.fill ?? '#000000'), stroke: color(a.stroke ?? 'none'), strokeWidth: n('stroke-width', 1), lineCap: a['stroke-linecap'] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const child of el.children)
|
|
91
|
+
if (child.type === NodeTypes.ELEMENT)
|
|
92
|
+
walk(child, a, [...parents, el]);
|
|
93
|
+
};
|
|
94
|
+
walk(node, {}, []);
|
|
95
|
+
return data;
|
|
96
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type VectorData } from './svg.js';
|
|
2
|
+
import type { IrProps } from '../ir/schema.js';
|
|
3
|
+
export interface TabBarSpec {
|
|
4
|
+
style: IrProps;
|
|
5
|
+
itemStyle: IrProps;
|
|
6
|
+
activeStyle: IrProps;
|
|
7
|
+
items: {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon: VectorData;
|
|
11
|
+
}[];
|
|
12
|
+
widthInset: number;
|
|
13
|
+
maxWidth?: number;
|
|
14
|
+
bottom: number;
|
|
15
|
+
}
|
|
16
|
+
/** 从声明式 Vue tabbar 的静态数据、SVG 和 CSS 提取原生导航外观。 */
|
|
17
|
+
export declare function parseTabBar(source: string, globalStyles?: string[]): TabBarSpec;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parse, babelParse } from '@vue/compiler-sfc';
|
|
2
|
+
import { NodeTypes } from '@vue/compiler-core';
|
|
3
|
+
import { extractCssVariables, parseCss, resolveCssRules } from './css.js';
|
|
4
|
+
import { parseSvg } from './svg.js';
|
|
5
|
+
/** 从声明式 Vue tabbar 的静态数据、SVG 和 CSS 提取原生导航外观。 */
|
|
6
|
+
export function parseTabBar(source, globalStyles = []) {
|
|
7
|
+
const { descriptor } = parse(source);
|
|
8
|
+
const script = descriptor.scriptSetup?.content ?? '';
|
|
9
|
+
const ast = babelParse(script, { sourceType: 'module', plugins: ['typescript'] });
|
|
10
|
+
const items = [];
|
|
11
|
+
for (const declaration of ast.program.body)
|
|
12
|
+
if (declaration.type === 'VariableDeclaration') {
|
|
13
|
+
for (const d of declaration.declarations)
|
|
14
|
+
if (d.init?.type === 'ArrayExpression')
|
|
15
|
+
for (const item of d.init.elements) {
|
|
16
|
+
if (item?.type !== 'ObjectExpression')
|
|
17
|
+
continue;
|
|
18
|
+
const values = {};
|
|
19
|
+
for (const p of item.properties)
|
|
20
|
+
if (p.type === 'ObjectProperty' && !p.computed && p.value.type === 'StringLiteral') {
|
|
21
|
+
const key = p.key.type === 'Identifier' ? p.key.name : p.key.type === 'StringLiteral' ? p.key.value : '';
|
|
22
|
+
values[key] = p.value.value;
|
|
23
|
+
}
|
|
24
|
+
if (!values.key || !values.label || !values.icon)
|
|
25
|
+
continue;
|
|
26
|
+
const svg = parse(`<template><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${values.icon}</svg></template>`).descriptor.template.ast.children[0];
|
|
27
|
+
items.push({ id: values.key, label: values.label, icon: parseSvg(svg, () => ({}), []) });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!items.length)
|
|
31
|
+
throw new Error('tabBarComponent 需要包含 {key,label,icon} 字符串项的静态数组');
|
|
32
|
+
const root = descriptor.template?.ast?.children.find((n) => n.type === NodeTypes.ELEMENT);
|
|
33
|
+
const classAttr = root?.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'class');
|
|
34
|
+
const classes = classAttr && 'value' in classAttr ? classAttr.value?.content.split(/\s+/) ?? [] : [];
|
|
35
|
+
const own = descriptor.styles.map((s) => s.content).join('\n');
|
|
36
|
+
const rules = parseCss(globalStyles.join('\n') + '\n' + own, [], extractCssVariables(globalStyles)).styleRules;
|
|
37
|
+
const container = { tag: root?.tag ?? 'nav', classes };
|
|
38
|
+
const style = resolveCssRules(rules, container, [], '', []);
|
|
39
|
+
const itemStyle = resolveCssRules(rules, { tag: 'button', classes: ['tab'] }, [container], '', []);
|
|
40
|
+
const activeStyle = resolveCssRules(rules, { tag: 'button', classes: ['tab', 'on'] }, [container], '', []);
|
|
41
|
+
const width = own.match(/width:\s*min\(calc\(100%\s*-\s*([\d.]+)px\),\s*([\d.]+)px\)/);
|
|
42
|
+
const bottom = own.match(/margin:[^;]*?calc\(([\d.]+)px/);
|
|
43
|
+
return { style, itemStyle, activeStyle, items, widthInset: Number(width?.[1] ?? 0), maxWidth: width ? Number(width[2]) : undefined, bottom: Number(bottom?.[1] ?? 0) };
|
|
44
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** CSS 函数参数分割,保留 rgba()/var() 内的逗号。 */
|
|
2
|
+
export declare function splitCssArgs(value: string): string[];
|
|
3
|
+
export interface LinearGradientValue {
|
|
4
|
+
kind?: 'linear' | 'radial';
|
|
5
|
+
center?: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
angle: number;
|
|
10
|
+
stops: {
|
|
11
|
+
color: string;
|
|
12
|
+
offset: number;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export declare function parseLinearGradient(value: string): LinearGradientValue | null;
|
|
16
|
+
/** 按顶层空白拆 CSS 简写,calc()/rgba() 内的空格不拆开。 */
|
|
17
|
+
export declare function splitCssWords(value: string): string[];
|