@lingfan-engine/editor 0.1.0 → 0.1.3

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.
@@ -1,161 +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
- }
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
+ }
@@ -1,71 +1,71 @@
1
- /**
2
- * i18n 工具链 · 缺译 / 多译对账。
3
- *
4
- * 对账 = 原文键集合与各语言 overlay 键集合的双向差:
5
- * - **缺译**(正向)= 原文 − 该语言 overlay(按语言分组;运行期「缺译文回退原文」的预警)
6
- * - **多译**(反向)= overlay 键 − 原文(与诊断 `unused-translation` 同口径:未使用 = 冗余)
7
- *
8
- * 纯函数 + 可读报告文本(编辑器面板一节 / CLI stdout 共用)。加密 overlay(`.json.enc`)
9
- * 走同一解密供给后与明文同口径参与(供给侧职责见 adapters `loadDiagnosticSupply`)。
10
- */
11
-
12
- import type { EditorTexts } from "../contracts/index.ts";
13
- import { resolveEditorTexts } from "../contracts/index.ts";
14
-
15
- export interface TranslationReconcileReport {
16
- /** 每语言缺译键(稳定排序;语言无缺译时为空数组——「齐」也是显式状态) */
17
- missingByLang: ReadonlyMap<string, readonly string[]>;
18
- /** 多译键(跨语言并集口径,稳定排序;= unused-translation 诊断同源) */
19
- unused: readonly string[];
20
- }
21
-
22
- /**
23
- * 三态对账:某语言缺译 / 多译 / 齐。`overlayKeysByLang` 的键 = 语言码
24
- * (单语言工具传单键对象即可)。
25
- */
26
- export function reconcileTranslations(
27
- sources: readonly string[],
28
- overlayKeysByLang: Readonly<Record<string, readonly string[]>>,
29
- ): TranslationReconcileReport {
30
- const sourceSet = new Set(sources);
31
- const missingByLang = new Map<string, readonly string[]>();
32
- const unused = new Set<string>();
33
- for (const [lang, keys] of Object.entries(overlayKeysByLang)) {
34
- const keySet = new Set(keys);
35
- for (const key of keySet) {
36
- if (!sourceSet.has(key)) unused.add(key);
37
- }
38
- const missing: string[] = [];
39
- for (const source of sourceSet) {
40
- if (!keySet.has(source)) missing.push(source);
41
- }
42
- missing.sort();
43
- missingByLang.set(lang, missing);
44
- }
45
- return { missingByLang, unused: [...unused].sort() };
46
- }
47
-
48
- /** 可读报告(编辑器面板一节 / CLI stdout 共用);逐语言缺译 + 多译两节。
49
- * `texts`(可选)= 集中文案表注入(缺省 defaultEditorTexts)。 */
50
- export function formatTranslationReport(
51
- report: TranslationReconcileReport,
52
- texts: Partial<EditorTexts> = {},
53
- ): string {
54
- const t = resolveEditorTexts(texts);
55
- const lines: string[] = [];
56
- for (const [lang, missing] of [...report.missingByLang.entries()].sort()) {
57
- if (missing.length === 0) {
58
- lines.push(t.missingZero(lang));
59
- continue;
60
- }
61
- lines.push(t.missingCount(lang, missing.length));
62
- for (const key of missing) lines.push(` - ${key}`);
63
- }
64
- if (report.unused.length === 0) {
65
- lines.push(t.unusedZero());
66
- } else {
67
- lines.push(t.unusedCount(report.unused.length));
68
- for (const key of report.unused) lines.push(` - ${key}`);
69
- }
70
- return lines.join("\n");
71
- }
1
+ /**
2
+ * i18n 工具链 · 缺译 / 多译对账。
3
+ *
4
+ * 对账 = 原文键集合与各语言 overlay 键集合的双向差:
5
+ * - **缺译**(正向)= 原文 − 该语言 overlay(按语言分组;运行期「缺译文回退原文」的预警)
6
+ * - **多译**(反向)= overlay 键 − 原文(与诊断 `unused-translation` 同口径:未使用 = 冗余)
7
+ *
8
+ * 纯函数 + 可读报告文本(编辑器面板一节 / CLI stdout 共用)。加密 overlay(`.json.enc`)
9
+ * 走同一解密供给后与明文同口径参与(供给侧职责见 adapters `loadDiagnosticSupply`)。
10
+ */
11
+
12
+ import type { EditorTexts } from "../contracts/index.ts";
13
+ import { resolveEditorTexts } from "../contracts/index.ts";
14
+
15
+ export interface TranslationReconcileReport {
16
+ /** 每语言缺译键(稳定排序;语言无缺译时为空数组——「齐」也是显式状态) */
17
+ missingByLang: ReadonlyMap<string, readonly string[]>;
18
+ /** 多译键(跨语言并集口径,稳定排序;= unused-translation 诊断同源) */
19
+ unused: readonly string[];
20
+ }
21
+
22
+ /**
23
+ * 三态对账:某语言缺译 / 多译 / 齐。`overlayKeysByLang` 的键 = 语言码
24
+ * (单语言工具传单键对象即可)。
25
+ */
26
+ export function reconcileTranslations(
27
+ sources: readonly string[],
28
+ overlayKeysByLang: Readonly<Record<string, readonly string[]>>,
29
+ ): TranslationReconcileReport {
30
+ const sourceSet = new Set(sources);
31
+ const missingByLang = new Map<string, readonly string[]>();
32
+ const unused = new Set<string>();
33
+ for (const [lang, keys] of Object.entries(overlayKeysByLang)) {
34
+ const keySet = new Set(keys);
35
+ for (const key of keySet) {
36
+ if (!sourceSet.has(key)) unused.add(key);
37
+ }
38
+ const missing: string[] = [];
39
+ for (const source of sourceSet) {
40
+ if (!keySet.has(source)) missing.push(source);
41
+ }
42
+ missing.sort();
43
+ missingByLang.set(lang, missing);
44
+ }
45
+ return { missingByLang, unused: [...unused].sort() };
46
+ }
47
+
48
+ /** 可读报告(编辑器面板一节 / CLI stdout 共用);逐语言缺译 + 多译两节。
49
+ * `texts`(可选)= 集中文案表注入(缺省 defaultEditorTexts)。 */
50
+ export function formatTranslationReport(
51
+ report: TranslationReconcileReport,
52
+ texts: Partial<EditorTexts> = {},
53
+ ): string {
54
+ const t = resolveEditorTexts(texts);
55
+ const lines: string[] = [];
56
+ for (const [lang, missing] of [...report.missingByLang.entries()].sort()) {
57
+ if (missing.length === 0) {
58
+ lines.push(t.missingZero(lang));
59
+ continue;
60
+ }
61
+ lines.push(t.missingCount(lang, missing.length));
62
+ for (const key of missing) lines.push(` - ${key}`);
63
+ }
64
+ if (report.unused.length === 0) {
65
+ lines.push(t.unusedZero());
66
+ } else {
67
+ lines.push(t.unusedCount(report.unused.length));
68
+ for (const key of report.unused) lines.push(` - ${key}`);
69
+ }
70
+ return lines.join("\n");
71
+ }