@dongmingzhixiu/vuexir-compiler 0.6.0 → 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.
@@ -0,0 +1,3 @@
1
+ import type { IrProps } from '../ir/schema.js';
2
+ export declare function swiftVector(p: IrProps, color: (s: string) => string, binding?: (s: string) => string): string;
3
+ export declare function kotlinVector(p: IrProps, color: (s: string) => string, modifier: string, binding?: (s: string) => string): string;
@@ -0,0 +1,165 @@
1
+ export function swiftVector(p, color, binding = (s) => s) {
2
+ const data = p.vector;
3
+ const [x, y, w, h] = data.viewBox;
4
+ if (data.dynamic)
5
+ return swiftDynamicVector(p, color, binding);
6
+ const lines = ['Canvas { context, size in', ` context.scaleBy(x: size.width / ${w}, y: size.height / ${h})`, ` context.translateBy(x: ${-x}, y: ${-y})`];
7
+ data.shapes.forEach((shape, i) => {
8
+ lines.push(` var p${i} = Path()`);
9
+ let px = 0, py = 0;
10
+ for (const c of shape.commands) {
11
+ const n = c.slice(1);
12
+ switch (String(c[0]).toUpperCase()) {
13
+ case 'M':
14
+ lines.push(` p${i}.move(to: CGPoint(x: ${n[0]}, y: ${n[1]}))`);
15
+ px = n[0];
16
+ py = n[1];
17
+ break;
18
+ case 'L':
19
+ lines.push(` p${i}.addLine(to: CGPoint(x: ${n[0]}, y: ${n[1]}))`);
20
+ px = n[0];
21
+ py = n[1];
22
+ break;
23
+ case 'H':
24
+ px = n[0];
25
+ lines.push(` p${i}.addLine(to: CGPoint(x: ${px}, y: ${py}))`);
26
+ break;
27
+ case 'V':
28
+ py = n[0];
29
+ lines.push(` p${i}.addLine(to: CGPoint(x: ${px}, y: ${py}))`);
30
+ break;
31
+ case 'C':
32
+ lines.push(` p${i}.addCurve(to: CGPoint(x: ${n[4]}, y: ${n[5]}), control1: CGPoint(x: ${n[0]}, y: ${n[1]}), control2: CGPoint(x: ${n[2]}, y: ${n[3]}))`);
33
+ px = n[4];
34
+ py = n[5];
35
+ break;
36
+ case 'Q':
37
+ lines.push(` p${i}.addQuadCurve(to: CGPoint(x: ${n[2]}, y: ${n[3]}), control: CGPoint(x: ${n[0]}, y: ${n[1]}))`);
38
+ px = n[2];
39
+ py = n[3];
40
+ break;
41
+ case 'Z':
42
+ lines.push(` p${i}.closeSubpath()`);
43
+ break;
44
+ }
45
+ }
46
+ const c = (s) => color(s === 'currentColor' || s.startsWith('url(') ? p.color ?? '#000000' : s);
47
+ if (shape.fill)
48
+ lines.push(` context.fill(p${i}, with: .color(${c(shape.fill)}))`);
49
+ if (shape.trim)
50
+ lines.push(` p${i} = p${i}.trimmedPath(from: 0, to: max(0,min(1,Double(String(${binding(shape.trim)})) ?? 0)))`);
51
+ if (shape.stroke)
52
+ lines.push(` context.stroke(p${i}, with: .color(${c(shape.stroke)}), style: StrokeStyle(lineWidth: ${shape.strokeWidth}, lineCap: .${shape.lineCap === 'round' ? 'round' : shape.lineCap === 'square' ? 'square' : 'butt'}))`);
53
+ });
54
+ lines.push('}');
55
+ return lines.join('\n');
56
+ }
57
+ export function kotlinVector(p, color, modifier, binding = (s) => s) {
58
+ const data = p.vector, [x, y, w, h] = data.viewBox;
59
+ if (data.dynamic)
60
+ return kotlinDynamicVector(p, color, modifier, binding);
61
+ const lines = [`androidx.compose.foundation.Canvas(modifier = ${modifier}) {`, ' val canvasScaleX = size.width / ' + w + 'f', ' val canvasScaleY = size.height / ' + h + 'f',
62
+ ' withTransform({ scale(canvasScaleX, canvasScaleY, pivot = Offset.Zero); translate(' + (-x) + 'f, ' + (-y) + 'f) }) {'];
63
+ data.shapes.forEach((shape, i) => {
64
+ lines.push(` val p${i} = androidx.compose.ui.graphics.Path()`);
65
+ let px = 0, py = 0;
66
+ for (const c of shape.commands) {
67
+ const n = c.slice(1), args = n.map((v) => `${v}f`).join(', ');
68
+ switch (String(c[0]).toUpperCase()) {
69
+ case 'M':
70
+ lines.push(` p${i}.moveTo(${args})`);
71
+ px = n[0];
72
+ py = n[1];
73
+ break;
74
+ case 'L':
75
+ lines.push(` p${i}.lineTo(${args})`);
76
+ px = n[0];
77
+ py = n[1];
78
+ break;
79
+ case 'H':
80
+ px = n[0];
81
+ lines.push(` p${i}.lineTo(${px}f, ${py}f)`);
82
+ break;
83
+ case 'V':
84
+ py = n[0];
85
+ lines.push(` p${i}.lineTo(${px}f, ${py}f)`);
86
+ break;
87
+ case 'C':
88
+ lines.push(` p${i}.cubicTo(${args})`);
89
+ px = n[4];
90
+ py = n[5];
91
+ break;
92
+ case 'Q':
93
+ lines.push(` p${i}.quadraticBezierTo(${args})`);
94
+ px = n[2];
95
+ py = n[3];
96
+ break;
97
+ case 'Z':
98
+ lines.push(` p${i}.close()`);
99
+ break;
100
+ }
101
+ }
102
+ const c = (s) => color(s === 'currentColor' || s.startsWith('url(') ? p.color ?? '#000000' : s);
103
+ if (shape.fill)
104
+ lines.push(` drawPath(p${i}, ${c(shape.fill)})`);
105
+ if (shape.trim)
106
+ lines.push(` val measured${i} = androidx.compose.ui.graphics.PathMeasure().apply { setPath(p${i},false) }; val part${i}=androidx.compose.ui.graphics.Path(); measured${i}.getSegment(0f,measured${i}.length * ((${binding(shape.trim)}.toString().toFloatOrNull() ?: 0f).coerceIn(0f,1f)),part${i},true); p${i}.reset(); p${i}.addPath(part${i})`);
107
+ if (shape.stroke)
108
+ lines.push(` drawPath(p${i}, ${c(shape.stroke)}, style = androidx.compose.ui.graphics.drawscope.Stroke(width = ${shape.strokeWidth}f, cap = androidx.compose.ui.graphics.StrokeCap.${shape.lineCap === 'round' ? 'Round' : shape.lineCap === 'square' ? 'Square' : 'Butt'}))`);
109
+ });
110
+ lines.push(' }', '}');
111
+ return lines.join('\n');
112
+ }
113
+ function swiftDynamicVector(p, color, binding) {
114
+ const data = p.vector, [x, y, w, h] = data.viewBox;
115
+ const colors = (data.colors ?? []).map((c) => `case ${JSON.stringify(c)}: return ${color(c)}`).join('\n');
116
+ return `Canvas { context, size in
117
+ context.scaleBy(x: size.width / ${w}, y: size.height / ${h})
118
+ context.translateBy(x: ${-x}, y: ${-y})
119
+ func paint(_ c:String) -> Color {
120
+ switch c { ${colors}
121
+ case "none", "transparent": return .clear
122
+ default: return ${color(p.color ?? '#d9b36c')} }
123
+ }
124
+ if let records = try? JSONSerialization.jsonObject(with: Data(${binding(data.dynamic)}.utf8)) as? [[String: Any]] {
125
+ for record in records {
126
+ if let text = record["text"] as? String {
127
+ let label=Text(text).font(.system(size:record["size"] as? Double ?? 10)).foregroundColor(paint(record["fill"] as? String ?? ""))
128
+ let anchor=record["anchor"] as? String ?? "start"
129
+ context.draw(label,at:CGPoint(x:record["x"] as? Double ?? 0,y:record["y"] as? Double ?? 0),anchor:anchor == "end" ? .bottomTrailing : anchor == "middle" ? .bottom : .bottomLeading)
130
+ } else {
131
+ let path=vrnSvgPath(record["d"] as? String ?? "")
132
+ let fill=record["fill"] as? String ?? "none", stroke=record["stroke"] as? String ?? "none"
133
+ var layer=context;layer.opacity=record["opacity"] as? Double ?? 1
134
+ if fill != "none" {var f=layer;f.opacity *= record["fillOpacity"] as? Double ?? 1;f.fill(path,with:.color(paint(fill)))}
135
+ if stroke != "none" {layer.opacity *= record["strokeOpacity"] as? Double ?? 1;layer.stroke(path,with:.color(paint(stroke)),lineWidth:record["width"] as? Double ?? 1)}
136
+ }
137
+ }
138
+ }
139
+ }`;
140
+ }
141
+ function kotlinDynamicVector(p, color, modifier, binding) {
142
+ const data = p.vector, [x, y, w, h] = data.viewBox;
143
+ const colors = (data.colors ?? []).map((c) => `${JSON.stringify(c)} -> ${color(c)}`).join('\n');
144
+ return `androidx.compose.foundation.Canvas(modifier = ${modifier}) {
145
+ fun paint(c:String):Color = when(c) {${colors}
146
+ "none", "transparent" -> Color.Transparent
147
+ else -> ${color(p.color ?? '#d9b36c')}
148
+ }
149
+ val records=runCatching {org.json.JSONArray(${binding(data.dynamic)}.toString())}.getOrDefault(org.json.JSONArray())
150
+ withTransform({scale(size.width / ${w}f,size.height / ${h}f,pivot = Offset.Zero);translate(${-x}f,${-y}f)}) {
151
+ for(i in 0 until records.length()) {
152
+ val record=records.optJSONObject(i) ?: continue
153
+ if(record.has("text")) {
154
+ val ink=android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG).apply {this.color=paint(record.optString("fill")).toArgb();textSize=record.optDouble("size",10.0).toFloat();textAlign=when(record.optString("anchor")){"end" -> android.graphics.Paint.Align.RIGHT;"middle" -> android.graphics.Paint.Align.CENTER;else -> android.graphics.Paint.Align.LEFT}}
155
+ drawContext.canvas.nativeCanvas.drawText(record.optString("text"),record.optDouble("x").toFloat(),record.optDouble("y").toFloat(),ink)
156
+ } else {
157
+ val path=runCatching {androidx.core.graphics.PathParser.createPathFromPathData(record.optString("d"))?.asComposePath()}.getOrNull() ?: continue
158
+ val fill=record.optString("fill","none");val stroke=record.optString("stroke","none");val alpha=record.optDouble("opacity",1.0).toFloat().coerceIn(0f,1f)
159
+ if(fill != "none")drawPath(path,paint(fill),alpha = alpha * record.optDouble("fillOpacity",1.0).toFloat().coerceIn(0f,1f))
160
+ if(stroke != "none")drawPath(path,paint(stroke),alpha = alpha * record.optDouble("strokeOpacity",1.0).toFloat().coerceIn(0f,1f),style = androidx.compose.ui.graphics.drawscope.Stroke(width = record.optDouble("width",1.0).toFloat()))
161
+ }
162
+ }
163
+ }
164
+ }`;
165
+ }
package/dist/index.d.ts CHANGED
@@ -6,12 +6,19 @@
6
6
  * - 平台差异只体现在 codegen 目录(swiftui / compose),转换层与 IR 双端共享
7
7
  * - 工程级生成入口(runTabs / gen-* 脚本)在 @dongmingzhixiu/vuexir;本包只暴露可组合的纯函数
8
8
  */
9
- export { vueToIr, VueCompileError, extractScreenState, extractScreenConstArrays, extractScreenNavBar, extractFunctions, extractOnMounted, rewriteStateRefs, generateRuntimeSource, swiftExpr, resolveSetCall, } from './transform/vue2ir.js';
9
+ export { vueToIr, VueCompileError, extractScreenState, extractScreenConstArrays, extractScreenNavBar, extractFunctions, extractOnMounted, rewriteStateRefs, generateRuntimeSource, RUNTIME_BOOTSTRAP_SOURCE, swiftExpr, resolveSetCall, } from './transform/vue2ir.js';
10
10
  export type { Vue2IrOptions, ScreenStateInfo } from './transform/vue2ir.js';
11
- export { parseCss, parseInlineStyle, mergeIrProps, CssParseError } from './transform/css.js';
11
+ export { parseCss, extractCssVariables, parseInlineStyle, mergeIrProps, CssParseError } from './transform/css.js';
12
12
  export type { CssParseResult } from './transform/css.js';
13
13
  export { validateIr, parseAndValidateIr } from './ir/schema.js';
14
14
  export type { IrScreen, IrNode, IrProps, IrValidationIssue } from './ir/schema.js';
15
15
  export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, VRN_HELPERS_SWIFT, collectTheme, buildTheme, } from './codegen/swiftui.js';
16
16
  export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs, findProjectRoot } from './codegen/compose.js';
17
17
  export type { RunTabsOptions } from './codegen/compose.js';
18
+ export { loadProjectStyles } from './codegen/project-styles.js';
19
+ export { loadProjectAssets, materializeProjectAssets } from './codegen/project-assets.js';
20
+ export { parseTabBar } from './transform/tab-bar.js';
21
+ export type { TabBarSpec } from './transform/tab-bar.js';
22
+ export { swiftVector } from './codegen/vector.js';
23
+ export { boxModifiers as generateSwiftBoxModifiers } from './codegen/swiftui.js';
24
+ export { colorExpr as swiftColorExpression } from './codegen/swiftui.js';
package/dist/index.js CHANGED
@@ -6,9 +6,15 @@
6
6
  * - 平台差异只体现在 codegen 目录(swiftui / compose),转换层与 IR 双端共享
7
7
  * - 工程级生成入口(runTabs / gen-* 脚本)在 @dongmingzhixiu/vuexir;本包只暴露可组合的纯函数
8
8
  */
9
- export { vueToIr, VueCompileError, extractScreenState, extractScreenConstArrays, extractScreenNavBar, extractFunctions, extractOnMounted, rewriteStateRefs, generateRuntimeSource, swiftExpr, resolveSetCall, } from './transform/vue2ir.js';
10
- export { parseCss, parseInlineStyle, mergeIrProps, CssParseError } from './transform/css.js';
9
+ export { vueToIr, VueCompileError, extractScreenState, extractScreenConstArrays, extractScreenNavBar, extractFunctions, extractOnMounted, rewriteStateRefs, generateRuntimeSource, RUNTIME_BOOTSTRAP_SOURCE, swiftExpr, resolveSetCall, } from './transform/vue2ir.js';
10
+ export { parseCss, extractCssVariables, parseInlineStyle, mergeIrProps, CssParseError } from './transform/css.js';
11
11
  export { validateIr, parseAndValidateIr } from './ir/schema.js';
12
12
  export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, VRN_HELPERS_SWIFT, collectTheme, buildTheme, } from './codegen/swiftui.js';
13
13
  // 工程脚手架入口(runTabs):保留在 core 供 CLI 与旧脚本调用;模板由调用方注入(P2)
14
14
  export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs, findProjectRoot } from './codegen/compose.js';
15
+ export { loadProjectStyles } from './codegen/project-styles.js';
16
+ export { loadProjectAssets, materializeProjectAssets } from './codegen/project-assets.js';
17
+ export { parseTabBar } from './transform/tab-bar.js';
18
+ export { swiftVector } from './codegen/vector.js';
19
+ export { boxModifiers as generateSwiftBoxModifiers } from './codegen/swiftui.js';
20
+ export { colorExpr as swiftColorExpression } from './codegen/swiftui.js';
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * 预留(本阶段不实现,仅类型占位):bind / repeat / condition / events.action
10
10
  */
11
- export declare const NODE_TYPES: readonly ["root", "group", "for", "text", "image", "button", "input", "scroll", "spacer", "divider", "component"];
11
+ export declare const NODE_TYPES: readonly ["root", "group", "for", "text", "image", "vector", "button", "input", "scroll", "spacer", "divider", "component"];
12
12
  export type NodeType = (typeof NODE_TYPES)[number];
13
13
  /** 尺寸:fill=填满父容器主轴,auto=由内容决定,number=固定像素(仅限固定物) */
14
14
  export type Size = number | 'fill' | 'auto';
@@ -21,6 +21,10 @@ export type ObjectFit = 'cover' | 'contain';
21
21
  export interface IrProps {
22
22
  /** 尺寸 */
23
23
  width?: Size;
24
+ widthFraction?: number;
25
+ widthFractionDynamic?: string;
26
+ widthDynamic?: string;
27
+ heightDynamic?: string;
24
28
  height?: Size;
25
29
  /** 内边距:数字=四边相等;对象=指定边 */
26
30
  padding?: number | {
@@ -74,6 +78,7 @@ export interface IrProps {
74
78
  fallback?: string;
75
79
  };
76
80
  /** M4b 混合文本:静态段 + 插值/动态字段段 → Text("\(bind)静态\(dyn)") */
81
+ textRuns?: IrProps[];
77
82
  bindParts?: ({
78
83
  text: string;
79
84
  } | {
@@ -94,8 +99,39 @@ export interface IrProps {
94
99
  flexDirection?: FlexDirection;
95
100
  justifyContent?: Justify;
96
101
  alignItems?: Align;
102
+ alignSelf?: Align;
103
+ flexWrap?: boolean;
97
104
  /** 视觉 */
105
+ icon?: {
106
+ vector: import('../transform/svg.js').VectorData;
107
+ width: number;
108
+ height: number;
109
+ };
110
+ vector?: import('../transform/svg.js').VectorData;
98
111
  background?: string;
112
+ zIndex?: number;
113
+ minWidth?: number;
114
+ minHeight?: number;
115
+ letterSpacing?: number;
116
+ fontFamily?: 'sans' | 'serif' | 'monospace';
117
+ gradient?: {
118
+ kind?: 'linear' | 'radial';
119
+ center?: {
120
+ x: number;
121
+ y: number;
122
+ };
123
+ angle: number;
124
+ stops: {
125
+ color: string;
126
+ offset: number;
127
+ }[];
128
+ };
129
+ shadow?: {
130
+ x: number;
131
+ y: number;
132
+ blur: number;
133
+ color: string;
134
+ };
99
135
  radius?: number;
100
136
  borderWidth?: number;
101
137
  borderColor?: string;
@@ -127,10 +163,13 @@ export interface IrProps {
127
163
  fontWeight?: FontWeight;
128
164
  textAlign?: TextAlign;
129
165
  lineHeight?: number;
166
+ /** CSS 无单位行高,随当前文本字号计算,不按父字号提前固定。 */
167
+ lineHeightMultiplier?: number;
130
168
  color?: string;
131
169
  /** image 专属 */
132
170
  src?: string;
133
171
  objectFit?: ObjectFit;
172
+ aspectRatio?: number;
134
173
  /** button 专属 */
135
174
  label?: string;
136
175
  /** button 点击事件(@click 绑定的处理函数名,运行时由 JS 引擎执行) */
@@ -289,6 +328,7 @@ export interface IrScreen {
289
328
  * backText: 返回文案(默认 Back);immersive: 背景顶到状态栏(edge-to-edge)
290
329
  */
291
330
  navBar?: {
331
+ hidden?: boolean;
292
332
  mode?: 'fixed' | 'transparent' | 'scroll';
293
333
  title?: string;
294
334
  bg?: string;
package/dist/ir/schema.js CHANGED
@@ -14,6 +14,7 @@ export const NODE_TYPES = [
14
14
  'for',
15
15
  'text',
16
16
  'image',
17
+ 'vector',
17
18
  'button',
18
19
  'input',
19
20
  'scroll',
@@ -31,6 +32,24 @@ function isSize(v) {
31
32
  return v === 'fill' || v === 'auto' || (typeof v === 'number' && v >= 0 && Number.isFinite(v));
32
33
  }
33
34
  const PROPS_VALIDATORS = {
35
+ zIndex: (v) => typeof v === 'number' && Number.isFinite(v),
36
+ minWidth: (v) => typeof v === 'number' && v >= 0,
37
+ minHeight: (v) => typeof v === 'number' && v >= 0,
38
+ fontFamily: (v) => ['sans', 'serif', 'monospace'].includes(String(v)),
39
+ letterSpacing: (v) => typeof v === 'number' && Number.isFinite(v),
40
+ gradient: (v) => typeof v === 'object' && v !== null && Number.isFinite(v.angle) &&
41
+ Array.isArray(v.stops) && v.stops.length >= 2 &&
42
+ v.stops.every((s) => isColor(s.color) && Number.isFinite(s.offset)),
43
+ shadow: (v) => typeof v === 'object' && v !== null && isColor(v.color) &&
44
+ ['x', 'y', 'blur'].every((k) => Number.isFinite(v[k])) && v.blur >= 0,
45
+ icon: (v) => typeof v === 'object' && v !== null && !!v.vector,
46
+ aspectRatio: (v) => typeof v === 'number' && v > 0,
47
+ vector: (v) => typeof v === 'object' && v !== null && Array.isArray(v.viewBox) && Array.isArray(v.shapes),
48
+ widthFraction: (v) => typeof v === 'number' && v >= 0,
49
+ widthFractionDynamic: (v) => typeof v === 'string',
50
+ widthDynamic: (v) => typeof v === 'string',
51
+ heightDynamic: (v) => typeof v === 'string',
52
+ textRuns: (v) => Array.isArray(v) && v.every((r) => typeof r === 'object' && r !== null),
34
53
  width: isSize,
35
54
  height: isSize,
36
55
  padding: (v) => typeof v === 'number' && v >= 0 || (typeof v === 'object' && v !== null),
@@ -50,6 +69,8 @@ const PROPS_VALIDATORS = {
50
69
  isColor(v.color),
51
70
  flexDirection: (v) => v === 'row' || v === 'column',
52
71
  justifyContent: (v) => ['start', 'center', 'end', 'spaceBetween', 'spaceAround'].includes(v),
72
+ flexWrap: (v) => typeof v === 'boolean',
73
+ alignSelf: (v) => ['start', 'center', 'end', 'stretch'].includes(v),
53
74
  alignItems: (v) => ['start', 'center', 'end', 'stretch'].includes(v),
54
75
  background: isColor,
55
76
  radius: (v) => typeof v === 'number' && v >= 0,
@@ -76,6 +97,7 @@ const PROPS_VALIDATORS = {
76
97
  fontSize: (v) => typeof v === 'number' && v > 0,
77
98
  fontWeight: (v) => ['normal', 'medium', 'bold'].includes(v),
78
99
  textAlign: (v) => ['left', 'center', 'right'].includes(v),
100
+ lineHeightMultiplier: (v) => typeof v === 'number' && v > 0,
79
101
  lineHeight: (v) => typeof v === 'number' && v > 0,
80
102
  color: isColor,
81
103
  src: (v) => typeof v === 'string',
@@ -115,17 +137,18 @@ const PROPS_VALIDATORS = {
115
137
  };
116
138
  /** 各节点允许的 props 键(P0 最小集,防 Taro 化:未知 prop 即报错) */
117
139
  const NODE_ALLOWED_PROPS = {
118
- root: ['width', 'height', 'background'],
119
- group: ['width', 'height', 'padding', 'gap', 'flex', 'flexDirection', 'justifyContent', 'alignItems', 'background', 'radius', 'borderWidth', 'borderColor', 'marginTopAuto', 'marginTop', 'marginLeftAuto', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'gridColumns'],
120
- text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'background', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
121
- image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
122
- button: ['width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
123
- input: ['width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
124
- scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex', 'justifyContent', 'alignItems', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'borderSides', 'clip'],
125
- spacer: ['width', 'height', 'flex', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'alignSelf', 'opacity'],
126
- divider: ['width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'radius', 'flex', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
127
- component: ['name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'marginBottom', 'marginLeft', 'marginRight', 'background', 'radius', 'borderWidth', 'borderColor', 'padding', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'onClick'],
128
- for: ['flexDirection', 'gap', 'padding', 'background', 'flex', 'width', 'height', 'showsIndicators', 'borderSides', 'clip', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
140
+ root: ['minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'background'],
141
+ group: ['flexWrap', 'alignSelf', 'widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'padding', 'gap', 'flex', 'flexDirection', 'justifyContent', 'alignItems', 'background', 'radius', 'borderWidth', 'borderColor', 'marginTopAuto', 'marginTop', 'marginLeftAuto', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'gridColumns'],
142
+ text: ['alignSelf', 'widthFraction', 'widthFractionDynamic', 'textRuns', 'zIndex', 'fontFamily', 'widthDynamic', 'heightDynamic', 'letterSpacing', 'minWidth', 'minHeight', 'gradient', 'shadow', 'height', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'marginBottom', 'marginRight', 'marginLeft', 'width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'lineHeightMultiplier', 'color', 'padding', 'background', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
143
+ vector: ['aspectRatio', 'widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'vector', 'width', 'height', 'flex', 'color', 'opacity', 'padding', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'onClick'],
144
+ image: ['widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'padding', 'background', 'onClick', 'onSet', 'minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
145
+ button: ['alignSelf', 'widthFraction', 'widthFractionDynamic', 'icon', 'fontFamily', 'letterSpacing', 'zIndex', 'widthDynamic', 'heightDynamic', 'marginLeftAuto', 'minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
146
+ input: ['widthFraction', 'widthFractionDynamic', 'fontFamily', 'letterSpacing', 'zIndex', 'widthDynamic', 'heightDynamic', 'opacity', 'minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
147
+ scroll: ['widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'minWidth', 'minHeight', 'gradient', 'shadow', 'marginBottom', 'marginRight', 'marginLeft', 'marginTopAuto', 'marginLeftAuto', 'width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex', 'justifyContent', 'alignItems', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'borderSides', 'clip'],
148
+ spacer: ['borderSides', 'onSet', 'onClick', 'marginRight', 'marginBottom', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'clip', 'zIndex', 'widthFraction', 'widthFractionDynamic', 'widthDynamic', 'heightDynamic', 'minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'flex', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'marginTop', 'alignSelf', 'opacity'],
149
+ divider: ['minWidth', 'minHeight', 'gradient', 'shadow', 'width', 'height', 'background', 'borderWidth', 'borderColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'radius', 'flex', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
150
+ component: ['widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'minWidth', 'minHeight', 'gradient', 'shadow', 'name', 'attrs', 'flex', 'width', 'height', 'marginTop', 'marginTopAuto', 'marginLeftAuto', 'marginBottom', 'marginLeft', 'marginRight', 'background', 'radius', 'borderWidth', 'borderColor', 'padding', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity', 'onClick'],
151
+ for: ['widthFraction', 'widthFractionDynamic', 'zIndex', 'widthDynamic', 'heightDynamic', 'minWidth', 'minHeight', 'gradient', 'shadow', 'flexDirection', 'gap', 'padding', 'background', 'flex', 'width', 'height', 'showsIndicators', 'borderSides', 'clip', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
129
152
  };
130
153
  /** 必须有 children 的节点 */
131
154
  const CONTAINER_TYPES = ['root', 'group', 'scroll', 'for'];
@@ -255,16 +278,12 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
255
278
  // state 交叉校验:bind/value/show 必须指向页面已声明状态(M1 契约,防原生端引用未声明变量)。
256
279
  // bind 例外:for 循环项内可绑定循环变量(item/i),进入 for 子树后跳过
257
280
  if (stateNames.size > 0 && (k === 'value' || k === 'show' || (k === 'bind' && !inLoop)) && typeof v === 'string') {
258
- if (!stateNames.has(v)) {
281
+ if (!stateNames.has(v) && !v.startsWith('__vrnLoop:')) {
259
282
  issues.push({ path: p, message: `「${k}: ${v}」未在 screen.state 中声明(原生端将引用未定义变量)` });
260
283
  }
261
284
  }
262
285
  }
263
- // 尺寸约束(R16):group/text/button 未声明 flex 时若 width 为 number 且 > 设计稿 360 的一半,提示
264
- if (node.type !== 'image' && node.type !== 'spacer' && node.props.width !== undefined
265
- && typeof node.props.width === 'number' && node.props.width > 200) {
266
- issues.push({ path: `${path}.props.width`, message: `width=${node.props.width} 为绝对像素且超过 200,违反 R16 相对布局优先(应使用 fill 或按比例 flex)` });
267
- }
286
+ // CSS 真实项目允许固定宽度预览卡、圆环等;相对布局是建议,不应拒绝合法尺寸。
268
287
  }
269
288
  // 容器/叶子约束
270
289
  if (CONTAINER_TYPES.includes(node.type) && (!node.children || node.children.length === 0)) {
@@ -274,7 +293,7 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
274
293
  issues.push({ path, message: `${node.type} 为叶子节点,不允许 children` });
275
294
  }
276
295
  // 文本内容:text 节点需 props.text / props.ref(组件 prop 引用)/ props.bind(页面 state,M1)或 bind
277
- if (node.type === 'text' && !node.bind && (!node.props || (node.props.text === undefined && node.props.ref === undefined && node.props.bind === undefined && node.props.bindExpr === undefined && node.props.dynField === undefined && node.props.bindParts === undefined && node.props.fieldOf === undefined))) {
296
+ if (node.type === 'text' && !node.bind && (!node.props || (node.props.textRuns === undefined && node.props.text === undefined && node.props.ref === undefined && node.props.bind === undefined && node.props.bindExpr === undefined && node.props.dynField === undefined && node.props.bindParts === undefined && node.props.fieldOf === undefined))) {
278
297
  issues.push({ path, message: 'text 节点必须有 props.text / props.ref / props.bind / props.bindExpr / props.fieldOf 或 bind' });
279
298
  }
280
299
  // R108:按钮文案可以是静态 label,也可以是动态 bind(运行时求值)
@@ -298,10 +317,10 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
298
317
  issues.push({ path: `${path}.vfor.list`, message: 'vfor.list 元素必须为 number / string / 对象字面量' });
299
318
  }
300
319
  }
301
- else if (!vf.listState || !/^[A-Za-z_]\w*$/.test(vf.listState)) {
320
+ else if (!vf.listState || (!/^[A-Za-z_]\w*$/.test(vf.listState) && !vf.listState.startsWith('__vrnLoop:'))) {
302
321
  issues.push({ path: `${path}.vfor.listState`, message: 'vfor.listState 必填且为合法标识符(M4b 动态列表)' });
303
322
  }
304
- else if (stateNames.size > 0 && !stateNames.has(vf.listState)) {
323
+ else if (stateNames.size > 0 && !stateNames.has(vf.listState) && !vf.listState.startsWith('__vrnLoop:')) {
305
324
  issues.push({ path: `${path}.vfor.listState`, message: `listState「${vf.listState}」未在 screen.state 中声明(原生端将引用未定义变量)` });
306
325
  }
307
326
  if (!vf.itemKey || !/^[A-Za-z_]\w*$/.test(vf.itemKey)) {
@@ -1,4 +1,22 @@
1
+ export interface CssElement {
2
+ tag: string;
3
+ classes: string[];
4
+ id?: string;
5
+ }
6
+ export interface CssStyleRule {
7
+ selector: {
8
+ compound: string;
9
+ direct: boolean;
10
+ }[];
11
+ specificity: number;
12
+ important: boolean;
13
+ props: Record<string, unknown>;
14
+ }
15
+ /** ancestors 按根到父排列;回溯匹配,避免多层后代选择器被扁平化而误命中。 */
16
+ export declare function matchesCssSelector(selector: CssStyleRule['selector'], element: CssElement, ancestors: CssElement[]): boolean;
17
+ export declare function resolveCssRules(rules: CssStyleRule[], element: CssElement, ancestors: CssElement[], inline: string, warnings: string[]): Record<string, unknown>;
1
18
  export interface CssParseResult {
19
+ styleRules: CssStyleRule[];
2
20
  /** className(去点)→ IR props */
3
21
  classProps: Map<string, Record<string, unknown>>;
4
22
  /** 组合选择器规则(如 .a.b):元素同时具备全部类时生效,优先级最高 */
@@ -30,7 +48,9 @@ export declare class CssParseError extends Error {
30
48
  * 直接覆盖会让同一 class 内的多条声明互相清空。
31
49
  */
32
50
  export declare function mergeIrProps<T extends object>(target: T, src: object): T;
51
+ /** 只采集默认 :root,避免最后一个未启用皮肤覆盖当前默认值。 */
52
+ export declare function extractCssVariables(styles: string[]): Record<string, string>;
33
53
  /** 解析 `<style>` 内容 → 类规则表 */
34
- export declare function parseCss(source: string, warnings: string[]): CssParseResult;
54
+ export declare function parseCss(source: string, warnings: string[], variables?: Record<string, string>): CssParseResult;
35
55
  /** 解析内联 style="..."(分号分隔声明) */
36
56
  export declare function parseInlineStyle(styleText: string, warnings: string[]): Record<string, unknown>;