@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,378 @@
1
+ /**
2
+ * 步骤布局:把每列切分为「步骤」序列(步骤 = 等待态边界),并给出列间分支边。
3
+ *
4
+ * **步骤语义归引擎**:哪个 op 建立等待一律取自引擎的等待声明表
5
+ * (`waitingStateOfOp` / `waitSpecOfOp`),本模块**不含任何逐 op 判定**——它只做结构切分:
6
+ * 「一条命令是否构成边界」= 该命令自身或其可触达体内存在等待点。
7
+ * 等待声明读 `StepOptions.surface` 的合并面(`OpSurface.waiting`——含 wait:true 的
8
+ * 扩展 op);未合并/无条目回退内建表口径,内建行为不变。
9
+ *
10
+ * 块体与调用(两条规则都直接派生自引擎的检查点坐标语义 `checkpointCoord`:
11
+ * 块帧内的等待点,坐标回退到**块进入命令**,故边界落在块命令本身):
12
+ * - 块体(if/while/for/foreach/switch)内含等待 ⇒ 该块命令即边界;块体内部不再另切步骤
13
+ * (同一次等待只算一步);
14
+ * - `call` 到「体含等待」的函数 ⇒ 该 `call` 即边界(函数体的等待确实在调用处建立检查点);
15
+ * `func` **定义体不执行**,故 `func` 命令自身不是边界、其体也不参与切分与出边。
16
+ *
17
+ * 列尾若无等待点,剩余命令成一步 `kind: "exit"`(流程出口步)。
18
+ * 容器与块体字段的遍历复用编辑器既有单点:`walkCommandBodies`(表单描述符递归),
19
+ * 函数体解析用 `indexStory` 的函数索引(指针 → `getAtPointer`)。
20
+ */
21
+ import type {
22
+ Story,
23
+ StoryColumn,
24
+ WaitingState,
25
+ WaitSpec,
26
+ } from "@lingfan-engine/core";
27
+ import { waitSpecOfOp, waitingStateOfOp } from "@lingfan-engine/core";
28
+ import { getAtPointer } from "../editing/index.ts";
29
+ import { indexStory } from "../diagnostics/index.ts";
30
+ import { BUILTIN_OP_SURFACE, type OpSurface } from "../schema/index.ts";
31
+ import { walkCommandBodies } from "../schema/index.ts";
32
+ import { isPlainObject } from "../shared/index.ts";
33
+
34
+ /** 步骤出边(分叉来源):菜单选项 / 跳转 / 导航 */
35
+ export interface StepFork {
36
+ kind: "menu" | "jump" | "navigate";
37
+ /** 目标列 id */
38
+ target: string;
39
+ /** 菜单选项文案(jump/navigate 无) */
40
+ label?: string;
41
+ /** 产生该出边的字段指针 */
42
+ pointer: string;
43
+ }
44
+
45
+ /** 一个步骤:一组连续命令,以等待点收尾(或列尾出口) */
46
+ export interface StoryStep {
47
+ columnId: string;
48
+ /** 列内步序(自 0 起) */
49
+ index: number;
50
+ /** wait = 以等待点收尾;exit = 列尾无等待点的流程出口 */
51
+ kind: "wait" | "exit";
52
+ /** 收尾等待态(exit 步为 "none") */
53
+ waiting: WaitingState;
54
+ /** 硬等待(如 `pause`:不随时间/点击推进) */
55
+ hard: boolean;
56
+ /** 首个成员命令指针 */
57
+ startPointer: string;
58
+ /** 末个成员命令指针(点击定位用) */
59
+ endPointer: string;
60
+ /** 成员命令指针(列内**顶层**命令,有序——与时间线行一一对应) */
61
+ commands: string[];
62
+ /** 该步可触达的列间出边(含块体与被调函数体内的转移) */
63
+ forks: StepFork[];
64
+ }
65
+
66
+ /** 一条泳道 = 一列 */
67
+ export interface StepLane {
68
+ columnId: string;
69
+ columnIndex: number;
70
+ kind: "flow" | "scene";
71
+ steps: StoryStep[];
72
+ /** scene 列的元素数(元素是声明式空间层,**不计入步骤**,仅作泳道徽标) */
73
+ elementCount: number;
74
+ /** BFS 分层(自入口列起;未达列归末层) */
75
+ layer: number;
76
+ }
77
+
78
+ /** 步骤间连边(自某步到目标列首步) */
79
+ export interface StepEdge {
80
+ fromColumnId: string;
81
+ /** 源列内步序 */
82
+ fromStep: number;
83
+ toColumnId: string;
84
+ kind: "menu" | "jump" | "navigate";
85
+ label?: string;
86
+ pointer: string;
87
+ }
88
+
89
+ export interface StepLayout {
90
+ lanes: StepLane[];
91
+ edges: StepEdge[];
92
+ /** 列内无步骤(空列)或步骤数,按列 id 索引——便于视图头部显示徽标 */
93
+ stepCounts: Map<string, number>;
94
+ }
95
+
96
+ export interface StepOptions {
97
+ /** op 合并面(扩展注册后由组合根传入;缺省 = 内建) */
98
+ surface?: OpSurface;
99
+ }
100
+
101
+ function opOf(cmd: unknown): string | undefined {
102
+ return isPlainObject(cmd) && typeof cmd.op === "string" ? cmd.op : undefined;
103
+ }
104
+
105
+ /** 可触达命令的收集上下文(函数索引 + 防环栈) */
106
+ interface ReachContext {
107
+ story: Story;
108
+ functions: Map<string, { pointer: string }>;
109
+ surface: OpSurface;
110
+ }
111
+
112
+ /**
113
+ * 收集一条命令**可直接触达**的命令(自身 + 块体;`call` 追进被调函数体;`func` 定义体不追)。
114
+ * 返回列表含每条可触达命令的指针,供边界判定与出边收集共用(同一次遍历产两个事实)。
115
+ */
116
+ function collectReachable(
117
+ cmd: unknown,
118
+ pointer: string,
119
+ ctx: ReachContext,
120
+ out: { cmd: Record<string, unknown>; pointer: string }[],
121
+ callStack: Set<string>,
122
+ ): void {
123
+ if (!isPlainObject(cmd)) return;
124
+ out.push({ cmd, pointer });
125
+ const op = opOf(cmd);
126
+ if (op === undefined) return;
127
+ if (op === "func") return; // 定义体不执行
128
+ if (op === "call") {
129
+ const name = typeof cmd.target === "string" ? cmd.target : "";
130
+ if (name === "" || callStack.has(name)) return; // 未知名 / 互为环:到此为止
131
+ const entry = ctx.functions.get(name);
132
+ if (entry === undefined) return;
133
+ const definition = getAtPointer(ctx.story, entry.pointer);
134
+ if (!isPlainObject(definition)) return;
135
+ const body = definition.body;
136
+ if (!Array.isArray(body)) return;
137
+ callStack.add(name);
138
+ body.forEach((child, index) => {
139
+ collectReachable(
140
+ child,
141
+ `${entry.pointer}/body/${index}`,
142
+ ctx,
143
+ out,
144
+ callStack,
145
+ );
146
+ });
147
+ callStack.delete(name);
148
+ return;
149
+ }
150
+ walkCommandBodies(
151
+ cmd,
152
+ pointer,
153
+ (child, childPointer) => {
154
+ collectReachable(child, childPointer, ctx, out, callStack);
155
+ },
156
+ ctx.surface,
157
+ );
158
+ }
159
+
160
+ /** 出边收集:菜单选项(带文案)/ 跳转 / 导航;目标栏位名与引擎 `execNavigate` 同序(scene 优先) */
161
+ function collectForks(
162
+ reachable: { cmd: Record<string, unknown>; pointer: string }[],
163
+ ): StepFork[] {
164
+ const forks: StepFork[] = [];
165
+ for (const { cmd, pointer } of reachable) {
166
+ const op = opOf(cmd);
167
+ if (op === "menu" && Array.isArray(cmd.options)) {
168
+ cmd.options.forEach((option, index) => {
169
+ if (!isPlainObject(option)) return;
170
+ if (typeof option.target !== "string" || option.target === "") return;
171
+ forks.push({
172
+ kind: "menu",
173
+ target: option.target,
174
+ label: typeof option.text === "string" ? option.text : undefined,
175
+ pointer: `${pointer}/options/${index}/target`,
176
+ });
177
+ });
178
+ continue;
179
+ }
180
+ if (op === "jump") {
181
+ if (typeof cmd.target === "string" && cmd.target !== "") {
182
+ forks.push({
183
+ kind: "jump",
184
+ target: cmd.target,
185
+ pointer: `${pointer}/target`,
186
+ });
187
+ }
188
+ continue;
189
+ }
190
+ if (op === "navigate") {
191
+ const target =
192
+ typeof cmd.scene === "string" && cmd.scene !== ""
193
+ ? cmd.scene
194
+ : typeof cmd.path === "string"
195
+ ? cmd.path
196
+ : "";
197
+ if (target !== "") {
198
+ forks.push({ kind: "navigate", target, pointer });
199
+ }
200
+ }
201
+ }
202
+ return forks;
203
+ }
204
+
205
+ /**
206
+ * 列内步骤切分:非边界命令并入「开放步」;遇边界收步;列尾非空则产出口步。
207
+ * 成员只含**顶层**命令指针(与时间线行一一对应);边界判定才下探块体与函数体。
208
+ */
209
+ export function columnSteps(
210
+ story: Story,
211
+ columnIndex: number,
212
+ options?: StepOptions,
213
+ ): StoryStep[] {
214
+ const column: StoryColumn | undefined = story?.columns?.[columnIndex];
215
+ if (column === undefined || column === null) return [];
216
+ const columnId = typeof column.id === "string" ? column.id : "";
217
+ // 命令容器:flow → commands;scene → entry(与 walkStoryCommands 同口径)
218
+ const field: "commands" | "entry" =
219
+ column.kind === "flow" ? "commands" : "entry";
220
+ const list: unknown = field === "commands" ? column.commands : column.entry;
221
+ if (!Array.isArray(list)) return [];
222
+
223
+ const ctx: ReachContext = {
224
+ story,
225
+ functions: new Map(
226
+ [...indexStory(story).functions].map(([name, entry]) => [
227
+ name,
228
+ { pointer: entry.pointer },
229
+ ]),
230
+ ),
231
+ surface: options?.surface ?? BUILTIN_OP_SURFACE,
232
+ };
233
+
234
+ const steps: StoryStep[] = [];
235
+ let open: string[] = [];
236
+ let pendingForks: StepFork[] = [];
237
+ const rootPointer = `/columns/${columnIndex}/${field}`;
238
+
239
+ const close = (
240
+ kind: StoryStep["kind"],
241
+ spec: { state: WaitingState; hard: boolean; forks: StepFork[] },
242
+ ): void => {
243
+ if (open.length === 0) return;
244
+ steps.push({
245
+ columnId,
246
+ index: steps.length,
247
+ kind,
248
+ waiting: spec.state,
249
+ hard: spec.hard,
250
+ startPointer: open[0]!,
251
+ endPointer: open[open.length - 1]!,
252
+ commands: open,
253
+ forks: spec.forks,
254
+ });
255
+ open = [];
256
+ pendingForks = [];
257
+ };
258
+
259
+ list.forEach((cmd, index) => {
260
+ const pointer = `${rootPointer}/${index}`;
261
+ open.push(pointer);
262
+ const reachable: { cmd: Record<string, unknown>; pointer: string }[] = [];
263
+ collectReachable(cmd, pointer, ctx, reachable, new Set());
264
+ pendingForks.push(...collectForks(reachable));
265
+ // 边界 = 可触达集合里存在等待点(自身或块体/被调函数体)。
266
+ // 等待声明读合并面:`ctx.surface.waiting` 先查(含 wait:true 的扩展 op),
267
+ // 未合并/无条目回退内建表口径 `waitingStateOfOp`——内建行为不变,扩展等待点被切分。
268
+ const waitingSpecOf = (
269
+ cmd: Record<string, unknown>,
270
+ ): { state: WaitingState | "none"; spec?: WaitSpec } => {
271
+ const op = opOf(cmd);
272
+ const merged = ctx.surface.waiting;
273
+ if (op !== undefined && merged !== undefined) {
274
+ const spec = merged.get(op);
275
+ if (spec !== undefined) return { state: spec.state, spec };
276
+ }
277
+ return { state: waitingStateOfOp(op) };
278
+ };
279
+ const boundary = reachable.find(
280
+ (item) => waitingSpecOf(item.cmd).state !== "none",
281
+ );
282
+ if (boundary !== undefined) {
283
+ const boundaryOp = opOf(boundary.cmd);
284
+ const spec =
285
+ (boundaryOp !== undefined
286
+ ? ctx.surface.waiting?.get(boundaryOp)
287
+ : undefined) ?? waitSpecOfOp(boundaryOp);
288
+ close("wait", {
289
+ state: spec?.state ?? "wait",
290
+ hard: spec?.hard === true,
291
+ forks: pendingForks,
292
+ });
293
+ }
294
+ });
295
+ close("exit", { state: "none", hard: false, forks: pendingForks });
296
+ return steps;
297
+ }
298
+
299
+ /** 列间边(源列 → 目标列)供泳道分层;目标可不存在(由诊断报 missing-target) */
300
+ function columnAdjacency(
301
+ story: Story,
302
+ options?: StepOptions,
303
+ ): { ids: string[]; idSet: Set<string>; adjacency: Map<string, Set<string>> } {
304
+ const ids = story.columns.map((c) => c.id);
305
+ const idSet = new Set(ids);
306
+ const adjacency = new Map<string, Set<string>>();
307
+ for (const id of ids) adjacency.set(id, new Set());
308
+ story.columns.forEach((_, index) => {
309
+ const from = ids[index];
310
+ if (from === undefined) return;
311
+ for (const step of columnSteps(story, index, options)) {
312
+ for (const fork of step.forks) {
313
+ adjacency.get(from)?.add(fork.target);
314
+ }
315
+ }
316
+ });
317
+ return { ids, idSet, adjacency };
318
+ }
319
+
320
+ /**
321
+ * 步骤布局总装:逐列切分(泳道)+ 汇总步骤间连边 + 自入口列 BFS 分层(环安全)。
322
+ * 未达列(无入边的孤立列)归末层,保证全部列可见。
323
+ */
324
+ export function storySteps(story: Story, options?: StepOptions): StepLayout {
325
+ const { ids, idSet, adjacency } = columnAdjacency(story, options);
326
+
327
+ const layerOf = new Map<string, number>();
328
+ const queue: string[] = [];
329
+ const root = idSet.has(story.entry) ? story.entry : ids[0];
330
+ if (root !== undefined) {
331
+ layerOf.set(root, 0);
332
+ queue.push(root);
333
+ }
334
+ for (let head = 0; head < queue.length; head += 1) {
335
+ const current = queue[head]!;
336
+ for (const next of adjacency.get(current) ?? []) {
337
+ if (!layerOf.has(next)) {
338
+ layerOf.set(next, layerOf.get(current)! + 1);
339
+ queue.push(next);
340
+ }
341
+ }
342
+ }
343
+ const fallback = Math.max(0, ...layerOf.values()) + 1;
344
+ for (const id of ids) if (!layerOf.has(id)) layerOf.set(id, fallback);
345
+
346
+ const lanes: StepLane[] = story.columns.map((column, index) => {
347
+ const steps = columnSteps(story, index, options);
348
+ return {
349
+ columnId: column.id,
350
+ columnIndex: index,
351
+ kind: column.kind === "flow" ? "flow" : "scene",
352
+ steps,
353
+ elementCount: Array.isArray(column.elements) ? column.elements.length : 0,
354
+ layer: layerOf.get(column.id) ?? fallback,
355
+ };
356
+ });
357
+
358
+ const edges: StepEdge[] = [];
359
+ for (const lane of lanes) {
360
+ for (const step of lane.steps) {
361
+ for (const fork of step.forks) {
362
+ if (!idSet.has(fork.target)) continue; // 悬空目标交诊断(视图不画幽灵边)
363
+ edges.push({
364
+ fromColumnId: lane.columnId,
365
+ fromStep: step.index,
366
+ toColumnId: fork.target,
367
+ kind: fork.kind,
368
+ label: fork.label,
369
+ pointer: fork.pointer,
370
+ });
371
+ }
372
+ }
373
+ }
374
+
375
+ const stepCounts = new Map<string, number>();
376
+ for (const lane of lanes) stepCounts.set(lane.columnId, lane.steps.length);
377
+ return { lanes, edges, stepCounts };
378
+ }