@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.
- 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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表单契约:属性面板表单描述符,由 op schema(Zod 单一事实源)派生。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** op 分组(属性面板/时间线插入菜单的呈现归类) */
|
|
6
|
+
export type OpGroup =
|
|
7
|
+
| "narrative"
|
|
8
|
+
| "presentation"
|
|
9
|
+
| "flow"
|
|
10
|
+
| "variables"
|
|
11
|
+
| "save"
|
|
12
|
+
| "audio"
|
|
13
|
+
| "video"
|
|
14
|
+
| "minigame";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 字段呈现语义(驱动诊断扫描面与表单控件选型):
|
|
18
|
+
* - text:插值面(先 Translate 后插值 + {var} 求值,行内标记透传)
|
|
19
|
+
* - expression:表达式面(cond/on/in;失败即停机)
|
|
20
|
+
* - value:任意值(字符串 {expr}/复合赋值 = 表达式,其余字面量)
|
|
21
|
+
* - identifier:变量/列/函数/槽位名
|
|
22
|
+
* - resource:资源逻辑路径(经 ResourcePort 寻址)
|
|
23
|
+
*/
|
|
24
|
+
export type FieldKind =
|
|
25
|
+
| "string"
|
|
26
|
+
| "number"
|
|
27
|
+
| "integer"
|
|
28
|
+
| "boolean"
|
|
29
|
+
| "enum"
|
|
30
|
+
| "text"
|
|
31
|
+
| "expression"
|
|
32
|
+
| "value"
|
|
33
|
+
| "identifier"
|
|
34
|
+
| "resource"
|
|
35
|
+
| "array"
|
|
36
|
+
| "object"
|
|
37
|
+
| "tuple"
|
|
38
|
+
/** 命令体(if.then/while.body 等):子面板递归编辑 */
|
|
39
|
+
| "body";
|
|
40
|
+
|
|
41
|
+
export interface FieldDescriptor {
|
|
42
|
+
/** 本层字段名(嵌套项内的字段相对该项根) */
|
|
43
|
+
key: string;
|
|
44
|
+
label: string;
|
|
45
|
+
kind: FieldKind;
|
|
46
|
+
required: boolean;
|
|
47
|
+
hasDefault: boolean;
|
|
48
|
+
defaultValue?: unknown;
|
|
49
|
+
/** enum 可选值 */
|
|
50
|
+
enumValues?: readonly string[];
|
|
51
|
+
/** array/tuple 的元素描述 */
|
|
52
|
+
item?: FieldDescriptor;
|
|
53
|
+
tupleItems?: FieldDescriptor[];
|
|
54
|
+
/** object 的子字段 */
|
|
55
|
+
properties?: FieldDescriptor[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface OpMeta {
|
|
59
|
+
op: string;
|
|
60
|
+
label: string;
|
|
61
|
+
group: OpGroup;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface OpFormDescriptor {
|
|
65
|
+
op: string;
|
|
66
|
+
label: string;
|
|
67
|
+
group: OpGroup;
|
|
68
|
+
fields: FieldDescriptor[];
|
|
69
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
AnalyzeOptions,
|
|
3
|
+
Diagnostic,
|
|
4
|
+
DiagnosticSeverity,
|
|
5
|
+
SymbolIndex,
|
|
6
|
+
} from "./diagnostics.ts";
|
|
7
|
+
export { escapePointerToken, joinPointer } from "../shared/index.ts";
|
|
8
|
+
export type {
|
|
9
|
+
FieldDescriptor,
|
|
10
|
+
FieldKind,
|
|
11
|
+
OpFormDescriptor,
|
|
12
|
+
OpGroup,
|
|
13
|
+
OpMeta,
|
|
14
|
+
} from "./forms.ts";
|
|
15
|
+
export type {
|
|
16
|
+
ContainerField,
|
|
17
|
+
ContainerInfo,
|
|
18
|
+
StoryListener,
|
|
19
|
+
UndoEntry,
|
|
20
|
+
} from "./editing.ts";
|
|
21
|
+
export type { EditorTexts } from "./texts.ts";
|
|
22
|
+
export { defaultEditorTexts, resolveEditorTexts } from "./texts.ts";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑器集中文案表(contract 层只放类型 + 缺省实现,与 engine 的 contracts/messages.ts 同形)。
|
|
3
|
+
*
|
|
4
|
+
* 规则:
|
|
5
|
+
* - 所有面向用户的中文文案都从这里走;缺省串即编辑器的既有文案(只搬位置,不改文案)。
|
|
6
|
+
* - 消费方经函数参数可选注入 `texts?: Partial<EditorTexts>`,缺省 `defaultEditorTexts`;
|
|
7
|
+
* 禁止模块级可变全局或注册表 setter。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 编辑器集中文案表:每个方法对应一条已迁移的用户可见文案(方法名即文案身份)。 */
|
|
11
|
+
export interface EditorTexts {
|
|
12
|
+
/** diagnostics:columnId 重复(columnId 全局唯一)。 */
|
|
13
|
+
duplicateColumn(columnId: string): string;
|
|
14
|
+
/** diagnostics:入口列不存在。 */
|
|
15
|
+
missingEntryColumn(entry: string): string;
|
|
16
|
+
/** diagnostics:跳转目标列不存在。 */
|
|
17
|
+
missingJumpTargetColumn(target: string): string;
|
|
18
|
+
/** diagnostics:调用未注册的函数。 */
|
|
19
|
+
unknownFunction(target: string): string;
|
|
20
|
+
/** diagnostics:调用目标既不是 func 也不是列。 */
|
|
21
|
+
missingCallTarget(target: string): string;
|
|
22
|
+
/** diagnostics:表达式里使用未定义变量。 */
|
|
23
|
+
undefinedVariableExpression(name: string): string;
|
|
24
|
+
/** diagnostics:插值里使用未定义变量。 */
|
|
25
|
+
undefinedVariableInterpolation(name: string): string;
|
|
26
|
+
/** diagnostics:资源路径不存在。 */
|
|
27
|
+
missingResource(path: string): string;
|
|
28
|
+
/** diagnostics:未使用的译文键。 */
|
|
29
|
+
unusedTranslationKey(key: string): string;
|
|
30
|
+
/** diagnostics:原文未有任何译文。 */
|
|
31
|
+
missingTranslationOriginal(original: string): string;
|
|
32
|
+
/** diagnostics:元素属性已声明但无渲染语义。 */
|
|
33
|
+
unimplementedElementAttr(attr: string): string;
|
|
34
|
+
/** diagnostics:元素动作序列里不可用的动作。 */
|
|
35
|
+
elementOpsBlockedOp(op: string): string;
|
|
36
|
+
|
|
37
|
+
/** schema/validate:命令带未知负载字段(编辑期 fail-closed)。 */
|
|
38
|
+
unknownPayloadFields(op: string, keys: string): string;
|
|
39
|
+
/** schema/validate:必填字段缺失(path 为空时无点段)。 */
|
|
40
|
+
missingRequired(op: string, path: string): string;
|
|
41
|
+
/** schema/validate:字段值不合法(localized 是 zod issue 的中文翻译)。 */
|
|
42
|
+
invalidValue(op: string, path: string, localized: string): string;
|
|
43
|
+
/** schema/validate:命令必须为对象。 */
|
|
44
|
+
commandMustBeObject(): string;
|
|
45
|
+
/** schema/validate:命令必须含非空 op 字符串。 */
|
|
46
|
+
commandMustHaveOp(): string;
|
|
47
|
+
/** schema/validate:未知或未实现的命令。 */
|
|
48
|
+
unknownCommand(op: string): string;
|
|
49
|
+
/** schema/validate:资源路径不能为空。 */
|
|
50
|
+
resourcePathEmpty(): string;
|
|
51
|
+
/** schema/validate:资源路径不能为空(要停止该通道请用 ${stopOp})。 */
|
|
52
|
+
resourcePathEmptyStop(stopOp: string): string;
|
|
53
|
+
/** schema/validate:数值太小。 */
|
|
54
|
+
valueTooSmall(at: string, min: number): string;
|
|
55
|
+
/** schema/validate:值太小(无数值上下文)。 */
|
|
56
|
+
valueTooSmallBare(at: string): string;
|
|
57
|
+
/** schema/validate:数值太大。 */
|
|
58
|
+
valueTooBig(at: string, max: number): string;
|
|
59
|
+
/** schema/validate:值太大(无数值上下文)。 */
|
|
60
|
+
valueTooBigBare(at: string): string;
|
|
61
|
+
/** schema/validate:类型不对(expected 缺省时无括注)。 */
|
|
62
|
+
invalidType(at: string, expected: string): string;
|
|
63
|
+
/** schema/validate:格式不对(format 缺省时无括注)。 */
|
|
64
|
+
invalidFormat(at: string, format: string): string;
|
|
65
|
+
|
|
66
|
+
/** i18n/table:译文表不是合法 JSON。 */
|
|
67
|
+
invalidJson(error: string): string;
|
|
68
|
+
/** i18n/table:译文表根节点必须是对象。 */
|
|
69
|
+
tableRootMustBeObject(): string;
|
|
70
|
+
/** i18n/table:键不能为空。 */
|
|
71
|
+
keyEmpty(): string;
|
|
72
|
+
/** i18n/table:键的首尾不能有空白。 */
|
|
73
|
+
keySurroundingWhitespace(): string;
|
|
74
|
+
/** i18n/table:键不能含控制字符。 */
|
|
75
|
+
keyControlChars(): string;
|
|
76
|
+
/** i18n/table:键已存在。 */
|
|
77
|
+
keyAlreadyExists(key: string): string;
|
|
78
|
+
/** i18n/table:目标键不在表中。 */
|
|
79
|
+
keyNotInTable(key: string): string;
|
|
80
|
+
/** i18n/table:改名目标键已存在,不能合并两行。 */
|
|
81
|
+
renameConflict(key: string): string;
|
|
82
|
+
|
|
83
|
+
/** i18n/skeleton:语言码不合法。 */
|
|
84
|
+
invalidLangCode(lang: string): string;
|
|
85
|
+
/** i18n/skeleton:故事路径不合法。 */
|
|
86
|
+
invalidStoryPath(storyPath: string): string;
|
|
87
|
+
|
|
88
|
+
/** i18n/reconcile:某语言缺译 0 条(覆盖完整)。 */
|
|
89
|
+
missingZero(lang: string): string;
|
|
90
|
+
/** i18n/reconcile:某语言缺译 N 条。 */
|
|
91
|
+
missingCount(lang: string, count: number): string;
|
|
92
|
+
/** i18n/reconcile:多译 0 条(无冗余译文键)。 */
|
|
93
|
+
unusedZero(): string;
|
|
94
|
+
/** i18n/reconcile:多译 N 条。 */
|
|
95
|
+
unusedCount(count: number): string;
|
|
96
|
+
|
|
97
|
+
/** script/expr:未 defineVars 就使用 expr。 */
|
|
98
|
+
exprRequiresDefineVars(): string;
|
|
99
|
+
/** script/expr:不支持的插值类型。 */
|
|
100
|
+
unsupportedInterpolationType(type: string): string;
|
|
101
|
+
/** script/expr 警告:插值落在字符串字面量内部。 */
|
|
102
|
+
interpolationInsideString(): string;
|
|
103
|
+
/** script/expr 警告:未闭合的字符串字面量。 */
|
|
104
|
+
unclosedStringLiteral(): string;
|
|
105
|
+
/** script/expr:变量句柄路径非法。 */
|
|
106
|
+
invalidVarHandlePath(prop: string, prefix: string): string;
|
|
107
|
+
/** script/expr:未注册的变量。 */
|
|
108
|
+
unregisteredVariable(key: string): string;
|
|
109
|
+
/** script/expr 审计:操作数类型不匹配。 */
|
|
110
|
+
operandKindMismatch(op: string, side: string, want: string, described: string, note: string): string;
|
|
111
|
+
/** script/expr 审计:三元条件必须为 boolean。 */
|
|
112
|
+
ternaryCondNotBool(described: string): string;
|
|
113
|
+
/** script/expr 审计:不支持跨类型比较。 */
|
|
114
|
+
crossTypeComparison(op: string, left: string, right: string): string;
|
|
115
|
+
/** script/expr 审计:未知类型的操作数标签。 */
|
|
116
|
+
unknownOperandKind(): string;
|
|
117
|
+
|
|
118
|
+
/** schema/surface:扩展 op 归组不合法。 */
|
|
119
|
+
invalidOpGroup(extensionId: string, op: string, declaredGroup: string, validGroups: string): string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 缺省文案表:即编辑器的既有文案(只搬位置,不改文案)。 */
|
|
123
|
+
export const defaultEditorTexts: EditorTexts = {
|
|
124
|
+
duplicateColumn: (columnId) => `columnId 重复:${columnId}(columnId 全局唯一)`,
|
|
125
|
+
missingEntryColumn: (entry) => `入口列 ${entry} 不存在`,
|
|
126
|
+
missingJumpTargetColumn: (target) => `跳转目标列不存在:${target}`,
|
|
127
|
+
unknownFunction: (target) => `调用未注册的函数:${target}`,
|
|
128
|
+
missingCallTarget: (target) => `调用目标不存在:${target}(既不是 func 也不是列)`,
|
|
129
|
+
undefinedVariableExpression: (name) =>
|
|
130
|
+
`未定义变量:${name}(表达式里使用,求值会失败并**停机**——请先用 set/define 定义它)`,
|
|
131
|
+
undefinedVariableInterpolation: (name) =>
|
|
132
|
+
`未定义变量:${name}(插值失败将按原文保留——若它应是变量请先定义,若只是普通文字请去掉花括号)`,
|
|
133
|
+
missingResource: (path) => `资源路径不存在:${path}`,
|
|
134
|
+
unusedTranslationKey: (key) =>
|
|
135
|
+
`未使用的译文键:${key}(say / menu / input / notify 四个翻译面均未命中原文——该键运行期不会生效;若这段文字只用于元素文本或宿主界面,本条可忽略)`,
|
|
136
|
+
missingTranslationOriginal: (original) =>
|
|
137
|
+
`原文未有任何译文:${original}(所有语言的 overlay 均未命中——运行期回退原文;专有名词等有意保留原文的可忽略)`,
|
|
138
|
+
unimplementedElementAttr: (attr) => `元素属性 ${attr} 已声明但当前无渲染语义(写入不生效)`,
|
|
139
|
+
elementOpsBlockedOp: (op) =>
|
|
140
|
+
`元素动作序列里的 ${op} 不可用(等待/位置/存档类会打断当前叙事流);跳列请用 nav 属性`,
|
|
141
|
+
|
|
142
|
+
unknownPayloadFields: (op, keys) => `${op} 未知负载字段:${keys}(编辑期 fail-closed)`,
|
|
143
|
+
missingRequired: (op, path) => `${op}${path ? `.${path}` : ""} 必填`,
|
|
144
|
+
invalidValue: (op, path, localized) => `${op}${path ? `.${path}` : ""}:${localized}`,
|
|
145
|
+
commandMustBeObject: () => "命令必须为对象",
|
|
146
|
+
commandMustHaveOp: () => "命令必须为含非空 op 字符串的命令对象",
|
|
147
|
+
unknownCommand: (op) => `未知或未实现的命令:${op}`,
|
|
148
|
+
resourcePathEmpty: () => "资源路径不能为空",
|
|
149
|
+
resourcePathEmptyStop: (stopOp) => `资源路径不能为空(要**停止**该通道请用 \`${stopOp}\`)`,
|
|
150
|
+
valueTooSmall: (at, min) => `${at}不能小于 ${min}`,
|
|
151
|
+
valueTooSmallBare: (at) => `${at}值太小`,
|
|
152
|
+
valueTooBig: (at, max) => `${at}不能大于 ${max}`,
|
|
153
|
+
valueTooBigBare: (at) => `${at}值太大`,
|
|
154
|
+
invalidType: (at, expected) =>
|
|
155
|
+
expected === "" ? `${at}类型不对` : `${at}类型不对(需要 ${expected})`,
|
|
156
|
+
invalidFormat: (at, format) =>
|
|
157
|
+
format === "" ? `${at}格式不对` : `${at}格式不对(要求 ${format})`,
|
|
158
|
+
|
|
159
|
+
invalidJson: (error) => `不是合法 JSON:${error}`,
|
|
160
|
+
tableRootMustBeObject: () => "译文表根节点必须是对象(键 → 译文)",
|
|
161
|
+
keyEmpty: () => "键不能为空",
|
|
162
|
+
keySurroundingWhitespace: () => "键的首尾不能有空白",
|
|
163
|
+
keyControlChars: () => "键不能含控制字符",
|
|
164
|
+
keyAlreadyExists: (key) => `键「${key}」已存在`,
|
|
165
|
+
keyNotInTable: (key) => `键「${key}」不在表中`,
|
|
166
|
+
renameConflict: (key) => `键「${key}」已存在,不能合并两行`,
|
|
167
|
+
|
|
168
|
+
invalidLangCode: (lang) => `语言码不合法:${lang}(须为单段安全文件名)`,
|
|
169
|
+
invalidStoryPath: (storyPath) =>
|
|
170
|
+
`故事路径不合法:${storyPath}(须为相对 Stories/ 的段级安全路径,如 chapter1/tavern)`,
|
|
171
|
+
|
|
172
|
+
missingZero: (lang) => `[${lang}] 缺译 0 条(覆盖完整)`,
|
|
173
|
+
missingCount: (lang, count) => `[${lang}] 缺译 ${count} 条(运行期回退原文):`,
|
|
174
|
+
unusedZero: () => "多译 0 条(无冗余译文键)",
|
|
175
|
+
unusedCount: (count) => `多译 ${count} 条(未使用 = 冗余):`,
|
|
176
|
+
|
|
177
|
+
exprRequiresDefineVars: () => "expr 需要先 defineVars(变量注册是表达式句柄的前提)",
|
|
178
|
+
unsupportedInterpolationType: (type) =>
|
|
179
|
+
`expr 不支持的插值类型:${type}——若是变量句柄,请确认已在 defineVars 注册`,
|
|
180
|
+
interpolationInsideString: () =>
|
|
181
|
+
"插值落在字符串字面量内部(引号未闭合)——插值文本会被并进字面量,不会成为表达式操作数",
|
|
182
|
+
unclosedStringLiteral: () => "表达式存在未闭合的字符串字面量(引擎将 parse-error)",
|
|
183
|
+
invalidVarHandlePath: (prop, prefix) => `变量句柄路径非法:${prop}(前缀 ${prefix})`,
|
|
184
|
+
unregisteredVariable: (key) => `未注册的变量:${key}——请在 defineVars 清单中声明后再引用`,
|
|
185
|
+
operandKindMismatch: (op, side, want, described, note) =>
|
|
186
|
+
`'${op}' ${side}操作数必须为 ${want}——${described}(引擎 type-error${note})`,
|
|
187
|
+
ternaryCondNotBool: (described) =>
|
|
188
|
+
`三元 '?' 条件必须为 boolean(引擎 type-error)——条件${described}`,
|
|
189
|
+
crossTypeComparison: (op, left, right) =>
|
|
190
|
+
`'${op}' 不支持跨类型比较(${left} vs ${right})——引擎仅同类型标量相等`,
|
|
191
|
+
unknownOperandKind: () => "未知类型",
|
|
192
|
+
|
|
193
|
+
invalidOpGroup: (extensionId, op, declaredGroup, validGroups) =>
|
|
194
|
+
`扩展「${extensionId}」的 op「${op}」归组不合法:${declaredGroup}(须为 ${validGroups} 之一)`,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 把调用方可选注入的部分文案合并成完整表:`{ ...defaultEditorTexts, ...texts }`。
|
|
199
|
+
* 各消费函数内部用它把 `texts?: Partial<EditorTexts>` 归一成 `EditorTexts`。
|
|
200
|
+
* (契约层只放类型:以 const 箭头函数形态导出——export function 违反包结构互锁测试。)
|
|
201
|
+
*/
|
|
202
|
+
export const resolveEditorTexts = (
|
|
203
|
+
texts?: Partial<EditorTexts>,
|
|
204
|
+
): EditorTexts => ({ ...defaultEditorTexts, ...texts });
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分析入口:先取符号索引(./symbols),再叠加结构校验与逐条语义诊断。
|
|
3
|
+
* 行为契约(诊断族与缺省跳过口径)见 analyzeStory 的函数注释。
|
|
4
|
+
*/
|
|
5
|
+
import type { Story } from "@lingfan-engine/core";
|
|
6
|
+
import { ELEMENT_OPS_BLOCKED } from "@lingfan-engine/core";
|
|
7
|
+
import type { AnalyzeOptions, Diagnostic, EditorTexts } from "../contracts/index.ts";
|
|
8
|
+
import { escapePointerToken, resolveEditorTexts } from "../contracts/index.ts";
|
|
9
|
+
import { UNIMPLEMENTED_ELEMENT_ATTRS } from "../schema/index.ts";
|
|
10
|
+
import { walkStoryElements } from "../schema/index.ts";
|
|
11
|
+
import { validateStory } from "../schema/index.ts";
|
|
12
|
+
import { isPlainObject } from "../shared/index.ts";
|
|
13
|
+
import { indexStory } from "./symbols.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 编辑期诊断集(结构校验 + 语义诊断,一律带 JSON Pointer)。
|
|
17
|
+
* resourceFiles / overlayKeys 缺省时对应诊断族跳过(供给侧数据未接入不误报)。
|
|
18
|
+
* `texts`(可选)= 集中文案表注入(缺省 defaultEditorTexts)。
|
|
19
|
+
*/
|
|
20
|
+
export function analyzeStory(
|
|
21
|
+
story: Story,
|
|
22
|
+
options: AnalyzeOptions = {},
|
|
23
|
+
): Diagnostic[] {
|
|
24
|
+
const texts: EditorTexts = resolveEditorTexts(options.texts);
|
|
25
|
+
const out: Diagnostic[] = validateStory(story);
|
|
26
|
+
const index = indexStory(story);
|
|
27
|
+
|
|
28
|
+
for (const duplicate of index.duplicateColumns) {
|
|
29
|
+
out.push({
|
|
30
|
+
code: "duplicate-column",
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: texts.duplicateColumn(duplicate.id),
|
|
33
|
+
pointer: duplicate.pointer,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
typeof index.entry === "string" &&
|
|
38
|
+
index.entry !== "" &&
|
|
39
|
+
!index.columnPointers.has(index.entry)
|
|
40
|
+
) {
|
|
41
|
+
out.push({
|
|
42
|
+
code: "missing-entry",
|
|
43
|
+
severity: "error",
|
|
44
|
+
message: texts.missingEntryColumn(index.entry),
|
|
45
|
+
pointer: "/entry",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
for (const target of index.targets) {
|
|
49
|
+
const found =
|
|
50
|
+
target.kind === "column"
|
|
51
|
+
? index.columnPointers.has(target.target)
|
|
52
|
+
: target.kind === "function"
|
|
53
|
+
? index.functions.has(target.target)
|
|
54
|
+
: // callable:列或 func **任一存在**即通过
|
|
55
|
+
index.columnPointers.has(target.target) ||
|
|
56
|
+
index.functions.has(target.target);
|
|
57
|
+
if (!found) {
|
|
58
|
+
out.push({
|
|
59
|
+
// `"function"` 分支当前**无字段产出**(`call` 用的是 `callable`)——
|
|
60
|
+
// 保留它是**契约完备性**(`TargetKind` 有三种,判定要覆盖三种),
|
|
61
|
+
// 将来若出现「只允许 func」的新字段即可直接复用(当前无产出路径,非冗余代码)。
|
|
62
|
+
code:
|
|
63
|
+
target.kind === "function" ? "unknown-function" : "missing-target",
|
|
64
|
+
severity: "error",
|
|
65
|
+
message:
|
|
66
|
+
target.kind === "column"
|
|
67
|
+
? texts.missingJumpTargetColumn(target.target)
|
|
68
|
+
: target.kind === "function"
|
|
69
|
+
? texts.unknownFunction(target.target)
|
|
70
|
+
: texts.missingCallTarget(target.target),
|
|
71
|
+
pointer: target.pointer,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const ref of index.variableRefs) {
|
|
76
|
+
if (ref.name.startsWith("_")) continue;
|
|
77
|
+
if (index.definedKeys.has(ref.name)) continue;
|
|
78
|
+
out.push({
|
|
79
|
+
code: "undefined-variable",
|
|
80
|
+
severity: ref.kind === "expression" ? "error" : "warning",
|
|
81
|
+
message:
|
|
82
|
+
ref.kind === "expression"
|
|
83
|
+
? texts.undefinedVariableExpression(ref.name)
|
|
84
|
+
: texts.undefinedVariableInterpolation(ref.name),
|
|
85
|
+
pointer: ref.pointer,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (options.resourceFiles !== undefined) {
|
|
89
|
+
for (const resource of index.resources) {
|
|
90
|
+
if (!options.resourceFiles.has(resource.path)) {
|
|
91
|
+
out.push({
|
|
92
|
+
code: "missing-resource",
|
|
93
|
+
// 提醒级:素材"先写引用后补"是正常工作流,
|
|
94
|
+
// error 会拦保存门禁;运行期缺资源由运行时自己 fail-closed 兜底。
|
|
95
|
+
severity: "warning",
|
|
96
|
+
message: texts.missingResource(resource.path),
|
|
97
|
+
pointer: resource.pointer,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (options.overlayKeys !== undefined) {
|
|
103
|
+
for (const key of options.overlayKeys) {
|
|
104
|
+
if (!index.originals.has(key)) {
|
|
105
|
+
out.push({
|
|
106
|
+
code: "unused-translation",
|
|
107
|
+
severity: "warning",
|
|
108
|
+
message: texts.unusedTranslationKey(key),
|
|
109
|
+
pointer: "",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// 缺译(正向,与 unused-translation 反向对称):原文在**全部语言**的 overlay
|
|
114
|
+
// 里都没有 = 玩家必然看到原文(提醒级,不拦保存)。仅在「工程确实启用了 i18n」
|
|
115
|
+
// (overlay 非空)时提醒——不启用 i18n 的工程原文直出是常态("不需要多语言
|
|
116
|
+
// 就不需要 i18n"),不制造全量噪声。
|
|
117
|
+
if (options.overlayKeys.size > 0) {
|
|
118
|
+
for (const original of index.originals) {
|
|
119
|
+
if (!options.overlayKeys.has(original)) {
|
|
120
|
+
out.push({
|
|
121
|
+
code: "missing-translation",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message: texts.missingTranslationOriginal(original),
|
|
124
|
+
pointer: "",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 未实现属性清单:已声明但**当前无渲染语义**的元素属性(写了不生效且静默)→ warning。
|
|
131
|
+
// 指针精确到该属性;清单与表单下架同源(`UNIMPLEMENTED_ELEMENT_ATTRS`),避免两份事实。
|
|
132
|
+
walkStoryElements(story, (node, pointer) => {
|
|
133
|
+
if (!isPlainObject(node)) return; // 非对象由 invalid-element 负责
|
|
134
|
+
for (const attr of UNIMPLEMENTED_ELEMENT_ATTRS) {
|
|
135
|
+
if (node[attr] === undefined) continue;
|
|
136
|
+
out.push({
|
|
137
|
+
code: "unimplemented-element-attr",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: texts.unimplementedElementAttr(attr),
|
|
140
|
+
pointer: `${pointer}/${escapePointerToken(attr)}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// 点击动作序列里的**执行期必拒** op:编辑期即报,别等运行时才发现点不动。
|
|
144
|
+
// 清单与执行期同源(引擎契约 `ELEMENT_OPS_BLOCKED`)——两处各写一份必然漂移。
|
|
145
|
+
const ops = node.ops;
|
|
146
|
+
if (Array.isArray(ops)) {
|
|
147
|
+
ops.forEach((item, i) => {
|
|
148
|
+
if (!isPlainObject(item)) return; // 形态问题由 invalid-element 负责
|
|
149
|
+
const op = item.op;
|
|
150
|
+
if (typeof op !== "string" || !ELEMENT_OPS_BLOCKED.has(op)) return;
|
|
151
|
+
out.push({
|
|
152
|
+
code: "element-ops-blocked-op",
|
|
153
|
+
severity: "error",
|
|
154
|
+
message: texts.elementOpsBlockedOp(op),
|
|
155
|
+
pointer: `${pointer}/ops/${i}`,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断面板 · **按类型分组与折叠判据**(纯函数,可测)。
|
|
3
|
+
*
|
|
4
|
+
* 存在理由:诊断面板是编辑期的「仪表盘」,而工程里**同质诊断会淹没异质诊断**
|
|
5
|
+
* —— 常见情形是 33 条里 31 条都是 `unused-translation`,把 2 条真错误
|
|
6
|
+
* (`undefined-variable`)埋掉了。根因是**信息架构**(无分组/折叠),
|
|
7
|
+
* **不是排版**:调间距/字号救不了「扫读」。
|
|
8
|
+
*
|
|
9
|
+
* 排序与折叠两条规则都写在这里(视图只渲染 + 收集意图):
|
|
10
|
+
*
|
|
11
|
+
* 1. **严重度优先**(`error` 在前)⇒ 真错误永远浮在顶部,不被警告挤下去。
|
|
12
|
+
* 2. **同严重度按条数降序** ⇒ 最「成灾」的类型最显眼(1 条 warning 不该压过 8 条 warning)。
|
|
13
|
+
* 3. **同条数按 code 码元序** ⇒ 组间顺序确定,不随输入顺序抖动。
|
|
14
|
+
*
|
|
15
|
+
* **组内也按 pointer 排序**(不是保留输入顺序):若组顺序确定但组内
|
|
16
|
+
* 随输入顺序变,`JSON.stringify` 结果就不同 ⇒ 同一份诊断两次渲染出不同 DOM,
|
|
17
|
+
* 一切「按结果判等」的守卫都会假绿。**pointer 是稳定的天然键**(无 pointer 的
|
|
18
|
+
* 全局诊断排在最后、同pointer 组内再按 code 排,仍是确定的)。
|
|
19
|
+
*
|
|
20
|
+
* **折叠不是「一律折」**:`error` 组**永不默认折叠**(错误被藏起来 = 仪表盘失效);
|
|
21
|
+
* 只有 `warning` 组在条数 > 1 时可默认折叠,且**只对每组第一条生效**(其余由用户手动控制)。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { Diagnostic, DiagnosticSeverity } from "../contracts/index.ts";
|
|
25
|
+
|
|
26
|
+
/** 严重度排序权重(数字小 = 靠前) */
|
|
27
|
+
const SEVERITY_RANK: Readonly<Record<DiagnosticSeverity, number>> = {
|
|
28
|
+
error: 0,
|
|
29
|
+
warning: 1,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 诊断码的中文标签(面板组头显示;**未知码如实回显原码**,不编造也不隐藏) */
|
|
33
|
+
const CODE_LABELS: Readonly<Record<string, string>> = {
|
|
34
|
+
"unknown-op": "未知命令",
|
|
35
|
+
"unknown-field": "未知字段",
|
|
36
|
+
"missing-required": "缺必填字段",
|
|
37
|
+
"invalid-value": "值不合法",
|
|
38
|
+
"duplicate-column": "列 id 重复",
|
|
39
|
+
"missing-target": "跳转目标缺失",
|
|
40
|
+
"unknown-function": "函数不存在",
|
|
41
|
+
"undefined-variable": "未定义变量",
|
|
42
|
+
"missing-resource": "资源不存在",
|
|
43
|
+
"missing-entry": "入口列缺失",
|
|
44
|
+
"invalid-structure": "结构不合法",
|
|
45
|
+
"unused-translation": "未使用的译文键",
|
|
46
|
+
"missing-translation": "缺译文",
|
|
47
|
+
"invalid-element": "元素不合法",
|
|
48
|
+
"unimplemented-element-attr": "元素属性未实现",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** 组头显示的标签(未知码 ⇒ 原样回显,**不吞信息**) */
|
|
52
|
+
export function diagnosticCodeLabel(code: string): string {
|
|
53
|
+
return CODE_LABELS[code] ?? code;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 一个诊断分组 */
|
|
57
|
+
export interface DiagnosticGroup {
|
|
58
|
+
/** 诊断码(同组同码) */
|
|
59
|
+
readonly code: string;
|
|
60
|
+
/** 组头标签(中文;未知码为原码) */
|
|
61
|
+
readonly label: string;
|
|
62
|
+
/** 该组最严重的严重度(组内可能混档,取最高) */
|
|
63
|
+
readonly severity: DiagnosticSeverity;
|
|
64
|
+
/** 组内条目(**按 pointer 稳定排序**——确定性,见文件头排序规则) */
|
|
65
|
+
readonly items: readonly Diagnostic[];
|
|
66
|
+
/** **是否建议默认折叠**(`error` 组恒 `false`) */
|
|
67
|
+
readonly collapsedByDefault: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 分组判据。空输入 ⇒ 空数组(空态由视图表达,不塞占位组) */
|
|
71
|
+
export function groupDiagnostics(
|
|
72
|
+
diagnostics: readonly Diagnostic[],
|
|
73
|
+
): readonly DiagnosticGroup[] {
|
|
74
|
+
const buckets = new Map<string, Diagnostic[]>();
|
|
75
|
+
for (const d of diagnostics) {
|
|
76
|
+
const bucket = buckets.get(d.code);
|
|
77
|
+
if (bucket === undefined) buckets.set(d.code, [d]);
|
|
78
|
+
else bucket.push(d);
|
|
79
|
+
}
|
|
80
|
+
const groups: DiagnosticGroup[] = [];
|
|
81
|
+
for (const [code, items] of buckets) {
|
|
82
|
+
// 组严重度 = 取最高(error > warning)——同组可能混档
|
|
83
|
+
const severity: DiagnosticSeverity = items.some(
|
|
84
|
+
(d) => d.severity === "error",
|
|
85
|
+
)
|
|
86
|
+
? "error"
|
|
87
|
+
: "warning";
|
|
88
|
+
groups.push({
|
|
89
|
+
code,
|
|
90
|
+
label: diagnosticCodeLabel(code),
|
|
91
|
+
severity,
|
|
92
|
+
// 组内排序:pointer 非空优先(可定位的排前面),再按 pointer 码元序。
|
|
93
|
+
// 空 pointer = 全局诊断,天然沉底。
|
|
94
|
+
items: [...items].sort(compareItems),
|
|
95
|
+
// error 组永不默认折叠;warning 组多条时才建议折(单条折了没意义且更烦)
|
|
96
|
+
collapsedByDefault: severity !== "error" && items.length > 1,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return groups.sort(compareGroups);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 组内比较:有 pointer 优先 → pointer 码元序 → message 码元序(**全序,确定性**) */
|
|
103
|
+
function compareItems(a: Diagnostic, b: Diagnostic): number {
|
|
104
|
+
if ((a.pointer === "") !== (b.pointer === ""))
|
|
105
|
+
return a.pointer === "" ? 1 : -1;
|
|
106
|
+
if (a.pointer !== b.pointer) return a.pointer < b.pointer ? -1 : 1;
|
|
107
|
+
return a.message < b.message ? -1 : a.message > b.message ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 组间比较:严重度 → 条数降序 → code 码元序(**确定性**,不抖) */
|
|
111
|
+
function compareGroups(a: DiagnosticGroup, b: DiagnosticGroup): number {
|
|
112
|
+
const bySeverity = SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
|
|
113
|
+
if (bySeverity !== 0) return bySeverity;
|
|
114
|
+
if (a.items.length !== b.items.length) return b.items.length - a.items.length;
|
|
115
|
+
return a.code < b.code ? -1 : a.code > b.code ? 1 : 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 面板顶部汇总文案(**不逐条罗列**——那正是淹没的来源):`3 条 · 2 类 · 1 个错误` */
|
|
119
|
+
export interface DiagnosticSummary {
|
|
120
|
+
readonly total: number;
|
|
121
|
+
readonly kinds: number;
|
|
122
|
+
readonly errors: number;
|
|
123
|
+
readonly warnings: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function summarizeDiagnostics(
|
|
127
|
+
groups: readonly DiagnosticGroup[],
|
|
128
|
+
): DiagnosticSummary {
|
|
129
|
+
let errors = 0;
|
|
130
|
+
let warnings = 0;
|
|
131
|
+
for (const g of groups) {
|
|
132
|
+
for (const d of g.items) {
|
|
133
|
+
if (d.severity === "error") errors += 1;
|
|
134
|
+
else warnings += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { total: errors + warnings, kinds: groups.length, errors, warnings };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 汇总文本(**零诊断时给「无」而不是「0 条 · 0 类」**——那不是状态,是空数据的算术) */
|
|
141
|
+
export function diagnosticSummaryText(summary: DiagnosticSummary): string {
|
|
142
|
+
if (summary.total === 0) return "无";
|
|
143
|
+
const parts: string[] = [];
|
|
144
|
+
if (summary.errors > 0) parts.push(`${summary.errors} 个错误`);
|
|
145
|
+
if (summary.warnings > 0) parts.push(`${summary.warnings} 个警告`);
|
|
146
|
+
return `${summary.total} 条 · ${parts.join(" · ")}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 严重度筛选(**null = 全部**)。
|
|
151
|
+
*
|
|
152
|
+
* 为什么进纯函数:徽章「可点筛选」的语义只有一条——按 severity 等值过滤、
|
|
153
|
+
* **不重排不改写**(排序仍归 `groupDiagnostics`)。放这里组件就没有机会
|
|
154
|
+
* 在筛选时夹带别的规则(比如把全局诊断滤掉),两处口径永不漂移。
|
|
155
|
+
*/
|
|
156
|
+
export type SeverityFilter = "error" | "warning" | null;
|
|
157
|
+
|
|
158
|
+
export function filterDiagnosticsBySeverity(
|
|
159
|
+
diagnostics: readonly Diagnostic[],
|
|
160
|
+
filter: SeverityFilter,
|
|
161
|
+
): readonly Diagnostic[] {
|
|
162
|
+
if (filter === null) return diagnostics;
|
|
163
|
+
return diagnostics.filter((d) => d.severity === filter);
|
|
164
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 诊断(编辑期):符号索引 + 诊断集,全部带 JSON Pointer。
|
|
3
|
+
* - 未定义变量:定义集 = defines + set/define/let/local/array/dict 键 + random.var +
|
|
4
|
+
* 循环变量 + input.store(并集保守策略:let/local 块级精度为已知限界,不误报优先);
|
|
5
|
+
* `_` 前缀豁免;行内标记白名单镜像执行器语义(已定义变量 > 行内标记)。
|
|
6
|
+
* - 跳转目标不存在、未知函数、重复 columnId、入口列缺失、资源路径缺失、
|
|
7
|
+
* 未使用翻译键(overlay 键 − 可翻译原文)、已声明但无渲染语义的元素属性。
|
|
8
|
+
*
|
|
9
|
+
* 实现分布:表达式引用抽取在 ./refs,符号索引在 ./symbols,分析入口在 ./analyze
|
|
10
|
+
* ——本文件只做转发,不承载实现。
|
|
11
|
+
*/
|
|
12
|
+
export { extractExpressionRefs } from "./refs.ts";
|
|
13
|
+
export { indexStory } from "./symbols.ts";
|
|
14
|
+
export { analyzeStory } from "./analyze.ts";
|
|
15
|
+
export type { AnalyzeOptions } from "../contracts/index.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 诊断集的分组与摘要(可按严重度筛选),以及消息文本的拆分与一句话简述。
|
|
19
|
+
* 两族住在同域的其他文件里,此处一并转发,让本目录只有一个取用入口。
|
|
20
|
+
*/
|
|
21
|
+
export {
|
|
22
|
+
diagnosticCodeLabel,
|
|
23
|
+
diagnosticSummaryText,
|
|
24
|
+
filterDiagnosticsBySeverity,
|
|
25
|
+
groupDiagnostics,
|
|
26
|
+
summarizeDiagnostics,
|
|
27
|
+
type DiagnosticGroup,
|
|
28
|
+
type DiagnosticSummary,
|
|
29
|
+
type SeverityFilter,
|
|
30
|
+
} from "./grouping.ts";
|
|
31
|
+
export {
|
|
32
|
+
diagnosticBrief,
|
|
33
|
+
splitDiagnosticMessage,
|
|
34
|
+
type DiagnosticMessageParts,
|
|
35
|
+
} from "./message.ts";
|