mellos-mapping 0.20.2 → 0.22.1
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/README.md +125 -88
- package/README.zh-CN.md +122 -84
- package/dist/hook-session-start.mjs +25 -19
- package/dist/mmap.mjs +302 -170
- package/dist/preview.mjs +1418 -0
- package/dist/server.mjs +1198 -400
- package/dist/store-paths.mjs +40 -21
- package/dist/terminal-worker.mjs +3188 -0
- package/dist/watch.mjs +523 -280
- package/dist/web/TERMINAL-LICENSES.txt +70 -0
- package/dist/web/app.css +967 -0
- package/dist/web/app.js +1356 -0
- package/dist/web/index.html +9 -0
- package/dist/web/terminal.css +9 -0
- package/dist/web/terminal.html +7 -0
- package/dist/web/terminal.js +9293 -0
- package/dist/web/xterm.css +285 -0
- package/dist/web.mjs +5535 -0
- package/docs/codex.md +183 -0
- package/lib/domain/text.d.ts +9 -0
- package/lib/domain/text.js +43 -0
- package/lib/domain/types.js +10 -1
- package/lib/preview/index.d.ts +3 -0
- package/lib/preview/index.js +3 -0
- package/lib/preview/markdown.d.ts +8 -0
- package/lib/preview/markdown.js +54 -0
- package/lib/preview/presentation.d.ts +6 -0
- package/lib/preview/presentation.js +14 -0
- package/lib/preview/publisher.d.ts +23 -0
- package/lib/preview/publisher.js +143 -0
- package/lib/preview/svg.d.ts +3 -0
- package/lib/preview/svg.js +74 -0
- package/lib/preview/text.d.ts +4 -0
- package/lib/preview/text.js +13 -0
- package/lib/render/canvas.d.ts +1 -1
- package/lib/render/canvas.js +4 -2
- package/lib/render/draw.js +9 -6
- package/lib/render/render.d.ts +6 -0
- package/lib/render/render.js +50 -18
- package/lib/render/width.js +3 -1
- package/lib/store/atomic.d.ts +18 -0
- package/lib/store/atomic.js +86 -0
- package/lib/store/channels.d.ts +40 -0
- package/lib/store/channels.js +135 -0
- package/lib/store/format.js +3 -1
- package/lib/store/json-text.d.ts +9 -0
- package/lib/store/json-text.js +16 -0
- package/lib/store/maps.d.ts +12 -0
- package/lib/store/maps.js +42 -0
- package/lib/store/migration.d.ts +12 -0
- package/lib/store/migration.js +46 -0
- package/lib/store/pages.d.ts +46 -0
- package/lib/store/pages.js +89 -0
- package/lib/store/policy.d.ts +74 -0
- package/lib/store/policy.js +144 -0
- package/lib/store/store.d.ts +9 -256
- package/lib/store/store.js +10 -694
- package/lib/store/viewers.d.ts +81 -0
- package/lib/store/viewers.js +186 -0
- package/package.json +25 -6
- package/scripts/codex-cli.mjs +42 -0
- package/scripts/codex-register.mjs +27 -94
- package/scripts/mmap.mjs +26 -16
- package/scripts/open-pane.mjs +37 -18
- package/scripts/pane-core.mjs +57 -220
- package/scripts/terminal-session.mjs +137 -0
- package/scripts/tmux-session.mjs +90 -0
- package/scripts/watcher-command.mjs +16 -0
package/dist/preview.mjs
ADDED
|
@@ -0,0 +1,1418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/preview/cli.ts
|
|
4
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
5
|
+
import { join as join4, resolve as resolve2 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/domain/types.ts
|
|
9
|
+
var ok = (value) => ({ ok: true, value });
|
|
10
|
+
var err = (error) => ({ ok: false, error });
|
|
11
|
+
var ID_RULE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
12
|
+
var ID_RULE_TEXT = "lowercase letters, digits and dashes, starting with a letter or digit, 1-64 chars";
|
|
13
|
+
function makeNodeId(raw) {
|
|
14
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
15
|
+
}
|
|
16
|
+
function makeLayerId(raw) {
|
|
17
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
18
|
+
}
|
|
19
|
+
function makeGroupId(raw) {
|
|
20
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
21
|
+
}
|
|
22
|
+
function makeLaneId(raw) {
|
|
23
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
24
|
+
}
|
|
25
|
+
function makeNodeKind(raw) {
|
|
26
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
27
|
+
}
|
|
28
|
+
function makeSubmapRef(raw) {
|
|
29
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
30
|
+
}
|
|
31
|
+
var RANK_MIN = 0;
|
|
32
|
+
var RANK_MAX = 99;
|
|
33
|
+
var RANK_RULE_TEXT = `an integer in ${RANK_MIN}..${RANK_MAX}, 0 = bottom / most primitive`;
|
|
34
|
+
function makeRank(raw) {
|
|
35
|
+
return Number.isInteger(raw) && raw >= RANK_MIN && raw <= RANK_MAX ? ok(raw) : err({ kind: "invalid-rank", raw, rule: RANK_RULE_TEXT });
|
|
36
|
+
}
|
|
37
|
+
var MAP_KINDS = ["dev", "architecture", "dataflow", "behavior-tree", "sequence"];
|
|
38
|
+
function makeMapKind(raw) {
|
|
39
|
+
return MAP_KINDS.includes(raw) ? ok(raw) : err({ kind: "invalid-map-kind", raw });
|
|
40
|
+
}
|
|
41
|
+
var NODE_STATUSES = ["planned", "in-progress", "done", "regressed"];
|
|
42
|
+
function makeNodeStatus(raw) {
|
|
43
|
+
return NODE_STATUSES.includes(raw) ? ok(raw) : err({ kind: "invalid-status", raw });
|
|
44
|
+
}
|
|
45
|
+
var EMPTY_MAP = { layers: [], groups: [], lanes: [], nodes: [], edges: [] };
|
|
46
|
+
function describeMapError(e) {
|
|
47
|
+
switch (e.kind) {
|
|
48
|
+
case "invalid-id":
|
|
49
|
+
return `invalid id "${e.raw}" (rule: ${e.rule})`;
|
|
50
|
+
case "invalid-rank":
|
|
51
|
+
return `invalid rank ${e.raw} (rule: ${e.rule})`;
|
|
52
|
+
case "invalid-status":
|
|
53
|
+
return `invalid status "${e.raw}" (expected: ${NODE_STATUSES.join(" | ")})`;
|
|
54
|
+
case "duplicate-layer":
|
|
55
|
+
return `layer "${e.id}" already exists`;
|
|
56
|
+
case "duplicate-rank":
|
|
57
|
+
return `rank ${e.rank} is already taken by layer "${e.existing}"`;
|
|
58
|
+
case "duplicate-node":
|
|
59
|
+
return `node "${e.id}" already exists`;
|
|
60
|
+
case "unknown-layer":
|
|
61
|
+
return `layer "${e.id}" does not exist`;
|
|
62
|
+
case "unknown-node":
|
|
63
|
+
return `node "${e.id}" does not exist`;
|
|
64
|
+
case "duplicate-edge":
|
|
65
|
+
return `edge ${e.from} -> ${e.to} already exists`;
|
|
66
|
+
case "unknown-edge":
|
|
67
|
+
return `edge ${e.from} -> ${e.to} does not exist`;
|
|
68
|
+
case "self-edge":
|
|
69
|
+
return `node "${e.id}" cannot depend on itself`;
|
|
70
|
+
case "duplicate-group":
|
|
71
|
+
return `group "${e.id}" already exists`;
|
|
72
|
+
case "id-collision":
|
|
73
|
+
return `id "${e.id}" already names a ${e.taken} on this map; nodes and groups share one id namespace (both render as boxes, so one id must mean one box) \u2014 rename "${e.id}"`;
|
|
74
|
+
case "unknown-group":
|
|
75
|
+
return `group "${e.id}" does not exist`;
|
|
76
|
+
case "invalid-map-kind":
|
|
77
|
+
return `invalid map kind "${e.raw}" (expected: ${MAP_KINDS.join(" | ")})`;
|
|
78
|
+
case "duplicate-lane":
|
|
79
|
+
return `lane "${e.id}" already exists`;
|
|
80
|
+
case "unknown-lane":
|
|
81
|
+
return `lane "${e.id}" does not exist`;
|
|
82
|
+
case "group-layer-mismatch":
|
|
83
|
+
return `node "${e.node}" (layer ${e.nodeLayer}) cannot join group "${e.group}" (layer ${e.groupLayer}); groups cluster nodes within one band`;
|
|
84
|
+
case "layer-not-empty":
|
|
85
|
+
return `layer "${e.id}" still holds node "${e.occupant}"; move its nodes to another band (moveNode) or remove them (removeNode) first`;
|
|
86
|
+
case "layer-holds-group":
|
|
87
|
+
return `layer "${e.id}" still holds group "${e.occupant}"; remove its groups (removeGroup) first`;
|
|
88
|
+
case "edge-not-downward":
|
|
89
|
+
return `edge ${e.from} (rank ${e.fromRank}) -> ${e.to} (rank ${e.toRank}) is not strictly downward; ` + (e.fromRank === e.toRank ? `same-band siblings may not depend on each other \u2014 either "${e.to}" is really a lower concept (declare it on a lower band) or "${e.from}" and "${e.to}" are one node (merge them)` : `"${e.from}" would USE "${e.to}" from a lower band \u2014 reverse the edge if "${e.to}" is the user, otherwise move or re-rank so "${e.from}" sits above "${e.to}"`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/domain/ops.ts
|
|
94
|
+
function findLayer(map, id) {
|
|
95
|
+
return map.layers.find((l) => l.id === id);
|
|
96
|
+
}
|
|
97
|
+
function findNode(map, id) {
|
|
98
|
+
return map.nodes.find((n) => n.id === id);
|
|
99
|
+
}
|
|
100
|
+
function findGroup(map, id) {
|
|
101
|
+
return map.groups.find((g) => g.id === id);
|
|
102
|
+
}
|
|
103
|
+
function checkMembership(map, node, nodeLayer, group) {
|
|
104
|
+
const g = findGroup(map, group);
|
|
105
|
+
if (!g) return { kind: "unknown-group", id: group };
|
|
106
|
+
if (g.layer !== nodeLayer)
|
|
107
|
+
return { kind: "group-layer-mismatch", node, nodeLayer, group, groupLayer: g.layer };
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
function hasEdge(map, from, to) {
|
|
111
|
+
return map.edges.some((e) => e.from === from && e.to === to);
|
|
112
|
+
}
|
|
113
|
+
function checkIdSpace(map, id, declaring) {
|
|
114
|
+
const taken = declaring === "node" ? map.groups.some((g) => g.id === id) : map.nodes.some((n) => n.id === id);
|
|
115
|
+
return taken ? { kind: "id-collision", id, taken: declaring === "node" ? "group" : "node" } : void 0;
|
|
116
|
+
}
|
|
117
|
+
function setTitle(map, title) {
|
|
118
|
+
if (title === null || title === void 0) {
|
|
119
|
+
const { title: _dropped, ...rest } = map;
|
|
120
|
+
return rest;
|
|
121
|
+
}
|
|
122
|
+
return { ...map, title };
|
|
123
|
+
}
|
|
124
|
+
function setKind(map, kind) {
|
|
125
|
+
return { ...map, kind };
|
|
126
|
+
}
|
|
127
|
+
function findLane(map, id) {
|
|
128
|
+
return map.lanes.find((l) => l.id === id);
|
|
129
|
+
}
|
|
130
|
+
function declareLane(map, input) {
|
|
131
|
+
if (findLane(map, input.id)) return err({ kind: "duplicate-lane", id: input.id });
|
|
132
|
+
return ok({ ...map, lanes: [...map.lanes, { id: input.id, label: input.label }] });
|
|
133
|
+
}
|
|
134
|
+
function declareLayer(map, input) {
|
|
135
|
+
if (findLayer(map, input.id)) return err({ kind: "duplicate-layer", id: input.id });
|
|
136
|
+
const rankHolder = map.layers.find((l) => l.rank === input.rank);
|
|
137
|
+
if (rankHolder) return err({ kind: "duplicate-rank", rank: input.rank, existing: rankHolder.id });
|
|
138
|
+
return ok({ ...map, layers: [...map.layers, { id: input.id, name: input.name, rank: input.rank }] });
|
|
139
|
+
}
|
|
140
|
+
function declareGroup(map, input) {
|
|
141
|
+
if (findGroup(map, input.id)) return err({ kind: "duplicate-group", id: input.id });
|
|
142
|
+
const collision = checkIdSpace(map, input.id, "group");
|
|
143
|
+
if (collision) return err(collision);
|
|
144
|
+
if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
|
|
145
|
+
return ok({ ...map, groups: [...map.groups, { id: input.id, label: input.label, layer: input.layer }] });
|
|
146
|
+
}
|
|
147
|
+
function declareNode(map, input) {
|
|
148
|
+
if (findNode(map, input.id)) return err({ kind: "duplicate-node", id: input.id });
|
|
149
|
+
const collision = checkIdSpace(map, input.id, "node");
|
|
150
|
+
if (collision) return err(collision);
|
|
151
|
+
if (!findLayer(map, input.layer)) return err({ kind: "unknown-layer", id: input.layer });
|
|
152
|
+
if (input.group !== void 0) {
|
|
153
|
+
const bad = checkMembership(map, input.id, input.layer, input.group);
|
|
154
|
+
if (bad) return err(bad);
|
|
155
|
+
}
|
|
156
|
+
if (input.lane !== void 0 && !findLane(map, input.lane)) return err({ kind: "unknown-lane", id: input.lane });
|
|
157
|
+
const node = {
|
|
158
|
+
id: input.id,
|
|
159
|
+
label: input.label,
|
|
160
|
+
layer: input.layer,
|
|
161
|
+
status: input.status ?? "planned",
|
|
162
|
+
...input.evidence !== void 0 ? { evidence: input.evidence } : {},
|
|
163
|
+
...input.detail !== void 0 ? { detail: input.detail } : {},
|
|
164
|
+
...input.group !== void 0 ? { group: input.group } : {},
|
|
165
|
+
...input.kind !== void 0 ? { kind: input.kind } : {},
|
|
166
|
+
...input.lane !== void 0 ? { lane: input.lane } : {},
|
|
167
|
+
...input.submap !== void 0 ? { submap: input.submap } : {}
|
|
168
|
+
};
|
|
169
|
+
return ok({ ...map, nodes: [...map.nodes, node] });
|
|
170
|
+
}
|
|
171
|
+
function linkNodes(map, from, to, label) {
|
|
172
|
+
if (from === to) return err({ kind: "self-edge", id: from });
|
|
173
|
+
const fromNode = findNode(map, from);
|
|
174
|
+
if (!fromNode) return err({ kind: "unknown-node", id: from });
|
|
175
|
+
const toNode = findNode(map, to);
|
|
176
|
+
if (!toNode) return err({ kind: "unknown-node", id: to });
|
|
177
|
+
if (hasEdge(map, from, to)) return err({ kind: "duplicate-edge", from, to });
|
|
178
|
+
const fromRank = findLayer(map, fromNode.layer).rank;
|
|
179
|
+
const toRank = findLayer(map, toNode.layer).rank;
|
|
180
|
+
if (fromRank <= toRank) return err({ kind: "edge-not-downward", from, fromRank, to, toRank });
|
|
181
|
+
return ok({ ...map, edges: [...map.edges, { from, to, ...label !== void 0 ? { label } : {} }] });
|
|
182
|
+
}
|
|
183
|
+
function resolveOptional(input, current) {
|
|
184
|
+
return input === void 0 ? current : input === null ? void 0 : input;
|
|
185
|
+
}
|
|
186
|
+
function updateNode(map, input) {
|
|
187
|
+
const node = findNode(map, input.id);
|
|
188
|
+
if (!node) return err({ kind: "unknown-node", id: input.id });
|
|
189
|
+
if (input.group !== void 0 && input.group !== null) {
|
|
190
|
+
const bad = checkMembership(map, node.id, node.layer, input.group);
|
|
191
|
+
if (bad) return err(bad);
|
|
192
|
+
}
|
|
193
|
+
if (input.lane !== void 0 && input.lane !== null && !findLane(map, input.lane)) {
|
|
194
|
+
return err({ kind: "unknown-lane", id: input.lane });
|
|
195
|
+
}
|
|
196
|
+
const {
|
|
197
|
+
group: currentGroup,
|
|
198
|
+
kind: currentKind,
|
|
199
|
+
lane: currentLane,
|
|
200
|
+
submap: currentSubmap,
|
|
201
|
+
evidence: currentEvidence,
|
|
202
|
+
detail: currentDetail,
|
|
203
|
+
...bare
|
|
204
|
+
} = node;
|
|
205
|
+
const nextGroup = resolveOptional(input.group, currentGroup);
|
|
206
|
+
const nextKind = resolveOptional(input.kind, currentKind);
|
|
207
|
+
const nextLane = resolveOptional(input.lane, currentLane);
|
|
208
|
+
const nextSubmap = resolveOptional(input.submap, currentSubmap);
|
|
209
|
+
const nextEvidence = resolveOptional(input.evidence, currentEvidence);
|
|
210
|
+
const nextDetail = resolveOptional(input.detail, currentDetail);
|
|
211
|
+
const updated = {
|
|
212
|
+
...bare,
|
|
213
|
+
...nextEvidence !== void 0 ? { evidence: nextEvidence } : {},
|
|
214
|
+
...nextDetail !== void 0 ? { detail: nextDetail } : {},
|
|
215
|
+
...nextGroup !== void 0 ? { group: nextGroup } : {},
|
|
216
|
+
...nextKind !== void 0 ? { kind: nextKind } : {},
|
|
217
|
+
...nextLane !== void 0 ? { lane: nextLane } : {},
|
|
218
|
+
...nextSubmap !== void 0 ? { submap: nextSubmap } : {},
|
|
219
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
220
|
+
...input.label !== void 0 ? { label: input.label } : {}
|
|
221
|
+
};
|
|
222
|
+
return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/domain/text.ts
|
|
226
|
+
var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
|
|
227
|
+
var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
|
|
228
|
+
var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
|
|
229
|
+
var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
|
|
230
|
+
function mapTextError(map) {
|
|
231
|
+
const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
|
|
232
|
+
let error = check("title", map.title);
|
|
233
|
+
if (error) return error;
|
|
234
|
+
for (const [i, layer] of map.layers.entries()) {
|
|
235
|
+
error = check(`layers[${i}].name`, layer.name);
|
|
236
|
+
if (error) return error;
|
|
237
|
+
}
|
|
238
|
+
for (const name of ["lanes", "groups"]) {
|
|
239
|
+
for (const [i, item] of map[name].entries()) {
|
|
240
|
+
error = check(`${name}[${i}].label`, item.label);
|
|
241
|
+
if (error) return error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const [i, node] of map.nodes.entries()) {
|
|
245
|
+
for (const name of ["label", "evidence", "detail"]) {
|
|
246
|
+
error = check(`nodes[${i}].${name}`, node[name], name !== "label");
|
|
247
|
+
if (error) return error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const [i, edge] of map.edges.entries()) {
|
|
251
|
+
error = check(`edges[${i}].label`, edge.label);
|
|
252
|
+
if (error) return error;
|
|
253
|
+
}
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
function terminalText(text, multiline = false) {
|
|
257
|
+
return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/store/format.ts
|
|
261
|
+
var STATE_FILE_VERSION = 1;
|
|
262
|
+
function makePageId(raw) {
|
|
263
|
+
return ID_RULE.test(raw) ? ok(raw) : err({ kind: "invalid-id", raw, rule: ID_RULE_TEXT });
|
|
264
|
+
}
|
|
265
|
+
function describeStoreError(e) {
|
|
266
|
+
switch (e.kind) {
|
|
267
|
+
case "not-found":
|
|
268
|
+
return `no map file at ${e.path}`;
|
|
269
|
+
case "malformed-json":
|
|
270
|
+
return `map file ${e.path} is not valid JSON: ${e.detail}`;
|
|
271
|
+
case "bad-shape":
|
|
272
|
+
return `map file ${e.path} has an unexpected shape: ${e.detail}`;
|
|
273
|
+
case "invariant-violation":
|
|
274
|
+
return `map file ${e.path} violates a structural invariant: ${describeMapError(e.violation)}`;
|
|
275
|
+
case "save-failed":
|
|
276
|
+
return `could not write ${e.path}: ${e.detail}`;
|
|
277
|
+
case "delete-failed":
|
|
278
|
+
return `could not delete ${e.path}: ${e.detail}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function isRecord(v) {
|
|
282
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
283
|
+
}
|
|
284
|
+
function describeValue(v) {
|
|
285
|
+
if (v === void 0) return "missing";
|
|
286
|
+
if (v === null) return "null";
|
|
287
|
+
if (Array.isArray(v)) return "an array";
|
|
288
|
+
return `a ${typeof v}`;
|
|
289
|
+
}
|
|
290
|
+
function badShape(path, where, expected, got) {
|
|
291
|
+
return err({ kind: "bad-shape", path, detail: `${where} is ${describeValue(got)}, expected ${expected}` });
|
|
292
|
+
}
|
|
293
|
+
function arrayField(raw, key, path, presence) {
|
|
294
|
+
const v = raw[key];
|
|
295
|
+
if (Array.isArray(v)) return ok(v);
|
|
296
|
+
if (v === void 0 && presence === "optional") return ok([]);
|
|
297
|
+
return badShape(path, `"${key}"`, "an array", v);
|
|
298
|
+
}
|
|
299
|
+
function requiredString(rec, key, where, path) {
|
|
300
|
+
const v = rec[key];
|
|
301
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
302
|
+
}
|
|
303
|
+
function optionalString(rec, key, where, path) {
|
|
304
|
+
const v = rec[key];
|
|
305
|
+
if (v === void 0) return ok(void 0);
|
|
306
|
+
return typeof v === "string" ? ok(v) : badShape(path, `${where}.${key}`, "a string", v);
|
|
307
|
+
}
|
|
308
|
+
function parseMap(raw, path) {
|
|
309
|
+
if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
|
|
310
|
+
if (raw["version"] !== STATE_FILE_VERSION) {
|
|
311
|
+
return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
|
|
312
|
+
}
|
|
313
|
+
const layers = arrayField(raw, "layers", path, "required");
|
|
314
|
+
if (!layers.ok) return layers;
|
|
315
|
+
const nodes = arrayField(raw, "nodes", path, "required");
|
|
316
|
+
if (!nodes.ok) return nodes;
|
|
317
|
+
const edges = arrayField(raw, "edges", path, "required");
|
|
318
|
+
if (!edges.ok) return edges;
|
|
319
|
+
const lanes = arrayField(raw, "lanes", path, "optional");
|
|
320
|
+
if (!lanes.ok) return lanes;
|
|
321
|
+
const groups = arrayField(raw, "groups", path, "optional");
|
|
322
|
+
if (!groups.ok) return groups;
|
|
323
|
+
let map = EMPTY_MAP;
|
|
324
|
+
const title = optionalString(raw, "title", "map", path);
|
|
325
|
+
if (!title.ok) return title;
|
|
326
|
+
if (title.value !== void 0) map = setTitle(map, title.value);
|
|
327
|
+
const rawKind = optionalString(raw, "kind", "map", path);
|
|
328
|
+
if (!rawKind.ok) return rawKind;
|
|
329
|
+
if (rawKind.value !== void 0) {
|
|
330
|
+
const kind = makeMapKind(rawKind.value);
|
|
331
|
+
if (!kind.ok) return err({ kind: "invariant-violation", path, violation: kind.error });
|
|
332
|
+
map = setKind(map, kind.value);
|
|
333
|
+
}
|
|
334
|
+
for (const [i, rawLayer] of layers.value.entries()) {
|
|
335
|
+
const where = `layers[${i}]`;
|
|
336
|
+
if (!isRecord(rawLayer)) return badShape(path, where, "an object", rawLayer);
|
|
337
|
+
const rawId = requiredString(rawLayer, "id", where, path);
|
|
338
|
+
if (!rawId.ok) return rawId;
|
|
339
|
+
const id = makeLayerId(rawId.value);
|
|
340
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
341
|
+
const name = requiredString(rawLayer, "name", where, path);
|
|
342
|
+
if (!name.ok) return name;
|
|
343
|
+
const rawRank = rawLayer["rank"];
|
|
344
|
+
if (typeof rawRank !== "number") return badShape(path, `${where}.rank`, "a number", rawRank);
|
|
345
|
+
const rank = makeRank(rawRank);
|
|
346
|
+
if (!rank.ok) return err({ kind: "invariant-violation", path, violation: rank.error });
|
|
347
|
+
const next = declareLayer(map, { id: id.value, name: name.value, rank: rank.value });
|
|
348
|
+
if (!next.ok) return err({ kind: "invariant-violation", path, violation: next.error });
|
|
349
|
+
map = next.value;
|
|
350
|
+
}
|
|
351
|
+
for (const [i, rawLane] of lanes.value.entries()) {
|
|
352
|
+
const where = `lanes[${i}]`;
|
|
353
|
+
if (!isRecord(rawLane)) return badShape(path, where, "an object", rawLane);
|
|
354
|
+
const rawId = requiredString(rawLane, "id", where, path);
|
|
355
|
+
if (!rawId.ok) return rawId;
|
|
356
|
+
const id = makeLaneId(rawId.value);
|
|
357
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
358
|
+
const label = requiredString(rawLane, "label", where, path);
|
|
359
|
+
if (!label.ok) return label;
|
|
360
|
+
const declared = declareLane(map, { id: id.value, label: label.value });
|
|
361
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
362
|
+
map = declared.value;
|
|
363
|
+
}
|
|
364
|
+
for (const [i, rawGroup] of groups.value.entries()) {
|
|
365
|
+
const where = `groups[${i}]`;
|
|
366
|
+
if (!isRecord(rawGroup)) return badShape(path, where, "an object", rawGroup);
|
|
367
|
+
const rawId = requiredString(rawGroup, "id", where, path);
|
|
368
|
+
if (!rawId.ok) return rawId;
|
|
369
|
+
const id = makeGroupId(rawId.value);
|
|
370
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
371
|
+
const rawLayer = requiredString(rawGroup, "layer", where, path);
|
|
372
|
+
if (!rawLayer.ok) return rawLayer;
|
|
373
|
+
const layer = makeLayerId(rawLayer.value);
|
|
374
|
+
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
375
|
+
const label = requiredString(rawGroup, "label", where, path);
|
|
376
|
+
if (!label.ok) return label;
|
|
377
|
+
const declared = declareGroup(map, { id: id.value, label: label.value, layer: layer.value });
|
|
378
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
379
|
+
map = declared.value;
|
|
380
|
+
}
|
|
381
|
+
for (const [i, rawNode] of nodes.value.entries()) {
|
|
382
|
+
const where = `nodes[${i}]`;
|
|
383
|
+
if (!isRecord(rawNode)) return badShape(path, where, "an object", rawNode);
|
|
384
|
+
const rawId = requiredString(rawNode, "id", where, path);
|
|
385
|
+
if (!rawId.ok) return rawId;
|
|
386
|
+
const id = makeNodeId(rawId.value);
|
|
387
|
+
if (!id.ok) return err({ kind: "invariant-violation", path, violation: id.error });
|
|
388
|
+
const rawLayer = requiredString(rawNode, "layer", where, path);
|
|
389
|
+
if (!rawLayer.ok) return rawLayer;
|
|
390
|
+
const layer = makeLayerId(rawLayer.value);
|
|
391
|
+
if (!layer.ok) return err({ kind: "invariant-violation", path, violation: layer.error });
|
|
392
|
+
const rawStatus = requiredString(rawNode, "status", where, path);
|
|
393
|
+
if (!rawStatus.ok) return rawStatus;
|
|
394
|
+
const status = makeNodeStatus(rawStatus.value);
|
|
395
|
+
if (!status.ok) return err({ kind: "invariant-violation", path, violation: status.error });
|
|
396
|
+
const label = requiredString(rawNode, "label", where, path);
|
|
397
|
+
if (!label.ok) return label;
|
|
398
|
+
const detail = optionalString(rawNode, "detail", where, path);
|
|
399
|
+
if (!detail.ok) return detail;
|
|
400
|
+
const rawGroup = optionalString(rawNode, "group", where, path);
|
|
401
|
+
if (!rawGroup.ok) return rawGroup;
|
|
402
|
+
let group;
|
|
403
|
+
if (rawGroup.value !== void 0) {
|
|
404
|
+
const made = makeGroupId(rawGroup.value);
|
|
405
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
406
|
+
group = made.value;
|
|
407
|
+
}
|
|
408
|
+
const rawNodeKind = optionalString(rawNode, "kind", where, path);
|
|
409
|
+
if (!rawNodeKind.ok) return rawNodeKind;
|
|
410
|
+
let nodeKind;
|
|
411
|
+
if (rawNodeKind.value !== void 0) {
|
|
412
|
+
const made = makeNodeKind(rawNodeKind.value);
|
|
413
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
414
|
+
nodeKind = made.value;
|
|
415
|
+
}
|
|
416
|
+
const rawLane = optionalString(rawNode, "lane", where, path);
|
|
417
|
+
if (!rawLane.ok) return rawLane;
|
|
418
|
+
let lane;
|
|
419
|
+
if (rawLane.value !== void 0) {
|
|
420
|
+
const made = makeLaneId(rawLane.value);
|
|
421
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
422
|
+
lane = made.value;
|
|
423
|
+
}
|
|
424
|
+
const rawSubmap = optionalString(rawNode, "submap", where, path);
|
|
425
|
+
if (!rawSubmap.ok) return rawSubmap;
|
|
426
|
+
let submap;
|
|
427
|
+
if (rawSubmap.value !== void 0) {
|
|
428
|
+
const made = makeSubmapRef(rawSubmap.value);
|
|
429
|
+
if (!made.ok) return err({ kind: "invariant-violation", path, violation: made.error });
|
|
430
|
+
submap = made.value;
|
|
431
|
+
}
|
|
432
|
+
const declared = declareNode(map, {
|
|
433
|
+
id: id.value,
|
|
434
|
+
label: label.value,
|
|
435
|
+
layer: layer.value,
|
|
436
|
+
status: status.value,
|
|
437
|
+
...detail.value !== void 0 ? { detail: detail.value } : {},
|
|
438
|
+
...group !== void 0 ? { group } : {},
|
|
439
|
+
...nodeKind !== void 0 ? { kind: nodeKind } : {},
|
|
440
|
+
...lane !== void 0 ? { lane } : {},
|
|
441
|
+
...submap !== void 0 ? { submap } : {}
|
|
442
|
+
});
|
|
443
|
+
if (!declared.ok) return err({ kind: "invariant-violation", path, violation: declared.error });
|
|
444
|
+
map = declared.value;
|
|
445
|
+
const evidence = optionalString(rawNode, "evidence", where, path);
|
|
446
|
+
if (!evidence.ok) return evidence;
|
|
447
|
+
if (evidence.value !== void 0) {
|
|
448
|
+
const updated = updateNode(map, { id: id.value, evidence: evidence.value });
|
|
449
|
+
if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
|
|
450
|
+
map = updated.value;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const [i, rawEdge] of edges.value.entries()) {
|
|
454
|
+
const where = `edges[${i}]`;
|
|
455
|
+
if (!isRecord(rawEdge)) return badShape(path, where, "an object", rawEdge);
|
|
456
|
+
const rawFrom = requiredString(rawEdge, "from", where, path);
|
|
457
|
+
if (!rawFrom.ok) return rawFrom;
|
|
458
|
+
const from = makeNodeId(rawFrom.value);
|
|
459
|
+
if (!from.ok) return err({ kind: "invariant-violation", path, violation: from.error });
|
|
460
|
+
const rawTo = requiredString(rawEdge, "to", where, path);
|
|
461
|
+
if (!rawTo.ok) return rawTo;
|
|
462
|
+
const to = makeNodeId(rawTo.value);
|
|
463
|
+
if (!to.ok) return err({ kind: "invariant-violation", path, violation: to.error });
|
|
464
|
+
const label = optionalString(rawEdge, "label", where, path);
|
|
465
|
+
if (!label.ok) return label;
|
|
466
|
+
const linked = linkNodes(map, from.value, to.value, label.value);
|
|
467
|
+
if (!linked.ok) return err({ kind: "invariant-violation", path, violation: linked.error });
|
|
468
|
+
map = linked.value;
|
|
469
|
+
}
|
|
470
|
+
const textError = mapTextError(map);
|
|
471
|
+
return textError ? err({ kind: "bad-shape", path, detail: textError }) : ok(map);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/store/atomic.ts
|
|
475
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
476
|
+
import { dirname } from "node:path";
|
|
477
|
+
var RENAME_MAX_ATTEMPTS = 10;
|
|
478
|
+
var RENAME_BACKOFF_STEP_MS = 10;
|
|
479
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOENT"]);
|
|
480
|
+
function sleepSync(ms) {
|
|
481
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
482
|
+
}
|
|
483
|
+
function discardTemp(tmp) {
|
|
484
|
+
try {
|
|
485
|
+
rmSync(tmp, { force: true });
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function errnoOf(e) {
|
|
490
|
+
return e.code ?? e.message;
|
|
491
|
+
}
|
|
492
|
+
function writeFileAtomic(path, contents) {
|
|
493
|
+
return writeAtomic(path, contents, RENAME_MAX_ATTEMPTS);
|
|
494
|
+
}
|
|
495
|
+
function writeAtomic(path, contents, maxAttempts) {
|
|
496
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
|
|
497
|
+
try {
|
|
498
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
499
|
+
writeFileSync(tmp, contents, "utf8");
|
|
500
|
+
} catch (e) {
|
|
501
|
+
discardTemp(tmp);
|
|
502
|
+
return err({ kind: "save-failed", path, detail: `writing the temp file failed: ${errnoOf(e)}` });
|
|
503
|
+
}
|
|
504
|
+
let attempt = 1;
|
|
505
|
+
for (; ; ) {
|
|
506
|
+
try {
|
|
507
|
+
renameSync(tmp, path);
|
|
508
|
+
return ok(void 0);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
const code = errnoOf(e);
|
|
511
|
+
if (!TRANSIENT_RENAME_CODES.has(code) || attempt >= maxAttempts) {
|
|
512
|
+
discardTemp(tmp);
|
|
513
|
+
return err({ kind: "save-failed", path, detail: `${code} after ${attempt} attempt(s)` });
|
|
514
|
+
}
|
|
515
|
+
sleepSync(attempt * RENAME_BACKOFF_STEP_MS);
|
|
516
|
+
attempt += 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/store/pages.ts
|
|
522
|
+
import { existsSync, readdirSync, rmSync as rmSync2 } from "node:fs";
|
|
523
|
+
import { basename, dirname as dirname2, join } from "node:path";
|
|
524
|
+
var STORE_DIR_NAME = ".mellos";
|
|
525
|
+
var STATE_FILE_RELATIVE_PATH = join(STORE_DIR_NAME, "map.json");
|
|
526
|
+
var PAGES_DIR_NAME = "pages";
|
|
527
|
+
function pageIdOfFile(defaultFile, path) {
|
|
528
|
+
if (path === defaultFile) return void 0;
|
|
529
|
+
const name = basename(path);
|
|
530
|
+
return name.endsWith(".json") ? name.slice(0, -".json".length) : name;
|
|
531
|
+
}
|
|
532
|
+
function listPageFiles(defaultFile) {
|
|
533
|
+
const out = [];
|
|
534
|
+
if (existsSync(defaultFile)) out.push(defaultFile);
|
|
535
|
+
let entries = [];
|
|
536
|
+
try {
|
|
537
|
+
entries = readdirSync(join(dirname2(defaultFile), PAGES_DIR_NAME));
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
for (const e of entries.sort()) {
|
|
541
|
+
if (e.endsWith(".json")) out.push(join(dirname2(defaultFile), PAGES_DIR_NAME, e));
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/store/json-text.ts
|
|
547
|
+
function stripBom(text) {
|
|
548
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/store/migration.ts
|
|
552
|
+
import { dirname as dirname3, join as join2 } from "node:path";
|
|
553
|
+
var LEGACY_STATE_FILE_RELATIVE_PATH = join2(".claude", "mellos-mapping.json");
|
|
554
|
+
|
|
555
|
+
// src/store/maps.ts
|
|
556
|
+
import { readFileSync } from "node:fs";
|
|
557
|
+
function loadMapFile(path) {
|
|
558
|
+
let text;
|
|
559
|
+
try {
|
|
560
|
+
text = readFileSync(path, "utf8");
|
|
561
|
+
} catch (e) {
|
|
562
|
+
const code = e.code;
|
|
563
|
+
if (code === "ENOENT") return err({ kind: "not-found", path });
|
|
564
|
+
throw e;
|
|
565
|
+
}
|
|
566
|
+
let raw;
|
|
567
|
+
try {
|
|
568
|
+
raw = JSON.parse(stripBom(text));
|
|
569
|
+
} catch (e) {
|
|
570
|
+
return err({ kind: "malformed-json", path, detail: e.message });
|
|
571
|
+
}
|
|
572
|
+
return parseMap(raw, path);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/preview/publisher.ts
|
|
576
|
+
import { createHash } from "node:crypto";
|
|
577
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync, rmdirSync } from "node:fs";
|
|
578
|
+
import { dirname as dirname4, join as join3, resolve } from "node:path";
|
|
579
|
+
|
|
580
|
+
// src/semantics/vocabulary.ts
|
|
581
|
+
var STATUS_GLYPHS = {
|
|
582
|
+
planned: ["\xB7", "."],
|
|
583
|
+
"in-progress": ["\u283F", "*"],
|
|
584
|
+
done: ["\u25A0", "#"],
|
|
585
|
+
regressed: ["\u2717", "X"]
|
|
586
|
+
};
|
|
587
|
+
function statusGlyph(status, unicode) {
|
|
588
|
+
const [uni, ascii] = STATUS_GLYPHS[status];
|
|
589
|
+
return unicode ? uni : ascii;
|
|
590
|
+
}
|
|
591
|
+
var UNVERIFIED_DONE_GLYPHS = ["\u25A1", "o"];
|
|
592
|
+
function unverifiedDoneGlyph(unicode) {
|
|
593
|
+
const [uni, ascii] = UNVERIFIED_DONE_GLYPHS;
|
|
594
|
+
return unicode ? uni : ascii;
|
|
595
|
+
}
|
|
596
|
+
var NODE_KIND_GLYPHS = {
|
|
597
|
+
selector: ["?", "?"],
|
|
598
|
+
sequence: ["\xBB", ">"],
|
|
599
|
+
parallel: ["\u2016", "="],
|
|
600
|
+
decorator: ["\u25CC", "o"],
|
|
601
|
+
condition: ["\u25C7", "c"],
|
|
602
|
+
action: ["\xB7", "."],
|
|
603
|
+
source: ["\u25CB", "o"],
|
|
604
|
+
transform: ["\u25D0", "%"],
|
|
605
|
+
sink: ["\u25CF", "*"],
|
|
606
|
+
service: ["\u25C6", "S"],
|
|
607
|
+
db: ["\u25A4", "D"],
|
|
608
|
+
queue: ["\u2263", "Q"],
|
|
609
|
+
ui: ["\u25A3", "U"]
|
|
610
|
+
};
|
|
611
|
+
function kindGlyph(kind, unicode) {
|
|
612
|
+
const pair = NODE_KIND_GLYPHS[kind];
|
|
613
|
+
return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/semantics/semantics.ts
|
|
617
|
+
function zoomMode(zoom) {
|
|
618
|
+
if (zoom >= 1) return "detail";
|
|
619
|
+
if (zoom <= -4) return "overview";
|
|
620
|
+
return "boxes";
|
|
621
|
+
}
|
|
622
|
+
function isNeutralKind(map) {
|
|
623
|
+
return map.kind !== void 0 && map.kind !== "dev";
|
|
624
|
+
}
|
|
625
|
+
function flipForSequence(map) {
|
|
626
|
+
if (map.kind !== "sequence") return map;
|
|
627
|
+
return {
|
|
628
|
+
...map,
|
|
629
|
+
// VIOLATION: state-explicit-in-types - `-l.rank as Rank` produces a value
|
|
630
|
+
// the Rank brand promises cannot exist: mirroring 0..99 gives -99..0, and
|
|
631
|
+
// makeRank would refuse every one of them. The alternative is a second
|
|
632
|
+
// ordered-position type (an unbranded `order` field) threaded through the
|
|
633
|
+
// renderer's whole layout stage purely so this one derived map can be
|
|
634
|
+
// typed — a large change to express "these ranks are an order, not a
|
|
635
|
+
// stored value". What makes it safe is the same thing that makes it
|
|
636
|
+
// wrong: this map only ever reaches a renderer, which compares ranks and
|
|
637
|
+
// never writes them (same contract as aggregateMap).
|
|
638
|
+
layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
|
|
639
|
+
edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/preview/presentation.ts
|
|
644
|
+
var LABELS = { planned: "\u5F85\u5F00\u53D1", "in-progress": "\u5F00\u53D1\u4E2D", done: "\u5DF2\u9A8C\u8BC1", regressed: "\u51FA\u73B0\u56DE\u5F52" };
|
|
645
|
+
function isVerified(node) {
|
|
646
|
+
return node.status === "done" && node.evidence !== void 0;
|
|
647
|
+
}
|
|
648
|
+
function statusText(node) {
|
|
649
|
+
return node.status === "done" && !isVerified(node) ? `${unverifiedDoneGlyph(true)} \u5B8C\u6210\u4F46\u7F3A\u5C11\u8BC1\u636E` : `${statusGlyph(node.status, true)} ${LABELS[node.status]}`;
|
|
650
|
+
}
|
|
651
|
+
function documentName(page) {
|
|
652
|
+
return page === void 0 ? "map.md" : `page-${page}.md`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/preview/text.ts
|
|
656
|
+
function xml(text) {
|
|
657
|
+
return text.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ufffe\uffff]/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
658
|
+
}
|
|
659
|
+
function markdown(text) {
|
|
660
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/([\\`*_[\]{}()#+.!|~-])/g, "\\$1").replace(/\r?\n/g, " \n");
|
|
661
|
+
}
|
|
662
|
+
function cell(text) {
|
|
663
|
+
return markdown(text).replace(/ \n/g, "<br>");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/preview/markdown.ts
|
|
667
|
+
function renderMapMarkdown(map, image, pages) {
|
|
668
|
+
const neutral = isNeutralKind(map);
|
|
669
|
+
const rows = [
|
|
670
|
+
`# ${markdown(map.title ?? "\u6885\u52D2\u65AF\u5730\u56FE")}`,
|
|
671
|
+
"",
|
|
672
|
+
"[\u6240\u6709\u5730\u56FE](index.md)",
|
|
673
|
+
"",
|
|
674
|
+
"> \u81EA\u52A8\u751F\u6210\u7684\u5730\u56FE\u9884\u89C8\uFF1B\u4FEE\u6539\u5730\u56FE\u6570\u636E\u540E\u91CD\u65B0\u751F\u6210\u3002",
|
|
675
|
+
""
|
|
676
|
+
];
|
|
677
|
+
if (!neutral) {
|
|
678
|
+
const active = map.nodes.filter((n) => n.status === "in-progress");
|
|
679
|
+
rows.push(
|
|
680
|
+
`**\u5F53\u524D\uFF1A** ${active.length ? active.map((n) => markdown(n.label)).join("\u3001") : "\u6682\u65E0\u8FDB\u884C\u4E2D\u7684\u6A21\u5757"}`,
|
|
681
|
+
"",
|
|
682
|
+
`\u5DF2\u9A8C\u8BC1 **${map.nodes.filter(isVerified).length} / ${map.nodes.length}**\u3000\uFF5C\u3000\u56DE\u5F52 **${map.nodes.filter((n) => n.status === "regressed").length}**`,
|
|
683
|
+
""
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
rows.push("## \u5206\u5C42\u4F9D\u8D56", "", ``, "");
|
|
687
|
+
if (!neutral) rows.push("\xB7 \u5F85\u5F00\u53D1\u3000\u283F \u5F00\u53D1\u4E2D\u3000\u25A0 \u5DF2\u9A8C\u8BC1\u3000\u2717 \u51FA\u73B0\u56DE\u5F52\u3000\u25A1 \u5B8C\u6210\u4F46\u7F3A\u5C11\u8BC1\u636E", "");
|
|
688
|
+
rows.push(map.kind === "sequence" ? "\u65F6\u95F4\u4ECE\u4E0A\u5411\u4E0B\u63A8\u8FDB\uFF1B\u7BAD\u5934\u4FDD\u7559\u5730\u56FE\u4E2D\u7684\u4F9D\u8D56\u65B9\u5411\u3002" : "\u7BAD\u5934\u7531\u4F7F\u7528\u65B9\u6307\u5411\u5B83\u4F9D\u8D56\u7684\u6A21\u5757\uFF1B\u57FA\u7840\u5C42\u4F4D\u4E8E\u4E0B\u65B9\u3002", "", "## \u6A21\u5757\u8BE6\u60C5", "");
|
|
689
|
+
const nodes = new Map(map.nodes.map((n) => [n.id, n]));
|
|
690
|
+
const layers = [...map.layers].sort((a, b) => map.kind === "sequence" ? a.rank - b.rank : b.rank - a.rank);
|
|
691
|
+
for (const layer of layers) {
|
|
692
|
+
rows.push(`### ${markdown(layer.name)}`, "");
|
|
693
|
+
const members = map.nodes.filter((n) => n.layer === layer.id);
|
|
694
|
+
if (!members.length) rows.push("\u5C1A\u672A\u58F0\u660E\u6A21\u5757\u3002", "");
|
|
695
|
+
for (const node of members) {
|
|
696
|
+
rows.push(`#### ${markdown(node.label)}${neutral ? "" : `\u3000${statusText(node)}`}`, "");
|
|
697
|
+
if (node.detail !== void 0) rows.push(markdown(node.detail), "");
|
|
698
|
+
const used = map.edges.filter((e) => e.from === node.id);
|
|
699
|
+
rows.push(`**\u4F9D\u8D56\uFF1A** ${used.length ? used.map((e) => `${markdown(nodes.get(e.to).label)}${e.label !== void 0 ? `\uFF08${markdown(e.label)}\uFF09` : ""}`).join("\u3001") : "\u65E0"}`, "");
|
|
700
|
+
const meta = [
|
|
701
|
+
node.group === void 0 ? void 0 : map.groups.find((g) => g.id === node.group)?.label,
|
|
702
|
+
node.lane === void 0 ? void 0 : map.lanes.find((l) => l.id === node.lane)?.label,
|
|
703
|
+
node.kind
|
|
704
|
+
].filter((v) => v !== void 0);
|
|
705
|
+
if (meta.length) rows.push(`**\u5F52\u5C5E / \u7C7B\u578B\uFF1A** ${meta.map(markdown).join(" \xB7 ")}`, "");
|
|
706
|
+
if (node.evidence !== void 0) rows.push(`**\u9A8C\u8BC1\u8BB0\u5F55\uFF1A** ${markdown(node.evidence)}`, "");
|
|
707
|
+
if (node.submap !== void 0) rows.push(pages.some((p) => p.page === node.submap) ? `[\u6253\u5F00\u5B50\u56FE\uFF1A${markdown(node.submap)}](${documentName(node.submap)})` : `\u5B50\u56FE\u5C1A\u672A\u521B\u5EFA\uFF1A${markdown(node.submap)}`, "");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (!neutral) {
|
|
711
|
+
rows.push("## \u9A8C\u8BC1\u8BB0\u5F55", "", "| \u6A21\u5757 | \u72B6\u6001 | \u6700\u8FD1\u8BC1\u636E |", "| --- | --- | --- |");
|
|
712
|
+
for (const node of map.nodes) rows.push(`| ${cell(node.label)} | ${statusText(node)} | ${node.evidence === void 0 ? "\u5C1A\u672A\u8BB0\u5F55" : cell(node.evidence)} |`);
|
|
713
|
+
rows.push("");
|
|
714
|
+
}
|
|
715
|
+
rows.push("---", "", "\u9759\u6001\u6587\u6863\uFF1A\u66F4\u65B0\u65F6\u91CD\u65B0\u751F\u6210\u5730\u56FE\u56FE\u7247\u4E0E\u6587\u5B57\u3002\u56FE\u4E2D\u8282\u70B9\u4E0D\u652F\u6301\u62D6\u62FD\u3001\u60AC\u505C\u5C55\u5F00\u6216\u52A8\u753B\u3002", "");
|
|
716
|
+
return rows.join("\n");
|
|
717
|
+
}
|
|
718
|
+
function renderPreviewIndex(pages) {
|
|
719
|
+
return [
|
|
720
|
+
"# \u6885\u52D2\u65AF\u5730\u56FE \xB7 \u9875\u9762\u76EE\u5F55",
|
|
721
|
+
"",
|
|
722
|
+
...pages.map((p) => `- [${markdown(p.map.title ?? p.page ?? "\u9ED8\u8BA4\u5730\u56FE")}](${documentName(p.page)})`),
|
|
723
|
+
"",
|
|
724
|
+
"\u5730\u56FE\u9884\u89C8\u7531\u9879\u76EE\u5185\u7684\u5730\u56FE\u6570\u636E\u751F\u6210\u3002",
|
|
725
|
+
""
|
|
726
|
+
].join("\n");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/render/zoom-geometry.ts
|
|
730
|
+
var BOX_H = 3;
|
|
731
|
+
var BOX_GAP = 2;
|
|
732
|
+
var LEFT_MARGIN = 2;
|
|
733
|
+
var BAR_MIN_RUN = 7;
|
|
734
|
+
var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
|
|
735
|
+
var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
|
|
736
|
+
function zoomGeometry(zoom) {
|
|
737
|
+
const m = zoomMode(zoom);
|
|
738
|
+
const mode = m === "overview" ? "constellation" : m;
|
|
739
|
+
switch (zoom) {
|
|
740
|
+
case 2:
|
|
741
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
|
|
742
|
+
case 1:
|
|
743
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
|
|
744
|
+
case 0:
|
|
745
|
+
return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
746
|
+
case -1:
|
|
747
|
+
return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
|
|
748
|
+
case -2:
|
|
749
|
+
return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
|
|
750
|
+
case -3:
|
|
751
|
+
return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
752
|
+
case -4:
|
|
753
|
+
return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/render/width.ts
|
|
758
|
+
var WIDE_RANGES = [
|
|
759
|
+
[4352, 4447],
|
|
760
|
+
// Hangul Jamo
|
|
761
|
+
// Wide symbols scattered through the BMP — mostly emoji that predate the
|
|
762
|
+
// emoji planes (⌚ ⏰ ⚡ ✅ ✨ ❌ ❓ ⭐ ⬛ …).
|
|
763
|
+
[8986, 8987],
|
|
764
|
+
[9001, 9002],
|
|
765
|
+
[9193, 9196],
|
|
766
|
+
[9200, 9200],
|
|
767
|
+
[9203, 9203],
|
|
768
|
+
[9725, 9726],
|
|
769
|
+
[9748, 9749],
|
|
770
|
+
[9800, 9811],
|
|
771
|
+
[9855, 9855],
|
|
772
|
+
[9875, 9875],
|
|
773
|
+
[9889, 9889],
|
|
774
|
+
[9898, 9899],
|
|
775
|
+
[9917, 9918],
|
|
776
|
+
[9924, 9925],
|
|
777
|
+
[9934, 9934],
|
|
778
|
+
[9940, 9940],
|
|
779
|
+
[9962, 9962],
|
|
780
|
+
[9970, 9971],
|
|
781
|
+
[9973, 9973],
|
|
782
|
+
[9978, 9978],
|
|
783
|
+
[9981, 9981],
|
|
784
|
+
[9989, 9989],
|
|
785
|
+
[9994, 9995],
|
|
786
|
+
[10024, 10024],
|
|
787
|
+
[10060, 10060],
|
|
788
|
+
[10062, 10062],
|
|
789
|
+
[10067, 10069],
|
|
790
|
+
[10071, 10071],
|
|
791
|
+
[10133, 10135],
|
|
792
|
+
[10160, 10160],
|
|
793
|
+
[10175, 10175],
|
|
794
|
+
[11035, 11036],
|
|
795
|
+
[11088, 11088],
|
|
796
|
+
[11093, 11093],
|
|
797
|
+
[11904, 42191],
|
|
798
|
+
// CJK radicals .. Yi (covers CJK Unified Ideographs)
|
|
799
|
+
[43360, 43391],
|
|
800
|
+
[44032, 55203],
|
|
801
|
+
// Hangul syllables
|
|
802
|
+
[63744, 64255],
|
|
803
|
+
// CJK compatibility ideographs
|
|
804
|
+
[65040, 65049],
|
|
805
|
+
[65072, 65135],
|
|
806
|
+
[65280, 65376],
|
|
807
|
+
// fullwidth forms
|
|
808
|
+
[65504, 65510],
|
|
809
|
+
[127744, 128591],
|
|
810
|
+
// pictographs, transport, emoticons (🚀 🎯 😀 …)
|
|
811
|
+
[128640, 128767],
|
|
812
|
+
[129280, 129535],
|
|
813
|
+
// supplemental symbols (🤖 🧱 …)
|
|
814
|
+
[129648, 129791],
|
|
815
|
+
// symbols extended-A
|
|
816
|
+
[131072, 262141]
|
|
817
|
+
// CJK extension planes
|
|
818
|
+
];
|
|
819
|
+
var ZERO_WIDTH_RANGES = [
|
|
820
|
+
[768, 879],
|
|
821
|
+
// combining diacritical marks (decomposed 'e' + ´)
|
|
822
|
+
[6832, 6911],
|
|
823
|
+
[7616, 7679],
|
|
824
|
+
[8203, 8207],
|
|
825
|
+
// zero-width space .. RLM, zero-width joiner among them
|
|
826
|
+
[8400, 8432],
|
|
827
|
+
// combining marks for symbols
|
|
828
|
+
[65024, 65039],
|
|
829
|
+
// variation selectors, VS16 (emoji presentation) included
|
|
830
|
+
[65056, 65071],
|
|
831
|
+
// combining half marks
|
|
832
|
+
[127995, 127999]
|
|
833
|
+
// emoji skin tone modifiers — always applied to a base
|
|
834
|
+
];
|
|
835
|
+
function inRanges(cp, ranges) {
|
|
836
|
+
for (const [lo, hi] of ranges) {
|
|
837
|
+
if (cp >= lo && cp <= hi) return true;
|
|
838
|
+
}
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
function charWidth(cp) {
|
|
842
|
+
if (inRanges(cp, ZERO_WIDTH_RANGES)) return 0;
|
|
843
|
+
return inRanges(cp, WIDE_RANGES) ? 2 : 1;
|
|
844
|
+
}
|
|
845
|
+
function displayWidth(text) {
|
|
846
|
+
let w = 0;
|
|
847
|
+
for (const ch of text) w += charWidth(ch.codePointAt(0));
|
|
848
|
+
return w;
|
|
849
|
+
}
|
|
850
|
+
function fitWidth(s, width) {
|
|
851
|
+
s = terminalText(s);
|
|
852
|
+
if (displayWidth(s) <= width) return s;
|
|
853
|
+
let out = "";
|
|
854
|
+
let w = 0;
|
|
855
|
+
for (const ch of s) {
|
|
856
|
+
const cw = displayWidth(ch);
|
|
857
|
+
if (w + cw > width - 1) break;
|
|
858
|
+
out += ch;
|
|
859
|
+
w += cw;
|
|
860
|
+
}
|
|
861
|
+
return out + "\u2026";
|
|
862
|
+
}
|
|
863
|
+
function wrapWidth(s, width) {
|
|
864
|
+
const lines = [];
|
|
865
|
+
let line = "";
|
|
866
|
+
let w = 0;
|
|
867
|
+
for (const ch of terminalText(s.replace(/\r/g, "").replace(/\t/g, " "), true)) {
|
|
868
|
+
if (ch === "\n") {
|
|
869
|
+
lines.push(line);
|
|
870
|
+
line = "";
|
|
871
|
+
w = 0;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
const cw = displayWidth(ch);
|
|
875
|
+
if (w + cw > width) {
|
|
876
|
+
lines.push(line);
|
|
877
|
+
line = "";
|
|
878
|
+
w = 0;
|
|
879
|
+
}
|
|
880
|
+
line += ch;
|
|
881
|
+
w += cw;
|
|
882
|
+
}
|
|
883
|
+
if (line !== "") lines.push(line);
|
|
884
|
+
return lines;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/render/layout.ts
|
|
888
|
+
var LABEL_BUDGET_MIN = 4;
|
|
889
|
+
function boxSpec(node, geo, unicode, neutral) {
|
|
890
|
+
const glyph = node.kind !== void 0 ? kindGlyph(node.kind, unicode) : void 0;
|
|
891
|
+
const badge = node.submap !== void 0 ? unicode ? " \u229E" : " +" : "";
|
|
892
|
+
const badgeW = displayWidth(badge);
|
|
893
|
+
const text = !neutral && glyph !== void 0 ? `${glyph} ${node.label}` : node.label;
|
|
894
|
+
if (geo.mode === "constellation") {
|
|
895
|
+
return { w: 3, h: 1, label: "", pad: 0, borderless: true, extra: [] };
|
|
896
|
+
}
|
|
897
|
+
if (geo.mode === "detail" && geo.detail !== void 0) {
|
|
898
|
+
const budget2 = geo.detail;
|
|
899
|
+
const innerW = Math.min(Math.max(displayWidth(text) + badgeW + 4, budget2.innerMin), budget2.innerMax);
|
|
900
|
+
const extra = [];
|
|
901
|
+
if (node.evidence !== void 0) extra.push({ text: fitWidth(` ${node.evidence}`, innerW), style: "faint" });
|
|
902
|
+
if (node.detail !== void 0) {
|
|
903
|
+
const wrapped = wrapWidth(node.detail, innerW - 2);
|
|
904
|
+
for (let i = 0; i < Math.min(wrapped.length, budget2.noteRows); i++) {
|
|
905
|
+
const cut = i === budget2.noteRows - 1 && wrapped.length > budget2.noteRows;
|
|
906
|
+
extra.push({ text: ` ${cut ? fitWidth(wrapped[i] + "\u2026", innerW - 2) : wrapped[i]}`, style: "none" });
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
w: innerW + 2,
|
|
911
|
+
h: BOX_H + extra.length,
|
|
912
|
+
label: fitWidth(text, innerW - 4 - badgeW) + badge,
|
|
913
|
+
pad: 1,
|
|
914
|
+
borderless: false,
|
|
915
|
+
extra
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
const budget = Math.max(LABEL_BUDGET_MIN, Math.ceil(displayWidth(text) * geo.scale));
|
|
919
|
+
const label = fitWidth(text, budget) + badge;
|
|
920
|
+
return {
|
|
921
|
+
w: displayWidth(label) + 4 + 2 * geo.pad,
|
|
922
|
+
h: BOX_H,
|
|
923
|
+
label,
|
|
924
|
+
pad: geo.pad,
|
|
925
|
+
borderless: false,
|
|
926
|
+
extra: []
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function layoutColumns(map, geo, unicode, neutral) {
|
|
930
|
+
const bands = [...map.layers].sort((a, b) => b.rank - a.rank);
|
|
931
|
+
const bandIndexOf = new Map(bands.map((l, i) => [l.id, i]));
|
|
932
|
+
const sized = /* @__PURE__ */ new Map();
|
|
933
|
+
const bandSized = bands.map(() => []);
|
|
934
|
+
for (const node of map.nodes) {
|
|
935
|
+
const spec = { node, ...boxSpec(node, geo, unicode, neutral) };
|
|
936
|
+
bandSized[bandIndexOf.get(node.layer)].push(spec);
|
|
937
|
+
sized.set(node.id, spec);
|
|
938
|
+
}
|
|
939
|
+
const columnOf = /* @__PURE__ */ new Map();
|
|
940
|
+
const lanes = [];
|
|
941
|
+
if (map.lanes.length === 0) {
|
|
942
|
+
for (const row of bandSized) {
|
|
943
|
+
let x = LEFT_MARGIN;
|
|
944
|
+
for (const spec of row) {
|
|
945
|
+
columnOf.set(spec, x);
|
|
946
|
+
x += spec.w + geo.boxGap;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} else {
|
|
950
|
+
const laneCount = map.lanes.length;
|
|
951
|
+
const laneGap = geo.boxGap + 2;
|
|
952
|
+
const laneIndexOf = new Map(map.lanes.map((l, i) => [l.id, i]));
|
|
953
|
+
const regions = laneCount + 1;
|
|
954
|
+
const grouped = bandSized.map((row) => {
|
|
955
|
+
const cells = Array.from({ length: regions }, () => []);
|
|
956
|
+
for (const spec of row) {
|
|
957
|
+
const lane = spec.node.lane;
|
|
958
|
+
cells[lane !== void 0 ? laneIndexOf.get(lane) : regions - 1].push(spec);
|
|
959
|
+
}
|
|
960
|
+
return cells;
|
|
961
|
+
});
|
|
962
|
+
const regionW = Array.from({ length: regions }, () => 0);
|
|
963
|
+
for (const cells of grouped) {
|
|
964
|
+
for (let i = 0; i < regions; i++) {
|
|
965
|
+
const rowW = cells[i].reduce((sum, b, k) => sum + b.w + (k > 0 ? geo.boxGap : 0), 0);
|
|
966
|
+
regionW[i] = Math.max(regionW[i], rowW);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
for (let i = 0; i < laneCount; i++) regionW[i] = Math.max(regionW[i], displayWidth(map.lanes[i].label) + 2);
|
|
970
|
+
let x0 = LEFT_MARGIN;
|
|
971
|
+
for (let i = 0; i < regions; i++) {
|
|
972
|
+
lanes.push({ x: x0, w: regionW[i] });
|
|
973
|
+
x0 += regionW[i] + laneGap;
|
|
974
|
+
}
|
|
975
|
+
for (const cells of grouped) {
|
|
976
|
+
for (let i = 0; i < regions; i++) {
|
|
977
|
+
let x = lanes[i].x;
|
|
978
|
+
for (const spec of cells[i]) {
|
|
979
|
+
columnOf.set(spec, x);
|
|
980
|
+
x += spec.w + geo.boxGap;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const placed = /* @__PURE__ */ new Map();
|
|
986
|
+
for (const [, spec] of sized) placed.set(spec, { ...spec, x: columnOf.get(spec) ?? LEFT_MARGIN });
|
|
987
|
+
const bandBoxes = bandSized.map((row) => row.map((spec) => placed.get(spec)));
|
|
988
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const node of map.nodes) boxOf.set(node.id, placed.get(sized.get(node.id)));
|
|
990
|
+
const bandLabel = bands.map((l, i) => {
|
|
991
|
+
const row = bandBoxes[i];
|
|
992
|
+
const done = row.filter((b) => b.node.status === "done").length;
|
|
993
|
+
return geo.bandCounts && row.length > 0 && !neutral ? ` ${l.name} ${done}/${row.length}` : ` ${l.name}`;
|
|
994
|
+
});
|
|
995
|
+
let contentWidth = LEFT_MARGIN + BAR_MIN_RUN;
|
|
996
|
+
for (const row of bandBoxes) for (const box of row) contentWidth = Math.max(contentWidth, box.x + box.w);
|
|
997
|
+
for (const lane of lanes) contentWidth = Math.max(contentWidth, lane.x + lane.w);
|
|
998
|
+
return { bands, bandIndexOf, bandBoxes, boxOf, lanes, bandLabel, contentWidth };
|
|
999
|
+
}
|
|
1000
|
+
function layoutRows(columns, geo, gapRowCount, hasTitle, hasLanes) {
|
|
1001
|
+
let y = 0;
|
|
1002
|
+
if (hasTitle) y += 1 + geo.titleGap;
|
|
1003
|
+
let laneHeaderY;
|
|
1004
|
+
if (hasLanes) {
|
|
1005
|
+
laneHeaderY = y;
|
|
1006
|
+
y += 1 + geo.barGap;
|
|
1007
|
+
}
|
|
1008
|
+
const barY = [];
|
|
1009
|
+
const gapTrackStartY = [];
|
|
1010
|
+
const bandBoxes = [];
|
|
1011
|
+
const placed = /* @__PURE__ */ new Map();
|
|
1012
|
+
const gapCount = columns.bands.length - 1;
|
|
1013
|
+
for (let b = 0; b < columns.bands.length; b++) {
|
|
1014
|
+
barY.push(y);
|
|
1015
|
+
y += 1 + geo.barGap;
|
|
1016
|
+
const row = columns.bandBoxes[b];
|
|
1017
|
+
for (const box of row) placed.set(box, { ...box, y });
|
|
1018
|
+
bandBoxes.push(row.map((box) => placed.get(box)));
|
|
1019
|
+
y += row.reduce((max, box) => Math.max(max, box.h), geo.mode === "constellation" ? 1 : BOX_H);
|
|
1020
|
+
if (b < gapCount) {
|
|
1021
|
+
y += geo.breathe;
|
|
1022
|
+
gapTrackStartY.push(y);
|
|
1023
|
+
y += gapRowCount[b];
|
|
1024
|
+
y += geo.breathe;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
1028
|
+
for (const [id, box] of columns.boxOf) boxOf.set(id, placed.get(box));
|
|
1029
|
+
return { boxOf, bandBoxes, barY, gapTrackStartY, laneHeaderY, legendY: y + 1 };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/render/routing.ts
|
|
1033
|
+
function routeEdges(map, columns) {
|
|
1034
|
+
const { bandIndexOf, bandBoxes, boxOf, contentWidth } = columns;
|
|
1035
|
+
const pending = map.edges.map((e) => {
|
|
1036
|
+
const from = boxOf.get(e.from);
|
|
1037
|
+
const to = boxOf.get(e.to);
|
|
1038
|
+
return {
|
|
1039
|
+
from,
|
|
1040
|
+
to,
|
|
1041
|
+
fromBand: bandIndexOf.get(from.node.layer),
|
|
1042
|
+
toBand: bandIndexOf.get(to.node.layer)
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
const gapVerticals = Array.from(
|
|
1046
|
+
{ length: Math.max(0, columns.bands.length - 1) },
|
|
1047
|
+
() => /* @__PURE__ */ new Map()
|
|
1048
|
+
);
|
|
1049
|
+
const verticalFree = (gap, x, edge) => {
|
|
1050
|
+
const owner = gapVerticals[gap]?.get(x);
|
|
1051
|
+
return owner === void 0 || owner === edge;
|
|
1052
|
+
};
|
|
1053
|
+
const takeVertical = (gap, x, edge) => {
|
|
1054
|
+
gapVerticals[gap]?.set(x, edge);
|
|
1055
|
+
};
|
|
1056
|
+
const claimedColumns = /* @__PURE__ */ new Map();
|
|
1057
|
+
const isFree = (box, x) => !(claimedColumns.get(box)?.has(x) ?? false);
|
|
1058
|
+
const claim = (box, x) => {
|
|
1059
|
+
let set = claimedColumns.get(box);
|
|
1060
|
+
if (!set) claimedColumns.set(box, set = /* @__PURE__ */ new Set());
|
|
1061
|
+
set.add(x);
|
|
1062
|
+
return x;
|
|
1063
|
+
};
|
|
1064
|
+
for (const r of pending) {
|
|
1065
|
+
if (r.toBand - r.fromBand !== 1) continue;
|
|
1066
|
+
const lo = Math.max(r.from.x + 1, r.to.x + 1);
|
|
1067
|
+
const hi = Math.min(r.from.x + r.from.w - 2, r.to.x + r.to.w - 2);
|
|
1068
|
+
if (lo > hi) continue;
|
|
1069
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
1070
|
+
for (let d = 0; d <= hi - lo && r.straightX === void 0; d++) {
|
|
1071
|
+
for (const x of d === 0 ? [mid] : [mid - d, mid + d]) {
|
|
1072
|
+
if (x >= lo && x <= hi && isFree(r.from, x) && isFree(r.to, x) && verticalFree(r.fromBand, x, r)) {
|
|
1073
|
+
r.straightX = claim(r.to, claim(r.from, x));
|
|
1074
|
+
takeVertical(r.fromBand, x, r);
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const bent = pending.filter((r) => r.straightX === void 0);
|
|
1081
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
1082
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
1083
|
+
for (const r of bent) {
|
|
1084
|
+
outgoing.set(r.from, [...outgoing.get(r.from) ?? [], r]);
|
|
1085
|
+
incoming.set(r.to, [...incoming.get(r.to) ?? [], r]);
|
|
1086
|
+
}
|
|
1087
|
+
const freeSlot = (box, k, n, edge, gap) => {
|
|
1088
|
+
const lo = box.x + 1;
|
|
1089
|
+
const hi = box.x + box.w - 2;
|
|
1090
|
+
const ideal = box.x + Math.min(box.w - 2, Math.max(1, Math.round((k + 1) * (box.w - 1) / (n + 1))));
|
|
1091
|
+
for (let d = 0; d <= hi - lo; d++) {
|
|
1092
|
+
for (const x of d === 0 ? [ideal] : [ideal - d, ideal + d]) {
|
|
1093
|
+
if (x >= lo && x <= hi && isFree(box, x) && verticalFree(gap, x, edge)) {
|
|
1094
|
+
takeVertical(gap, x, edge);
|
|
1095
|
+
return claim(box, x);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return ideal;
|
|
1100
|
+
};
|
|
1101
|
+
for (const r of bent) {
|
|
1102
|
+
const outs = outgoing.get(r.from);
|
|
1103
|
+
const ins = incoming.get(r.to);
|
|
1104
|
+
r.exitX = freeSlot(r.from, outs.indexOf(r), outs.length, r, r.fromBand);
|
|
1105
|
+
r.entryX = freeSlot(r.to, ins.indexOf(r), ins.length, r, r.toBand - 1);
|
|
1106
|
+
}
|
|
1107
|
+
const usedDescent = /* @__PURE__ */ new Set();
|
|
1108
|
+
let fallbackCount = 0;
|
|
1109
|
+
const blockedByBox = (band, x) => bandBoxes[band].some((b) => x >= b.x && x <= b.x + b.w - 1);
|
|
1110
|
+
const descentGapsFree = (r, c) => {
|
|
1111
|
+
for (let g = r.fromBand; g <= r.toBand - 1; g++) {
|
|
1112
|
+
if (!verticalFree(g, c, r)) return false;
|
|
1113
|
+
}
|
|
1114
|
+
return true;
|
|
1115
|
+
};
|
|
1116
|
+
for (const r of bent.filter((e) => e.toBand - e.fromBand > 1)) {
|
|
1117
|
+
const ex = r.entryX;
|
|
1118
|
+
let chosen;
|
|
1119
|
+
for (let d = 0; d <= contentWidth && chosen === void 0; d++) {
|
|
1120
|
+
for (const c of d === 0 ? [ex] : [ex - d, ex + d]) {
|
|
1121
|
+
if (c < LEFT_MARGIN || c > contentWidth + 1 || usedDescent.has(c)) continue;
|
|
1122
|
+
if (!descentGapsFree(r, c)) continue;
|
|
1123
|
+
let blocked = false;
|
|
1124
|
+
for (let b = r.fromBand + 1; b < r.toBand && !blocked; b++) blocked = blockedByBox(b, c);
|
|
1125
|
+
if (!blocked) {
|
|
1126
|
+
chosen = c;
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
if (chosen === void 0) chosen = contentWidth + 2 + fallbackCount++ * 2;
|
|
1132
|
+
usedDescent.add(chosen);
|
|
1133
|
+
for (let g = r.fromBand; g <= r.toBand - 1; g++) takeVertical(g, chosen, r);
|
|
1134
|
+
r.descentX = chosen;
|
|
1135
|
+
}
|
|
1136
|
+
const gapCount = Math.max(0, columns.bands.length - 1);
|
|
1137
|
+
const gapSegments = Array.from(
|
|
1138
|
+
{ length: gapCount },
|
|
1139
|
+
() => []
|
|
1140
|
+
);
|
|
1141
|
+
for (const r of bent) {
|
|
1142
|
+
const sx = r.exitX;
|
|
1143
|
+
const ex = r.entryX;
|
|
1144
|
+
if (r.descentX === void 0) {
|
|
1145
|
+
gapSegments[r.toBand - 1].push({
|
|
1146
|
+
edge: r,
|
|
1147
|
+
kind: "landing",
|
|
1148
|
+
segment: { lo: Math.min(sx, ex), hi: Math.max(sx, ex) }
|
|
1149
|
+
});
|
|
1150
|
+
} else {
|
|
1151
|
+
const c = r.descentX;
|
|
1152
|
+
gapSegments[r.fromBand].push({ edge: r, kind: "exit", segment: { lo: Math.min(sx, c), hi: Math.max(sx, c) } });
|
|
1153
|
+
gapSegments[r.toBand - 1].push({
|
|
1154
|
+
edge: r,
|
|
1155
|
+
kind: "landing",
|
|
1156
|
+
segment: { lo: Math.min(c, ex), hi: Math.max(c, ex) }
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const exitRow = /* @__PURE__ */ new Map();
|
|
1161
|
+
const landingRow = /* @__PURE__ */ new Map();
|
|
1162
|
+
const gapRowCount = gapSegments.map((entries) => {
|
|
1163
|
+
const rowEnds = [];
|
|
1164
|
+
for (const e of [...entries].sort((a, b) => a.segment.lo - b.segment.lo)) {
|
|
1165
|
+
let row = rowEnds.findIndex((end) => e.segment.lo > end + 1);
|
|
1166
|
+
if (row === -1) {
|
|
1167
|
+
rowEnds.push(e.segment.hi);
|
|
1168
|
+
row = rowEnds.length - 1;
|
|
1169
|
+
} else {
|
|
1170
|
+
rowEnds[row] = Math.max(rowEnds[row], e.segment.hi);
|
|
1171
|
+
}
|
|
1172
|
+
(e.kind === "exit" ? exitRow : landingRow).set(e.edge, row);
|
|
1173
|
+
}
|
|
1174
|
+
return rowEnds.length;
|
|
1175
|
+
});
|
|
1176
|
+
const edges = pending.map((r) => {
|
|
1177
|
+
const common = { from: r.from, to: r.to, fromBand: r.fromBand, toBand: r.toBand };
|
|
1178
|
+
if (r.straightX !== void 0) return { ...common, kind: "straight", x: r.straightX };
|
|
1179
|
+
if (r.descentX === void 0) {
|
|
1180
|
+
return { ...common, kind: "dogleg", exitX: r.exitX, entryX: r.entryX, landingRow: landingRow.get(r) };
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
...common,
|
|
1184
|
+
kind: "thread",
|
|
1185
|
+
exitX: r.exitX,
|
|
1186
|
+
entryX: r.entryX,
|
|
1187
|
+
descentX: r.descentX,
|
|
1188
|
+
exitRow: exitRow.get(r),
|
|
1189
|
+
landingRow: landingRow.get(r)
|
|
1190
|
+
};
|
|
1191
|
+
});
|
|
1192
|
+
return { edges, gapRowCount, fallbackCount };
|
|
1193
|
+
}
|
|
1194
|
+
function edgePolyline(edge, rows) {
|
|
1195
|
+
const from = rows.boxOf.get(edge.from.node.id);
|
|
1196
|
+
const to = rows.boxOf.get(edge.to.node.id);
|
|
1197
|
+
const sy = from.y + from.h - 1;
|
|
1198
|
+
const ey = to.y;
|
|
1199
|
+
if (edge.kind === "straight") {
|
|
1200
|
+
return [
|
|
1201
|
+
[edge.x, sy],
|
|
1202
|
+
[edge.x, ey]
|
|
1203
|
+
];
|
|
1204
|
+
}
|
|
1205
|
+
const landingY = rows.gapTrackStartY[edge.toBand - 1] + edge.landingRow;
|
|
1206
|
+
if (edge.kind === "dogleg") {
|
|
1207
|
+
return [
|
|
1208
|
+
[edge.exitX, sy],
|
|
1209
|
+
[edge.exitX, landingY],
|
|
1210
|
+
[edge.entryX, landingY],
|
|
1211
|
+
[edge.entryX, ey]
|
|
1212
|
+
];
|
|
1213
|
+
}
|
|
1214
|
+
const exitY = rows.gapTrackStartY[edge.fromBand] + edge.exitRow;
|
|
1215
|
+
return [
|
|
1216
|
+
[edge.exitX, sy],
|
|
1217
|
+
[edge.exitX, exitY],
|
|
1218
|
+
[edge.descentX, exitY],
|
|
1219
|
+
[edge.descentX, landingY],
|
|
1220
|
+
[edge.entryX, landingY],
|
|
1221
|
+
[edge.entryX, ey]
|
|
1222
|
+
];
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/preview/svg.ts
|
|
1226
|
+
var X = 8;
|
|
1227
|
+
var Y = 26;
|
|
1228
|
+
var TOP = 20;
|
|
1229
|
+
var PALETTES = {
|
|
1230
|
+
planned: ["#f5f7fa", "#98a3b2", "#566477"],
|
|
1231
|
+
"in-progress": ["#fff4d9", "#c48c24", "#805910"],
|
|
1232
|
+
done: ["#e9f5ee", "#67a883", "#286247"],
|
|
1233
|
+
regressed: ["#fdecec", "#cc7575", "#923d3d"],
|
|
1234
|
+
unverified: ["#fff6e8", "#b49a77", "#785e3e"],
|
|
1235
|
+
neutral: ["#f2f5f9", "#a1adbc", "#364558"]
|
|
1236
|
+
};
|
|
1237
|
+
function renderMapSvg(map) {
|
|
1238
|
+
const neutral = isNeutralKind(map);
|
|
1239
|
+
const originals = new Map(map.nodes.map((n) => [n.id, n]));
|
|
1240
|
+
const oriented = flipForSequence(map);
|
|
1241
|
+
const shaped = { ...oriented, nodes: oriented.nodes.map((n) => {
|
|
1242
|
+
const label = fitWidth(n.label, 32);
|
|
1243
|
+
return { ...n, label: label + " ".repeat(Math.max(0, 20 - displayWidth(label))) };
|
|
1244
|
+
}) };
|
|
1245
|
+
const geo = { ...zoomGeometry(0), boxGap: 6 };
|
|
1246
|
+
const columns = layoutColumns(shaped, geo, true, neutral);
|
|
1247
|
+
const routing = routeEdges(shaped, columns);
|
|
1248
|
+
const rows = layoutRows(columns, geo, routing.gapRowCount, false, map.lanes.length > 0);
|
|
1249
|
+
const width = Math.max(440, (columns.contentWidth + 4 + routing.fallbackCount * 2) * X);
|
|
1250
|
+
const height = Math.max(100, TOP + rows.legendY * Y);
|
|
1251
|
+
const parts = [
|
|
1252
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="map-title map-desc">`,
|
|
1253
|
+
`<title id="map-title">${xml(map.title ?? "\u6885\u52D2\u65AF\u5730\u56FE")}</title>`,
|
|
1254
|
+
`<desc id="map-desc">${map.nodes.length} \u4E2A\u8282\u70B9\uFF0C${map.edges.length} \u6761\u4F9D\u8D56\u3002\u5B8C\u6574\u8BF4\u660E\u4E0E\u9A8C\u8BC1\u8BB0\u5F55\u5728\u5730\u56FE\u6587\u6863\u4E2D\u3002</desc>`,
|
|
1255
|
+
'<defs><marker id="arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 L8 4 L0 8 Z" fill="#8995a5"/></marker></defs>',
|
|
1256
|
+
`<rect width="${width}" height="${height}" fill="#ffffff"/>`,
|
|
1257
|
+
'<g font-family="Segoe UI, Microsoft YaHei, Noto Sans CJK SC, sans-serif">'
|
|
1258
|
+
];
|
|
1259
|
+
if (map.nodes.length === 0) parts.push('<text x="20" y="52" font-size="14" fill="#657286">\u5C1A\u672A\u58F0\u660E\u6A21\u5757\uFF0C\u7B49\u5F85\u5730\u56FE\u66F4\u65B0\u3002</text>');
|
|
1260
|
+
columns.bands.forEach((band, i) => {
|
|
1261
|
+
const top = TOP + rows.barY[i] * Y;
|
|
1262
|
+
const boxes = rows.bandBoxes[i];
|
|
1263
|
+
const bottom = boxes.reduce((max, b) => Math.max(max, TOP + (b.y + b.h - 1) * Y), top + 60);
|
|
1264
|
+
parts.push(`<rect x="8" y="${top - 10}" width="${width - 16}" height="${bottom - top + 26}" rx="5" fill="#fafbfc"/>`);
|
|
1265
|
+
parts.push(`<text x="16" y="${top + 5}" font-size="12" fill="#6a7687">${xml(fitWidth(band.name, Math.floor((width - 40) / X)))}</text>`);
|
|
1266
|
+
});
|
|
1267
|
+
if (rows.laneHeaderY !== void 0) map.lanes.forEach((lane, i) => {
|
|
1268
|
+
const region = columns.lanes[i];
|
|
1269
|
+
parts.push(`<text x="${(region.x + region.w / 2) * X}" y="${TOP + rows.laneHeaderY * Y + 5}" text-anchor="middle" font-size="12" fill="#566477">${xml(fitWidth(lane.label, region.w))}</text>`);
|
|
1270
|
+
});
|
|
1271
|
+
for (const edge of routing.edges) {
|
|
1272
|
+
const points = edgePolyline(edge, rows).map(([x, y]) => `${x * X},${TOP + y * Y}`).join(" ");
|
|
1273
|
+
parts.push(`<polyline points="${points}" fill="none" stroke="#8995a5" stroke-width="1.4" marker-end="url(#arrow)"/>`);
|
|
1274
|
+
}
|
|
1275
|
+
for (const box of rows.boxOf.values()) {
|
|
1276
|
+
const node = originals.get(box.node.id);
|
|
1277
|
+
const palette = neutral ? PALETTES.neutral : node.status === "done" && !isVerified(node) ? PALETTES.unverified : PALETTES[node.status];
|
|
1278
|
+
const x = box.x * X, y = TOP + box.y * Y, w = (box.w - 1) * X, h = (box.h - 1) * Y;
|
|
1279
|
+
const dash = !neutral && node.status === "planned" ? ' stroke-dasharray="5 4"' : "";
|
|
1280
|
+
parts.push(`<g data-node="${xml(node.id)}"><title>${xml(node.label)}${neutral ? "" : ` \xB7 ${xml(statusText(node))}`}</title>`);
|
|
1281
|
+
parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="5" fill="${palette[0]}" stroke="${palette[1]}" stroke-width="1.5"${dash}/>`);
|
|
1282
|
+
parts.push(`<text x="${x + w / 2}" y="${y + 21}" text-anchor="middle" font-size="14" fill="${palette[2]}">${xml(box.label.trim())}</text>`);
|
|
1283
|
+
const secondary = neutral ? node.kind ?? node.id : statusText(node);
|
|
1284
|
+
parts.push(`<text x="${x + w / 2}" y="${y + 40}" text-anchor="middle" font-size="11" fill="${palette[2]}">${xml(fitWidth(secondary, box.w - 4))}</text></g>`);
|
|
1285
|
+
}
|
|
1286
|
+
parts.push("</g></svg>");
|
|
1287
|
+
return parts.join("\n");
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// src/preview/publisher.ts
|
|
1291
|
+
var PREVIEW_DIR_NAME = "previews";
|
|
1292
|
+
var ENABLED = ".enabled";
|
|
1293
|
+
var PUBLISH_LOCK = ".publish-lock";
|
|
1294
|
+
function previewDirectory(defaultFile) {
|
|
1295
|
+
return join3(dirname4(defaultFile), PREVIEW_DIR_NAME);
|
|
1296
|
+
}
|
|
1297
|
+
function previewFile(defaultFile, page) {
|
|
1298
|
+
if (page !== void 0 && !ID_RULE.test(page)) throw new Error("Invalid preview page id.");
|
|
1299
|
+
return join3(previewDirectory(defaultFile), documentName(page));
|
|
1300
|
+
}
|
|
1301
|
+
function save(path, contents) {
|
|
1302
|
+
try {
|
|
1303
|
+
if (readFileSync2(path, "utf8") === contents) return;
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error.code !== "ENOENT") throw error;
|
|
1306
|
+
}
|
|
1307
|
+
const result = writeFileAtomic(path, contents);
|
|
1308
|
+
if (!result.ok) throw new Error(describeStoreError(result.error));
|
|
1309
|
+
}
|
|
1310
|
+
function ownedDirectory(path) {
|
|
1311
|
+
mkdirSync2(path, { recursive: true });
|
|
1312
|
+
const expected = join3(realpathSync(dirname4(path)), path.slice(dirname4(path).length + 1));
|
|
1313
|
+
const actual = realpathSync(path);
|
|
1314
|
+
if (process.platform === "win32" ? actual.toLowerCase() !== expected.toLowerCase() : actual !== expected) {
|
|
1315
|
+
throw new Error(`Preview directory redirects outside its parent: ${path}`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function acquireLock(directory) {
|
|
1319
|
+
const path = join3(directory, PUBLISH_LOCK);
|
|
1320
|
+
const deadline = Date.now() + 2e3;
|
|
1321
|
+
while (true) {
|
|
1322
|
+
try {
|
|
1323
|
+
mkdirSync2(path);
|
|
1324
|
+
return () => rmdirSync(path);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if (error.code !== "EEXIST") throw error;
|
|
1327
|
+
if (Date.now() >= deadline) throw new Error(`Preview export is busy or was interrupted. Retry; if no exporter is running, remove the stale lock directory: ${path}`);
|
|
1328
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function createPreviewPublisher(defaultFile) {
|
|
1333
|
+
const directory = previewDirectory(defaultFile);
|
|
1334
|
+
const enabledFile = join3(directory, ENABLED);
|
|
1335
|
+
const enabled = () => existsSync2(enabledFile);
|
|
1336
|
+
const refresh = (page) => {
|
|
1337
|
+
try {
|
|
1338
|
+
const path = previewFile(defaultFile, page);
|
|
1339
|
+
ownedDirectory(directory);
|
|
1340
|
+
const release = acquireLock(directory);
|
|
1341
|
+
try {
|
|
1342
|
+
const pages = [];
|
|
1343
|
+
for (const source of listPageFiles(defaultFile)) {
|
|
1344
|
+
const slug = pageIdOfFile(defaultFile, source);
|
|
1345
|
+
if (slug !== void 0 && !ID_RULE.test(slug)) throw new Error(`Invalid map page filename: ${source}`);
|
|
1346
|
+
const loaded = loadMapFile(source);
|
|
1347
|
+
if (!loaded.ok) throw new Error(describeStoreError(loaded.error));
|
|
1348
|
+
pages.push({ page: slug, map: loaded.value });
|
|
1349
|
+
}
|
|
1350
|
+
if (page !== void 0 && !pages.some((p) => p.page === page)) return err(`No map page named "${page}".`);
|
|
1351
|
+
if (page === void 0 && !pages.some((p) => p.page === void 0)) pages.unshift({ page: void 0, map: EMPTY_MAP });
|
|
1352
|
+
const images = join3(directory, "images");
|
|
1353
|
+
ownedDirectory(images);
|
|
1354
|
+
const present = /* @__PURE__ */ new Set();
|
|
1355
|
+
for (const item of pages) {
|
|
1356
|
+
const svg = renderMapSvg(item.map);
|
|
1357
|
+
const digest = createHash("sha256").update(svg).digest("hex");
|
|
1358
|
+
const image = `images/${digest}.svg`;
|
|
1359
|
+
save(join3(images, `${digest}.svg`), svg);
|
|
1360
|
+
const filename = documentName(item.page);
|
|
1361
|
+
save(join3(directory, filename), renderMapMarkdown(item.map, image, pages));
|
|
1362
|
+
present.add(filename);
|
|
1363
|
+
}
|
|
1364
|
+
for (const filename of readdirSync2(directory)) {
|
|
1365
|
+
if (/^(map|page-[a-z0-9][a-z0-9-]{0,63})\.md$/.test(filename) && !present.has(filename)) {
|
|
1366
|
+
save(join3(directory, filename), "# \u5730\u56FE\u5DF2\u5220\u9664\n\n\u6B64\u9875\u9762\u5DF2\u4E0D\u5728\u9879\u76EE\u5730\u56FE\u4E2D\u3002\n\n[\u8FD4\u56DE\u5730\u56FE\u76EE\u5F55](index.md)\n");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const index = join3(directory, "index.md");
|
|
1370
|
+
save(index, renderPreviewIndex(pages));
|
|
1371
|
+
return ok({ path: resolve(path), index: resolve(index), pages: pages.length });
|
|
1372
|
+
} finally {
|
|
1373
|
+
release();
|
|
1374
|
+
}
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
return err(error instanceof Error ? error.message : String(error));
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
const activate = (page) => {
|
|
1380
|
+
const published = refresh(page);
|
|
1381
|
+
if (!published.ok) return published;
|
|
1382
|
+
try {
|
|
1383
|
+
save(enabledFile, "Markdown preview updates are enabled for this project.\n");
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
return err(error instanceof Error ? error.message : String(error));
|
|
1386
|
+
}
|
|
1387
|
+
return published;
|
|
1388
|
+
};
|
|
1389
|
+
return { enabled, refresh, activate };
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// src/preview/cli.ts
|
|
1393
|
+
function runPreview(args) {
|
|
1394
|
+
const usage = "usage: mellos-mapping-preview <project-directory> [--page <slug>]";
|
|
1395
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
1396
|
+
console.log(usage);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (!(args.length === 1 || args.length === 3 && args[1] === "--page") || args[0].startsWith("--")) throw new Error(usage);
|
|
1400
|
+
const project = resolve2(args[0]);
|
|
1401
|
+
if (!existsSync3(project) || !statSync(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
|
|
1402
|
+
const parsed = args[2] === void 0 ? void 0 : makePageId(args[2]);
|
|
1403
|
+
if (parsed !== void 0 && !parsed.ok) throw new Error(`Invalid page slug: ${args[2]}`);
|
|
1404
|
+
const result = createPreviewPublisher(join4(project, STATE_FILE_RELATIVE_PATH)).activate(parsed?.value);
|
|
1405
|
+
if (!result.ok) throw new Error(result.error);
|
|
1406
|
+
console.log(JSON.stringify({ surface: "markdown", ...result.value, visibility: "unconfirmed" }));
|
|
1407
|
+
}
|
|
1408
|
+
if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(resolve2(process.argv[1])).href) {
|
|
1409
|
+
try {
|
|
1410
|
+
runPreview(process.argv.slice(2));
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1413
|
+
process.exitCode = 1;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
export {
|
|
1417
|
+
runPreview
|
|
1418
|
+
};
|