@lingfan-engine/editor 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/package.json +39 -3
- package/src/chapters/build.ts +180 -0
- package/src/chapters/index.ts +34 -0
- package/src/chapters/types.ts +71 -0
- package/src/contracts/diagnostics.ts +62 -0
- package/src/contracts/editing.ts +26 -0
- package/src/contracts/forms.ts +69 -0
- package/src/contracts/index.ts +22 -0
- package/src/contracts/texts.ts +204 -0
- package/src/diagnostics/analyze.ts +161 -0
- package/src/diagnostics/grouping.ts +164 -0
- package/src/diagnostics/index.ts +35 -0
- package/src/diagnostics/message.ts +60 -0
- package/src/diagnostics/refs.ts +123 -0
- package/src/diagnostics/symbols.ts +221 -0
- package/src/editing/columnGrouping.ts +331 -0
- package/src/editing/graphConnect.ts +88 -0
- package/src/editing/index.ts +72 -0
- package/src/editing/pointers.ts +291 -0
- package/src/editing/saveNormalization.ts +58 -0
- package/src/editing/session.ts +134 -0
- package/src/editing/storyOps.ts +268 -0
- package/src/editing/tsSourceGuard.ts +40 -0
- package/src/element/drag.ts +22 -0
- package/src/element/dragCreate.ts +60 -0
- package/src/element/index.ts +11 -0
- package/src/i18n/coverage.ts +191 -0
- package/src/i18n/extract.ts +38 -0
- package/src/i18n/index.ts +47 -0
- package/src/i18n/reconcile.ts +71 -0
- package/src/i18n/skeleton.ts +112 -0
- package/src/i18n/storyKeys.ts +27 -0
- package/src/i18n/surfaces.ts +54 -0
- package/src/i18n/table.ts +235 -0
- package/src/index.ts +342 -0
- package/src/layout/index.ts +14 -0
- package/src/layout/steps.ts +378 -0
- package/src/schema/catalog.ts +300 -0
- package/src/schema/elementForms.ts +307 -0
- package/src/schema/elementPalette.ts +100 -0
- package/src/schema/forms.ts +196 -0
- package/src/schema/index.ts +55 -0
- package/src/schema/opSchemas/commands.ts +374 -0
- package/src/schema/opSchemas/fragments.ts +24 -0
- package/src/schema/opSchemas/index.ts +16 -0
- package/src/schema/opSchemas/map.ts +134 -0
- package/src/schema/opSchemas/types.ts +42 -0
- package/src/schema/opSchemas/validate.ts +184 -0
- package/src/schema/surface.ts +101 -0
- package/src/schema/validation.ts +92 -0
- package/src/schema/walk.ts +167 -0
- package/src/script/expr/audit/index.ts +8 -0
- package/src/script/expr/audit/parser.ts +370 -0
- package/src/script/expr/audit/token.ts +23 -0
- package/src/script/expr/build.ts +203 -0
- package/src/script/expr/index.ts +17 -0
- package/src/script/expr/registry.ts +130 -0
- package/src/script/index.ts +168 -0
- package/src/script/vocabulary-gen.ts +202 -0
- package/src/script/vocabulary.g.ts +175 -0
- package/src/script/words/audio.ts +114 -0
- package/src/script/words/cells.ts +78 -0
- package/src/script/words/compile-time.ts +27 -0
- package/src/script/words/composite.ts +125 -0
- package/src/script/words/elements.ts +202 -0
- package/src/script/words/extension.ts +25 -0
- package/src/script/words/flow.ts +391 -0
- package/src/script/words/index.ts +23 -0
- package/src/script/words/interaction.ts +44 -0
- package/src/script/words/minigame.ts +66 -0
- package/src/script/words/narrative.ts +261 -0
- package/src/script/words/save.ts +54 -0
- package/src/script/words/vars.ts +155 -0
- package/src/script/words/video.ts +80 -0
- package/src/shared/guards.ts +19 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/pointer.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 变量注册表与句柄:显式注册制的声明面。
|
|
3
|
+
*
|
|
4
|
+
* 引擎表达式是字符串、求值归引擎,本层只负责生成表达式文本——
|
|
5
|
+
* `defineVars` 声明一个故事源用到的变量(IDE 补全与拼写检查由此而来),
|
|
6
|
+
* 句柄树的叶子是 `VarHandle`,装配期按注册表核对:未注册的引用直接抛错。
|
|
7
|
+
*
|
|
8
|
+
* 注册表与警告池是模块态,一次导入 = 一个故事源。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { EditorTexts } from "../../contracts/index.ts";
|
|
12
|
+
import { resolveEditorTexts } from "../../contracts/index.ts";
|
|
13
|
+
|
|
14
|
+
/** 变量种类(注册时声明;构建期轻校验的判类依据) */
|
|
15
|
+
export type VarKind = "num" | "str" | "bool";
|
|
16
|
+
|
|
17
|
+
export interface VarHandle {
|
|
18
|
+
readonly kind: VarKind;
|
|
19
|
+
/** 引擎状态键(如 `player.gold`) */
|
|
20
|
+
readonly key: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const VAR_KINDS: ReadonlySet<string> = new Set(["num", "str", "bool"]);
|
|
24
|
+
|
|
25
|
+
/** 真句柄 = kind 为三种之一且 key 为字符串——未注册子树 Proxy 的 kind 是对象,判否 */
|
|
26
|
+
export function isVarHandle(value: unknown): value is VarHandle {
|
|
27
|
+
if (typeof value !== "object" || value === null) return false;
|
|
28
|
+
const candidate = value as { kind?: unknown; key?: unknown };
|
|
29
|
+
return (
|
|
30
|
+
typeof candidate.kind === "string" &&
|
|
31
|
+
VAR_KINDS.has(candidate.kind) &&
|
|
32
|
+
typeof candidate.key === "string"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 嵌套句柄树(`vars.player.gold` ⇒ handle{key: "player.gold"}) */
|
|
37
|
+
export interface VarTree {
|
|
38
|
+
[prop: string]: VarTree | VarHandle;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 当前注册表(`expr` 标签读取;`defineVars` 绑定——每个故事源文件声明一次) */
|
|
42
|
+
export let currentRegistry: ReadonlyMap<string, VarKind> | null = null;
|
|
43
|
+
|
|
44
|
+
/** 构建期警告池(与 currentRegistry 同范式的模块态:一次导入 = 一个故事源;drain 后清零;装配层写入) */
|
|
45
|
+
export let warningSink: ExpressionWarning[] = [];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 取走并清空构建期警告。stories-build 在源模块导入完成后调用一次进 BuildReport;
|
|
49
|
+
* 连续两次调用,第二次必为空(drain 即清零)。
|
|
50
|
+
*/
|
|
51
|
+
export function drainExpressionWarnings(): readonly ExpressionWarning[] {
|
|
52
|
+
const out = warningSink;
|
|
53
|
+
warningSink = [];
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 构建期轻类型校验警告(引擎 type-error 的构建期预告;不拦构建)。
|
|
59
|
+
* 规则名与引擎 `runtime/expr/` 的类型规则一一对应(见 `ExpressionAuditor`)。
|
|
60
|
+
*/
|
|
61
|
+
export interface ExpressionWarning {
|
|
62
|
+
/** 完整表达式文本(含 `{}` 包装) */
|
|
63
|
+
readonly expression: string;
|
|
64
|
+
readonly rule:
|
|
65
|
+
| "arith-non-num"
|
|
66
|
+
| "compare-non-num"
|
|
67
|
+
| "equality-kind-mismatch"
|
|
68
|
+
| "logical-non-bool"
|
|
69
|
+
| "unary-not-non-bool"
|
|
70
|
+
| "unary-minus-non-num"
|
|
71
|
+
| "ternary-cond-non-bool"
|
|
72
|
+
| "interpolation-inside-string"
|
|
73
|
+
| "unclosed-string-literal";
|
|
74
|
+
/** 人话说明(含引擎将抛的 type-error 语义与出错操作数) */
|
|
75
|
+
readonly message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeTree(
|
|
79
|
+
prefix: string,
|
|
80
|
+
handles: ReadonlyMap<string, VarHandle>,
|
|
81
|
+
texts: EditorTexts,
|
|
82
|
+
): VarTree {
|
|
83
|
+
return new Proxy({} as VarTree, {
|
|
84
|
+
get(_target, prop): VarTree | VarHandle {
|
|
85
|
+
if (typeof prop !== "string" || prop === "then") {
|
|
86
|
+
throw new Error(texts.invalidVarHandlePath(String(prop), prefix || "(根)"));
|
|
87
|
+
}
|
|
88
|
+
const key = prefix === "" ? prop : `${prefix}.${prop}`;
|
|
89
|
+
const handle = handles.get(key);
|
|
90
|
+
if (handle !== undefined) return handle;
|
|
91
|
+
// 未注册路径:继续给子树 Proxy(链式展开),expr 求值时校验终值
|
|
92
|
+
return makeTree(key, handles, texts);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 声明变量清单并绑定为本文件的 `expr` 上下文。
|
|
99
|
+
* 每个故事源文件调用一次(重复调用 = 覆盖前一份注册表——源文件间的变量清单
|
|
100
|
+
* 各自独立声明,不共享)。
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* const vars = defineVars({ "player.gold": "num", "player.name": "str" });
|
|
104
|
+
* cond`${vars.player.gold} >= 25` // ⇒ "{player.gold >= 25}"
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function defineVars(
|
|
108
|
+
spec: Record<string, VarKind>,
|
|
109
|
+
texts: Partial<EditorTexts> = {},
|
|
110
|
+
): VarTree {
|
|
111
|
+
const t = resolveEditorTexts(texts);
|
|
112
|
+
currentRegistry = new Map(Object.entries(spec));
|
|
113
|
+
const handles = new Map<string, VarHandle>(
|
|
114
|
+
Object.entries(spec).map(([key, kind]) => [key, { kind, key }]),
|
|
115
|
+
);
|
|
116
|
+
return makeTree("", handles, t);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 句柄对应的注册键(expr 内部用;未注册 ⇒ 抛错——拼写错误在构建期就红) */
|
|
120
|
+
export function varKeyOf(
|
|
121
|
+
handle: VarHandle,
|
|
122
|
+
registry: ReadonlyMap<string, VarKind>,
|
|
123
|
+
texts: Partial<EditorTexts> = {},
|
|
124
|
+
): string {
|
|
125
|
+
const t = resolveEditorTexts(texts);
|
|
126
|
+
if (!registry.has(handle.key)) {
|
|
127
|
+
throw new Error(t.unregisteredVariable(handle.key));
|
|
128
|
+
}
|
|
129
|
+
return handle.key;
|
|
130
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script 词汇层 · 出口(全量 op 覆盖 + 复合词 + 表达式句柄)。
|
|
3
|
+
*
|
|
4
|
+
* **SCRIPT_COVERAGE = builder ↔ op 双射的机器可读面**——生成物(./vocabulary.g.ts,
|
|
5
|
+
* 由 generate-vocabulary.ts 重建,勿手改):直接词条来自 words 源返回注解扫描,
|
|
6
|
+
* 辅助词走显式补充表,注册表键集快照同步入内。改注册表/词条后未重建 =
|
|
7
|
+
* tests/editor/script-vocabulary.test.ts 的生成物新鲜度用例红。
|
|
8
|
+
* `$` 前缀 = 辅助词(不直接产出独立 op)——互锁比较时跳过。
|
|
9
|
+
*/
|
|
10
|
+
export {
|
|
11
|
+
SCRIPT_COVERAGE,
|
|
12
|
+
VOCABULARY_OPS,
|
|
13
|
+
type VocabularyOp,
|
|
14
|
+
} from "./vocabulary.g.ts";
|
|
15
|
+
|
|
16
|
+
// —— 类型(裸写命令的强类型形态:op 判别联合,与 OP_SCHEMA_MAP / validateCommand 同源)——
|
|
17
|
+
// 裸对象 `const cmd: ScriptCommand = { op: "say", text, z }` ⇒ op 决定字段、未知字段编译期红;
|
|
18
|
+
// 扩展 op 不在联合内(闭合防逃生舱漏洞)——走 `extOp()` 或显式 `: StoryCommand` 注解。
|
|
19
|
+
export type {
|
|
20
|
+
CommandOf,
|
|
21
|
+
ScriptCommand,
|
|
22
|
+
ScriptOpName,
|
|
23
|
+
ScriptValue,
|
|
24
|
+
} from "../schema/index.ts";
|
|
25
|
+
|
|
26
|
+
// —— 表达式句柄 ——
|
|
27
|
+
export {
|
|
28
|
+
defineVars,
|
|
29
|
+
expr,
|
|
30
|
+
cond,
|
|
31
|
+
drainExpressionWarnings,
|
|
32
|
+
type VarKind,
|
|
33
|
+
type VarHandle,
|
|
34
|
+
type VarTree,
|
|
35
|
+
type ExpressionWarning,
|
|
36
|
+
} from "./expr/index.ts";
|
|
37
|
+
|
|
38
|
+
// —— 编译期构造(仅 Stories.src 源内可用,运行期不提供实现)——
|
|
39
|
+
export { staticFor } from "./words/index.ts";
|
|
40
|
+
export type { StoryFn } from "./words/index.ts";
|
|
41
|
+
|
|
42
|
+
// —— 具名实现槽位(build-time 声明词)——
|
|
43
|
+
export {
|
|
44
|
+
cell,
|
|
45
|
+
type CellHandle,
|
|
46
|
+
type ColumnNameRegistry,
|
|
47
|
+
type GuardNameRegistry,
|
|
48
|
+
type InterpolationKeyRegistry,
|
|
49
|
+
type KnownColumnName,
|
|
50
|
+
type KnownGuardName,
|
|
51
|
+
type KnownInterpolationKey,
|
|
52
|
+
} from "./words/index.ts";
|
|
53
|
+
|
|
54
|
+
// —— 叙事 ——
|
|
55
|
+
export { say, option, menu, input, notify, nvl, character, uncharacter } from "./words/index.ts";
|
|
56
|
+
export type {
|
|
57
|
+
MenuOptionSpec,
|
|
58
|
+
SayOptions,
|
|
59
|
+
NotifyOptions,
|
|
60
|
+
NvlCommandMode,
|
|
61
|
+
CharacterOptions,
|
|
62
|
+
} from "./words/index.ts";
|
|
63
|
+
|
|
64
|
+
// —— 流程 ——
|
|
65
|
+
export {
|
|
66
|
+
type Stmt,
|
|
67
|
+
wait,
|
|
68
|
+
pause,
|
|
69
|
+
random,
|
|
70
|
+
jump,
|
|
71
|
+
navigate,
|
|
72
|
+
func,
|
|
73
|
+
call,
|
|
74
|
+
ret,
|
|
75
|
+
when,
|
|
76
|
+
whenChain,
|
|
77
|
+
whileDo,
|
|
78
|
+
forIn,
|
|
79
|
+
forEach,
|
|
80
|
+
switchOn,
|
|
81
|
+
breakLoop,
|
|
82
|
+
continueLoop,
|
|
83
|
+
assert,
|
|
84
|
+
guard,
|
|
85
|
+
} from "./words/index.ts";
|
|
86
|
+
|
|
87
|
+
// —— 变量 ——
|
|
88
|
+
export {
|
|
89
|
+
set,
|
|
90
|
+
define,
|
|
91
|
+
letVar,
|
|
92
|
+
localVar,
|
|
93
|
+
undef,
|
|
94
|
+
newArray,
|
|
95
|
+
arrayPush,
|
|
96
|
+
arrayPop,
|
|
97
|
+
dict,
|
|
98
|
+
dictSet,
|
|
99
|
+
} from "./words/index.ts";
|
|
100
|
+
|
|
101
|
+
// —— 存档 / 音频 / 视频 / 小游戏 ——
|
|
102
|
+
export { save, load, autoSave, saveDelete } from "./words/index.ts";
|
|
103
|
+
export {
|
|
104
|
+
bgm,
|
|
105
|
+
stopBgm,
|
|
106
|
+
se,
|
|
107
|
+
ambient,
|
|
108
|
+
stopAmbient,
|
|
109
|
+
voice,
|
|
110
|
+
stopVoice,
|
|
111
|
+
} from "./words/index.ts";
|
|
112
|
+
export type {
|
|
113
|
+
PlayOptions,
|
|
114
|
+
BgmOptions,
|
|
115
|
+
StopOptions,
|
|
116
|
+
VoiceOptions,
|
|
117
|
+
} from "./words/index.ts";
|
|
118
|
+
export {
|
|
119
|
+
video,
|
|
120
|
+
cutscene,
|
|
121
|
+
seekVideo,
|
|
122
|
+
pauseVideo,
|
|
123
|
+
resumeVideo,
|
|
124
|
+
stopVideo,
|
|
125
|
+
videoSkipable,
|
|
126
|
+
} from "./words/index.ts";
|
|
127
|
+
export type { VideoOptions, CutsceneOptions } from "./words/index.ts";
|
|
128
|
+
export { minigame, reward } from "./words/index.ts";
|
|
129
|
+
export type { MinigameOptions } from "./words/index.ts";
|
|
130
|
+
export { interaction } from "./words/index.ts";
|
|
131
|
+
export type { InteractionOptions } from "./words/index.ts";
|
|
132
|
+
|
|
133
|
+
// —— 扩展 ——
|
|
134
|
+
export { extOp } from "./words/index.ts";
|
|
135
|
+
|
|
136
|
+
// —— 元素 ——
|
|
137
|
+
export {
|
|
138
|
+
show,
|
|
139
|
+
hide,
|
|
140
|
+
background,
|
|
141
|
+
bgSwitch,
|
|
142
|
+
setZ,
|
|
143
|
+
style,
|
|
144
|
+
dialogWindow,
|
|
145
|
+
animate,
|
|
146
|
+
animateBlock,
|
|
147
|
+
transition,
|
|
148
|
+
shake,
|
|
149
|
+
textTypewriter,
|
|
150
|
+
} from "./words/index.ts";
|
|
151
|
+
export type {
|
|
152
|
+
ShowOptions,
|
|
153
|
+
AnimateOptions,
|
|
154
|
+
AnimateBlockOptions,
|
|
155
|
+
ShakeOptions,
|
|
156
|
+
TypewriterOptions,
|
|
157
|
+
DialogWindowMode,
|
|
158
|
+
} from "./words/index.ts";
|
|
159
|
+
|
|
160
|
+
// —— 复合词 ——
|
|
161
|
+
export {
|
|
162
|
+
sceneSetup,
|
|
163
|
+
buttonElement,
|
|
164
|
+
textElement,
|
|
165
|
+
imageElement,
|
|
166
|
+
sceneType,
|
|
167
|
+
} from "./words/index.ts";
|
|
168
|
+
export type { ElementOptions } from "./words/index.ts";
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 词汇层声明生成器(纯函数:无 fs、无副作用、无控制台输出)。
|
|
3
|
+
*
|
|
4
|
+
* 两个真相源在渲染时合流成一份机器可读声明(vocabulary.g.ts,由同目录
|
|
5
|
+
* generate-vocabulary.ts 重建),词表不再手写双份:
|
|
6
|
+
* - **op 注册表**(OP_SCHEMA_MAP 键集)→ `VOCABULARY_OPS`:注册表面的单一快照;
|
|
7
|
+
* - **words 域源码扫描**:返回注解为 `CommandOf<"op">` 的导出函数 = 直接词条。
|
|
8
|
+
* 不产独立 op 的导出词(复合词、菜单项、扩展出口等)走 `HELPER_WORDS` 显式补充表,
|
|
9
|
+
* 缺登记 = fail-closed(抛错),新词不可能静默漏网。
|
|
10
|
+
*/
|
|
11
|
+
import * as ts from "typescript";
|
|
12
|
+
|
|
13
|
+
/** 单个被扫描源文件(file = 展示与分组用短名,text = 全文) */
|
|
14
|
+
export interface VocabSource {
|
|
15
|
+
readonly file: string;
|
|
16
|
+
readonly text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 扫描出的直接词条(builder ↔ op) */
|
|
20
|
+
export interface DirectEntry {
|
|
21
|
+
readonly builder: string;
|
|
22
|
+
readonly op: string;
|
|
23
|
+
readonly file: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 扫描名单的排除集:barrel 出口 + 两处非词汇词文件
|
|
27
|
+
* (cells = 具名实现槽位、compile-time = 编译期构造,都不产词汇词)。 */
|
|
28
|
+
const SKIP_FILES = new Set(["index.ts", "cells.ts", "compile-time.ts"]);
|
|
29
|
+
|
|
30
|
+
/** 扫描名单规则(重建 CLI 与新鲜度测试共用,规则只有这一份) */
|
|
31
|
+
export function selectWordSources(fileNames: readonly string[]): string[] {
|
|
32
|
+
return fileNames
|
|
33
|
+
.filter((f) => f.endsWith(".ts") && !SKIP_FILES.has(f))
|
|
34
|
+
.sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 辅助词补充表:words 域里不产独立 op 的导出词。
|
|
38
|
+
* `$` 前缀 = 互锁比较跳过标记;新增辅助词必须在此登记,
|
|
39
|
+
* 未登记的新导出词会让重建失败(见 renderVocabulary 的缺登记核对)。 */
|
|
40
|
+
export const HELPER_WORDS: Readonly<Record<string, string>> = {
|
|
41
|
+
option: "$menu.option",
|
|
42
|
+
whenChain: "$if.chain",
|
|
43
|
+
reward: "$minigame.reward",
|
|
44
|
+
extOp: "$ext.op",
|
|
45
|
+
sceneSetup: "$composite.scene",
|
|
46
|
+
buttonElement: "$composite.button",
|
|
47
|
+
textElement: "$composite.text",
|
|
48
|
+
imageElement: "$composite.image",
|
|
49
|
+
sceneType: "$column.type",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 返回注解是否为 CommandOf<"op"> 字面量形态,是则取 op 名 */
|
|
53
|
+
function commandOfOpName(
|
|
54
|
+
returnType: ts.TypeNode | undefined,
|
|
55
|
+
): string | undefined {
|
|
56
|
+
if (!returnType || !ts.isTypeReferenceNode(returnType)) return undefined;
|
|
57
|
+
if (
|
|
58
|
+
!ts.isIdentifier(returnType.typeName) ||
|
|
59
|
+
returnType.typeName.text !== "CommandOf"
|
|
60
|
+
) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const arg = returnType.typeArguments?.[0];
|
|
64
|
+
if (!arg || !ts.isLiteralTypeNode(arg) || !ts.isStringLiteral(arg.literal)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return arg.literal.text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isExportedFunction(
|
|
71
|
+
node: ts.Statement,
|
|
72
|
+
): node is ts.FunctionDeclaration {
|
|
73
|
+
if (!ts.isFunctionDeclaration(node) || !node.name || !node.body) return false;
|
|
74
|
+
return (
|
|
75
|
+
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 扫描直接词条:导出函数中返回注解为 `CommandOf<"op">` 者。
|
|
80
|
+
* 同一 op 被两个 builder 声明 = 抛错(双射面不允许一 op 多词)。 */
|
|
81
|
+
export function scanDirectBuilders(
|
|
82
|
+
sources: readonly VocabSource[],
|
|
83
|
+
): DirectEntry[] {
|
|
84
|
+
const entries: DirectEntry[] = [];
|
|
85
|
+
const opOwner = new Map<string, string>();
|
|
86
|
+
for (const { file, text } of sources) {
|
|
87
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.ES2022, true);
|
|
88
|
+
for (const node of sf.statements) {
|
|
89
|
+
// FunctionDeclaration.name 可选:谓词收窄不含「名字存在」,此处再核一次
|
|
90
|
+
if (!isExportedFunction(node) || !node.name) continue;
|
|
91
|
+
const op = commandOfOpName(node.type);
|
|
92
|
+
if (op === undefined) continue;
|
|
93
|
+
const builder = node.name.text;
|
|
94
|
+
const clash = opOwner.get(op);
|
|
95
|
+
if (clash !== undefined) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`op "${op}" 被两个 builder 声明:${clash} 与 ${builder}(${file})`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
opOwner.set(op, builder);
|
|
101
|
+
entries.push({ builder, op, file });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return entries;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 扫描 words 域全部导出函数名(辅助词核对用) */
|
|
108
|
+
export function scanExportedFunctionNames(
|
|
109
|
+
sources: readonly VocabSource[],
|
|
110
|
+
): Set<string> {
|
|
111
|
+
const names = new Set<string>();
|
|
112
|
+
for (const { file, text } of sources) {
|
|
113
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.ES2022, true);
|
|
114
|
+
for (const node of sf.statements) {
|
|
115
|
+
if (!isExportedFunction(node) || !node.name) continue;
|
|
116
|
+
names.add(node.name.text);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return names;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 渲染词汇层声明全文(与入库存量逐字节比对的就是这个输出)。
|
|
123
|
+
* 三处不一致都会抛错而非静默产出:
|
|
124
|
+
* - 词条 op 不在注册表快照(词先于 schema);
|
|
125
|
+
* - 补充表登记了不存在的导出函数(幽灵登记);
|
|
126
|
+
* - words 域出现既非直接词条也无补充条目的导出函数(漏登记)。 */
|
|
127
|
+
export function renderVocabulary(
|
|
128
|
+
registryKeys: readonly string[],
|
|
129
|
+
sources: readonly VocabSource[],
|
|
130
|
+
): string {
|
|
131
|
+
const direct = scanDirectBuilders(sources);
|
|
132
|
+
const unknownOps = direct
|
|
133
|
+
.map((e) => e.op)
|
|
134
|
+
.filter((op) => !registryKeys.includes(op));
|
|
135
|
+
if (unknownOps.length > 0) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`词条产出注册表快照之外的 op:${unknownOps.join(", ")}——先在 OP_SCHEMA_MAP 登记再重建`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const exported = scanExportedFunctionNames(sources);
|
|
141
|
+
const ghostHelpers = Object.keys(HELPER_WORDS).filter(
|
|
142
|
+
(b) => !exported.has(b),
|
|
143
|
+
);
|
|
144
|
+
if (ghostHelpers.length > 0) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`辅助词补充表登记了不存在的导出函数:${ghostHelpers.join(", ")}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const missingHelpers = [...exported].filter(
|
|
150
|
+
(b) => !direct.some((e) => e.builder === b) && !(b in HELPER_WORDS),
|
|
151
|
+
);
|
|
152
|
+
if (missingHelpers.length > 0) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`words 域存在未登记的导出函数(既非直接词条也无补充表条目):${missingHelpers.join(", ")}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const lines: string[] = [
|
|
159
|
+
"/**",
|
|
160
|
+
" * 生成文件:词汇层声明(勿手改)。",
|
|
161
|
+
" *",
|
|
162
|
+
" * 由 packages/editor/src/script/generate-vocabulary.ts 重建(两个真相源):",
|
|
163
|
+
" * - OP_SCHEMA_MAP 键快照 → VOCABULARY_OPS(op 注册表面的单一来源);",
|
|
164
|
+
" * - words/*.ts 导出函数扫描 → SCRIPT_COVERAGE(builder ↔ op 双射面,",
|
|
165
|
+
' * 返回注解 CommandOf<"op"> = 直接词条;不产独立 op 的走补充表)。',
|
|
166
|
+
" * 手改会在下次重建时被覆盖;改注册表/词条后未重建 =",
|
|
167
|
+
" * tests/editor/script-vocabulary.test.ts 的生成物新鲜度用例红。",
|
|
168
|
+
" */",
|
|
169
|
+
"",
|
|
170
|
+
"/** op 注册表键快照(与 OP_SCHEMA_MAP 键集恒等;排序保证 diff 稳定) */",
|
|
171
|
+
"export const VOCABULARY_OPS = [",
|
|
172
|
+
];
|
|
173
|
+
for (const op of [...registryKeys].sort()) {
|
|
174
|
+
lines.push(` "${op}",`);
|
|
175
|
+
}
|
|
176
|
+
lines.push(
|
|
177
|
+
"] as const;",
|
|
178
|
+
"",
|
|
179
|
+
"/** op 名字面量联合(快照派生,与 ScriptOpName 同键集) */",
|
|
180
|
+
"export type VocabularyOp = (typeof VOCABULARY_OPS)[number];",
|
|
181
|
+
"",
|
|
182
|
+
"/**",
|
|
183
|
+
" * builder ↔ op 双射面(词汇层互锁守卫的数据源)。",
|
|
184
|
+
" * `$` 前缀 = 辅助词(不直接产出独立 op)——互锁比较时跳过。",
|
|
185
|
+
" */",
|
|
186
|
+
"export const SCRIPT_COVERAGE: Readonly<Record<string, string>> = {",
|
|
187
|
+
);
|
|
188
|
+
let lastFile = "";
|
|
189
|
+
for (const { builder, op, file } of direct) {
|
|
190
|
+
if (file !== lastFile) {
|
|
191
|
+
lines.push(` // —— ${file} ——`);
|
|
192
|
+
lastFile = file;
|
|
193
|
+
}
|
|
194
|
+
lines.push(` ${builder}: "${op}",`);
|
|
195
|
+
}
|
|
196
|
+
lines.push(" // —— 辅助词(补充表登记,不产独立 op) ——");
|
|
197
|
+
for (const [builder, op] of Object.entries(HELPER_WORDS)) {
|
|
198
|
+
lines.push(` ${builder}: "${op}",`);
|
|
199
|
+
}
|
|
200
|
+
lines.push("};", "");
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 生成文件:词汇层声明(勿手改)。
|
|
3
|
+
*
|
|
4
|
+
* 由 packages/editor/src/script/generate-vocabulary.ts 重建(两个真相源):
|
|
5
|
+
* - OP_SCHEMA_MAP 键快照 → VOCABULARY_OPS(op 注册表面的单一来源);
|
|
6
|
+
* - words/*.ts 导出函数扫描 → SCRIPT_COVERAGE(builder ↔ op 双射面,
|
|
7
|
+
* 返回注解 CommandOf<"op"> = 直接词条;不产独立 op 的走补充表)。
|
|
8
|
+
* 手改会在下次重建时被覆盖;改注册表/词条后未重建 =
|
|
9
|
+
* tests/editor/script-vocabulary.test.ts 的生成物新鲜度用例红。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** op 注册表键快照(与 OP_SCHEMA_MAP 键集恒等;排序保证 diff 稳定) */
|
|
13
|
+
export const VOCABULARY_OPS = [
|
|
14
|
+
"ambient",
|
|
15
|
+
"animate",
|
|
16
|
+
"animate_block",
|
|
17
|
+
"array",
|
|
18
|
+
"array_pop",
|
|
19
|
+
"array_push",
|
|
20
|
+
"assert",
|
|
21
|
+
"auto_save",
|
|
22
|
+
"background",
|
|
23
|
+
"bg_switch",
|
|
24
|
+
"bgm",
|
|
25
|
+
"break",
|
|
26
|
+
"call",
|
|
27
|
+
"character",
|
|
28
|
+
"continue",
|
|
29
|
+
"cutscene",
|
|
30
|
+
"define",
|
|
31
|
+
"dict",
|
|
32
|
+
"dict_set",
|
|
33
|
+
"for",
|
|
34
|
+
"foreach",
|
|
35
|
+
"func",
|
|
36
|
+
"guard",
|
|
37
|
+
"hide",
|
|
38
|
+
"if",
|
|
39
|
+
"input",
|
|
40
|
+
"interaction",
|
|
41
|
+
"jump",
|
|
42
|
+
"let",
|
|
43
|
+
"load",
|
|
44
|
+
"local",
|
|
45
|
+
"menu",
|
|
46
|
+
"minigame",
|
|
47
|
+
"navigate",
|
|
48
|
+
"notify",
|
|
49
|
+
"nvl",
|
|
50
|
+
"pause",
|
|
51
|
+
"pause_video",
|
|
52
|
+
"random",
|
|
53
|
+
"resume_video",
|
|
54
|
+
"return",
|
|
55
|
+
"save",
|
|
56
|
+
"save_delete",
|
|
57
|
+
"say",
|
|
58
|
+
"se",
|
|
59
|
+
"seek_video",
|
|
60
|
+
"set",
|
|
61
|
+
"shake",
|
|
62
|
+
"show",
|
|
63
|
+
"stop_ambient",
|
|
64
|
+
"stop_bgm",
|
|
65
|
+
"stop_video",
|
|
66
|
+
"stop_voice",
|
|
67
|
+
"style",
|
|
68
|
+
"switch",
|
|
69
|
+
"text_typewriter",
|
|
70
|
+
"transition",
|
|
71
|
+
"uncharacter",
|
|
72
|
+
"undef",
|
|
73
|
+
"video",
|
|
74
|
+
"video_skipable",
|
|
75
|
+
"voice",
|
|
76
|
+
"wait",
|
|
77
|
+
"while",
|
|
78
|
+
"window",
|
|
79
|
+
"zindex",
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
/** op 名字面量联合(快照派生,与 ScriptOpName 同键集) */
|
|
83
|
+
export type VocabularyOp = (typeof VOCABULARY_OPS)[number];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* builder ↔ op 双射面(词汇层互锁守卫的数据源)。
|
|
87
|
+
* `$` 前缀 = 辅助词(不直接产出独立 op)——互锁比较时跳过。
|
|
88
|
+
*/
|
|
89
|
+
export const SCRIPT_COVERAGE: Readonly<Record<string, string>> = {
|
|
90
|
+
// —— audio.ts ——
|
|
91
|
+
bgm: "bgm",
|
|
92
|
+
stopBgm: "stop_bgm",
|
|
93
|
+
se: "se",
|
|
94
|
+
ambient: "ambient",
|
|
95
|
+
stopAmbient: "stop_ambient",
|
|
96
|
+
voice: "voice",
|
|
97
|
+
stopVoice: "stop_voice",
|
|
98
|
+
// —— elements.ts ——
|
|
99
|
+
show: "show",
|
|
100
|
+
hide: "hide",
|
|
101
|
+
background: "background",
|
|
102
|
+
bgSwitch: "bg_switch",
|
|
103
|
+
setZ: "zindex",
|
|
104
|
+
style: "style",
|
|
105
|
+
dialogWindow: "window",
|
|
106
|
+
animate: "animate",
|
|
107
|
+
animateBlock: "animate_block",
|
|
108
|
+
transition: "transition",
|
|
109
|
+
shake: "shake",
|
|
110
|
+
textTypewriter: "text_typewriter",
|
|
111
|
+
// —— flow.ts ——
|
|
112
|
+
wait: "wait",
|
|
113
|
+
pause: "pause",
|
|
114
|
+
random: "random",
|
|
115
|
+
jump: "jump",
|
|
116
|
+
navigate: "navigate",
|
|
117
|
+
func: "func",
|
|
118
|
+
call: "call",
|
|
119
|
+
ret: "return",
|
|
120
|
+
when: "if",
|
|
121
|
+
whileDo: "while",
|
|
122
|
+
forIn: "for",
|
|
123
|
+
forEach: "foreach",
|
|
124
|
+
switchOn: "switch",
|
|
125
|
+
breakLoop: "break",
|
|
126
|
+
continueLoop: "continue",
|
|
127
|
+
assert: "assert",
|
|
128
|
+
guard: "guard",
|
|
129
|
+
// —— interaction.ts ——
|
|
130
|
+
interaction: "interaction",
|
|
131
|
+
// —— minigame.ts ——
|
|
132
|
+
minigame: "minigame",
|
|
133
|
+
// —— narrative.ts ——
|
|
134
|
+
say: "say",
|
|
135
|
+
menu: "menu",
|
|
136
|
+
input: "input",
|
|
137
|
+
notify: "notify",
|
|
138
|
+
nvl: "nvl",
|
|
139
|
+
character: "character",
|
|
140
|
+
uncharacter: "uncharacter",
|
|
141
|
+
// —— save.ts ——
|
|
142
|
+
save: "save",
|
|
143
|
+
load: "load",
|
|
144
|
+
autoSave: "auto_save",
|
|
145
|
+
saveDelete: "save_delete",
|
|
146
|
+
// —— vars.ts ——
|
|
147
|
+
set: "set",
|
|
148
|
+
define: "define",
|
|
149
|
+
letVar: "let",
|
|
150
|
+
localVar: "local",
|
|
151
|
+
undef: "undef",
|
|
152
|
+
newArray: "array",
|
|
153
|
+
arrayPush: "array_push",
|
|
154
|
+
arrayPop: "array_pop",
|
|
155
|
+
dict: "dict",
|
|
156
|
+
dictSet: "dict_set",
|
|
157
|
+
// —— video.ts ——
|
|
158
|
+
video: "video",
|
|
159
|
+
cutscene: "cutscene",
|
|
160
|
+
seekVideo: "seek_video",
|
|
161
|
+
pauseVideo: "pause_video",
|
|
162
|
+
resumeVideo: "resume_video",
|
|
163
|
+
stopVideo: "stop_video",
|
|
164
|
+
videoSkipable: "video_skipable",
|
|
165
|
+
// —— 辅助词(补充表登记,不产独立 op) ——
|
|
166
|
+
option: "$menu.option",
|
|
167
|
+
whenChain: "$if.chain",
|
|
168
|
+
reward: "$minigame.reward",
|
|
169
|
+
extOp: "$ext.op",
|
|
170
|
+
sceneSetup: "$composite.scene",
|
|
171
|
+
buttonElement: "$composite.button",
|
|
172
|
+
textElement: "$composite.text",
|
|
173
|
+
imageElement: "$composite.image",
|
|
174
|
+
sceneType: "$column.type",
|
|
175
|
+
};
|