@dongmingzhixiu/vuexir-compiler 0.3.17 → 0.3.19
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/compose.js +50 -6
- package/dist/codegen/swiftui.js +24 -2
- package/dist/transform/vue2ir.js +4 -0
- package/package.json +1 -1
package/dist/codegen/compose.js
CHANGED
|
@@ -427,13 +427,56 @@ function kotlinStr(s) {
|
|
|
427
427
|
return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
|
|
428
428
|
}
|
|
429
429
|
/** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
|
|
430
|
-
|
|
430
|
+
/** R114:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
|
|
431
|
+
function safeBind(name, ctx, loop) {
|
|
432
|
+
const base = name.split('.')[0];
|
|
433
|
+
const isState = (ctx.states ?? []).some((st) => st.name === base);
|
|
434
|
+
const isLoop = loop && (loop.itemKey === base || loop.indexKey === base);
|
|
435
|
+
const isGlobal = ['true', 'false', 'Math', 'String', 'Number'].includes(base);
|
|
436
|
+
if (isState || isLoop || isGlobal)
|
|
437
|
+
return loopField(name, loop);
|
|
438
|
+
ctx.warnings?.push?.(`模板引用「${name}」在原生端没有对应状态 → 已回退空串(R114)`);
|
|
439
|
+
return '""';
|
|
440
|
+
}
|
|
441
|
+
function kotlinCondExpr(expr, ctx, loop) {
|
|
442
|
+
// R114:条件里引用了原生端不存在的标识符 → 回退为 true(保留内容)并告警,避免整页编译失败
|
|
443
|
+
{
|
|
444
|
+
const ids = [...expr.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)/g)].map((m) => m[1]);
|
|
445
|
+
const known = new Set((ctx?.states ?? []).map((st) => st.name));
|
|
446
|
+
if (loop) {
|
|
447
|
+
known.add(loop.itemKey);
|
|
448
|
+
if (loop.indexKey)
|
|
449
|
+
known.add(loop.indexKey);
|
|
450
|
+
}
|
|
451
|
+
for (const k of ['true', 'false', 'Math', 'String', 'Number', 'stateStore'])
|
|
452
|
+
known.add(k);
|
|
453
|
+
const unknown = ids.filter((id) => !known.has(id));
|
|
454
|
+
if (unknown.length > 0 && ctx) {
|
|
455
|
+
ctx.warnings?.push?.(`条件「${expr.slice(0, 40)}」引用了原生端不存在的标识符(${unknown.join(', ')})→ 按 true 处理(R114)`);
|
|
456
|
+
return 'true';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
431
459
|
// R78 续(G12):对象字段条件 → 非空判断
|
|
432
460
|
const f = expr.match(/^__vrnField__:(\w+):(\w+)$/);
|
|
433
461
|
if (f)
|
|
434
462
|
return `vrnField(${f[1]}, "${f[2]}").isNotEmpty()`;
|
|
435
463
|
let s = expr.replace(/!==/g, '!=').replace(/===/g, '==');
|
|
436
464
|
s = s.replace(/'(\\[^]|[^'\\])*'/g, (m) => '"' + m.slice(1, -1).replace(/"/g, '\\"') + '"');
|
|
465
|
+
// R113:对象字段条件 → vrnField 取值判空
|
|
466
|
+
const negField = s.match(/^!([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
467
|
+
if (negField)
|
|
468
|
+
return `vrnField(${negField[1]}, "${negField[2]}").isEmpty()`;
|
|
469
|
+
const posField = s.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
470
|
+
if (posField)
|
|
471
|
+
return `vrnField(${posField[1]}, "${posField[2]}").isNotEmpty()`;
|
|
472
|
+
const cmpField = s.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*(==|!=)\s*(.+)$/);
|
|
473
|
+
if (cmpField)
|
|
474
|
+
return `vrnField(${cmpField[1]}, "${cmpField[2]}") ${cmpField[3]} ${cmpField[4]}`;
|
|
475
|
+
const andExpr = s.match(/^([A-Za-z_$][\w$.]*)\s*&&\s*([A-Za-z_$][\w$.]*)$/);
|
|
476
|
+
if (andExpr) {
|
|
477
|
+
const truthy = (v) => v.includes('.') ? `vrnField(${v.split('.')[0]}, "${v.split('.').slice(1).join('.')}").isNotEmpty()` : `${v}.toString().isNotEmpty()`;
|
|
478
|
+
return `${truthy(andExpr[1])} && ${truthy(andExpr[2])}`;
|
|
479
|
+
}
|
|
437
480
|
// R111:Kotlin 不接受 String/Int 当条件 → 统一判真
|
|
438
481
|
const neg = s.match(/^!([A-Za-z_$][\w$]*)(?:\.length)?$/);
|
|
439
482
|
if (neg)
|
|
@@ -446,7 +489,8 @@ function kotlinCondExpr(expr) {
|
|
|
446
489
|
}
|
|
447
490
|
/** Swift 风格字符串表达式 → Kotlin(三元 c ? a : b → if (c) a else b;引号归一双引号) */
|
|
448
491
|
function kotlinStringExpr(expr) {
|
|
449
|
-
let
|
|
492
|
+
let s0 = expr.replace(/vrnTruthy\(String\(([A-Za-z_$][\w$]*)\)\)/g, (_m, id) => `vrnTruthy(${id}.toString())`);
|
|
493
|
+
let s = s0.replace(/'(\\[^]|[^'\\])*'/g, (m) => '"' + m.slice(1, -1).replace(/"/g, '\\"') + '"');
|
|
450
494
|
const ternary = s.match(/^([^?]+)\?([^:]+):(.+)$/);
|
|
451
495
|
if (ternary) {
|
|
452
496
|
return `if (${ternary[1].trim()}) ${ternary[2].trim()} else ${ternary[3].trim()}`;
|
|
@@ -971,7 +1015,7 @@ function emitNode(node, ctx, depth, loop, useParamModifier = false, parentStretc
|
|
|
971
1015
|
const pad = ' '.repeat(depth);
|
|
972
1016
|
// v-if → Kotlin if 包裹(表达式为双端兼容子集 ==/!=/&&/||/!,=== / !== 由 kotlinCondExpr 归一)
|
|
973
1017
|
if (node.condition && node.type !== 'root') {
|
|
974
|
-
return (pad + 'if (' + kotlinCondExpr(node.condition) + ') {\n' +
|
|
1018
|
+
return (pad + 'if (' + kotlinCondExpr(node.condition, ctx, loop) + ') {\n' +
|
|
975
1019
|
emitNodeInner(node, ctx, depth + 1, loop, useParamModifier, parentStretch) +
|
|
976
1020
|
'\n' + pad + '}\n');
|
|
977
1021
|
}
|
|
@@ -1122,7 +1166,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1122
1166
|
if ('text' in seg)
|
|
1123
1167
|
return String(seg.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n');
|
|
1124
1168
|
if ('bind' in seg)
|
|
1125
|
-
return '${' +
|
|
1169
|
+
return '${' + safeBind(seg.bind, ctx, loop) + '}';
|
|
1126
1170
|
if ('fieldOf' in seg)
|
|
1127
1171
|
return ('${vrnField(' +
|
|
1128
1172
|
seg.fieldOf.obj +
|
|
@@ -1153,7 +1197,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1153
1197
|
textExpr = kotlinStringExpr(p.bindExpr);
|
|
1154
1198
|
}
|
|
1155
1199
|
else if (p.bind !== undefined) {
|
|
1156
|
-
textExpr = '"${' +
|
|
1200
|
+
textExpr = '"${' + safeBind(String(p.bind), ctx, loop) + '}"';
|
|
1157
1201
|
}
|
|
1158
1202
|
else if (p.ref !== undefined) {
|
|
1159
1203
|
textExpr = '"${' + loopField(p.ref, loop) + '}"';
|
|
@@ -1196,7 +1240,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1196
1240
|
case 'button': {
|
|
1197
1241
|
const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absModifiers(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
|
|
1198
1242
|
// R108:动态按钮文案(props.bind)优先;Kotlin 里用字符串模板插值
|
|
1199
|
-
const label = p.bind !== undefined ?
|
|
1243
|
+
const label = p.bind !== undefined ? String(p.bind) : (p.label ?? '');
|
|
1200
1244
|
const click = kotlinClickAction(p, ctx, loop);
|
|
1201
1245
|
const labelStyle = [];
|
|
1202
1246
|
if (p.fontSize !== undefined)
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -502,13 +502,21 @@ function emitNode(node, ctx) {
|
|
|
502
502
|
if (node.condition) {
|
|
503
503
|
const L = ctx.level;
|
|
504
504
|
const inner = emitNodeInner(node, ctx);
|
|
505
|
-
const
|
|
505
|
+
const known = ctx.stateNames ?? new Set();
|
|
506
|
+
const swiftCond = swiftCondExpr(node.condition, known);
|
|
506
507
|
return `${indent(L)}if ${swiftCond} {\n${inner}\n${indent(L)}}`;
|
|
507
508
|
}
|
|
508
509
|
return emitNodeInner(node, ctx);
|
|
509
510
|
}
|
|
510
511
|
/** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
|
|
511
|
-
function swiftCondExpr(expr) {
|
|
512
|
+
function swiftCondExpr(expr, known) {
|
|
513
|
+
// R114:条件引用原生端不存在的标识符 → true(保留内容),避免整页编译失败
|
|
514
|
+
if (known) {
|
|
515
|
+
const ids = [...expr.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)/g)].map((m) => m[1]);
|
|
516
|
+
const unknown = ids.filter((id) => !known.has(id) && !['true', 'false', 'Math', 'String', 'Number'].includes(id));
|
|
517
|
+
if (unknown.length > 0)
|
|
518
|
+
return 'true';
|
|
519
|
+
}
|
|
512
520
|
// R78 续(G12):对象字段条件 → 非空判断
|
|
513
521
|
const f = expr.match(/^__vrnField__:(\w+):(\w+)$/);
|
|
514
522
|
if (f)
|
|
@@ -527,6 +535,17 @@ function swiftCondExpr(expr) {
|
|
|
527
535
|
// R110:数值状态直接当条件(Swift 不接受 Int 当 Bool)
|
|
528
536
|
if (/^[A-Za-z_$][\w$]*$/.test(s))
|
|
529
537
|
return `vrnTruthy(String(${s}))`;
|
|
538
|
+
// R113:对象字段条件(form.avatarData)→ 走 vrnField 取 JSON 字段
|
|
539
|
+
const negField = s.match(/^!([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
540
|
+
if (negField)
|
|
541
|
+
return `vrnField(${negField[1]}, "${negField[2]}").isEmpty`;
|
|
542
|
+
const posField = s.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/);
|
|
543
|
+
if (posField)
|
|
544
|
+
return `!vrnField(${posField[1]}, "${posField[2]}").isEmpty`;
|
|
545
|
+
// R113:对象字段比较(form.calendar == "gregorian")→ vrnField 取值后比较
|
|
546
|
+
const cmpField = s.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*(==|!=)\s*(.+)$/);
|
|
547
|
+
if (cmpField)
|
|
548
|
+
return `vrnField(${cmpField[1]}, "${cmpField[2]}") ${cmpField[3]} ${cmpField[4]}`;
|
|
530
549
|
// R110:裸标识符条件(状态可能是字符串/对象/布尔)→ 统一走判真
|
|
531
550
|
if (/^[A-Za-z_$][\w$]*$/.test(s) && !['true', 'false'].includes(s))
|
|
532
551
|
return `vrnTruthy(String(${s}))`;
|
|
@@ -601,6 +620,7 @@ function emitNodeInner(node, ctx) {
|
|
|
601
620
|
parentAxis: 'column',
|
|
602
621
|
level: L + 1,
|
|
603
622
|
definiteHeight: true, // root 撑满全屏 → 子项建议高度确定(R-H3 可分配)
|
|
623
|
+
stateNames: ctx.stateNames,
|
|
604
624
|
warnings: ctx.warnings,
|
|
605
625
|
path: `${ctx.path}[${i}]`,
|
|
606
626
|
inheritGap: gapInheritFor(c, props.gap, node.children ?? []),
|
|
@@ -1104,6 +1124,7 @@ function navBarModifiers(screen, warnings) {
|
|
|
1104
1124
|
return mods.join('\n');
|
|
1105
1125
|
}
|
|
1106
1126
|
export function generateScreenSource(screen, opts = {}) {
|
|
1127
|
+
const __stateNames = new Set((screen.state ?? []).map((st) => st.name));
|
|
1107
1128
|
const rt = opts.runtime ?? 'Runtime.shared';
|
|
1108
1129
|
// R42:色板存在时颜色/字体走 Theme token;R43:frame 走 View 扩展短调用
|
|
1109
1130
|
currentColorNames = opts.theme?.colorNames;
|
|
@@ -1118,6 +1139,7 @@ export function generateScreenSource(screen, opts = {}) {
|
|
|
1118
1139
|
// R-PAGESCROLL:根容器始终为可滚动视口(见 emitNode root 分支),无需内容高度启发式
|
|
1119
1140
|
const dataProps = [];
|
|
1120
1141
|
const body = emitNode(screen.ui, {
|
|
1142
|
+
stateNames: __stateNames,
|
|
1121
1143
|
parentAxis: 'column',
|
|
1122
1144
|
level: 1,
|
|
1123
1145
|
definiteHeight: true, // root 撑满全屏(maxHeight:.infinity)→ 根容器高度确定
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -1413,6 +1413,10 @@ function matchFieldPath(name) {
|
|
|
1413
1413
|
}
|
|
1414
1414
|
function parseTernaryExpr(exp) {
|
|
1415
1415
|
const t = exp.trim();
|
|
1416
|
+
// R113:三元里出现对象字段访问(form.avatarData)时,静态翻译做不到 →
|
|
1417
|
+
// 直接返回 null,交给表达式下沉在 JS 运行时求值(结果一致且两端都能用)
|
|
1418
|
+
if (/[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*/.test(t))
|
|
1419
|
+
return null;
|
|
1416
1420
|
const m = t.match(/^(.+?)\s*\?\s*'([^']+)'\s*:\s*'([^']+)'$/);
|
|
1417
1421
|
if (!m)
|
|
1418
1422
|
return null;
|