@lingfan-engine/editor 0.0.0-stage → 0.1.2

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.
Files changed (79) hide show
  1. package/LICENSE +201 -0
  2. package/package.json +39 -3
  3. package/src/chapters/build.ts +180 -0
  4. package/src/chapters/index.ts +34 -0
  5. package/src/chapters/types.ts +71 -0
  6. package/src/contracts/diagnostics.ts +62 -0
  7. package/src/contracts/editing.ts +26 -0
  8. package/src/contracts/forms.ts +69 -0
  9. package/src/contracts/index.ts +22 -0
  10. package/src/contracts/texts.ts +204 -0
  11. package/src/diagnostics/analyze.ts +161 -0
  12. package/src/diagnostics/grouping.ts +164 -0
  13. package/src/diagnostics/index.ts +35 -0
  14. package/src/diagnostics/message.ts +60 -0
  15. package/src/diagnostics/refs.ts +123 -0
  16. package/src/diagnostics/symbols.ts +221 -0
  17. package/src/editing/columnGrouping.ts +331 -0
  18. package/src/editing/graphConnect.ts +88 -0
  19. package/src/editing/index.ts +72 -0
  20. package/src/editing/pointers.ts +291 -0
  21. package/src/editing/saveNormalization.ts +58 -0
  22. package/src/editing/session.ts +134 -0
  23. package/src/editing/storyOps.ts +268 -0
  24. package/src/editing/tsSourceGuard.ts +40 -0
  25. package/src/element/drag.ts +22 -0
  26. package/src/element/dragCreate.ts +60 -0
  27. package/src/element/index.ts +11 -0
  28. package/src/i18n/coverage.ts +191 -0
  29. package/src/i18n/extract.ts +38 -0
  30. package/src/i18n/index.ts +47 -0
  31. package/src/i18n/reconcile.ts +71 -0
  32. package/src/i18n/skeleton.ts +112 -0
  33. package/src/i18n/storyKeys.ts +27 -0
  34. package/src/i18n/surfaces.ts +54 -0
  35. package/src/i18n/table.ts +235 -0
  36. package/src/index.ts +342 -0
  37. package/src/layout/index.ts +14 -0
  38. package/src/layout/steps.ts +378 -0
  39. package/src/schema/catalog.ts +300 -0
  40. package/src/schema/elementForms.ts +307 -0
  41. package/src/schema/elementPalette.ts +100 -0
  42. package/src/schema/forms.ts +196 -0
  43. package/src/schema/index.ts +55 -0
  44. package/src/schema/opSchemas/commands.ts +374 -0
  45. package/src/schema/opSchemas/fragments.ts +24 -0
  46. package/src/schema/opSchemas/index.ts +16 -0
  47. package/src/schema/opSchemas/map.ts +134 -0
  48. package/src/schema/opSchemas/types.ts +42 -0
  49. package/src/schema/opSchemas/validate.ts +184 -0
  50. package/src/schema/surface.ts +101 -0
  51. package/src/schema/validation.ts +92 -0
  52. package/src/schema/walk.ts +167 -0
  53. package/src/script/expr/audit/index.ts +8 -0
  54. package/src/script/expr/audit/parser.ts +370 -0
  55. package/src/script/expr/audit/token.ts +23 -0
  56. package/src/script/expr/build.ts +203 -0
  57. package/src/script/expr/index.ts +17 -0
  58. package/src/script/expr/registry.ts +130 -0
  59. package/src/script/index.ts +168 -0
  60. package/src/script/vocabulary-gen.ts +202 -0
  61. package/src/script/vocabulary.g.ts +175 -0
  62. package/src/script/words/audio.ts +114 -0
  63. package/src/script/words/cells.ts +78 -0
  64. package/src/script/words/compile-time.ts +27 -0
  65. package/src/script/words/composite.ts +125 -0
  66. package/src/script/words/elements.ts +202 -0
  67. package/src/script/words/extension.ts +25 -0
  68. package/src/script/words/flow.ts +391 -0
  69. package/src/script/words/index.ts +23 -0
  70. package/src/script/words/interaction.ts +44 -0
  71. package/src/script/words/minigame.ts +66 -0
  72. package/src/script/words/narrative.ts +261 -0
  73. package/src/script/words/save.ts +54 -0
  74. package/src/script/words/vars.ts +155 -0
  75. package/src/script/words/video.ts +80 -0
  76. package/src/shared/guards.ts +19 -0
  77. package/src/shared/index.ts +6 -0
  78. package/src/shared/pointer.ts +23 -0
  79. package/README.md +0 -3
@@ -0,0 +1,370 @@
1
+ /**
2
+ * 表达式审计:把装配时产出的 token 流按引擎的类型规则判类,违规记入警告池。
3
+ *
4
+ * 判类器是一个整体(方法共享游标 `pos` 与警告池),所以实现文件刻意只留一条导入。
5
+ */
6
+
7
+ import {
8
+ BAIL,
9
+ type AuditToken,
10
+ type ExpressionWarning,
11
+ type OperandKind,
12
+ type VarKind,
13
+ } from "./token.ts";
14
+ import type { EditorTexts } from "../../../contracts/index.ts";
15
+ import { resolveEditorTexts } from "../../../contracts/index.ts";
16
+
17
+ /**
18
+ * 递归下降判类器——**优先级链与引擎逐层一致**:`?: → || → && → ==/!= → 比较 →
19
+ * + - → * / % → 一元 → 成员/调用`。相邻配对会在 `isVIP || gold == 1` 这类
20
+ * 优先级场景错怪操作数,故必须按结构配对。
21
+ *
22
+ * 类型传播口径(与引擎「输入合型 ⇒ 有结果必为输出型」一致):
23
+ * - 违规(已知操作数不合运算符要求)⇒ 记警告,结果按 unknown(防连锁噪声);
24
+ * - 无违规 ⇒ 结果 = 运算符输出型(算术 num / 比较·相等·逻辑·`!` bool / 一元减 num),
25
+ * 即使另一侧 unknown——引擎在「不报错」的前提下结果型是确定的。
26
+ */
27
+ export class ExpressionAuditor {
28
+ private pos = 0;
29
+
30
+ private readonly tokens: readonly AuditToken[];
31
+ private readonly pending: Array<{
32
+ rule: ExpressionWarning["rule"];
33
+ message: string;
34
+ }>;
35
+ private readonly texts: EditorTexts;
36
+
37
+ constructor(
38
+ tokens: readonly AuditToken[],
39
+ pending: Array<{
40
+ rule: ExpressionWarning["rule"];
41
+ message: string;
42
+ }>,
43
+ texts: Partial<EditorTexts> = {},
44
+ ) {
45
+ this.tokens = tokens;
46
+ this.pending = pending;
47
+ this.texts = resolveEditorTexts(texts);
48
+ }
49
+
50
+ /** 入口:整体解析一次;任何结构不理解 / 残留 token ⇒ 全部放弃(零误报) */
51
+ audit(): void {
52
+ const saved = [...this.pending];
53
+ try {
54
+ this.ternary();
55
+ if (this.pos !== this.tokens.length) throw BAIL;
56
+ } catch {
57
+ this.pending.length = 0;
58
+ this.pending.push(...saved);
59
+ }
60
+ }
61
+
62
+ private peek(): AuditToken | undefined {
63
+ return this.tokens[this.pos];
64
+ }
65
+
66
+ private isOp(text: string): boolean {
67
+ const tok = this.peek();
68
+ return tok !== undefined && tok.t === "op" && tok.text === text;
69
+ }
70
+
71
+ private isPunct(text: string): boolean {
72
+ const tok = this.peek();
73
+ return tok !== undefined && tok.t === "punct" && tok.text === text;
74
+ }
75
+
76
+ private eatOp(text: string): boolean {
77
+ if (this.isOp(text)) {
78
+ this.pos += 1;
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ private describe(operand: { kind: OperandKind; label: string }): string {
85
+ return `${operand.kind === "unknown" ? this.texts.unknownOperandKind() : operand.kind}「${operand.label}」`;
86
+ }
87
+
88
+ /** 单侧核对:已知操作数必须为 want,否则记警告并返回 true(违规)。note 追加引擎语义注记 */
89
+ private requireKind(
90
+ op: string,
91
+ rule: ExpressionWarning["rule"],
92
+ side: string,
93
+ operand: { kind: OperandKind; label: string } | undefined,
94
+ want: VarKind,
95
+ note = "",
96
+ ): boolean {
97
+ if (operand === undefined || operand.kind === "unknown") return false;
98
+ if (operand.kind === want) return false;
99
+ this.pending.push({
100
+ rule,
101
+ message: this.texts.operandKindMismatch(
102
+ op,
103
+ side,
104
+ want,
105
+ this.describe(operand),
106
+ note,
107
+ ),
108
+ });
109
+ return true;
110
+ }
111
+
112
+ private ternary(): OperandKind {
113
+ const condKind = this.or();
114
+ if (!this.isPunct("?")) return condKind;
115
+ this.pos += 1;
116
+ const whenTrue = this.ternary();
117
+ if (!this.isPunct(":")) throw BAIL;
118
+ this.pos += 1;
119
+ const whenFalse = this.ternary();
120
+ let violated = false;
121
+ if (condKind !== "unknown" && condKind !== "bool") {
122
+ this.pending.push({
123
+ rule: "ternary-cond-non-bool",
124
+ message: this.texts.ternaryCondNotBool(
125
+ this.describe({ kind: condKind, label: "条件表达式" }),
126
+ ),
127
+ });
128
+ violated = true;
129
+ }
130
+ if (violated) return "unknown";
131
+ return whenTrue !== "unknown" ? whenTrue : whenFalse;
132
+ }
133
+
134
+ private or(): OperandKind {
135
+ let left = this.and();
136
+ while (this.eatOp("||")) {
137
+ const right = this.and();
138
+ const violated =
139
+ this.requireKind(
140
+ "||",
141
+ "logical-non-bool",
142
+ "左",
143
+ { kind: left, label: "左操作数" },
144
+ "bool",
145
+ ) ||
146
+ this.requireKind(
147
+ "||",
148
+ "logical-non-bool",
149
+ "右",
150
+ { kind: right, label: "右操作数" },
151
+ "bool",
152
+ ";右支仅在被求值时检查——'||' 左支为真即短路",
153
+ );
154
+ left = violated ? "unknown" : "bool";
155
+ }
156
+ return left;
157
+ }
158
+
159
+ private and(): OperandKind {
160
+ let left = this.equality();
161
+ while (this.eatOp("&&")) {
162
+ const right = this.equality();
163
+ const violated =
164
+ this.requireKind(
165
+ "&&",
166
+ "logical-non-bool",
167
+ "左",
168
+ { kind: left, label: "左操作数" },
169
+ "bool",
170
+ ) ||
171
+ this.requireKind(
172
+ "&&",
173
+ "logical-non-bool",
174
+ "右",
175
+ { kind: right, label: "右操作数" },
176
+ "bool",
177
+ ";右支仅在被求值时检查——'&&' 左支为假即短路",
178
+ );
179
+ left = violated ? "unknown" : "bool";
180
+ }
181
+ return left;
182
+ }
183
+
184
+ /** 相等级:至多一个 ==/!=(引擎不支持链式比较);仅同类型标量相等 */
185
+ private equality(): OperandKind {
186
+ const left = this.comparison();
187
+ let op: "==" | "!=" | null = null;
188
+ if (this.eatOp("==")) op = "==";
189
+ else if (this.eatOp("!=")) op = "!=";
190
+ if (op === null) return left;
191
+ const right = this.comparison();
192
+ if (left !== "unknown" && right !== "unknown" && left !== right) {
193
+ this.pending.push({
194
+ rule: "equality-kind-mismatch",
195
+ message: this.texts.crossTypeComparison(op, left, right),
196
+ });
197
+ return "unknown";
198
+ }
199
+ return "bool";
200
+ }
201
+
202
+ /** 比较级:至多一个比较符(引擎不支持链式比较);只支持 number */
203
+ private comparison(): OperandKind {
204
+ const left = this.additive();
205
+ let op: string | null = null;
206
+ for (const candidate of [">=", "<=", ">", "<"]) {
207
+ if (this.eatOp(candidate)) {
208
+ op = candidate;
209
+ break;
210
+ }
211
+ }
212
+ if (op === null) return left;
213
+ const right = this.additive();
214
+ const violated =
215
+ this.requireKind(
216
+ op,
217
+ "compare-non-num",
218
+ "左",
219
+ { kind: left, label: "左操作数" },
220
+ "num",
221
+ ) ||
222
+ this.requireKind(
223
+ op,
224
+ "compare-non-num",
225
+ "右",
226
+ { kind: right, label: "右操作数" },
227
+ "num",
228
+ );
229
+ return violated ? "unknown" : "bool";
230
+ }
231
+
232
+ private additive(): OperandKind {
233
+ let left = this.multiplicative();
234
+ for (;;) {
235
+ const op = this.eatOp("+") ? "+" : this.eatOp("-") ? "-" : null;
236
+ if (op === null) return left;
237
+ const right = this.multiplicative();
238
+ const violated =
239
+ this.requireKind(
240
+ op,
241
+ "arith-non-num",
242
+ "左",
243
+ { kind: left, label: "左操作数" },
244
+ "num",
245
+ ) ||
246
+ this.requireKind(
247
+ op,
248
+ "arith-non-num",
249
+ "右",
250
+ { kind: right, label: "右操作数" },
251
+ "num",
252
+ );
253
+ left = violated ? "unknown" : "num";
254
+ }
255
+ }
256
+
257
+ private multiplicative(): OperandKind {
258
+ let left = this.unary();
259
+ for (;;) {
260
+ const op = this.eatOp("*")
261
+ ? "*"
262
+ : this.eatOp("/")
263
+ ? "/"
264
+ : this.eatOp("%")
265
+ ? "%"
266
+ : null;
267
+ if (op === null) return left;
268
+ const right = this.unary();
269
+ const violated =
270
+ this.requireKind(
271
+ op,
272
+ "arith-non-num",
273
+ "左",
274
+ { kind: left, label: "左操作数" },
275
+ "num",
276
+ ) ||
277
+ this.requireKind(
278
+ op,
279
+ "arith-non-num",
280
+ "右",
281
+ { kind: right, label: "右操作数" },
282
+ "num",
283
+ );
284
+ left = violated ? "unknown" : "num";
285
+ }
286
+ }
287
+
288
+ private unary(): OperandKind {
289
+ if (this.eatOp("!")) {
290
+ const inner = this.unary();
291
+ if (
292
+ this.requireKind(
293
+ "!",
294
+ "unary-not-non-bool",
295
+ "",
296
+ { kind: inner, label: "操作数" },
297
+ "bool",
298
+ )
299
+ ) {
300
+ return "unknown";
301
+ }
302
+ return "bool";
303
+ }
304
+ if (this.eatOp("-")) {
305
+ const inner = this.unary();
306
+ if (
307
+ this.requireKind(
308
+ "-",
309
+ "unary-minus-non-num",
310
+ "",
311
+ { kind: inner, label: "操作数" },
312
+ "num",
313
+ )
314
+ ) {
315
+ return "unknown";
316
+ }
317
+ return "num";
318
+ }
319
+ return this.primary();
320
+ }
321
+
322
+ private primary(): OperandKind {
323
+ const tok = this.peek();
324
+ if (tok === undefined) throw BAIL;
325
+ if (tok.t === "operand") {
326
+ this.pos += 1;
327
+ // 函数调用:ident( … ) ⇒ 结果未知(内置函数参数规则不进本层)
328
+ if (this.isPunct("(")) {
329
+ this.skipBalancedParens();
330
+ return "unknown";
331
+ }
332
+ // 成员路径:ident.ident … ⇒ 结果未知(字典下钻的值型构建期不可知)
333
+ if (this.isPunct(".") && tok.kind === "unknown") {
334
+ while (this.isPunct(".")) {
335
+ this.pos += 1;
336
+ const seg = this.peek();
337
+ if (seg === undefined || seg.t !== "operand") throw BAIL;
338
+ this.pos += 1;
339
+ }
340
+ return "unknown";
341
+ }
342
+ return tok.kind;
343
+ }
344
+ if (tok.t === "punct" && tok.text === "(") {
345
+ this.pos += 1;
346
+ const inner = this.ternary();
347
+ if (!this.isPunct(")")) throw BAIL;
348
+ this.pos += 1;
349
+ return inner;
350
+ }
351
+ throw BAIL;
352
+ }
353
+
354
+ /** 括号平衡跳过(函数调用参数整体不判型) */
355
+ private skipBalancedParens(): void {
356
+ let depth = 0;
357
+ for (;;) {
358
+ const tok = this.peek();
359
+ if (tok === undefined) throw BAIL;
360
+ this.pos += 1;
361
+ if (tok.t === "punct") {
362
+ if (tok.text === "(") depth += 1;
363
+ if (tok.text === ")") {
364
+ depth -= 1;
365
+ if (depth === 0) return;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 判类器使用的词法词汇:装配层产出的 token 流,与「结构不认识」的中止信号。
3
+ *
4
+ * `OperandKind` 是判类结果(三种变量种类 + 未知);注册表侧的两个类型在此转出,
5
+ * 判类器的实现文件便只需一条导入。
6
+ */
7
+
8
+ import type { ExpressionWarning, VarKind } from "../registry.ts";
9
+
10
+ export type { ExpressionWarning, VarKind };
11
+
12
+ export type OperandKind = VarKind | "unknown";
13
+
14
+ export type AuditToken =
15
+ | {
16
+ readonly t: "operand";
17
+ readonly kind: OperandKind;
18
+ readonly label: string;
19
+ }
20
+ | { readonly t: "op"; readonly text: string }
21
+ | { readonly t: "punct"; readonly text: string };
22
+
23
+ export const BAIL = Symbol("audit-bail");
@@ -0,0 +1,203 @@
1
+ /**
2
+ * 表达式标签的装配:把模板字面量拼成 `{…}` 文本,插值按句柄与字面量规则渲染。
3
+ *
4
+ * 组装同时做构建期轻类型校验(判类器在 `./audit`):警告进池、由
5
+ * `drainExpressionWarnings` 取走;表达式产物与既有行为逐字节一致(校验零侵入)。
6
+ */
7
+
8
+ import {
9
+ currentRegistry,
10
+ isVarHandle,
11
+ varKeyOf,
12
+ warningSink,
13
+ type ExpressionWarning,
14
+ type VarKind,
15
+ } from "./registry.ts";
16
+ import { ExpressionAuditor, type AuditToken, type OperandKind } from "./audit/index.ts";
17
+ import { defaultEditorTexts } from "../../contracts/index.ts";
18
+
19
+ /**
20
+ * expr 标签函数的文案取缺省表(例外登记):模板标签函数无法携带可选参数注入,
21
+ * 这四条是构建期开发者诊断(缺省不可变直引,无模块级 setter)。
22
+ * varKeyOf / ExpressionAuditor 的文案仍走调用点注入。
23
+ */
24
+ const texts = defaultEditorTexts;
25
+
26
+ /**
27
+ * 表达式标签:`expr\`${vars.player.gold} >= 25\` ⇒ "{player.gold >= 25}"`。
28
+ * 插值规则:VarHandle ⇒ 注册键(未注册抛错);number/boolean ⇒ 字面量;
29
+ * string ⇒ JSON 字面量(带引号,与引擎字符串字面量口径一致)。
30
+ *
31
+ * 组装同时做轻类型校验(见文件头):警告进池,由 `drainExpressionWarnings` 取走;
32
+ * 表达式产物与既有行为逐字节一致(校验零侵入)。
33
+ *
34
+ * 产物是 `"{…}"` 表达式串:引擎在**执行到包含它的命令时**求值——
35
+ * 条件语境({@link when} / whileDo)求出布尔;赋值语境({@link set} 等)
36
+ * 求出值;台词/通知文本在显示时插值。
37
+ *
38
+ * 必须先经 defineVars 注册变量,否则抛错。
39
+ *
40
+ * @param strings 模板字面量的静态片段。
41
+ * @param parts 插值:变量句柄 / number / boolean / string。
42
+ * @returns `"{…}"` 表达式串(放进故事数据,运行期求值)。
43
+ * @example
44
+ * expr`${vars.player.gold} >= 25` // ⇒ "{player.gold >= 25}"
45
+ * expr`${vars.greeting}, 旅行者` // string 插值 ⇒ 带引号字面量
46
+ */
47
+ export function expr(
48
+ strings: TemplateStringsArray,
49
+ ...parts: unknown[]
50
+ ): string {
51
+ const registry = currentRegistry;
52
+ if (registry === null) {
53
+ throw new Error(texts.exprRequiresDefineVars());
54
+ }
55
+ const tokens: AuditToken[] = [];
56
+ const pending: Array<{ rule: ExpressionWarning["rule"]; message: string }> =
57
+ [];
58
+ // 引号状态跨片段/插值跟踪(`"pre${name}post"` 的吞并 footgun:插值会被并进字面量)
59
+ let quote: string | null = null;
60
+ let escaped = false;
61
+
62
+ /** 片段词法:引号(跨片段)、双字运算符优先、字面量/数字/标识符;括号与结构标点进流 */
63
+ const lexFragment = (segment: string): void => {
64
+ for (let i = 0; i < segment.length; i += 1) {
65
+ const ch = segment[i]!;
66
+ if (quote !== null) {
67
+ if (escaped) {
68
+ escaped = false;
69
+ continue;
70
+ }
71
+ if (ch === "\\") {
72
+ escaped = true;
73
+ continue;
74
+ }
75
+ if (ch === quote) quote = null;
76
+ continue;
77
+ }
78
+ if (ch === '"' || ch === "'") {
79
+ quote = ch;
80
+ tokens.push({ t: "operand", kind: "str", label: "字符串字面量" });
81
+ continue;
82
+ }
83
+ const two = segment.slice(i, i + 2);
84
+ if (
85
+ two === "==" ||
86
+ two === "!=" ||
87
+ two === "&&" ||
88
+ two === "||" ||
89
+ two === ">=" ||
90
+ two === "<="
91
+ ) {
92
+ tokens.push({ t: "op", text: two });
93
+ i += 1;
94
+ continue;
95
+ }
96
+ if ("+-*/%<>!".includes(ch)) {
97
+ tokens.push({ t: "op", text: ch });
98
+ continue;
99
+ }
100
+ if ("(),?:.[]".includes(ch)) {
101
+ tokens.push({ t: "punct", text: ch });
102
+ continue;
103
+ }
104
+ if (/[0-9]/.test(ch)) {
105
+ let j = i;
106
+ while (j < segment.length && /[0-9.]/.test(segment[j]!)) j += 1;
107
+ const literal = segment.slice(i, j);
108
+ i = j - 1;
109
+ tokens.push({ t: "operand", kind: "num", label: literal });
110
+ continue;
111
+ }
112
+ if (/[A-Za-z_]/.test(ch)) {
113
+ let j = i;
114
+ while (j < segment.length && /[A-Za-z0-9_]/.test(segment[j]!)) j += 1;
115
+ const word = segment.slice(i, j);
116
+ i = j - 1;
117
+ tokens.push(
118
+ word === "true" || word === "false"
119
+ ? { t: "operand", kind: "bool", label: word }
120
+ : { t: "operand", kind: "unknown", label: word },
121
+ );
122
+ continue;
123
+ }
124
+ // 其余字符(空白等):词法噪声,不进流
125
+ }
126
+ };
127
+
128
+ /** 插值贡献(文本 + 判类):产物文本与既有行为逐字节一致(校验零侵入) */
129
+ const appendPart = (
130
+ part: unknown,
131
+ ): { text: string; kind: OperandKind; label: string } => {
132
+ if (isVarHandle(part)) {
133
+ const key = varKeyOf(part, registry, texts);
134
+ return { text: key, kind: part.kind, label: `变量 ${key}` };
135
+ }
136
+ if (typeof part === "number" || typeof part === "boolean") {
137
+ return {
138
+ text: String(part),
139
+ kind: typeof part as VarKind,
140
+ label: String(part),
141
+ };
142
+ }
143
+ if (typeof part === "string") {
144
+ return {
145
+ text: JSON.stringify(part),
146
+ kind: "str",
147
+ label: JSON.stringify(part),
148
+ };
149
+ }
150
+ throw new Error(
151
+ texts.unsupportedInterpolationType(typeof part),
152
+ );
153
+ };
154
+
155
+ let out = "{";
156
+ strings.forEach((segment, i) => {
157
+ lexFragment(segment);
158
+ out += segment;
159
+ if (i >= parts.length) return;
160
+ const part = parts[i];
161
+ if (quote !== null) {
162
+ // 校验零侵入:警告照记,但文本照旧并入产物(与既有行为逐字节一致)
163
+ pending.push({
164
+ rule: "interpolation-inside-string",
165
+ message: texts.interpolationInsideString(),
166
+ });
167
+ out += appendPart(part).text;
168
+ return;
169
+ }
170
+ const applied = appendPart(part);
171
+ out += applied.text;
172
+ tokens.push({ t: "operand", kind: applied.kind, label: applied.label });
173
+ });
174
+
175
+ const finalize = (): string => `${out}}`;
176
+ if (quote !== null) {
177
+ // 字面量到表达式末尾都没闭合:引擎将 parse-error——类型核查无意义,只报这一条
178
+ pending.push({
179
+ rule: "unclosed-string-literal",
180
+ message: texts.unclosedStringLiteral(),
181
+ });
182
+ warningSink.push(
183
+ ...pending.map((item) => ({ expression: finalize(), ...item })),
184
+ );
185
+ return finalize();
186
+ }
187
+ new ExpressionAuditor(tokens, pending, texts).audit();
188
+ for (const item of pending) {
189
+ warningSink.push({ expression: finalize(), ...item });
190
+ }
191
+ return finalize();
192
+ }
193
+
194
+ /**
195
+ * `expr` 的别名(条件语境的语义化写法;同一实现、同一校验)。
196
+ *
197
+ * @param strings 模板字面量的静态片段。
198
+ * @param parts 插值:变量句柄 / number / boolean / string。
199
+ * @returns `"{…}"` 条件表达式串(引擎执行条件命令时求值,须产出布尔)。
200
+ * @example
201
+ * when(cond`${vars.door.locked} == false`, [navigate("hall")]);
202
+ */
203
+ export const cond = expr;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 表达式层出口:变量句柄、表达式标签与构建期警告。
3
+ *
4
+ * 实现按职责分文件——注册表与句柄、判类器、装配;消费方(`script/index.ts`)
5
+ * 只从这里取。
6
+ */
7
+
8
+ export {
9
+ defineVars,
10
+ drainExpressionWarnings,
11
+ varKeyOf,
12
+ type ExpressionWarning,
13
+ type VarHandle,
14
+ type VarKind,
15
+ type VarTree,
16
+ } from "./registry.ts";
17
+ export { cond, expr } from "./build.ts";