@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.
- 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/vue2ir.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { transformSync } from 'esbuild';
|
|
1
2
|
/**
|
|
2
3
|
* Vue SFC → IR 最小转换器(P1-lite)
|
|
3
4
|
*
|
|
@@ -15,10 +16,49 @@
|
|
|
15
16
|
* - 插值 {{ }}、v-for、v-if/v-show、:style/:class 绑定、组件、自定义标签
|
|
16
17
|
* - script 逻辑(事件绑定 @click 忽略 + 提示;R12 逻辑分仓 L1/L2/L3 属后续阶段)
|
|
17
18
|
*/
|
|
18
|
-
import { parse } from '@vue/compiler-sfc';
|
|
19
|
-
import { NodeTypes } from '@vue/compiler-core';
|
|
20
|
-
import { CssParseError, mergeIrProps, parseCss,
|
|
19
|
+
import { parse, babelParse } from '@vue/compiler-sfc';
|
|
20
|
+
import { NodeTypes, walkIdentifiers } from '@vue/compiler-core';
|
|
21
|
+
import { CssParseError, mergeIrProps, parseCss, extractCssVariables, resolveCssRules } from './css.js';
|
|
22
|
+
import { parseSvg } from './svg.js';
|
|
21
23
|
import { buildStoreRegistry, findStoreAliases, parseSharedConstArrays } from './stores.js';
|
|
24
|
+
// 样式上下文仅用于编译,不写入公开 IR。
|
|
25
|
+
let inlineComponentsFlag = false;
|
|
26
|
+
let hasLoopExpressions = false;
|
|
27
|
+
let liveStoreObjectsFlag = false;
|
|
28
|
+
let pageScriptBundleFlag = false;
|
|
29
|
+
let componentScopeSeq = 0;
|
|
30
|
+
let componentSlots = [];
|
|
31
|
+
let componentPath = [];
|
|
32
|
+
let templateAssets = {};
|
|
33
|
+
let styleRuleTables = new WeakMap();
|
|
34
|
+
let inheritedTextStyles = new WeakMap();
|
|
35
|
+
const TEXT_STYLE_KEYS = ['color', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'lineHeightMultiplier', 'letterSpacing', 'fontFamily'];
|
|
36
|
+
function cssElement(node) {
|
|
37
|
+
const attr = (name) => node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === name);
|
|
38
|
+
const cls = attr('class');
|
|
39
|
+
const id = attr('id');
|
|
40
|
+
return { tag: node.tag, classes: cls && 'value' in cls ? cls.value?.content.split(/\s+/).filter(Boolean) ?? [] : [],
|
|
41
|
+
id: id && 'value' in id ? id.value?.content : undefined };
|
|
42
|
+
}
|
|
43
|
+
function applyTextInheritance(node, parent = {}) {
|
|
44
|
+
const own = inheritedTextStyles.get(node) ?? {};
|
|
45
|
+
const effective = { ...parent };
|
|
46
|
+
mergeIrProps(effective, own);
|
|
47
|
+
if (node.type === 'text' || node.type === 'button' || node.type === 'input' || node.type === 'vector') {
|
|
48
|
+
node.props ??= {};
|
|
49
|
+
const allowed = node.type === 'vector' ? ['color'] : node.type === 'text' ? TEXT_STYLE_KEYS : node.type === 'button'
|
|
50
|
+
? ['color', 'fontSize', 'fontWeight'] : ['color', 'fontSize'];
|
|
51
|
+
for (const k of allowed) {
|
|
52
|
+
if ((k === 'lineHeight' && node.props.lineHeightMultiplier !== undefined) ||
|
|
53
|
+
(k === 'lineHeightMultiplier' && node.props.lineHeight !== undefined))
|
|
54
|
+
continue;
|
|
55
|
+
if ((own[k] === undefined || node.props[k] === undefined) && effective[k] !== undefined)
|
|
56
|
+
node.props[k] = effective[k];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const child of node.children ?? [])
|
|
60
|
+
applyTextInheritance(child, effective);
|
|
61
|
+
}
|
|
22
62
|
export class VueCompileError extends Error {
|
|
23
63
|
}
|
|
24
64
|
/** R90 store getter 降级告警(flattenStorePath 无 warnings 参数,先收集后并入) */
|
|
@@ -34,10 +74,39 @@ let jsExprStateNames = [];
|
|
|
34
74
|
/** 把一段 JS 表达式注册为运行时求值状态,返回状态名(模板 bind 用) */
|
|
35
75
|
function addJsExprState(expr) {
|
|
36
76
|
const name = `_vrnE${jsExprSeq++}`;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
let
|
|
40
|
-
|
|
77
|
+
const edits = [];
|
|
78
|
+
const aliases = activeLoops.flatMap((l) => [l.itemKey, ...(l.indexKey ? [l.indexKey] : [])]);
|
|
79
|
+
let scoped = false;
|
|
80
|
+
let ast;
|
|
81
|
+
try {
|
|
82
|
+
ast = babelParse(`(${expr})`, { sourceType: 'module', plugins: ['typescript'] });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
throw new VueCompileError(`模板表达式无法解析「${expr.slice(0, 320)}」:${error.message}`);
|
|
86
|
+
}
|
|
87
|
+
walkIdentifiers(ast.program, (id, parent, _stack, isReference, isLocal) => {
|
|
88
|
+
if (!isReference || isLocal)
|
|
89
|
+
return;
|
|
90
|
+
if (aliases.includes(id.name))
|
|
91
|
+
scoped = true;
|
|
92
|
+
if (!jsExprStateNames.includes(id.name))
|
|
93
|
+
return;
|
|
94
|
+
const read = `__vrnRead(${JSON.stringify(id.name)})`;
|
|
95
|
+
if (parent?.type === 'MemberExpression' && parent.object === id && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'value') {
|
|
96
|
+
edits.push({ start: parent.start - 1, end: parent.end - 1, value: read });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const shorthand = parent?.type === 'ObjectProperty' && parent.shorthand;
|
|
100
|
+
edits.push({ start: id.start - 1, end: id.end - 1, value: shorthand ? `${id.name}: ${read}` : read });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
let js = expr;
|
|
104
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
105
|
+
js = js.slice(0, edit.start) + edit.value + js.slice(edit.end);
|
|
106
|
+
if (scoped) {
|
|
107
|
+
hasLoopExpressions = true;
|
|
108
|
+
return '__vrnLoop:' + JSON.stringify({ expression: js, aliases });
|
|
109
|
+
}
|
|
41
110
|
jsExprStates.push({ name, init: '', computed: js });
|
|
42
111
|
return name;
|
|
43
112
|
}
|
|
@@ -53,7 +122,7 @@ let storeAliases = new Map();
|
|
|
53
122
|
function flattenStorePath(name) {
|
|
54
123
|
// R123:页面脚本自己的响应式绑定(soon/nearest/…)由运行时同步填充,
|
|
55
124
|
// 绝不能再被 store 折叠改写成 store_xxx(否则 v-for 会去读静态状态,永远是空)
|
|
56
|
-
if (jsExprStateNames.includes(name))
|
|
125
|
+
if (jsExprStateNames.includes(name) || (liveStoreObjectsFlag && jsExprStateNames.includes(name.split('.')[0])))
|
|
57
126
|
return name;
|
|
58
127
|
// R89:computed 折叠出的派生名(all → ui_upcoming;all.length → ui_upcoming.length)
|
|
59
128
|
if (derivedNames.size > 0) {
|
|
@@ -307,8 +376,76 @@ export function rewriteStateRefs(body, names) {
|
|
|
307
376
|
const re = new RegExp(`\\b(${names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\.value\\b`, 'g');
|
|
308
377
|
return body.replace(re, (_all, n) => `stateStore.${n}`);
|
|
309
378
|
}
|
|
379
|
+
export const RUNTIME_BOOTSTRAP_SOURCE = `
|
|
380
|
+
if (!globalThis.__vrnBootstrapReady) {
|
|
381
|
+
globalThis.__vrnBootstrapReady = true;
|
|
382
|
+
// 宿主没有浏览器 window;仅提供业务常用的全局别名与视口事件接口,不引入 DOM 渲染。
|
|
383
|
+
if (typeof window === "undefined") {
|
|
384
|
+
var window = globalThis;
|
|
385
|
+
window.innerWidth = 390;
|
|
386
|
+
window.innerHeight = 844;
|
|
387
|
+
window.location = {hash:'',href:'app://vuexir'};
|
|
388
|
+
window.history = {replaceState:function(_state,_title,url){window.location.hash=String(url);}};
|
|
389
|
+
window.addEventListener = function() {};
|
|
390
|
+
window.removeEventListener = function() {};
|
|
391
|
+
}
|
|
392
|
+
// R72 定时器宿主 shim:宿主(JavaScriptCore)未注入时用宿主桥实现,保证 Promise/await 延时可用;
|
|
393
|
+
// 浏览器预览器已有原生 setTimeout,此处不覆盖。
|
|
394
|
+
var __timerSeq = 0;
|
|
395
|
+
var __timers = {};
|
|
396
|
+
if (typeof setTimeout !== "function") {
|
|
397
|
+
var setTimeout = function (fn, ms) {
|
|
398
|
+
var id = ++__timerSeq;
|
|
399
|
+
if (typeof __nativeSetTimeout === "function") {
|
|
400
|
+
__nativeSetTimeout(id, Number(ms) || 0);
|
|
401
|
+
__timers[id] = fn;
|
|
402
|
+
return id;
|
|
403
|
+
}
|
|
404
|
+
return 0;
|
|
405
|
+
};
|
|
406
|
+
var clearTimeout = function (id) {
|
|
407
|
+
delete __timers[id];
|
|
408
|
+
if (typeof __nativeClearTimeout === "function") __nativeClearTimeout(Number(id));
|
|
409
|
+
};
|
|
410
|
+
// 宿主回调入口(__nativeSetTimeout 到期后由原生调用)
|
|
411
|
+
var __vrnRunTimer = function (id) {
|
|
412
|
+
var fn = __timers[id];
|
|
413
|
+
delete __timers[id];
|
|
414
|
+
if (typeof fn === "function") fn();
|
|
415
|
+
};
|
|
416
|
+
var setInterval = function (fn, ms) {
|
|
417
|
+
var id = ++__timerSeq;
|
|
418
|
+
__timers[id] = { fn: fn, ms: Number(ms) || 0, repeat: true };
|
|
419
|
+
if (typeof __nativeSetInterval === "function") __nativeSetInterval(id, Number(ms) || 0);
|
|
420
|
+
return id;
|
|
421
|
+
};
|
|
422
|
+
var clearInterval = function (id) {
|
|
423
|
+
delete __timers[id];
|
|
424
|
+
if (typeof __nativeClearInterval === "function") __nativeClearInterval(Number(id));
|
|
425
|
+
};
|
|
426
|
+
var __vrnRunInterval = function (id) {
|
|
427
|
+
var t = __timers[id];
|
|
428
|
+
if (t && t.repeat && typeof t.fn === "function") t.fn();
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (typeof queueMicrotask !== "function") {
|
|
432
|
+
var queueMicrotask = function (fn) {
|
|
433
|
+
Promise.resolve().then(fn);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if (typeof localStorage === "undefined") {
|
|
439
|
+
var localStorage = {
|
|
440
|
+
getItem: function(k) { return typeof __lsGet === "function" ? __lsGet("vrn." + k) : null; },
|
|
441
|
+
setItem: function(k,v) { if(typeof __lsSet === "function") __lsSet("vrn." + k,String(v)); },
|
|
442
|
+
removeItem: function(k) { if(typeof __lsSet === "function") __lsSet("vrn." + k,null); }
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
310
447
|
/** 生成 runtime.js 完整内容(M3:stateStore Proxy + handlers + onMounted + localStorage shim) */
|
|
311
|
-
export function generateRuntimeSource(states, handlers, onMounted, onMountedAsync = false) {
|
|
448
|
+
export function generateRuntimeSource(states, handlers, onMounted, onMountedAsync = false, externalScript = false) {
|
|
312
449
|
// R96 computed:JS 运行时按表达式求值并写回 stateStore(写回经 Proxy → pushState 直达原生)
|
|
313
450
|
const computedStates = states.filter((st) => st.computed);
|
|
314
451
|
const computedBlock = computedStates.length === 0
|
|
@@ -331,7 +468,7 @@ __vrnRecompute();
|
|
|
331
468
|
__vrnRecompute();
|
|
332
469
|
// R122:页面脚本(__vrnPage)的真实值必须最后写入,否则会被静态 computed 覆盖成空
|
|
333
470
|
__vrnSyncPage();
|
|
334
|
-
if (
|
|
471
|
+
if (typeof setTimeout === "function") setTimeout(__tick, 100);
|
|
335
472
|
}
|
|
336
473
|
__tick();
|
|
337
474
|
})();
|
|
@@ -352,7 +489,7 @@ __vrnRecompute();
|
|
|
352
489
|
: `"${s.init.replace(/"/g, '\\"')}"`;
|
|
353
490
|
return ` ${s.name}: ${v}`;
|
|
354
491
|
});
|
|
355
|
-
const handlerSrc = handlers
|
|
492
|
+
const handlerSrc = (externalScript ? [] : handlers)
|
|
356
493
|
.map((h) => `${h.async ? 'async ' : ''}function ${h.name}(${h.params}) {\n${indentBody(h.body)}\n${
|
|
357
494
|
// R96/R98:改完状态后重算派生值(computed / 表达式下沉),否则界面不会跟着变
|
|
358
495
|
(states.some((st) => st.computed) ? ' if (typeof __vrnRecompute === "function") __vrnRecompute();\n' : '')}}`)
|
|
@@ -367,6 +504,7 @@ Object.keys(__stateInit).forEach(function (k) { __stateStore[k] = __stateInit[k]
|
|
|
367
504
|
var __fromSwift = false;
|
|
368
505
|
var stateStore = new Proxy(__stateStore, {
|
|
369
506
|
set: function (t, k, v) {
|
|
507
|
+
if(t[k] === v) return true;
|
|
370
508
|
t[k] = v;
|
|
371
509
|
if (!__fromSwift && typeof __setState === "function") {
|
|
372
510
|
// M4b 动态列表:数组/对象 → JSON 字符串写回原生(Swift 侧解码渲染)
|
|
@@ -385,50 +523,7 @@ function fetch(url) {
|
|
|
385
523
|
});
|
|
386
524
|
});
|
|
387
525
|
}
|
|
388
|
-
|
|
389
|
-
// 浏览器预览器已有原生 setTimeout,此处不覆盖。
|
|
390
|
-
var __timerSeq = 0;
|
|
391
|
-
var __timers = {};
|
|
392
|
-
if (typeof setTimeout !== "function") {
|
|
393
|
-
var setTimeout = function (fn, ms) {
|
|
394
|
-
var id = ++__timerSeq;
|
|
395
|
-
if (typeof __nativeSetTimeout === "function") {
|
|
396
|
-
__nativeSetTimeout(id, Number(ms) || 0);
|
|
397
|
-
__timers[id] = fn;
|
|
398
|
-
return id;
|
|
399
|
-
}
|
|
400
|
-
return 0;
|
|
401
|
-
};
|
|
402
|
-
var clearTimeout = function (id) {
|
|
403
|
-
delete __timers[id];
|
|
404
|
-
if (typeof __nativeClearTimeout === "function") __nativeClearTimeout(Number(id));
|
|
405
|
-
};
|
|
406
|
-
// 宿主回调入口(__nativeSetTimeout 到期后由原生调用)
|
|
407
|
-
var __vrnRunTimer = function (id) {
|
|
408
|
-
var fn = __timers[id];
|
|
409
|
-
delete __timers[id];
|
|
410
|
-
if (typeof fn === "function") fn();
|
|
411
|
-
};
|
|
412
|
-
var setInterval = function (fn, ms) {
|
|
413
|
-
var id = ++__timerSeq;
|
|
414
|
-
__timers[id] = { fn: fn, ms: Number(ms) || 0, repeat: true };
|
|
415
|
-
if (typeof __nativeSetInterval === "function") __nativeSetInterval(id, Number(ms) || 0);
|
|
416
|
-
return id;
|
|
417
|
-
};
|
|
418
|
-
var clearInterval = function (id) {
|
|
419
|
-
delete __timers[id];
|
|
420
|
-
if (typeof __nativeClearInterval === "function") __nativeClearInterval(Number(id));
|
|
421
|
-
};
|
|
422
|
-
var __vrnRunInterval = function (id) {
|
|
423
|
-
var t = __timers[id];
|
|
424
|
-
if (t && t.repeat && typeof t.fn === "function") t.fn();
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
if (typeof queueMicrotask !== "function") {
|
|
428
|
-
var queueMicrotask = function (fn) {
|
|
429
|
-
Promise.resolve().then(fn);
|
|
430
|
-
};
|
|
431
|
-
}
|
|
526
|
+
${RUNTIME_BOOTSTRAP_SOURCE}
|
|
432
527
|
|
|
433
528
|
// R81 原生桥:bridge.toast / bridge.openUrl(iOS 由 __nativeToast/__nativeOpenUrl 宿主函数实现;
|
|
434
529
|
// Android 由编译器把 bridge.* 翻译成 VrnBridge.*;浏览器预览退回 alert/console)
|
|
@@ -478,23 +573,33 @@ var router = {
|
|
|
478
573
|
if (typeof __routerBack === "function") __routerBack();
|
|
479
574
|
}
|
|
480
575
|
};
|
|
576
|
+
// 模板求值使用真实页面 ref/computed,避免把对象 JSON 当对象、数值当字符串。
|
|
577
|
+
function __vrnWrite(name,value) {
|
|
578
|
+
var page=typeof __vrnPage !== "undefined" ? __vrnPage : undefined;
|
|
579
|
+
if(page && page[name] && typeof page[name] === "object" && "value" in page[name]) page[name].value=value;
|
|
580
|
+
stateStore[name]=value;
|
|
581
|
+
}
|
|
582
|
+
function __vrnRead(name) {
|
|
583
|
+
var page = typeof __vrnPage !== "undefined" ? __vrnPage : undefined;
|
|
584
|
+
if(page && name in page) {
|
|
585
|
+
var value=page[name];
|
|
586
|
+
return value && typeof value === "object" && "value" in value ? value.value : value;
|
|
587
|
+
}
|
|
588
|
+
return stateStore[name];
|
|
589
|
+
}
|
|
481
590
|
// R118 页面状态同步:把页面脚本 bundle(__vrnPage)里的 ref/computed 值写进 stateStore,
|
|
482
591
|
// 模板里 {{ nearest }} / {{ soon }} 这类页面级派生值就有了真实来源
|
|
483
592
|
// 把数组里的嵌套对象拍平成「点键」,让 v-for 里的 row.item.name 能直接取到
|
|
484
|
-
function __vrnFlat(v) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
return out;
|
|
494
|
-
}
|
|
495
|
-
if (Array.isArray(v)) return v.map(function (it) { return (it && typeof it === "object") ? flatObj(it, "", {}) : it; });
|
|
496
|
-
return v;
|
|
593
|
+
function __vrnFlat(v) { return v; }
|
|
594
|
+
function __vrnEvalTemplate(expression, json) {
|
|
595
|
+
try {
|
|
596
|
+
var bindings=JSON.parse(json), names=Object.keys(bindings), values=names.map(function(k){return bindings[k];});
|
|
597
|
+
var fn=Function.apply(null,names.concat('return ('+expression+');'));
|
|
598
|
+
var value=fn.apply(null,values);
|
|
599
|
+
return value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
600
|
+
} catch(e) { return ''; }
|
|
497
601
|
}
|
|
602
|
+
|
|
498
603
|
function __vrnSyncPage() {
|
|
499
604
|
try {
|
|
500
605
|
var page = typeof __vrnPage !== "undefined" ? __vrnPage : (typeof globalThis !== "undefined" ? globalThis.__vrnPage : undefined);
|
|
@@ -510,9 +615,11 @@ function __vrnSyncPage() {
|
|
|
510
615
|
var v = page[k];
|
|
511
616
|
var val;
|
|
512
617
|
if (v && typeof v === "object" && "value" in v) val = v.value; // ref / computed
|
|
618
|
+
else if (v && typeof v === "object" && Object.prototype.hasOwnProperty.call(__stateInit,k)) val=v;
|
|
513
619
|
else continue; // 函数/常量交给模板静态值
|
|
514
620
|
if (val === undefined || val === null) continue;
|
|
515
|
-
stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) :
|
|
621
|
+
stateStore[k] = (typeof val === "object") ? JSON.stringify(__vrnFlat(val)) :
|
|
622
|
+
typeof __stateInit[k] === "string" ? String(val) : val;
|
|
516
623
|
} catch (e) {}
|
|
517
624
|
}
|
|
518
625
|
} catch (e) {}
|
|
@@ -534,7 +641,7 @@ function __vrnSyncPage() {
|
|
|
534
641
|
// handlers(<script setup> 函数体:ref.value → stateStore 重写)
|
|
535
642
|
${handlerSrc}
|
|
536
643
|
${computedBlock}${"" /* R102 见 computedBlock 内:仅当存在派生状态时才调度重算循环,避免无谓的定时器 */}// onMounted(Swift onAppear → invoke("__onMounted"))
|
|
537
|
-
${onMounted !== undefined ? `${onMountedAsync ? 'async ' : ''}function __onMounted() {\n${indentBody(onMounted)}\n}` : ''}`;
|
|
644
|
+
${externalScript ? 'function __onMounted() { if(typeof __vrnSetNativeRoute === "function") __vrnSetNativeRoute(); if(typeof __vrnPage !== "undefined" && __vrnPage.__vrnMountPage) __vrnPage.__vrnMountPage(); }' : onMounted !== undefined ? `${onMountedAsync ? 'async ' : ''}function __onMounted() {\n${indentBody(onMounted)}\n}` : ''}`;
|
|
538
645
|
}
|
|
539
646
|
/** 简单缩进(handler 体统一 2 空格缩进) */
|
|
540
647
|
function indentBody(body) {
|
|
@@ -751,7 +858,21 @@ function parseDynamicStyle(exp, warnings, state) {
|
|
|
751
858
|
const key = m[1];
|
|
752
859
|
const val = m[2].trim();
|
|
753
860
|
const isState = /^[A-Za-z_]\w*$/.test(val) && state?.states.some((st) => st.name === val);
|
|
754
|
-
if (key === '
|
|
861
|
+
if (key === 'width' || key === 'height') {
|
|
862
|
+
if (/%/.test(val)) {
|
|
863
|
+
if (key === 'width')
|
|
864
|
+
out.widthFractionDynamic = addJsExprState(`String(parseFloat(${val})/100)`);
|
|
865
|
+
else
|
|
866
|
+
warnings.push(`:style ${key} 百分比高度尚未映射:${val}`);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
const literal = val.match(/^['"]([\d.]+)px['"]$/);
|
|
870
|
+
if (literal)
|
|
871
|
+
out[key] = Number(literal[1]);
|
|
872
|
+
else
|
|
873
|
+
out[key + 'Dynamic'] = addJsExprState(`String(parseFloat(${val}))`);
|
|
874
|
+
}
|
|
875
|
+
else if (key === 'color' && isState) {
|
|
755
876
|
out.colorDynamic = val;
|
|
756
877
|
}
|
|
757
878
|
else if (key === 'fontSize') {
|
|
@@ -977,6 +1098,16 @@ let compPosition = new Map();
|
|
|
977
1098
|
/** R-VFOBJ-C:script 顶层 const 数组表(const rows = [...] → rows: 数组内文);vueToIr 每次调用重置 */
|
|
978
1099
|
let vforConsts = {};
|
|
979
1100
|
export function vueToIr(source, opts = {}) {
|
|
1101
|
+
hasLoopExpressions = false;
|
|
1102
|
+
inlineComponentsFlag = opts.inlineComponents === true;
|
|
1103
|
+
liveStoreObjectsFlag = opts.liveStoreObjects === true;
|
|
1104
|
+
pageScriptBundleFlag = opts.pageScriptBundle === true;
|
|
1105
|
+
componentScopeSeq = 0;
|
|
1106
|
+
componentSlots = [];
|
|
1107
|
+
componentPath = [];
|
|
1108
|
+
templateAssets = opts.assets ?? {};
|
|
1109
|
+
styleRuleTables = new WeakMap();
|
|
1110
|
+
inheritedTextStyles = new WeakMap();
|
|
980
1111
|
compPosition = new Map();
|
|
981
1112
|
vforConsts = {};
|
|
982
1113
|
storeAliases = new Map();
|
|
@@ -984,6 +1115,7 @@ export function vueToIr(source, opts = {}) {
|
|
|
984
1115
|
storeConstArrays = {};
|
|
985
1116
|
derivedNames = new Map();
|
|
986
1117
|
storeGetterWarnings = [];
|
|
1118
|
+
jsExprStateNames = [];
|
|
987
1119
|
jsExprSeq = 0;
|
|
988
1120
|
jsExprStates = [];
|
|
989
1121
|
allowUnknownComponentsFlag = opts.allowUnknownComponents === true;
|
|
@@ -996,16 +1128,17 @@ export function vueToIr(source, opts = {}) {
|
|
|
996
1128
|
if (!descriptor.template) {
|
|
997
1129
|
throw new VueCompileError('缺少 <template> 块');
|
|
998
1130
|
}
|
|
1131
|
+
const cssVariables = extractCssVariables(opts.globalStyles ?? []);
|
|
999
1132
|
// 1) CSS 子集 → 类规则表
|
|
1000
1133
|
const cssWarnings = [];
|
|
1001
1134
|
const classProps = new Map();
|
|
1002
1135
|
let comboRules = [];
|
|
1003
1136
|
let scopedRules = [];
|
|
1004
1137
|
let pseudoRules = [];
|
|
1005
|
-
for (const styleBlock of descriptor.styles) {
|
|
1138
|
+
for (const styleBlock of [...(opts.globalStyles ?? []).map((content) => ({ content })), ...descriptor.styles]) {
|
|
1006
1139
|
let r;
|
|
1007
1140
|
try {
|
|
1008
|
-
r = parseCss(styleBlock.content, cssWarnings);
|
|
1141
|
+
r = parseCss(styleBlock.content, cssWarnings, cssVariables);
|
|
1009
1142
|
}
|
|
1010
1143
|
catch (e) {
|
|
1011
1144
|
if (e instanceof CssParseError)
|
|
@@ -1015,13 +1148,32 @@ export function vueToIr(source, opts = {}) {
|
|
|
1015
1148
|
for (const [cls, props] of r.classProps) {
|
|
1016
1149
|
classProps.set(cls, { ...(classProps.get(cls) ?? {}), ...props });
|
|
1017
1150
|
}
|
|
1151
|
+
styleRuleTables.set(classProps, [...(styleRuleTables.get(classProps) ?? []), ...r.styleRules]);
|
|
1018
1152
|
comboRules = comboRules.concat(r.comboRules);
|
|
1019
1153
|
scopedRules = scopedRules.concat(r.scopedRules);
|
|
1020
1154
|
pseudoRules = pseudoRules.concat(r.pseudoRules);
|
|
1021
1155
|
}
|
|
1022
1156
|
// 1.5) 组件表(R41 独立原生 View):components/*.vue → { name, template AST, 组件样式规则, defineProps 名 }
|
|
1023
1157
|
const compTable = [];
|
|
1024
|
-
|
|
1158
|
+
const availableComponents = opts.components ?? {};
|
|
1159
|
+
const reachable = new Set();
|
|
1160
|
+
const visitComponents = (template) => {
|
|
1161
|
+
const visit = (node) => {
|
|
1162
|
+
if (node.type === NodeTypes.ELEMENT) {
|
|
1163
|
+
const name = Object.keys(availableComponents).find((key) => key === node.tag || key === toPascalCase(node.tag));
|
|
1164
|
+
if (name && !reachable.has(name)) {
|
|
1165
|
+
reachable.add(name);
|
|
1166
|
+
const nested = parse(availableComponents[name]).descriptor.template?.ast;
|
|
1167
|
+
if (nested)
|
|
1168
|
+
visitComponents(nested);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
node.children?.forEach(visit);
|
|
1172
|
+
};
|
|
1173
|
+
visit(template);
|
|
1174
|
+
};
|
|
1175
|
+
visitComponents(descriptor.template.ast);
|
|
1176
|
+
for (const [name, src] of Object.entries(availableComponents).filter(([name]) => reachable.has(name))) {
|
|
1025
1177
|
const c = parse(src, { filename: `${name}.vue` });
|
|
1026
1178
|
if (c.errors.length > 0) {
|
|
1027
1179
|
throw new VueCompileError(`组件 ${name} 解析失败:${c.errors.map((e) => e.message).join('; ')}`);
|
|
@@ -1033,10 +1185,10 @@ export function vueToIr(source, opts = {}) {
|
|
|
1033
1185
|
let cc = [];
|
|
1034
1186
|
let cs = [];
|
|
1035
1187
|
let cps = [];
|
|
1036
|
-
for (const styleBlock of c.descriptor.styles) {
|
|
1188
|
+
for (const styleBlock of [...(opts.globalStyles ?? []).map((content) => ({ content })), ...c.descriptor.styles]) {
|
|
1037
1189
|
let r;
|
|
1038
1190
|
try {
|
|
1039
|
-
r = parseCss(styleBlock.content, cssWarnings);
|
|
1191
|
+
r = parseCss(styleBlock.content, cssWarnings, cssVariables);
|
|
1040
1192
|
}
|
|
1041
1193
|
catch (e) {
|
|
1042
1194
|
if (e instanceof CssParseError)
|
|
@@ -1045,6 +1197,7 @@ export function vueToIr(source, opts = {}) {
|
|
|
1045
1197
|
}
|
|
1046
1198
|
for (const [cls, props] of r.classProps)
|
|
1047
1199
|
cp.set(cls, { ...(cp.get(cls) ?? {}), ...props });
|
|
1200
|
+
styleRuleTables.set(cp, [...(styleRuleTables.get(cp) ?? []), ...r.styleRules]);
|
|
1048
1201
|
cc = cc.concat(r.comboRules);
|
|
1049
1202
|
cs = cs.concat(r.scopedRules);
|
|
1050
1203
|
cps = cps.concat(r.pseudoRules);
|
|
@@ -1112,6 +1265,8 @@ export function vueToIr(source, opts = {}) {
|
|
|
1112
1265
|
const registry = buildStoreRegistry(opts.storeSources);
|
|
1113
1266
|
storeAliases = findStoreAliases(src, registry);
|
|
1114
1267
|
for (const [alias, def] of storeAliases) {
|
|
1268
|
+
if (liveStoreObjectsFlag && !stateInfo.states.some((st) => st.name === alias))
|
|
1269
|
+
stateInfo.states.push({ name: alias, init: '{}', obj: true });
|
|
1115
1270
|
for (const f of def.state) {
|
|
1116
1271
|
const flat = `${alias}_${f.name}`;
|
|
1117
1272
|
if (stateInfo.states.some((st) => st.name === flat))
|
|
@@ -1243,14 +1398,15 @@ export function vueToIr(source, opts = {}) {
|
|
|
1243
1398
|
}
|
|
1244
1399
|
const hasComputed = (stateInfo?.states ?? []).some((st) => st.computed);
|
|
1245
1400
|
if (handlers.length > 0 || onMountedBody !== undefined || hasComputed) {
|
|
1246
|
-
runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers, onMountedBody, onMountedAsync);
|
|
1401
|
+
runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers, onMountedBody, onMountedAsync, opts.pageScriptBundle);
|
|
1247
1402
|
}
|
|
1248
1403
|
}
|
|
1249
1404
|
// 3.5) 组件树(R41):每个组件 → 独立 Swift View 的 IR 树(组件样式独立作用域;
|
|
1250
1405
|
// {{ prop }} → text ref;组件根布局 props(flex/width/marginTop)剥离并交使用点)。
|
|
1251
1406
|
// 必须先于页面 walk 构建:使用点 component 节点需要组件根剥离出的位置 props。
|
|
1252
|
-
const components = compTable.map((c) => {
|
|
1407
|
+
const components = (inlineComponentsFlag ? [] : compTable).map((c) => {
|
|
1253
1408
|
const tree = buildComponentTree(c, warnings);
|
|
1409
|
+
applyTextInheritance(tree.ui);
|
|
1254
1410
|
c.refs = collectRefs(tree.ui);
|
|
1255
1411
|
compPosition.set(c.name, tree.position);
|
|
1256
1412
|
return { name: c.name, ui: tree.ui };
|
|
@@ -1270,7 +1426,7 @@ export function vueToIr(source, opts = {}) {
|
|
|
1270
1426
|
if (onMountedBody !== undefined)
|
|
1271
1427
|
onMountedBody = renameSrc(onMountedBody);
|
|
1272
1428
|
}
|
|
1273
|
-
const children = ast.children
|
|
1429
|
+
const children = walkSiblings(ast.children, (c) => walk(c, classProps, comboRules, scopedRules, pseudoRules, warnings, stateInfo, compTable));
|
|
1274
1430
|
let pageRootBg;
|
|
1275
1431
|
// 3) 组装 screen:root(width fill + height 基准)包一层 group(承载根元素布局 props)
|
|
1276
1432
|
// M4b:模板根容器(.page 等)的 background 提升到 root——root ZStack 背景层必须铺满全屏,
|
|
@@ -1300,13 +1456,30 @@ export function vueToIr(source, opts = {}) {
|
|
|
1300
1456
|
const oc = n.props?.onClick;
|
|
1301
1457
|
if (typeof oc === 'string') {
|
|
1302
1458
|
const name = oc.replace(/\(\)$/, '');
|
|
1303
|
-
if (!handlerNames.has(name) && !/^(router\.|Router\.)/.test(name)) {
|
|
1459
|
+
if (!opts.pageScriptBundle && !handlerNames.has(name) && !/^(router\.|Router\.)/.test(name)) {
|
|
1304
1460
|
warnings.push(`@click「${oc}」引用的函数未提取为 runtime handler(仅支持 function 声明;箭头函数/Options API 暂不支持),原生端点击将无响应`);
|
|
1305
1461
|
}
|
|
1306
1462
|
}
|
|
1307
1463
|
for (const c of n.children ?? [])
|
|
1308
1464
|
checkClickHandlers(c);
|
|
1309
1465
|
};
|
|
1466
|
+
const documentStyles = {};
|
|
1467
|
+
const documentAncestors = [];
|
|
1468
|
+
for (const element of [{ tag: 'html', classes: [] }, { tag: 'body', classes: [] }, { tag: 'div', id: 'app', classes: [] }]) {
|
|
1469
|
+
const resolved = resolveCssRules(styleRuleTables.get(classProps) ?? [], element, documentAncestors, '', warnings);
|
|
1470
|
+
for (const k of TEXT_STYLE_KEYS)
|
|
1471
|
+
if (resolved[k] !== undefined)
|
|
1472
|
+
mergeIrProps(documentStyles, { [k]: resolved[k] });
|
|
1473
|
+
if (!ui.props?.background && !ui.props?.gradient && (resolved.background !== undefined || resolved.gradient !== undefined)) {
|
|
1474
|
+
ui.props ??= {};
|
|
1475
|
+
if (resolved.gradient)
|
|
1476
|
+
ui.props.gradient = resolved.gradient;
|
|
1477
|
+
else
|
|
1478
|
+
ui.props.background = resolved.background;
|
|
1479
|
+
}
|
|
1480
|
+
documentAncestors.push(element);
|
|
1481
|
+
}
|
|
1482
|
+
applyTextInheritance(ui, documentStyles);
|
|
1310
1483
|
checkClickHandlers(ui);
|
|
1311
1484
|
// R74 续:IR 树里的状态引用改名(bind/show/onSet/bindParts/fieldOf/condition/动态样式/输入值/列表态)
|
|
1312
1485
|
if (stateRenames.size > 0) {
|
|
@@ -1372,9 +1545,11 @@ export function vueToIr(source, opts = {}) {
|
|
|
1372
1545
|
if (!stateInfo.states.some((x) => x.name === st.name))
|
|
1373
1546
|
stateInfo.states.push({ ...st });
|
|
1374
1547
|
}
|
|
1375
|
-
runtime = generateRuntimeSource(stateInfo.states, handlers ?? [], onMountedBody, onMountedAsync);
|
|
1548
|
+
runtime = generateRuntimeSource(stateInfo.states, handlers ?? [], onMountedBody, onMountedAsync, opts.pageScriptBundle);
|
|
1376
1549
|
warnings.push(`模板表达式下沉:${jsExprStates.length} 处动态表达式改由 JS 运行时求值(R98)`);
|
|
1377
1550
|
}
|
|
1551
|
+
if (!runtime && (opts.pageScriptBundle || hasLoopExpressions))
|
|
1552
|
+
runtime = generateRuntimeSource(stateInfo?.states ?? [], handlers ?? [], onMountedBody, onMountedAsync, opts.pageScriptBundle);
|
|
1378
1553
|
for (const w of storeGetterWarnings)
|
|
1379
1554
|
if (!warnings.includes(w))
|
|
1380
1555
|
warnings.push(w);
|
|
@@ -1387,16 +1562,62 @@ export function vueToIr(source, opts = {}) {
|
|
|
1387
1562
|
height: opts.height ?? 640,
|
|
1388
1563
|
...(stateInfo && stateInfo.states.length > 0 ? { state: stateInfo.states } : {}),
|
|
1389
1564
|
...(handlers && handlers.length > 0 ? { handlers } : {}),
|
|
1390
|
-
...(onMountedBody !== undefined ? { onMounted: onMountedBody, ...(onMountedAsync ? { onMountedAsync: true } : {}) } : {}),
|
|
1565
|
+
...(opts.pageScriptBundle ? { onMounted: '__vrnPage.__vrnMountPage()' } : onMountedBody !== undefined ? { onMounted: onMountedBody, ...(onMountedAsync ? { onMountedAsync: true } : {}) } : {}),
|
|
1391
1566
|
...(navBarConfig !== null ? { navBar: navBarConfig } : {}),
|
|
1392
1567
|
ui,
|
|
1393
1568
|
},
|
|
1394
1569
|
...(components.length > 0 ? { components } : {}),
|
|
1395
|
-
warnings,
|
|
1570
|
+
warnings: [...new Set(warnings)],
|
|
1396
1571
|
...(runtime !== undefined ? { runtime } : {}),
|
|
1397
1572
|
};
|
|
1398
1573
|
}
|
|
1399
1574
|
/* ============ 模板 AST 遍历 ============ */
|
|
1575
|
+
/** 按 Vue 相邻兄弟语义编译条件链:只有前面所有分支都未命中,才检查当前分支。 */
|
|
1576
|
+
function walkSiblings(nodes, visit) {
|
|
1577
|
+
const result = [];
|
|
1578
|
+
let conditions;
|
|
1579
|
+
for (const node of nodes) {
|
|
1580
|
+
if (node.type === NodeTypes.COMMENT || (node.type === NodeTypes.TEXT && !node.content.trim())) {
|
|
1581
|
+
result.push(...visit(node));
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (node.type !== NodeTypes.ELEMENT) {
|
|
1585
|
+
conditions = undefined;
|
|
1586
|
+
result.push(...visit(node));
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
const branch = node.props.find((p) => p.type === NodeTypes.DIRECTIVE &&
|
|
1590
|
+
['if', 'else-if', 'else'].includes(p.name));
|
|
1591
|
+
if (!branch || branch.type !== NodeTypes.DIRECTIVE) {
|
|
1592
|
+
conditions = undefined;
|
|
1593
|
+
result.push(...visit(node));
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const own = branch.exp && branch.exp.type === NodeTypes.SIMPLE_EXPRESSION ? branch.exp.content.trim() : '';
|
|
1597
|
+
if (branch.name === 'if') {
|
|
1598
|
+
conditions = [own];
|
|
1599
|
+
result.push(...visit(node));
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!conditions)
|
|
1603
|
+
throw new VueCompileError(`v-${branch.name} 缺少相邻的 v-if / v-else-if`);
|
|
1604
|
+
if (branch.name === 'else-if' && !own)
|
|
1605
|
+
throw new VueCompileError('v-else-if 缺少条件表达式');
|
|
1606
|
+
const excluded = conditions.map((c) => `(${c})`).join(' || ');
|
|
1607
|
+
const expression = `!(${excluded})${own ? ` && (${own})` : ''}`;
|
|
1608
|
+
// 转成普通 v-if,让 template、组件、循环和普通元素复用同一条件处理路径。
|
|
1609
|
+
const rewritten = { ...node, props: node.props.map((p) => p === branch ? {
|
|
1610
|
+
...branch, name: 'if', exp: { type: NodeTypes.SIMPLE_EXPRESSION, content: expression,
|
|
1611
|
+
isStatic: false, constType: 0, loc: branch.loc },
|
|
1612
|
+
} : p) };
|
|
1613
|
+
result.push(...visit(rewritten));
|
|
1614
|
+
if (branch.name === 'else')
|
|
1615
|
+
conditions = undefined;
|
|
1616
|
+
else
|
|
1617
|
+
conditions.push(own);
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1620
|
+
}
|
|
1400
1621
|
function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable = [], propRefs, loopScope = [], ancestorClasses = []) {
|
|
1401
1622
|
switch (node.type) {
|
|
1402
1623
|
case NodeTypes.TEXT: {
|
|
@@ -1407,6 +1628,8 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
1407
1628
|
return [];
|
|
1408
1629
|
case NodeTypes.INTERPOLATION: {
|
|
1409
1630
|
const nameRaw = textOf(node.content).trim();
|
|
1631
|
+
if (activeLoops.length && !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(nameRaw))
|
|
1632
|
+
return [{ type: 'text', props: { bind: addJsExprState(nameRaw) } }];
|
|
1410
1633
|
// R86:store 访问先拍平(ui.dialog.title → ui_dialog.title);getter/action 返回哨兵
|
|
1411
1634
|
const name = flattenStorePath(normalizeTemplateExpr(nameRaw, warnings));
|
|
1412
1635
|
// 组件树模式(propRefs 传入=非空):{{ 任意名 }} → text ref(Swift Text(prop),
|
|
@@ -1481,6 +1704,10 @@ function walk(node, classProps, comboRules, scopedRules, pseudoRules, warnings,
|
|
|
1481
1704
|
return [{ type: 'text', props: { bind: addJsExprState(nameRaw) } }];
|
|
1482
1705
|
}
|
|
1483
1706
|
case NodeTypes.ELEMENT:
|
|
1707
|
+
if (node.tag === 'slot' && componentSlots.length) {
|
|
1708
|
+
const attr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'name');
|
|
1709
|
+
return componentSlots[componentSlots.length - 1](attr && 'value' in attr ? attr.value?.content ?? 'default' : 'default');
|
|
1710
|
+
}
|
|
1484
1711
|
return [walkElement(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)];
|
|
1485
1712
|
default:
|
|
1486
1713
|
warnings.push(`模板节点类型 ${node.type} 已忽略`);
|
|
@@ -1693,9 +1920,29 @@ function buildComponentTree(comp, warnings) {
|
|
|
1693
1920
|
}
|
|
1694
1921
|
function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable = [], propRefs, loopScope = [], ancestorClasses = []) {
|
|
1695
1922
|
let elementClasses = []; // M5 后代选择器:本元素 class 清单(子节点递归传祖先链)
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1923
|
+
if (node.tag === 'slot' && componentSlots.length) {
|
|
1924
|
+
const nameAttr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'name');
|
|
1925
|
+
const name = nameAttr && 'value' in nameAttr ? nameAttr.value?.content ?? 'default' : 'default';
|
|
1926
|
+
const kids = componentSlots[componentSlots.length - 1](name);
|
|
1927
|
+
return kids.length === 1 ? kids[0] : { type: 'group', props: { flexDirection: 'column' }, children: kids.length ? kids : [{ type: 'spacer', props: { height: 0 } }] };
|
|
1928
|
+
}
|
|
1929
|
+
if (node.tag === 'svg') {
|
|
1930
|
+
const rules = styleRuleTables.get(classProps) ?? [];
|
|
1931
|
+
const own = resolveCssRules(rules, cssElement(node), ancestorClasses, '', warnings);
|
|
1932
|
+
const attr = (name) => { const p = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === name); return p && 'value' in p ? p.value?.content : undefined; };
|
|
1933
|
+
const vector = parseSvg(node, (el, parents) => resolveCssRules(rules, cssElement(el), [...ancestorClasses, ...parents.map(cssElement)], '', warnings), warnings, addJsExprState);
|
|
1934
|
+
const props = { vector, width: own.width === 'fill' ? 'fill' : typeof own.width === 'number' ? own.width : Number(attr('width')) || 24,
|
|
1935
|
+
height: own.height === 'fill' || own.flex === 1 ? 'fill' : typeof own.height === 'number' ? own.height : Number(attr('height')) || 24 };
|
|
1936
|
+
if (vector.dynamic && own.width === 'fill' && own.height === undefined) {
|
|
1937
|
+
delete props.height;
|
|
1938
|
+
props.aspectRatio = vector.viewBox[2] / vector.viewBox[3];
|
|
1939
|
+
}
|
|
1940
|
+
for (const k of ['opacity', 'color', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform'])
|
|
1941
|
+
if (own[k] !== undefined)
|
|
1942
|
+
props[k] = own[k];
|
|
1943
|
+
const ir = { type: 'vector', props };
|
|
1944
|
+
inheritedTextStyles.set(ir, own);
|
|
1945
|
+
return ir;
|
|
1699
1946
|
}
|
|
1700
1947
|
// R104:<br> 换行 → 占位间距(原生端没有行内换行语义)
|
|
1701
1948
|
if (node.tag === 'br') {
|
|
@@ -1706,7 +1953,29 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1706
1953
|
if (forDir) {
|
|
1707
1954
|
// AttributeNode 无 exp;DirectiveNode 才有(find 命中 DIRECTIVE 但 TS 不窄化 union)
|
|
1708
1955
|
const exp = 'exp' in forDir ? forDir.exp : undefined;
|
|
1709
|
-
|
|
1956
|
+
let forExp = exp && exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : '';
|
|
1957
|
+
const tuple = forExp.match(/^\[\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*\]\s+(?:in|of)\s+([\s\S]+)$/);
|
|
1958
|
+
if (tuple) {
|
|
1959
|
+
const item = `_vrnTuple${jsExprSeq++}`;
|
|
1960
|
+
const copy = structuredClone(node);
|
|
1961
|
+
const rewrite = (n) => {
|
|
1962
|
+
if (n.type === NodeTypes.INTERPOLATION) {
|
|
1963
|
+
for (let i = 1; i <= 2; i++)
|
|
1964
|
+
n.content.content = n.content.content.replace(new RegExp(`(?<![.\\w$])${tuple[i]}\\b`, 'g'), `${item}[${i - 1}]`);
|
|
1965
|
+
}
|
|
1966
|
+
for (const p of n.props ?? [])
|
|
1967
|
+
if (p.type === NodeTypes.DIRECTIVE && p.exp && p.name !== 'for') {
|
|
1968
|
+
for (let i = 1; i <= 2; i++)
|
|
1969
|
+
p.exp.content = p.exp.content.replace(new RegExp(`(?<![.\\w$])${tuple[i]}\\b`, 'g'), `${item}[${i - 1}]`);
|
|
1970
|
+
}
|
|
1971
|
+
n.children?.forEach(rewrite);
|
|
1972
|
+
};
|
|
1973
|
+
rewrite(copy);
|
|
1974
|
+
const dir = copy.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'for');
|
|
1975
|
+
if (dir && 'exp' in dir && dir.exp && dir.exp.type === NodeTypes.SIMPLE_EXPRESSION)
|
|
1976
|
+
dir.exp.content = `${item} in ${tuple[3]}`;
|
|
1977
|
+
return walkElement(copy, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses);
|
|
1978
|
+
}
|
|
1710
1979
|
// R105:`v-for="x in stats"` 里的 stats 是 computed/普通变量(不是 ref 状态)
|
|
1711
1980
|
// → 下沉成运行时求值的 JSON 数组状态,列表照常渲染
|
|
1712
1981
|
let sunkList;
|
|
@@ -1771,7 +2040,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1771
2040
|
// 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
|
|
1772
2041
|
// R143:未声明的数据源(组件 props / 数值 / 未注册变量)→ 运行时自适应下沉,不再整页失败
|
|
1773
2042
|
if (vf.listState &&
|
|
1774
|
-
!/^_vrnE\d+$/.test(vf.listState) &&
|
|
2043
|
+
!/^_vrnE\d+$/.test(vf.listState) && !vf.listState.startsWith('__vrnLoop:') &&
|
|
1775
2044
|
!(state && state.states.some((s) => s.name === vf.listState)) &&
|
|
1776
2045
|
propRefs === undefined // 页面模式才下沉;组件模式(Set 存在,含空)保留数据源原名
|
|
1777
2046
|
) {
|
|
@@ -1794,9 +2063,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1794
2063
|
// 原生端无过渡动画/缓存语义,但内容必须保留(否则整页编译失败)
|
|
1795
2064
|
if (['Transition', 'TransitionGroup', 'KeepAlive', 'Teleport', 'Suspense', 'Fragment'].includes(node.tag)) {
|
|
1796
2065
|
const kids = [];
|
|
1797
|
-
|
|
1798
|
-
kids.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
|
|
1799
|
-
}
|
|
2066
|
+
kids.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)));
|
|
1800
2067
|
if (kids.length === 0) {
|
|
1801
2068
|
warnings.push(`<${node.tag}> 内无内容,已忽略`);
|
|
1802
2069
|
return { type: 'group', props: { flexDirection: 'column' }, children: [{ type: 'text', props: { text: ' ' } }] };
|
|
@@ -1816,18 +2083,12 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1816
2083
|
if (p.name === 'if' && p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION) {
|
|
1817
2084
|
cond = p.exp.content.trim();
|
|
1818
2085
|
}
|
|
1819
|
-
else if (p.name === 'else' || p.name === 'else-if') {
|
|
1820
|
-
// R15 不静默:v-else/v-else-if 不支持,静默渲染会恒显示该分支
|
|
1821
|
-
warnings.push(`<template v-${p.name}> 暂不支持——该分支将无条件渲染(R15 降级表)`);
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
for (const child of node.children) {
|
|
1825
|
-
children.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
|
|
1826
2086
|
}
|
|
2087
|
+
children.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses)));
|
|
1827
2088
|
if (cond) {
|
|
1828
2089
|
// 条件渲染容器:以 group 承载子节点(不参与布局,仅 if 分支)
|
|
1829
2090
|
// sourceTag='template':CSS 中 <template> 透明,父容器 gap 可透传(R-G2)
|
|
1830
|
-
return { type: 'group', props: { flexDirection: 'column' }, sourceTag: 'template', condition:
|
|
2091
|
+
return { type: 'group', props: { flexDirection: 'column' }, sourceTag: 'template', condition: `__vrnCond:${addJsExprState(`String(${cond})`)}`, children };
|
|
1831
2092
|
}
|
|
1832
2093
|
// 无 v-if:透传(合并到父 children)
|
|
1833
2094
|
// 透传多节点时由调用方展开;此处约定 template 内单根(Vue 本身要求多根可由父承载)
|
|
@@ -1837,12 +2098,122 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1837
2098
|
throw new VueCompileError('<template> 无内容');
|
|
1838
2099
|
return { type: 'group', props: { flexDirection: 'column' }, children };
|
|
1839
2100
|
}
|
|
1840
|
-
|
|
2101
|
+
let mapping = TAG_MAP[node.tag];
|
|
2102
|
+
if (inlineComponentsFlag && node.tag === 'button' && node.children.some((c) => c.type === NodeTypes.ELEMENT && c.tag !== 'svg' && c.tag !== 'br'))
|
|
2103
|
+
mapping = { type: 'group' };
|
|
1841
2104
|
if (!mapping) {
|
|
1842
2105
|
// R41 组件使用点:查组件表(精确名 + kebab→Pascal 兜底)→ component IR 节点(独立 View 引用)
|
|
1843
2106
|
const comp = compTable.find((c) => c.name === node.tag) ??
|
|
1844
2107
|
compTable.find((c) => c.name === toPascalCase(node.tag));
|
|
1845
2108
|
if (comp) {
|
|
2109
|
+
if (inlineComponentsFlag) {
|
|
2110
|
+
if (componentPath.includes(comp.name))
|
|
2111
|
+
throw new VueCompileError(`组件递归引用无法静态展开:${[...componentPath, comp.name].join(' → ')}`);
|
|
2112
|
+
const bindings = {};
|
|
2113
|
+
const script = comp.descriptor.scriptSetup?.content ?? '';
|
|
2114
|
+
const defaults = script.match(/defineProps[\s\S]*?>\s*\(\s*\)\s*,\s*\{([\s\S]*?)\}/)?.[1] ?? '';
|
|
2115
|
+
for (const pair of splitTopLevel(defaults)) {
|
|
2116
|
+
const m = pair.match(/^\s*(\w+)\s*:\s*([\s\S]+)$/);
|
|
2117
|
+
if (m)
|
|
2118
|
+
bindings[m[1]] = m[2].trim();
|
|
2119
|
+
}
|
|
2120
|
+
for (const name of comp.propNames)
|
|
2121
|
+
bindings[name] ??= "''";
|
|
2122
|
+
for (const p of node.props) {
|
|
2123
|
+
if (p.type === NodeTypes.ATTRIBUTE && comp.propNames.has(p.name))
|
|
2124
|
+
bindings[p.name] = p.value ? JSON.stringify(p.value.content) : 'true';
|
|
2125
|
+
if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind' && p.arg?.type === NodeTypes.SIMPLE_EXPRESSION && p.exp?.type === NodeTypes.SIMPLE_EXPRESSION)
|
|
2126
|
+
bindings[p.arg.content] = p.exp.content;
|
|
2127
|
+
}
|
|
2128
|
+
const prefix = `_vrnC${componentScopeSeq++}_`;
|
|
2129
|
+
for (const st of extractScreenState(script).states) {
|
|
2130
|
+
const name = prefix + st.name;
|
|
2131
|
+
bindings[st.name] = name;
|
|
2132
|
+
state?.states.push({ ...st, name });
|
|
2133
|
+
jsExprStateNames.push(name);
|
|
2134
|
+
}
|
|
2135
|
+
const rewrite = (expression) => {
|
|
2136
|
+
const edits = [];
|
|
2137
|
+
let ast;
|
|
2138
|
+
try {
|
|
2139
|
+
ast = babelParse(`(${expression})`, { sourceType: 'module', plugins: ['typescript'] });
|
|
2140
|
+
}
|
|
2141
|
+
catch (error) {
|
|
2142
|
+
throw new VueCompileError(`组件 ${comp.name} 表达式解析失败:${expression.slice(0, 500)};${error.message}`);
|
|
2143
|
+
}
|
|
2144
|
+
walkIdentifiers(ast.program, (id, parent, _stack, isReference, isLocal) => {
|
|
2145
|
+
if (!isReference || isLocal)
|
|
2146
|
+
return;
|
|
2147
|
+
if (id.name === 'props' && parent?.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && bindings[parent.property.name] !== undefined) {
|
|
2148
|
+
edits.push({ start: parent.start - 1, end: parent.end - 1, value: `(${bindings[parent.property.name]})` });
|
|
2149
|
+
}
|
|
2150
|
+
else if (bindings[id.name] !== undefined) {
|
|
2151
|
+
const member = parent?.type === 'MemberExpression' && parent.object === id && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'value' ? parent : id;
|
|
2152
|
+
edits.push({ start: member.start - 1, end: member.end - 1, value: parent?.type === 'ObjectProperty' && parent.shorthand ? `${id.name}: (${bindings[id.name]})` : `(${bindings[id.name]})` });
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
let out = expression;
|
|
2156
|
+
for (const e of edits.sort((a, b) => b.start - a.start))
|
|
2157
|
+
out = out.slice(0, e.start) + e.value + out.slice(e.end);
|
|
2158
|
+
return out;
|
|
2159
|
+
};
|
|
2160
|
+
const scriptAst = babelParse(script, { sourceType: 'module', plugins: ['typescript'] });
|
|
2161
|
+
const asJs = (value) => transformSync('(' + value + ')', { loader: 'ts', target: 'es2019' }).code.trim().replace(/;$/, '');
|
|
2162
|
+
for (const declaration of scriptAst.program.body) {
|
|
2163
|
+
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
|
|
2164
|
+
bindings[declaration.id.name] = rewrite(asJs(script.slice(declaration.start, declaration.end)));
|
|
2165
|
+
}
|
|
2166
|
+
if (declaration.type !== 'VariableDeclaration')
|
|
2167
|
+
continue;
|
|
2168
|
+
for (const d of declaration.declarations) {
|
|
2169
|
+
if (d.id.type !== 'Identifier' || !d.init || bindings[d.id.name] !== undefined)
|
|
2170
|
+
continue;
|
|
2171
|
+
if (['NumericLiteral', 'StringLiteral', 'BooleanLiteral', 'ObjectExpression', 'ArrayExpression'].includes(d.init.type))
|
|
2172
|
+
bindings[d.id.name] = rewrite(asJs(script.slice(d.init.start, d.init.end)));
|
|
2173
|
+
else if (d.init.type === 'CallExpression' && d.init.callee.type === 'Identifier' && d.init.callee.name === 'computed') {
|
|
2174
|
+
const arg = d.init.arguments[0];
|
|
2175
|
+
if (arg?.type === 'ArrowFunctionExpression')
|
|
2176
|
+
bindings[d.id.name] = rewrite('(' + asJs(script.slice(arg.start, arg.end)) + ')()');
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
for (const key of Object.keys(bindings))
|
|
2181
|
+
bindings[key] = rewrite(bindings[key]);
|
|
2182
|
+
const roots = structuredClone(comp.descriptor.template.ast.children);
|
|
2183
|
+
const patch = (n) => {
|
|
2184
|
+
if (n.type === NodeTypes.INTERPOLATION)
|
|
2185
|
+
n.content.content = rewrite(n.content.content);
|
|
2186
|
+
for (const p of n.props ?? [])
|
|
2187
|
+
if (p.type === NodeTypes.DIRECTIVE && p.exp) {
|
|
2188
|
+
const event = p.exp.content.match(/^emit\(\s*['"]([^'"]+)['"]\s*\)$/);
|
|
2189
|
+
const handler = event ? node.props.find((d) => d.type === NodeTypes.DIRECTIVE && d.name === 'on' && d.arg?.type === NodeTypes.SIMPLE_EXPRESSION && d.arg.content === event[1]) : undefined;
|
|
2190
|
+
p.exp.content = handler && 'exp' in handler && handler.exp?.type === NodeTypes.SIMPLE_EXPRESSION ? handler.exp.content : rewrite(p.exp.content);
|
|
2191
|
+
}
|
|
2192
|
+
n.children?.forEach(patch);
|
|
2193
|
+
};
|
|
2194
|
+
roots.forEach(patch);
|
|
2195
|
+
componentSlots.push((name) => {
|
|
2196
|
+
const children = node.children.flatMap((child) => {
|
|
2197
|
+
if (child.type === NodeTypes.ELEMENT && child.tag === 'template') {
|
|
2198
|
+
const slot = child.props.find((p) => p.type === NodeTypes.DIRECTIVE && p.name === 'slot');
|
|
2199
|
+
if (slot && slot.type === NodeTypes.DIRECTIVE && slot.arg?.type === NodeTypes.SIMPLE_EXPRESSION && slot.arg.content === name)
|
|
2200
|
+
return child.children;
|
|
2201
|
+
return [];
|
|
2202
|
+
}
|
|
2203
|
+
return name === 'default' ? [child] : [];
|
|
2204
|
+
});
|
|
2205
|
+
return walkSiblings(children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, ancestorClasses));
|
|
2206
|
+
});
|
|
2207
|
+
componentPath.push(comp.name);
|
|
2208
|
+
try {
|
|
2209
|
+
const kids = walkSiblings(roots, (child) => walk(child, comp.classProps, comp.comboRules, comp.scopedRules, comp.pseudoRules, warnings, state, compTable, undefined, loopScope, ancestorClasses));
|
|
2210
|
+
return kids.length === 1 ? kids[0] : { type: 'group', props: { flexDirection: 'column' }, children: kids };
|
|
2211
|
+
}
|
|
2212
|
+
finally {
|
|
2213
|
+
componentPath.pop();
|
|
2214
|
+
componentSlots.pop();
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
1846
2217
|
const pos = compPosition.get(comp.name) ?? {};
|
|
1847
2218
|
return componentUseNode(node, comp, pos, warnings);
|
|
1848
2219
|
}
|
|
@@ -1856,6 +2227,10 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1856
2227
|
let nodeType;
|
|
1857
2228
|
// 指令检测:v-for / v-if / v-show / :style / :class / 事件绑定 → 结构或逻辑绑定
|
|
1858
2229
|
const props = { ...(mapping.defaultProps ?? {}) };
|
|
2230
|
+
const inlineAttr = node.props.find((p) => p.type === NodeTypes.ATTRIBUTE && p.name === 'style');
|
|
2231
|
+
const inlineStyle = inlineAttr && 'value' in inlineAttr ? inlineAttr.value?.content ?? '' : '';
|
|
2232
|
+
const resolvedStyle = resolveCssRules(styleRuleTables.get(classProps) ?? [], cssElement(node), ancestorClasses, inlineStyle, warnings);
|
|
2233
|
+
mergeIrProps(props, resolvedStyle);
|
|
1859
2234
|
let cond;
|
|
1860
2235
|
let activeWhen;
|
|
1861
2236
|
let activeProps;
|
|
@@ -1875,7 +2250,9 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1875
2250
|
if (name === 'show') {
|
|
1876
2251
|
// M5:v-show → show prop(始终渲染占位 + opacity 动画切换;区别于 v-if 条件渲染)
|
|
1877
2252
|
if (p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION && p.exp.content.trim()) {
|
|
1878
|
-
|
|
2253
|
+
const raw = p.exp.content.trim();
|
|
2254
|
+
const flat = flattenStorePath(raw);
|
|
2255
|
+
props.show = /^[A-Za-z_$][\w$]*$/.test(flat) && state?.states.some((s) => s.name === flat) ? flat : addJsExprState(`String(Boolean(${raw}))`);
|
|
1879
2256
|
continue;
|
|
1880
2257
|
}
|
|
1881
2258
|
throw new VueCompileError('v-show 需要非空条件表达式');
|
|
@@ -1905,7 +2282,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1905
2282
|
}
|
|
1906
2283
|
}
|
|
1907
2284
|
else if (mapping.type === 'image' && argName === 'src') {
|
|
1908
|
-
|
|
2285
|
+
const source = p.exp && p.exp.type === NodeTypes.SIMPLE_EXPRESSION ? p.exp.content.trim() : '';
|
|
2286
|
+
if (templateAssets[source])
|
|
2287
|
+
props.src = templateAssets[source];
|
|
2288
|
+
else
|
|
2289
|
+
warnings.push(`:src ${source} 尚无原生图片映射,需提供 assets 或静态 src`);
|
|
1909
2290
|
}
|
|
1910
2291
|
else {
|
|
1911
2292
|
warnings.push(`:${argName} 动态绑定暂不支持,已忽略`);
|
|
@@ -1935,7 +2316,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1935
2316
|
// @click="handler" → 状态赋值(setter 内联)优先;否则保留 onClick(JS 运行时调用)
|
|
1936
2317
|
// M3 规范化:无参调用 `inc()` → `inc`(Swift invoke 统一 evaluateScript("<名>()"));带参保留原样
|
|
1937
2318
|
if (event === 'click' && handler) {
|
|
1938
|
-
const set = state && resolveSetCall(handler, state, (m) => warnings.push(m));
|
|
2319
|
+
const set = !pageScriptBundleFlag && state && resolveSetCall(handler, state, (m) => warnings.push(m));
|
|
1939
2320
|
if (set) {
|
|
1940
2321
|
Object.assign(props, { onSet: set });
|
|
1941
2322
|
}
|
|
@@ -1955,41 +2336,15 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1955
2336
|
const attrValue = p.value?.content;
|
|
1956
2337
|
if (attrName === 'class' && attrValue) {
|
|
1957
2338
|
elementClasses = attrValue.split(/\s+/).filter(Boolean);
|
|
1958
|
-
// 组合规则匹配:元素同时具备全部类时覆盖(优先级最高)
|
|
1959
|
-
const matchedCombos = comboRules.filter((r) => r.classes.every((c) => elementClasses.includes(c)));
|
|
1960
|
-
const comboMembers = new Set(matchedCombos.flatMap((r) => r.classes));
|
|
1961
|
-
for (const cls of elementClasses) {
|
|
1962
|
-
const p2 = classProps.get(cls);
|
|
1963
|
-
if (p2)
|
|
1964
|
-
mergeIrProps(props, p2);
|
|
1965
|
-
else if (!comboMembers.has(cls)) {
|
|
1966
|
-
warnings.push(`class「${cls}」无对应样式规则,已忽略`);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
for (const rule of matchedCombos)
|
|
1970
|
-
mergeIrProps(props, rule.props);
|
|
1971
|
-
// M5 后代/子选择器:目标类命中 + 祖先链匹配(direct:直接父含最近祖先类;后代:祖先链上存在)
|
|
1972
|
-
const matchedScoped = scopedRules.filter((r) => {
|
|
1973
|
-
if (!elementClasses.includes(r.target))
|
|
1974
|
-
return false;
|
|
1975
|
-
if (r.direct) {
|
|
1976
|
-
const parentCls = (ancestorClasses[0] ?? '').split(/\s+/).filter(Boolean);
|
|
1977
|
-
return r.ancestors.every((a) => parentCls.includes(a));
|
|
1978
|
-
}
|
|
1979
|
-
const chain = ancestorClasses.join(' ');
|
|
1980
|
-
return r.ancestors.every((a) => chain.split(/\s+/).includes(a));
|
|
1981
|
-
});
|
|
1982
|
-
for (const rule of matchedScoped)
|
|
1983
|
-
mergeIrProps(props, rule.props);
|
|
1984
2339
|
// M5 伪类按压态:.btn:active → pressBg(button 生成按压样式;:hover 在 iOS 无意义,仅取 active)
|
|
1985
2340
|
for (const cls of elementClasses) {
|
|
1986
2341
|
const pa = pseudoRules.find((r) => r.cls === cls && r.pseudo === 'active');
|
|
1987
|
-
if (pa)
|
|
2342
|
+
if (pa && mapping.type === 'button')
|
|
1988
2343
|
mergeIrProps(props, pa.props);
|
|
1989
2344
|
}
|
|
1990
2345
|
}
|
|
1991
2346
|
else if (attrName === 'style' && attrValue) {
|
|
1992
|
-
|
|
2347
|
+
// 已按 CSS 级联优先级统一处理,避免属性顺序影响样式。
|
|
1993
2348
|
}
|
|
1994
2349
|
else if (mapping.type === 'image' && attrName === 'src' && attrValue) {
|
|
1995
2350
|
props.src = attrValue;
|
|
@@ -2004,15 +2359,22 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2004
2359
|
warnings.push(`属性 ${attrName}="${attrValue}" 暂不支持,已忽略`);
|
|
2005
2360
|
}
|
|
2006
2361
|
}
|
|
2362
|
+
const inlineTags = new Set(['span', 'b', 'strong', 'i', 'em', 'small', 'u', 'code']);
|
|
2363
|
+
const content = node.children.filter((c) => c.type !== NodeTypes.COMMENT && !(c.type === NodeTypes.TEXT && !c.content.trim()));
|
|
2364
|
+
const readable = (n) => n.type === NodeTypes.INTERPOLATION || (n.type === NodeTypes.TEXT && !!n.content.trim()) || (n.type === NodeTypes.ELEMENT && n.children.some(readable));
|
|
2365
|
+
if (inlineComponentsFlag && mapping.type === 'group' && props.flexDirection === undefined && props.gridColumns === undefined && content.some(readable) && content.every((c) => c.type === NodeTypes.TEXT || c.type === NodeTypes.INTERPOLATION || (c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))))
|
|
2366
|
+
mapping = { type: 'text' };
|
|
2367
|
+
if (mapping.type === 'text' && content.length === 0 && ['i', 'span', 'b'].includes(node.tag))
|
|
2368
|
+
mapping = { type: 'spacer' };
|
|
2007
2369
|
// R108:button 只接受 label/bind —— 布局属性丢弃、缺 label 时占位(避免整页失败)
|
|
2008
2370
|
if (mapping.type === 'button') {
|
|
2009
|
-
const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'overflow', 'clip'];
|
|
2371
|
+
const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'lineHeightMultiplier', 'overflow', 'clip'];
|
|
2010
2372
|
for (const k of badProps)
|
|
2011
2373
|
delete props[k];
|
|
2012
2374
|
}
|
|
2013
2375
|
if (mapping.type === 'input') {
|
|
2014
2376
|
// R108:input 只接受 value/placeholder 等,文本样式属性一律丢弃
|
|
2015
|
-
for (const k of ['textAlign', 'lineHeight', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
|
|
2377
|
+
for (const k of ['textAlign', 'lineHeight', 'lineHeightMultiplier', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
|
|
2016
2378
|
delete props[k];
|
|
2017
2379
|
}
|
|
2018
2380
|
}
|
|
@@ -2022,8 +2384,39 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2022
2384
|
// 组件 View 内 {{ prop }}(propRefs)→ 单插值时可整体作为 text ref(Text(prop))
|
|
2023
2385
|
// - 其余(group/image):递归 walk
|
|
2024
2386
|
const children = [];
|
|
2025
|
-
|
|
2026
|
-
|
|
2387
|
+
if (mapping.type === 'text' && node.children.some((c) => c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))) {
|
|
2388
|
+
const runs = [];
|
|
2389
|
+
for (const child of node.children) {
|
|
2390
|
+
if (child.type === NodeTypes.COMMENT)
|
|
2391
|
+
continue;
|
|
2392
|
+
if (child.type === NodeTypes.TEXT) {
|
|
2393
|
+
runs.push({ text: child.content });
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (child.type === NodeTypes.ELEMENT && child.tag === 'br') {
|
|
2397
|
+
runs.push({ text: '\n' });
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const nodes = walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, cssElement(node)]);
|
|
2401
|
+
for (const n of nodes) {
|
|
2402
|
+
if (n.type !== 'text') {
|
|
2403
|
+
warnings.push(`行内元素 ${node.tag} 的非文本子节点需额外布局,已保留可读文字`);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const run = { ...n.props };
|
|
2407
|
+
if (inheritedTextStyles.get(n)?.fontSize === undefined && n.props?.fontSize === 14)
|
|
2408
|
+
delete run.fontSize;
|
|
2409
|
+
if (n.bind)
|
|
2410
|
+
run.bind = n.bind.key;
|
|
2411
|
+
runs.push(run);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
if (runs.length)
|
|
2415
|
+
props.textRuns = runs;
|
|
2416
|
+
else
|
|
2417
|
+
props.text = '';
|
|
2418
|
+
}
|
|
2419
|
+
else if (mapping.type === 'text') {
|
|
2027
2420
|
let text = '';
|
|
2028
2421
|
let refOnly;
|
|
2029
2422
|
let exprOnly;
|
|
@@ -2035,6 +2428,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2035
2428
|
parts.push({ text: child.content });
|
|
2036
2429
|
}
|
|
2037
2430
|
else if (child.type === NodeTypes.INTERPOLATION) {
|
|
2431
|
+
const rawExpression = textOf(child.content).trim();
|
|
2432
|
+
if (activeLoops.length && !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(rawExpression)) {
|
|
2433
|
+
parts.push({ bind: addJsExprState(rawExpression) });
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2038
2436
|
const name = flattenStorePath(normalizeTemplateExpr(textOf(child.content).trim(), warnings));
|
|
2039
2437
|
// 动态列表项属性 {{ item.field }}:基名(dot[0])命中循环变量才成立
|
|
2040
2438
|
const dot = name.split('.');
|
|
@@ -2177,6 +2575,14 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2177
2575
|
else {
|
|
2178
2576
|
for (const child of node.children) {
|
|
2179
2577
|
if (mapping.type === 'button') {
|
|
2578
|
+
if (child.type === NodeTypes.ELEMENT && child.tag === 'svg') {
|
|
2579
|
+
const rules = styleRuleTables.get(classProps) ?? [];
|
|
2580
|
+
const parents = [...ancestorClasses, cssElement(node)];
|
|
2581
|
+
const style = resolveCssRules(rules, cssElement(child), parents, '', warnings);
|
|
2582
|
+
props.icon = { vector: parseSvg(child, (el, path) => resolveCssRules(rules, cssElement(el), [...parents, ...path.map(cssElement)], '', warnings), warnings, addJsExprState),
|
|
2583
|
+
width: typeof style.width === 'number' ? style.width : 20, height: typeof style.height === 'number' ? style.height : 20 };
|
|
2584
|
+
continue;
|
|
2585
|
+
}
|
|
2180
2586
|
if (child.type === NodeTypes.ELEMENT && (child.tag === 'br' || !elementInnerText(child).trim()))
|
|
2181
2587
|
continue; // R106/R107:按钮内换行/纯图标忽略
|
|
2182
2588
|
if (child.type === NodeTypes.TEXT && child.content.trim()) {
|
|
@@ -2226,56 +2632,40 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2226
2632
|
}
|
|
2227
2633
|
continue;
|
|
2228
2634
|
}
|
|
2229
|
-
// R128:v-else / v-else-if 与前一兄弟互补(此前无条件渲染,空状态块常显)
|
|
2230
|
-
const __kids = walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [
|
|
2231
|
-
...ancestorClasses,
|
|
2232
|
-
...elementClasses,
|
|
2233
|
-
]);
|
|
2234
|
-
if (child.type === NodeTypes.ELEMENT) {
|
|
2235
|
-
const dirs = child.props.filter((p2) => p2.type === NodeTypes.DIRECTIVE);
|
|
2236
|
-
const isElse = dirs.some((d) => d.name === 'else');
|
|
2237
|
-
const elseIf = dirs.find((d) => d.name === 'else-if');
|
|
2238
|
-
if (isElse || elseIf) {
|
|
2239
|
-
const prev = prevCondExpr;
|
|
2240
|
-
const own = elseIf?.exp?.content?.trim();
|
|
2241
|
-
if (prev) {
|
|
2242
|
-
const condExpr = own ? `(${prev}) && (${own})` : `!(${prev})`;
|
|
2243
|
-
const stName = addJsExprState(`String(${condExpr})`);
|
|
2244
|
-
for (const k of __kids)
|
|
2245
|
-
k.condition = `__vrnCond:${stName}`;
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
const ifDir = dirs.find((d) => d.name === 'if');
|
|
2249
|
-
if (ifDir?.exp?.content)
|
|
2250
|
-
prevCondExpr = ifDir.exp.content.trim();
|
|
2251
|
-
}
|
|
2252
|
-
children.push(...__kids);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
// R108:文本节点丢弃布局类属性(原生端 width/height/flexDirection 只对容器有意义)
|
|
2256
|
-
if (mapping.type === 'text') {
|
|
2257
|
-
const LAYOUT_ONLY = [
|
|
2258
|
-
'height', 'width', 'flexDirection', 'justifyContent', 'alignItems', 'flex', 'gap',
|
|
2259
|
-
'clip', 'overflow', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides',
|
|
2260
|
-
'radius', 'opacity', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom',
|
|
2261
|
-
];
|
|
2262
|
-
const dropped = LAYOUT_ONLY.filter((k) => props[k] !== undefined);
|
|
2263
|
-
if (dropped.length > 0) {
|
|
2264
|
-
for (const k of dropped)
|
|
2265
|
-
delete props[k];
|
|
2266
|
-
warnings.push(`<${node.tag}> 是文本节点,布局属性(${dropped.join('/')})已忽略(R108)`);
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
// R88 续:容器节点不接受文本专属样式(color/fontSize/fontWeight/textAlign/lineHeight)
|
|
2270
|
-
// 原生端容器没有「字体继承」,这类属性会直接触发 IR 校验拒收 → 明确降级 + 警告
|
|
2271
|
-
if (mapping.type === 'group' || mapping.type === 'for') {
|
|
2272
|
-
const TEXT_ONLY = ['color', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight'];
|
|
2273
|
-
const dropped = TEXT_ONLY.filter((k) => props[k] !== undefined);
|
|
2274
|
-
if (dropped.length > 0) {
|
|
2275
|
-
for (const k of dropped)
|
|
2276
|
-
delete props[k];
|
|
2277
|
-
warnings.push(`<${node.tag}> 是容器,文本样式(${dropped.join('/')})在原生端不继承 → 已忽略;请把样式下沉到具体文本节点(R88)`);
|
|
2278
2635
|
}
|
|
2636
|
+
if (mapping.type !== 'button') {
|
|
2637
|
+
children.push(...walkSiblings(node.children, (child) => walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, cssElement(node)])));
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
// 静态装饰伪元素生成原生盒子,保持相对于所属元素的层级。
|
|
2641
|
+
if (mapping.type === 'group' && children.length) {
|
|
2642
|
+
const rules = styleRuleTables.get(classProps) ?? [];
|
|
2643
|
+
for (const marker of ['__vrnPseudoBefore', '__vrnPseudoAfter']) {
|
|
2644
|
+
const decorated = rules.filter((rule) => rule.selector.at(-1)?.compound === '.' + marker);
|
|
2645
|
+
if (!decorated.length)
|
|
2646
|
+
continue;
|
|
2647
|
+
const pseudo = resolveCssRules(decorated, { tag: 'span', classes: [marker] }, [...ancestorClasses, cssElement(node)], '', warnings);
|
|
2648
|
+
if (!Object.keys(pseudo).length)
|
|
2649
|
+
continue;
|
|
2650
|
+
const layer = { type: 'spacer', props: pseudo };
|
|
2651
|
+
const foreground = children.findIndex((child) => (child.props?.zIndex ?? 0) > 0);
|
|
2652
|
+
children.splice(foreground >= 0 ? foreground : marker === '__vrnPseudoBefore' ? 0 : children.length, 0, layer);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
// 文本节点保留已有双端映射的盒模型属性,只删除容器布局中转字段。
|
|
2656
|
+
if (mapping.type === 'text' || mapping.type === 'spacer') {
|
|
2657
|
+
for (const k of ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'overflow', 'gridColumns'])
|
|
2658
|
+
delete props[k];
|
|
2659
|
+
}
|
|
2660
|
+
const typography = {};
|
|
2661
|
+
for (const k of TEXT_STYLE_KEYS)
|
|
2662
|
+
if (props[k] !== undefined)
|
|
2663
|
+
typography[k] = props[k];
|
|
2664
|
+
if (['p', 'span', 'b', 'strong', 'i', 'em', 'u', 'option'].includes(node.tag) && resolvedStyle.fontSize === undefined)
|
|
2665
|
+
delete typography.fontSize;
|
|
2666
|
+
if (mapping.type === 'group' || mapping.type === 'for' || mapping.type === 'spacer') {
|
|
2667
|
+
for (const k of TEXT_STYLE_KEYS)
|
|
2668
|
+
delete props[k];
|
|
2279
2669
|
}
|
|
2280
2670
|
// R108 兜底:按钮最终必须有可见文案(label 或 bind),否则整页校验失败
|
|
2281
2671
|
if (mapping.type === 'button' && props.label === undefined && props.bind === undefined) {
|
|
@@ -2287,13 +2677,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2287
2677
|
warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
|
|
2288
2678
|
const keep = {};
|
|
2289
2679
|
const src = props;
|
|
2290
|
-
for (const k of ['width', 'height', 'flex', 'marginTop', 'background']) {
|
|
2680
|
+
for (const k of ['width', 'height', 'widthFraction', 'widthFractionDynamic', 'flex', 'marginTop', 'background', 'gradient', 'radius', 'shadow', 'borderWidth', 'borderColor', 'padding', 'minHeight', 'minWidth', 'opacity']) {
|
|
2291
2681
|
if (src[k] !== undefined)
|
|
2292
2682
|
keep[k] = src[k];
|
|
2293
2683
|
}
|
|
2294
2684
|
const spacer = { type: 'spacer', props: keep };
|
|
2295
2685
|
if (cond)
|
|
2296
|
-
spacer.condition = normCond(cond);
|
|
2686
|
+
spacer.condition = pageScriptBundleFlag ? `__vrnCond:${addJsExprState(`String(Boolean(${cond}))`)}` : normCond(cond);
|
|
2297
2687
|
return spacer;
|
|
2298
2688
|
}
|
|
2299
2689
|
// overflow:auto → scroll 节点(消费中转标记,避免进入 schema 校验白名单)
|
|
@@ -2303,6 +2693,7 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2303
2693
|
delete propsAny.overflow;
|
|
2304
2694
|
}
|
|
2305
2695
|
const irNode = { type: (nodeType ?? mapping.type), props };
|
|
2696
|
+
inheritedTextStyles.set(irNode, typography);
|
|
2306
2697
|
if (children.length > 0)
|
|
2307
2698
|
irNode.children = children;
|
|
2308
2699
|
if (cond) {
|
|
@@ -2317,14 +2708,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2317
2708
|
/^![A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)?$/.test(nc) ||
|
|
2318
2709
|
/^[A-Za-z_$][\w$]*(\.length)?\s*(==|!=|>|<|>=|<=)\s*('.*'|".*"|-?\d+)$/.test(nc);
|
|
2319
2710
|
const usesLoopVar = activeLoops.some((l) => new RegExp(`\\b${l.itemKey}\\b`).test(nc));
|
|
2320
|
-
if (!simpleCond
|
|
2321
|
-
const stName = addJsExprState(`String(${
|
|
2711
|
+
if (pageScriptBundleFlag || !simpleCond || usesLoopVar) {
|
|
2712
|
+
const stName = addJsExprState(`String(Boolean(${cond}))`);
|
|
2322
2713
|
irNode.condition = `__vrnCond:${stName}`;
|
|
2323
2714
|
}
|
|
2324
2715
|
else {
|
|
2325
2716
|
irNode.condition = ownerKnown && mCond ? `__vrnField__:${mCond[1]}:${mCond[2]}` : nc;
|
|
2326
2717
|
}
|
|
2327
2718
|
}
|
|
2719
|
+
if (activeWhen && activeLoops.length)
|
|
2720
|
+
activeWhen = '__vrnCond:' + addJsExprState(`String(Boolean(${activeWhen}))`);
|
|
2328
2721
|
if (activeWhen && activeProps) {
|
|
2329
2722
|
irNode.active = { when: activeWhen, props: activeProps };
|
|
2330
2723
|
// active 的 color 注入所有后代 text(选中态金色覆盖子元素显式色)
|