@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
|
@@ -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';
|
package/dist/ir/schema.d.ts
CHANGED
|
@@ -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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
//
|
|
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)) {
|
package/dist/transform/css.d.ts
CHANGED
|
@@ -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>;
|