@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视图偏好外置:列分组 / 折叠 = **UI 侧元数据**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不在故事里:**列序 = 文件路径码元序**是叙事语义,用存储层目录分组会隐式改写它
|
|
5
|
+
* (组内顺序、跨组顺序全变);归类是**作者视图偏好**,不是故事语义(编辑器只读写故事树)。
|
|
6
|
+
* 因此本模块三条约束:
|
|
7
|
+
* 1. **永不接受 `Story`**——只收 `readonly string[] columnIds`,结构上不可能改故事树;
|
|
8
|
+
* 2. 不产生 undo 单元、不影响 `dirty`、不出现在故事 JSON / 文本投影 / 写回文件集里;
|
|
9
|
+
* 3. 平台 API(localStorage)由宿主注入(`createColumnGroupingStore`),**本模块零 I/O**。
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 一个分组;`columns` 是**成员集合**——展示序恒按 `columnIds` 重排(见 `layoutColumns`),故不承载顺序语义 */
|
|
14
|
+
export interface ColumnGroup {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
columns: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 持久化形态:版本化(结构演进靠 `version` 判定;顶层不符一律降级为空视图) */
|
|
21
|
+
export interface ColumnGroupingView {
|
|
22
|
+
version: 1;
|
|
23
|
+
groups: ColumnGroup[];
|
|
24
|
+
/** 已折叠的分组 id(同样是视图态,不入故事) */
|
|
25
|
+
collapsed: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 渲染布局:`groups` 只承载分区与折叠,`ungrouped` 是未归类列的**码元序**投影 */
|
|
29
|
+
export interface GroupedLayout {
|
|
30
|
+
groups: {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
collapsed: boolean;
|
|
34
|
+
columns: string[];
|
|
35
|
+
}[];
|
|
36
|
+
ungrouped: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function emptyGroupingView(): ColumnGroupingView {
|
|
40
|
+
return { version: 1, groups: [], collapsed: [] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 解析存储值(接受字符串或已解析值)。
|
|
45
|
+
* 顶层形状 / `version` 不符 → **空视图**;逐条目坏项**跳过**(不因一个坏组丢整份缓存)。
|
|
46
|
+
* 重复归属按**首次归属优先**归一(后到者丢弃),未知分组 id 的折叠项丢弃。
|
|
47
|
+
*/
|
|
48
|
+
export function parseGroupingView(raw: unknown): ColumnGroupingView {
|
|
49
|
+
let value: unknown = raw;
|
|
50
|
+
if (typeof raw === "string") {
|
|
51
|
+
try {
|
|
52
|
+
value = JSON.parse(raw) as unknown;
|
|
53
|
+
} catch {
|
|
54
|
+
return emptyGroupingView(); // 坏 JSON = 忽略缓存
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
58
|
+
return emptyGroupingView();
|
|
59
|
+
}
|
|
60
|
+
const record = value as Record<string, unknown>;
|
|
61
|
+
if (record.version !== 1 || !Array.isArray(record.groups)) {
|
|
62
|
+
return emptyGroupingView();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const groups: ColumnGroup[] = [];
|
|
66
|
+
const seenGroupIds = new Set<string>();
|
|
67
|
+
const assigned = new Set<string>();
|
|
68
|
+
for (const entry of record.groups) {
|
|
69
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const group = entry as Record<string, unknown>;
|
|
73
|
+
const id = group.id;
|
|
74
|
+
if (typeof id !== "string" || id === "" || seenGroupIds.has(id)) continue;
|
|
75
|
+
const columns: string[] = [];
|
|
76
|
+
if (Array.isArray(group.columns)) {
|
|
77
|
+
for (const member of group.columns) {
|
|
78
|
+
if (typeof member !== "string" || member === "") continue;
|
|
79
|
+
if (assigned.has(member)) continue;
|
|
80
|
+
assigned.add(member);
|
|
81
|
+
columns.push(member);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
seenGroupIds.add(id);
|
|
85
|
+
groups.push({
|
|
86
|
+
id,
|
|
87
|
+
name:
|
|
88
|
+
typeof group.name === "string" && group.name !== "" ? group.name : id,
|
|
89
|
+
columns,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const collapsed: string[] = [];
|
|
94
|
+
if (Array.isArray(record.collapsed)) {
|
|
95
|
+
for (const id of record.collapsed) {
|
|
96
|
+
if (typeof id !== "string" || !seenGroupIds.has(id)) continue;
|
|
97
|
+
if (!collapsed.includes(id)) collapsed.push(id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { version: 1, groups, collapsed };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 规范形序列化(字段序固定 ⇒ 字节稳定,便于断言与差量) */
|
|
104
|
+
export function serializeGroupingView(view: ColumnGroupingView): string {
|
|
105
|
+
return JSON.stringify({
|
|
106
|
+
version: 1,
|
|
107
|
+
groups: view.groups.map((group) => ({
|
|
108
|
+
id: group.id,
|
|
109
|
+
name: group.name,
|
|
110
|
+
columns: [...group.columns],
|
|
111
|
+
})),
|
|
112
|
+
collapsed: [...view.collapsed],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 裁剪悬空成员(列被删/改名后调用);**空组保留**——作者可能先建组再收列。
|
|
118
|
+
* 无悬空时**原引用返回**(调用方可据此跳过落盘)。
|
|
119
|
+
*/
|
|
120
|
+
export function pruneGroupingView(
|
|
121
|
+
view: ColumnGroupingView,
|
|
122
|
+
columnIds: readonly string[],
|
|
123
|
+
): ColumnGroupingView {
|
|
124
|
+
const known = new Set(columnIds);
|
|
125
|
+
let dropped = false;
|
|
126
|
+
const groups = view.groups.map((group) => {
|
|
127
|
+
const columns = group.columns.filter((id) => known.has(id));
|
|
128
|
+
if (columns.length === group.columns.length) return group;
|
|
129
|
+
dropped = true;
|
|
130
|
+
return { ...group, columns };
|
|
131
|
+
});
|
|
132
|
+
return dropped
|
|
133
|
+
? { version: 1, groups, collapsed: [...view.collapsed] }
|
|
134
|
+
: view;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 渲染布局:**展示序恒按 `columnIds`(= 列序)重排**——分组只做分区与折叠,
|
|
139
|
+
* 永不改变列序;`groups` 为空时 `ungrouped` 即原列序(视图与平铺完全一致)。
|
|
140
|
+
*/
|
|
141
|
+
export function layoutColumns(
|
|
142
|
+
view: ColumnGroupingView,
|
|
143
|
+
columnIds: readonly string[],
|
|
144
|
+
): GroupedLayout {
|
|
145
|
+
const pruned = pruneGroupingView(view, columnIds);
|
|
146
|
+
const memberOf = new Map<string, string>();
|
|
147
|
+
for (const group of pruned.groups) {
|
|
148
|
+
for (const id of group.columns) {
|
|
149
|
+
if (!memberOf.has(id)) memberOf.set(id, group.id);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
groups: pruned.groups.map((group) => ({
|
|
154
|
+
id: group.id,
|
|
155
|
+
name: group.name,
|
|
156
|
+
collapsed: pruned.collapsed.includes(group.id),
|
|
157
|
+
columns: columnIds.filter((id) => memberOf.get(id) === group.id),
|
|
158
|
+
})),
|
|
159
|
+
ungrouped: columnIds.filter((id) => !memberOf.has(id)),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 新建分组:id = `group-N`(避撞),名字留空回退「新分组」 */
|
|
164
|
+
export function addGroup(
|
|
165
|
+
view: ColumnGroupingView,
|
|
166
|
+
name: string,
|
|
167
|
+
): ColumnGroupingView {
|
|
168
|
+
const used = new Set(view.groups.map((group) => group.id));
|
|
169
|
+
let index = 1;
|
|
170
|
+
while (used.has(`group-${index}`)) index += 1;
|
|
171
|
+
return {
|
|
172
|
+
version: 1,
|
|
173
|
+
groups: [
|
|
174
|
+
...view.groups,
|
|
175
|
+
{
|
|
176
|
+
id: `group-${index}`,
|
|
177
|
+
name: name.trim() === "" ? "新分组" : name,
|
|
178
|
+
columns: [],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
collapsed: [...view.collapsed],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 重命名分组;分组不存在或名字为空 → 原引用返回(无变化,调用方可据此跳过落盘) */
|
|
186
|
+
export function renameGroup(
|
|
187
|
+
view: ColumnGroupingView,
|
|
188
|
+
groupId: string,
|
|
189
|
+
name: string,
|
|
190
|
+
): ColumnGroupingView {
|
|
191
|
+
if (name === "") return view;
|
|
192
|
+
if (!view.groups.some((group) => group.id === groupId)) return view;
|
|
193
|
+
return {
|
|
194
|
+
version: 1,
|
|
195
|
+
groups: view.groups.map((group) =>
|
|
196
|
+
group.id === groupId ? { ...group, name } : group,
|
|
197
|
+
),
|
|
198
|
+
collapsed: [...view.collapsed],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 删除分组:组内列回到「未归类」(**不删列**),折叠态一并清掉 */
|
|
203
|
+
export function removeGroup(
|
|
204
|
+
view: ColumnGroupingView,
|
|
205
|
+
groupId: string,
|
|
206
|
+
): ColumnGroupingView {
|
|
207
|
+
if (!view.groups.some((group) => group.id === groupId)) return view;
|
|
208
|
+
return {
|
|
209
|
+
version: 1,
|
|
210
|
+
groups: view.groups.filter((group) => group.id !== groupId),
|
|
211
|
+
collapsed: view.collapsed.filter((id) => id !== groupId),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 归属变更:`null` = 移出分组(回「未归类」)。
|
|
217
|
+
* **未知分组 id → 原引用返回**(fail-closed:不静默把列改成未归类);已在目标分组 → 无变化。
|
|
218
|
+
*/
|
|
219
|
+
export function assignColumn(
|
|
220
|
+
view: ColumnGroupingView,
|
|
221
|
+
columnId: string,
|
|
222
|
+
groupId: string | null,
|
|
223
|
+
): ColumnGroupingView {
|
|
224
|
+
if (groupId !== null && !view.groups.some((group) => group.id === groupId)) {
|
|
225
|
+
return view;
|
|
226
|
+
}
|
|
227
|
+
const current =
|
|
228
|
+
view.groups.find((group) => group.columns.includes(columnId))?.id ?? null;
|
|
229
|
+
if (current === groupId) return view;
|
|
230
|
+
const stripped = view.groups.map((group) => ({
|
|
231
|
+
...group,
|
|
232
|
+
columns: group.columns.filter((id) => id !== columnId),
|
|
233
|
+
}));
|
|
234
|
+
return {
|
|
235
|
+
version: 1,
|
|
236
|
+
groups:
|
|
237
|
+
groupId === null
|
|
238
|
+
? stripped
|
|
239
|
+
: stripped.map((group) =>
|
|
240
|
+
group.id === groupId
|
|
241
|
+
? { ...group, columns: [...group.columns, columnId] }
|
|
242
|
+
: group,
|
|
243
|
+
),
|
|
244
|
+
collapsed: [...view.collapsed],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 列改名:成员 id 同步(`to` 已属别处则丢弃本次重复 = 首次归属优先) */
|
|
249
|
+
export function renameColumnMember(
|
|
250
|
+
view: ColumnGroupingView,
|
|
251
|
+
from: string,
|
|
252
|
+
to: string,
|
|
253
|
+
): ColumnGroupingView {
|
|
254
|
+
if (from === to) return view;
|
|
255
|
+
if (!view.groups.some((group) => group.columns.includes(from))) return view;
|
|
256
|
+
const seen = new Set<string>();
|
|
257
|
+
return {
|
|
258
|
+
version: 1,
|
|
259
|
+
groups: view.groups.map((group) => {
|
|
260
|
+
const columns: string[] = [];
|
|
261
|
+
for (const id of group.columns) {
|
|
262
|
+
const next = id === from ? to : id;
|
|
263
|
+
if (seen.has(next)) continue;
|
|
264
|
+
seen.add(next);
|
|
265
|
+
columns.push(next);
|
|
266
|
+
}
|
|
267
|
+
return { ...group, columns };
|
|
268
|
+
}),
|
|
269
|
+
collapsed: [...view.collapsed],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 折叠 / 展开;分组不存在 → 原引用返回 */
|
|
274
|
+
export function toggleCollapsed(
|
|
275
|
+
view: ColumnGroupingView,
|
|
276
|
+
groupId: string,
|
|
277
|
+
): ColumnGroupingView {
|
|
278
|
+
if (!view.groups.some((group) => group.id === groupId)) return view;
|
|
279
|
+
return {
|
|
280
|
+
version: 1,
|
|
281
|
+
groups: [...view.groups],
|
|
282
|
+
collapsed: view.collapsed.includes(groupId)
|
|
283
|
+
? view.collapsed.filter((id) => id !== groupId)
|
|
284
|
+
: [...view.collapsed, groupId],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 存储端口(与 `createWebStoragePreferencesPort` 相同的「注入式替身」形态):
|
|
290
|
+
* `localStorage` 由宿主取一次传入,本模块不碰平台 API,测试传内存替身即可。
|
|
291
|
+
*/
|
|
292
|
+
export interface KeyValueStorage {
|
|
293
|
+
getItem(key: string): string | null;
|
|
294
|
+
setItem(key: string, value: string): void;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** 按 `story.id` 归位(对齐节点图布局的 `lingfan-editor-nodepos:` 先例) */
|
|
298
|
+
export const COLUMN_GROUPING_KEY_PREFIX = "lingfan-editor-colgroups:";
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 分组视图的读写:**任何存储失败都不抛**——读失败 = 空视图(退回平铺),
|
|
302
|
+
* 写失败 = 仅本次会话有效(隐私模式 / 配额满)。编辑器永不因视图偏好而不可用。
|
|
303
|
+
*/
|
|
304
|
+
export function createColumnGroupingStore(
|
|
305
|
+
storage: KeyValueStorage | undefined,
|
|
306
|
+
): {
|
|
307
|
+
load(storyId: string): ColumnGroupingView;
|
|
308
|
+
save(storyId: string, view: ColumnGroupingView): void;
|
|
309
|
+
} {
|
|
310
|
+
return {
|
|
311
|
+
load(storyId: string): ColumnGroupingView {
|
|
312
|
+
try {
|
|
313
|
+
return parseGroupingView(
|
|
314
|
+
storage?.getItem(COLUMN_GROUPING_KEY_PREFIX + storyId) ?? null,
|
|
315
|
+
);
|
|
316
|
+
} catch {
|
|
317
|
+
return emptyGroupingView();
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
save(storyId: string, view: ColumnGroupingView): void {
|
|
321
|
+
try {
|
|
322
|
+
storage?.setItem(
|
|
323
|
+
COLUMN_GROUPING_KEY_PREFIX + storyId,
|
|
324
|
+
serializeGroupingView(view),
|
|
325
|
+
);
|
|
326
|
+
} catch {
|
|
327
|
+
/* 存储不可用(配额 / 隐私模式)= 分组仅本次会话有效 */
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 节点图连线建分支:**拉线语义的纯判定**——
|
|
3
|
+
* DOM/事件留在组件(命中检测、临时线),落库语义只在这里(可测、fail-closed)。
|
|
4
|
+
*
|
|
5
|
+
* 语义(列名即标签,jump/menu 的目标都是列):
|
|
6
|
+
* - 源列必须是 **flow**(scene 列的 entry 命令流是展示/交互层,流程分支在 flow 列表达);
|
|
7
|
+
* - 目标列必须是 **flow** 且 ≠ 源(自连无意义;非法 → `null`,调用方给 fail-closed 提示);
|
|
8
|
+
* - 源列**最后一个 `menu`** → 在其 `options` 末尾追加选项(`text` 缺省「新选项」);
|
|
9
|
+
* - 否则 → `commands` 末尾追加 `{op:"jump", target}`。
|
|
10
|
+
* 一次拉线 = 一次 `session.apply` = 一个 undo 单元(提交在宿主 api)。
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** 追加计划:`containerPointer` 相对列指针(如 `commands` / `commands/3/options`) */
|
|
15
|
+
export interface BranchPlan {
|
|
16
|
+
kind: "jump" | "menu-option";
|
|
17
|
+
containerPointer: string;
|
|
18
|
+
index: number;
|
|
19
|
+
/** menu 选项的预填文本(jump 无此字段) */
|
|
20
|
+
text?: string;
|
|
21
|
+
target: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 宽松列形状(编辑器 StoryColumn 兼容:字段缺失一律按「无」处理,不抛) */
|
|
25
|
+
export interface BranchColumnLike {
|
|
26
|
+
id?: unknown;
|
|
27
|
+
kind?: unknown;
|
|
28
|
+
commands?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 目标列是否可作为跳转/选项落点(scene 列是空间层,fail-closed 拒绝) */
|
|
32
|
+
export function isBranchTarget(column: BranchColumnLike | undefined): boolean {
|
|
33
|
+
return column?.kind === "flow";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isMenuCommand(command: unknown): boolean {
|
|
37
|
+
return (
|
|
38
|
+
typeof command === "object" &&
|
|
39
|
+
command !== null &&
|
|
40
|
+
(command as { op?: unknown }).op === "menu"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 拉线落库计划:非法组合(非 flow 源 / 自连)→ `null`;否则返回插入位置与预填值。
|
|
46
|
+
* 只读列形状,不改任何数据(插入由宿主经 `insertAtPointer` 提交)。
|
|
47
|
+
*/
|
|
48
|
+
export function planBranchInsertion(
|
|
49
|
+
column: BranchColumnLike | undefined,
|
|
50
|
+
targetColumnId: string,
|
|
51
|
+
optionText?: string,
|
|
52
|
+
): BranchPlan | null {
|
|
53
|
+
if (column?.kind !== "flow" || typeof column.id !== "string") return null;
|
|
54
|
+
if (column.id === targetColumnId) return null;
|
|
55
|
+
const commands = Array.isArray(column.commands) ? column.commands : [];
|
|
56
|
+
let lastMenuIndex = -1;
|
|
57
|
+
commands.forEach((command, index) => {
|
|
58
|
+
if (isMenuCommand(command)) lastMenuIndex = index;
|
|
59
|
+
});
|
|
60
|
+
if (lastMenuIndex >= 0) {
|
|
61
|
+
const menu = commands[lastMenuIndex] as { options?: unknown };
|
|
62
|
+
const options = Array.isArray(menu.options) ? menu.options : [];
|
|
63
|
+
return {
|
|
64
|
+
kind: "menu-option",
|
|
65
|
+
containerPointer: `commands/${lastMenuIndex}/options`,
|
|
66
|
+
index: options.length,
|
|
67
|
+
text: optionText ?? "新选项",
|
|
68
|
+
target: targetColumnId,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
kind: "jump",
|
|
73
|
+
containerPointer: "commands",
|
|
74
|
+
index: commands.length,
|
|
75
|
+
target: targetColumnId,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 引用指针 → 命令指针(边点击选中用):
|
|
81
|
+
* `/columns/i/commands/j/target` → `/columns/i/commands/j`;
|
|
82
|
+
* menu 选项 `/columns/i/commands/j/options/k/target` → 选中**整个 menu 命令**
|
|
83
|
+
* (属性面板一次可见全部选项);非命令字段指针原样返回(navigate 等未来形态)。
|
|
84
|
+
*/
|
|
85
|
+
export function branchPointerToCommand(pointer: string): string {
|
|
86
|
+
const match = /^(\/columns\/\d+\/commands\/\d+)/.exec(pointer);
|
|
87
|
+
return match === null ? pointer : match[1]!;
|
|
88
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑域出口:JSON Pointer 读取与不可变树编辑、列/命令的结构化改写、
|
|
3
|
+
* 编辑器会话状态、分支连线规划、列分组视图偏好与存档前规范化提示。
|
|
4
|
+
* 全部为纯映射器,只读写故事 JSON。
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
emptyGroupingView,
|
|
8
|
+
parseGroupingView,
|
|
9
|
+
serializeGroupingView,
|
|
10
|
+
pruneGroupingView,
|
|
11
|
+
layoutColumns,
|
|
12
|
+
addGroup,
|
|
13
|
+
renameGroup,
|
|
14
|
+
removeGroup,
|
|
15
|
+
assignColumn,
|
|
16
|
+
renameColumnMember,
|
|
17
|
+
toggleCollapsed,
|
|
18
|
+
COLUMN_GROUPING_KEY_PREFIX,
|
|
19
|
+
createColumnGroupingStore,
|
|
20
|
+
type ColumnGroup,
|
|
21
|
+
type ColumnGroupingView,
|
|
22
|
+
type GroupedLayout,
|
|
23
|
+
type KeyValueStorage,
|
|
24
|
+
} from "./columnGrouping.ts";
|
|
25
|
+
export {
|
|
26
|
+
isBranchTarget,
|
|
27
|
+
planBranchInsertion,
|
|
28
|
+
branchPointerToCommand,
|
|
29
|
+
type BranchPlan,
|
|
30
|
+
type BranchColumnLike,
|
|
31
|
+
} from "./graphConnect.ts";
|
|
32
|
+
export {
|
|
33
|
+
buildPointer,
|
|
34
|
+
parsePointer,
|
|
35
|
+
getAtPointer,
|
|
36
|
+
setAtPointer,
|
|
37
|
+
removeAtPointer,
|
|
38
|
+
insertAtPointer,
|
|
39
|
+
moveAtPointer,
|
|
40
|
+
nearestCommandPointer,
|
|
41
|
+
describeSelection,
|
|
42
|
+
type SelectionKind,
|
|
43
|
+
type SelectionNodeKind,
|
|
44
|
+
type SelectionDescription,
|
|
45
|
+
} from "./pointers.ts";
|
|
46
|
+
export {
|
|
47
|
+
NORMALIZATION_NOTICE_PREF_KEY,
|
|
48
|
+
readSkipNormalizationNotice,
|
|
49
|
+
writeSkipNormalizationNotice,
|
|
50
|
+
describeNormalization,
|
|
51
|
+
} from "./saveNormalization.ts";
|
|
52
|
+
export {
|
|
53
|
+
TS_SOURCE_MANIFEST_KEY,
|
|
54
|
+
TS_SOURCE_READONLY_HINT,
|
|
55
|
+
assertTsSourceWritable,
|
|
56
|
+
isTsSourceProject,
|
|
57
|
+
tsSourceWriteBlockers,
|
|
58
|
+
} from "./tsSourceGuard.ts";
|
|
59
|
+
export { EditorSession } from "./session.ts";
|
|
60
|
+
export {
|
|
61
|
+
columnContainers,
|
|
62
|
+
containerPointer,
|
|
63
|
+
suggestColumnId,
|
|
64
|
+
addColumn,
|
|
65
|
+
removeColumn,
|
|
66
|
+
firstColumnIdOfSourcePath,
|
|
67
|
+
renameColumn,
|
|
68
|
+
insertCommand,
|
|
69
|
+
removeCommand,
|
|
70
|
+
updateCommandField,
|
|
71
|
+
moveCommand,
|
|
72
|
+
} from "./storyOps.ts";
|