@dongmingzhixiu/vuexir-compiler 0.3.11 → 0.3.13

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.
@@ -1176,7 +1176,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1176
1176
  }
1177
1177
  case 'button': {
1178
1178
  const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absModifiers(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
1179
- const label = p.label ?? '';
1179
+ // R108:动态按钮文案(props.bind)优先;Kotlin 里用字符串模板插值
1180
+ const label = p.bind !== undefined ? '"${' + String(p.bind) + '}"' : (p.label ?? '');
1180
1181
  const click = kotlinClickAction(p, ctx, loop);
1181
1182
  const labelStyle = [];
1182
1183
  if (p.fontSize !== undefined)
@@ -824,7 +824,12 @@ function emitNodeInner(node, ctx) {
824
824
  return line;
825
825
  }
826
826
  case 'button': {
827
- const label = props.label !== undefined ? props.label : '按钮';
827
+ // R108:动态按钮文案(props.bind → @State)优先于静态 label
828
+ const label = props.bind !== undefined
829
+ ? props.bind
830
+ : props.label !== undefined
831
+ ? props.label
832
+ : '按钮';
828
833
  const fs = props.fontSize ?? 15;
829
834
  // 事件:onSet(状态直接赋值)优先,其次 onClick(JS 运行时),无事件则空 action
830
835
  // M1:onSet.kind==='expr' → 表达式原样嵌入(count + 1);literal → 数字原样/字符串加引号
package/dist/ir/schema.js CHANGED
@@ -118,7 +118,7 @@ const NODE_ALLOWED_PROPS = {
118
118
  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'],
119
119
  text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
120
120
  image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
121
- button: ['width', 'height', 'label', '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'],
121
+ button: ['width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
122
122
  input: ['width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
123
123
  scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex'],
124
124
  spacer: ['width', 'height', 'flex'],
@@ -276,7 +276,10 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
276
276
  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))) {
277
277
  issues.push({ path, message: 'text 节点必须有 props.text / props.ref / props.bind / props.bindExpr / props.fieldOf 或 bind' });
278
278
  }
279
- if (node.type === 'button' && !node.bind && (!node.props || node.props.label === undefined)) {
279
+ // R108:按钮文案可以是静态 label,也可以是动态 bind(运行时求值)
280
+ if (node.type === 'button' &&
281
+ !node.bind &&
282
+ (!node.props || (node.props.label === undefined && node.props.bind === undefined))) {
280
283
  issues.push({ path, message: 'button 节点必须有 props.label 或 bind' });
281
284
  }
282
285
  // condition / active
@@ -896,6 +896,7 @@ const TAG_MAP = {
896
896
  select: { type: 'group', defaultProps: { flexDirection: 'column' } },
897
897
  option: { type: 'text', defaultProps: { fontSize: 14 } },
898
898
  aside: { type: 'group', defaultProps: { flexDirection: 'column' } },
899
+ label: { type: 'text', defaultProps: { fontSize: 13 } },
899
900
  figure: { type: 'group', defaultProps: { flexDirection: 'column' } },
900
901
  figcaption: { type: 'text', defaultProps: { fontSize: 12 } },
901
902
  h1: { type: 'text', defaultProps: { fontSize: 24, fontWeight: 'bold' } },
@@ -1279,7 +1280,10 @@ export function vueToIr(source, opts = {}) {
1279
1280
  // 5) <script setup> 逻辑源码 → 运行时 JS(R12:事件逻辑由原生 JS 引擎执行)
1280
1281
  // 仅支持 JS 子集(无 TS 类型标注、无 import);超出即编译期警告(后续阶段扩展)
1281
1282
  // R98 P3:把模板表达式下沉产生的求值状态并入页面状态,并重建 runtime(含 recompute)
1282
- if (jsExprStates.length > 0 && stateInfo) {
1283
+ if (jsExprStates.length > 0) {
1284
+ // 无 <script setup> 的页面也要能承载表达式求值状态
1285
+ if (!stateInfo)
1286
+ stateInfo = { states: [], setters: {} };
1283
1287
  for (const st of jsExprStates) {
1284
1288
  if (!stateInfo.states.some((x) => x.name === st.name))
1285
1289
  stateInfo.states.push({ ...st });
@@ -1587,6 +1591,10 @@ function buildComponentTree(comp, warnings) {
1587
1591
  }
1588
1592
  function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable = [], propRefs, loopScope = [], ancestorClasses = []) {
1589
1593
  let elementClasses = []; // M5 后代选择器:本元素 class 清单(子节点递归传祖先链)
1594
+ // R107:SVG 图标(svg/path/circle/line/rect/polygon/g/defs/use)纯装饰 → 忽略,不阻断编译
1595
+ if (['svg', 'path', 'circle', 'line', 'rect', 'polygon', 'polyline', 'g', 'defs', 'use', 'text', 'ellipse'].includes(node.tag) && node.tag !== 'text') {
1596
+ return { type: 'spacer', props: { height: 0 } };
1597
+ }
1590
1598
  // R104:<br> 换行 → 占位间距(原生端没有行内换行语义)
1591
1599
  if (node.tag === 'br') {
1592
1600
  return { type: 'spacer', props: { height: 8 } };
@@ -1626,13 +1634,34 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1626
1634
  }
1627
1635
  }
1628
1636
  }
1629
- const vf = parseVFor(forExp, warnings, vforConsts);
1637
+ let vf = parseVFor(forExp, warnings, vforConsts);
1630
1638
  void sunkList;
1631
1639
  if (!vf) {
1632
- throw new VueCompileError('v-for 仅支持静态数组字面量(M2,如 v-for="item in [1,2,3]" / v-for="(item, i) in [\'a\',\'b\']";动态列表随 M3/M4)');
1640
+ // R108 兜底:任意 `item in <表达式>` 都能下沉——运行时求值成 JSON 数组即可
1641
+ const gm = forExp.trim().match(/^(?:\(([^)]+)\)|([A-Za-z_$][\w$]*))\s+in\s+([\s\S]+)$/);
1642
+ if (gm) {
1643
+ const expr = gm[3].trim();
1644
+ // Vue3 语义:`v-for="i in 3"` 迭代 1..3
1645
+ const rangeM = expr.match(/^(\d+)$/);
1646
+ const sunk = addJsExprState(rangeM
1647
+ ? `JSON.stringify(Array.from({ length: ${rangeM[1]} }, function (_, i) { return i + 1; }))`
1648
+ : `JSON.stringify(${expr})`);
1649
+ vf = {
1650
+ listState: sunk,
1651
+ itemKey: (gm[1] ? gm[1].split(',')[0] : gm[2]).trim(),
1652
+ indexKey: gm[1] ? gm[1].split(',')[1]?.trim() : undefined,
1653
+ };
1654
+ warnings.push(`v-for「${forExp.trim().slice(0, 40)}」的数据源下沉为运行时求值(R108)`);
1655
+ }
1656
+ else {
1657
+ throw new VueCompileError('v-for 语法无法解析(需 `item in <表达式>` 形态)');
1658
+ }
1633
1659
  }
1634
1660
  // M4b:动态列表数据源必须已声明为 ref 数组状态(编译期校验,防悬空引用)
1635
- if (vf.listState && !(state && state.states.some((s) => s.name === vf.listState))) {
1661
+ // 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
1662
+ if (vf.listState &&
1663
+ !/^_vrnE\d+$/.test(vf.listState) &&
1664
+ !(state && state.states.some((s) => s.name === vf.listState))) {
1636
1665
  throw new VueCompileError(`v-for 动态数据源「${vf.listState}」未在 <script setup> 中声明为 ref 数组(M4b)`);
1637
1666
  }
1638
1667
  const innerNode = { ...node, props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')) };
@@ -1852,6 +1881,18 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1852
1881
  warnings.push(`属性 ${attrName}="${attrValue}" 暂不支持,已忽略`);
1853
1882
  }
1854
1883
  }
1884
+ // R108:button 只接受 label/bind —— 布局属性丢弃、缺 label 时占位(避免整页失败)
1885
+ if (mapping.type === 'button') {
1886
+ const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'overflow', 'clip'];
1887
+ for (const k of badProps)
1888
+ delete props[k];
1889
+ }
1890
+ if (mapping.type === 'input') {
1891
+ // R108:input 只接受 value/placeholder 等,文本样式属性一律丢弃
1892
+ for (const k of ['textAlign', 'lineHeight', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
1893
+ delete props[k];
1894
+ }
1895
+ }
1855
1896
  // 子节点:
1856
1897
  // - button:文本 → label
1857
1898
  // - text 类型(p/span/h1~h4):全部文本子节点合并为 props.text;出现元素子节点 → 行内混合报错;
@@ -1905,6 +1946,10 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1905
1946
  refOnly = name;
1906
1947
  parts.push({ bind: name });
1907
1948
  }
1949
+ else if (scriptDeclaredNames.has(name)) {
1950
+ // R106:computed / 普通 const / 解构值 → 运行时求值
1951
+ parts.push({ bind: addJsExprState(textOf(child.content).trim()) });
1952
+ }
1908
1953
  else {
1909
1954
  throw new VueCompileError(`插值 {{ ${name} }}:未在 <script setup> 中声明为 ref 状态(M1 白名单:仅支持已声明状态)`);
1910
1955
  }
@@ -1959,6 +2004,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1959
2004
  else if (child.type === NodeTypes.COMMENT) {
1960
2005
  // 忽略注释
1961
2006
  }
2007
+ else if (child.type === NodeTypes.ELEMENT && mapping.type === 'text') {
2008
+ // R107:行内嵌套(<b>光<b/></b>、<span><svg/></span>)→ 有文字并入,纯图标忽略
2009
+ const innerText = elementInnerText(child).trim();
2010
+ if (innerText) {
2011
+ text += innerText;
2012
+ parts.push({ text: innerText }); // 同步进 parts,避免只走 bindParts 时丢失
2013
+ }
2014
+ else
2015
+ warnings.push(`<${node.tag}> 内嵌图标元素 <${child.tag}> 已忽略(R107)`);
2016
+ }
1962
2017
  else {
1963
2018
  throw new VueCompileError(`<${node.tag}> 内嵌子元素(行内混合)暂不支持(text 节点仅文本/插值)`);
1964
2019
  }
@@ -1985,18 +2040,31 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
1985
2040
  else {
1986
2041
  const t = text.trim();
1987
2042
  if (!t) {
1988
- throw new VueCompileError(`<${node.tag}> 无文本内容`);
2043
+ // R106:空的行内元素(图标占位 <i></i>)→ 空文本,内容不丢但也不阻断编译
2044
+ warnings.push(`<${node.tag}> 无文本内容 → 已降级为空占位(R106)`);
2045
+ props.text = ' ';
2046
+ }
2047
+ else {
2048
+ props.text = t;
1989
2049
  }
1990
- props.text = t;
1991
2050
  }
1992
2051
  }
1993
2052
  else {
1994
2053
  for (const child of node.children) {
1995
2054
  if (mapping.type === 'button') {
2055
+ if (child.type === NodeTypes.ELEMENT && (child.tag === 'br' || !elementInnerText(child).trim()))
2056
+ continue; // R106/R107:按钮内换行/纯图标忽略
1996
2057
  if (child.type === NodeTypes.TEXT && child.content.trim()) {
1997
2058
  props.label = ((props.label ?? '') + child.content.trim()).trim();
1998
2059
  continue;
1999
2060
  }
2061
+ if (child.type === NodeTypes.INTERPOLATION) {
2062
+ // R108:按钮文案是动态表达式 → bind(运行时求值),原生端照样显示
2063
+ const raw = textOf(child.content).trim();
2064
+ if (!props.bind && !props.label)
2065
+ props.bind = addJsExprState(raw);
2066
+ continue;
2067
+ }
2000
2068
  if (child.type === NodeTypes.ELEMENT) {
2001
2069
  // 常见写法兜底:<button><span>文字</span></button> → 内联文本并入 label(否则 IR 校验因缺 label 中断)
2002
2070
  const inner = elementInnerText(child).trim();
@@ -2013,6 +2081,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2013
2081
  children.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, ...elementClasses]));
2014
2082
  }
2015
2083
  }
2084
+ // R108:文本节点丢弃布局类属性(原生端 width/height/flexDirection 只对容器有意义)
2085
+ if (mapping.type === 'text') {
2086
+ const LAYOUT_ONLY = ['height', 'width', 'flexDirection', 'justifyContent', 'alignItems', 'flex', 'gap'];
2087
+ const dropped = LAYOUT_ONLY.filter((k) => props[k] !== undefined);
2088
+ if (dropped.length > 0) {
2089
+ for (const k of dropped)
2090
+ delete props[k];
2091
+ warnings.push(`<${node.tag}> 是文本节点,布局属性(${dropped.join('/')})已忽略(R108)`);
2092
+ }
2093
+ }
2016
2094
  // R88 续:容器节点不接受文本专属样式(color/fontSize/fontWeight/textAlign/lineHeight)
2017
2095
  // 原生端容器没有「字体继承」,这类属性会直接触发 IR 校验拒收 → 明确降级 + 警告
2018
2096
  if (mapping.type === 'group' || mapping.type === 'for') {
@@ -2024,6 +2102,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2024
2102
  warnings.push(`<${node.tag}> 是容器,文本样式(${dropped.join('/')})在原生端不继承 → 已忽略;请把样式下沉到具体文本节点(R88)`);
2025
2103
  }
2026
2104
  }
2105
+ // R108 兜底:按钮最终必须有可见文案(label 或 bind),否则整页校验失败
2106
+ if (mapping.type === 'button' && props.label === undefined && props.bind === undefined) {
2107
+ props.label = ' ';
2108
+ warnings.push(`<button> 无可读文案(可能是纯图标按钮)→ 已占位,建议补 aria-label 或文案(R108)`);
2109
+ }
2027
2110
  // R77 空容器(<div class="spacer" /> 等占位写法)→ 降级为 spacer 占位 + 警告,而不是中断整个编译
2028
2111
  if (mapping.type === 'group' && children.length === 0) {
2029
2112
  warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir-compiler",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Vue SFC → IR → 原生源码(SwiftUI / Compose)编译器核心:纯函数、零 IO",
5
5
  "type": "module",
6
6
  "license": "MIT",