flowfig 0.7.0 → 0.8.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 +55 -23
- package/dist/check.js +68 -6
- package/dist/cli.js +24 -13
- package/dist/code.d.ts +26 -0
- package/dist/code.js +568 -0
- package/dist/diff.js +12 -5
- package/dist/draw.js +1 -1
- package/dist/edges.d.ts +7 -0
- package/dist/edges.js +557 -0
- package/dist/geometry.d.ts +11 -2
- package/dist/geometry.js +255 -67
- package/dist/guide.d.ts +22 -2
- package/dist/guide.js +282 -219
- package/dist/imports.d.ts +11 -0
- package/dist/imports.js +235 -0
- package/dist/index.js +36 -28
- package/dist/load.d.ts +2 -0
- package/dist/load.js +24 -1
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +22 -9
- package/dist/model.d.ts +13 -2
- package/dist/model.js +54 -11
- package/dist/scene.d.ts +2 -0
- package/dist/source.d.ts +1 -1
- package/dist/source.js +3 -3
- package/dist/svg.js +49 -32
- package/dist/type-index.d.ts +55 -0
- package/dist/type-index.js +383 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.js +733 -0
- package/dist/verify.d.ts +25 -3
- package/dist/verify.js +67 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CodeFile, type Read } from './code.ts';
|
|
2
|
+
export type Import = {
|
|
3
|
+
name: string;
|
|
4
|
+
local: string;
|
|
5
|
+
path: string | null;
|
|
6
|
+
};
|
|
7
|
+
export declare const OUTSIDE = "#outside";
|
|
8
|
+
export declare function resolveTs(root: string, from: string, spec: string, read: Read): string | null;
|
|
9
|
+
export declare function resolvePy(root: string, from: string, mod: string, read: Read, top?: string): string | null;
|
|
10
|
+
export declare function importsOf(root: string, file: CodeFile, read: Read): Import[];
|
|
11
|
+
export declare function leadsTo(root: string, fromPath: string, toPath: string, name: string, read?: Read, cache?: Map<string, CodeFile | null>, depth?: number): string | null;
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { builtinModules } from 'node:module';
|
|
2
|
+
import { posix, resolve } from 'node:path';
|
|
3
|
+
import { codeFile, esc, langOf, outside, readFile } from './code.js';
|
|
4
|
+
export const OUTSIDE = '#outside';
|
|
5
|
+
const TS_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js'];
|
|
6
|
+
const readIn = (root, path, read) => (outside(root, path) ? null : read(resolve(root, path)));
|
|
7
|
+
const has = (root, path, read) => readIn(root, path, read) != null;
|
|
8
|
+
function tsConfig(root, read) {
|
|
9
|
+
const paths = [];
|
|
10
|
+
let baseUrl;
|
|
11
|
+
let cfg = 'tsconfig.json';
|
|
12
|
+
for (let k = 0; k < 4 && cfg; k++) {
|
|
13
|
+
const raw = readIn(root, cfg, read);
|
|
14
|
+
if (raw == null)
|
|
15
|
+
break;
|
|
16
|
+
const dir = posix.dirname(cfg);
|
|
17
|
+
const own = /"baseUrl"\s*:\s*"([^"]*)"/.exec(raw)?.[1];
|
|
18
|
+
if (own != null)
|
|
19
|
+
baseUrl ??= posix.join(dir, own);
|
|
20
|
+
const block = /"paths"\s*:\s*\{([^}]*)\}/.exec(raw)?.[1] ?? '';
|
|
21
|
+
for (const m of block.matchAll(/"([^"]+)"\s*:\s*\[\s*"([^"]+)"/g))
|
|
22
|
+
paths.push({ base: own != null ? posix.join(dir, own) : dir, key: m[1], target: m[2] });
|
|
23
|
+
const ext = /"extends"\s*:\s*"(\.[^"]+)"/.exec(raw)?.[1];
|
|
24
|
+
cfg = ext ? posix.join(dir, ext.endsWith('.json') ? ext : `${ext}.json`) : '';
|
|
25
|
+
}
|
|
26
|
+
return { paths, baseUrl };
|
|
27
|
+
}
|
|
28
|
+
export function resolveTs(root, from, spec, read) {
|
|
29
|
+
const bases = [];
|
|
30
|
+
if (spec.startsWith('.'))
|
|
31
|
+
bases.push(posix.join(posix.dirname(from), spec));
|
|
32
|
+
else {
|
|
33
|
+
const cfg = tsConfig(root, read);
|
|
34
|
+
for (const { base, key, target } of cfg.paths) {
|
|
35
|
+
const star = key.indexOf('*');
|
|
36
|
+
const pre = star < 0 ? key : key.slice(0, star);
|
|
37
|
+
const post = star < 0 ? '' : key.slice(star + 1);
|
|
38
|
+
if (star < 0 ? spec !== key : !(spec.startsWith(pre) && spec.endsWith(post) && spec.length >= key.length - 1))
|
|
39
|
+
continue;
|
|
40
|
+
bases.push(posix.join(base, star < 0 ? target : target.replace('*', spec.slice(pre.length, spec.length - post.length))));
|
|
41
|
+
}
|
|
42
|
+
if (cfg.baseUrl != null)
|
|
43
|
+
bases.push(posix.join(cfg.baseUrl, spec));
|
|
44
|
+
}
|
|
45
|
+
for (const b of bases) {
|
|
46
|
+
const base = b.replace(/\.(js|mjs|cjs|jsx)$/, '');
|
|
47
|
+
const hit = TS_EXT.map((e) => base + e).find((p) => has(root, p, read));
|
|
48
|
+
if (hit)
|
|
49
|
+
return hit;
|
|
50
|
+
}
|
|
51
|
+
return spec.startsWith('node:') || builtinModules.includes(spec.split('/')[0]) ? OUTSIDE : null;
|
|
52
|
+
}
|
|
53
|
+
function pyTop(root, from, read) {
|
|
54
|
+
let d = posix.dirname(from);
|
|
55
|
+
while (d !== '.' && has(root, `${d}/__init__.py`, read))
|
|
56
|
+
d = posix.dirname(d);
|
|
57
|
+
return d;
|
|
58
|
+
}
|
|
59
|
+
export function resolvePy(root, from, mod, read, top) {
|
|
60
|
+
const hit = (b) => [`${b}.py`, `${b}/__init__.py`].find((p) => has(root, p, read)) ?? null;
|
|
61
|
+
if (mod.startsWith('.')) {
|
|
62
|
+
const dots = /^\.+/.exec(mod)[0].length;
|
|
63
|
+
let d = posix.dirname(from);
|
|
64
|
+
for (let k = 1; k < dots; k++) {
|
|
65
|
+
if (d === '.')
|
|
66
|
+
return null;
|
|
67
|
+
d = posix.dirname(d);
|
|
68
|
+
}
|
|
69
|
+
return hit(posix.join(d, mod.slice(dots).replace(/\./g, '/')));
|
|
70
|
+
}
|
|
71
|
+
const rel = mod.replace(/\./g, '/');
|
|
72
|
+
const d = top ?? pyTop(root, from, read);
|
|
73
|
+
return hit(rel) ?? (d === '.' ? null : hit(posix.join(d, rel)));
|
|
74
|
+
}
|
|
75
|
+
function goModule(root, from, read) {
|
|
76
|
+
let dir = posix.dirname(from);
|
|
77
|
+
for (;;) {
|
|
78
|
+
const text = readIn(root, posix.join(dir, 'go.mod'), read);
|
|
79
|
+
if (text != null)
|
|
80
|
+
return { mod: /module\s+(\S+)/.exec(text)?.[1], dir, text };
|
|
81
|
+
if (dir === '.')
|
|
82
|
+
return { dir, text: '' };
|
|
83
|
+
dir = posix.dirname(dir);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function goWork(root, from, read) {
|
|
87
|
+
for (let d = posix.dirname(from);; d = posix.dirname(d)) {
|
|
88
|
+
const text = readIn(root, posix.join(d, 'go.work'), read);
|
|
89
|
+
if (text != null)
|
|
90
|
+
return [...text.matchAll(/^\s*(?:use\s+)?"?(\.[^\s")]*)"?\s*$/gm)].flatMap((m) => {
|
|
91
|
+
const dir = posix.join(d, m[1]);
|
|
92
|
+
const mod = /module\s+(\S+)/.exec(readIn(root, posix.join(dir, 'go.mod'), read) ?? '')?.[1];
|
|
93
|
+
return mod ? [{ mod, dir }] : [];
|
|
94
|
+
});
|
|
95
|
+
if (d === '.')
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const goRequired = (text, p) => [...text.matchAll(/^\s*(?:require\s+)?([\w.~-]+\/[\w.~/-]+)\s+v\S+/gm)].some((m) => (p === m[1] || p.startsWith(`${m[1]}/`)) && !new RegExp(`^\\s*(?:replace\\s+)?${esc(m[1])}\\b[^\\n]*=>`, 'm').test(text));
|
|
100
|
+
export function importsOf(root, file, read) {
|
|
101
|
+
const { keep, lang, path } = file;
|
|
102
|
+
const out = [];
|
|
103
|
+
const add = (name, local, to) => out.push({ name, local, path: to });
|
|
104
|
+
if (lang === 'ts') {
|
|
105
|
+
for (const m of keep.matchAll(/\bimport\s+(type\s+)?([\s\S]*?)\s+from\s+(["'])([^"']+)\3/g)) {
|
|
106
|
+
const to = resolveTs(root, path, m[4], read);
|
|
107
|
+
const clause = m[2];
|
|
108
|
+
const def = /^([A-Za-z_$][\w$]*)\s*(,|$)/.exec(clause);
|
|
109
|
+
if (def)
|
|
110
|
+
add('default', def[1], to);
|
|
111
|
+
const ns = /\*\s+as\s+([A-Za-z_$][\w$]*)/.exec(clause);
|
|
112
|
+
if (ns)
|
|
113
|
+
add('*', ns[1], to);
|
|
114
|
+
const named = /\{([\s\S]*)\}/.exec(clause);
|
|
115
|
+
if (named)
|
|
116
|
+
for (const part of named[1].split(',')) {
|
|
117
|
+
const p = part.trim().replace(/^type\s+/, '');
|
|
118
|
+
if (!p)
|
|
119
|
+
continue;
|
|
120
|
+
const [a, b] = p.split(/\s+as\s+/);
|
|
121
|
+
add(a.trim(), (b ?? a).trim(), to);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const m of keep.matchAll(/\b(?:const|let|var)\s+(\{[^}]*\}|[A-Za-z_$][\w$]*)\s*=\s*(?:await\s+)?(?:require|import)\((["'])([^"']+)\2\)/g)) {
|
|
125
|
+
const to = resolveTs(root, path, m[3], read);
|
|
126
|
+
if (m[1].startsWith('{'))
|
|
127
|
+
for (const p of m[1].slice(1, -1).split(',')) {
|
|
128
|
+
const [a, b] = p.split(':').map((x) => x.trim());
|
|
129
|
+
if (a)
|
|
130
|
+
add(a, b ?? a, to);
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
add('*', m[1], to);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (lang === 'py') {
|
|
137
|
+
const top = pyTop(root, path, read);
|
|
138
|
+
for (const m of keep.matchAll(/^[ \t]*from\s+([\w.]+)\s+import\s+(\([^)]*\)|[^\n]+)/gm)) {
|
|
139
|
+
const to = resolvePy(root, path, m[1], read, top);
|
|
140
|
+
for (const part of m[2].replace(/[()]/g, '').split(',')) {
|
|
141
|
+
const p = part.trim();
|
|
142
|
+
if (!p)
|
|
143
|
+
continue;
|
|
144
|
+
const [a, b] = p.split(/\s+as\s+/).map((x) => x.trim());
|
|
145
|
+
const sub = resolvePy(root, path, (m[1].endsWith('.') ? m[1] : `${m[1]}.`) + a, read, top);
|
|
146
|
+
if (sub)
|
|
147
|
+
add('*', b ?? a, sub);
|
|
148
|
+
else
|
|
149
|
+
add(a, b ?? a, to);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const m of keep.matchAll(/^[ \t]*import\s+([\w.]+)(\s+as\s+(\w+))?/gm))
|
|
153
|
+
add('*', m[3] ?? m[1].split('.')[0], resolvePy(root, path, m[1], read, top));
|
|
154
|
+
}
|
|
155
|
+
else if (lang === 'go') {
|
|
156
|
+
const { mod, dir, text } = goModule(root, path, read);
|
|
157
|
+
let work;
|
|
158
|
+
for (const m of keep.matchAll(/^\s*(?:import\s+)?(\w+|\.|_)?\s*"([^"]+)"/gm)) {
|
|
159
|
+
const p = m[2];
|
|
160
|
+
const inside = (w) => w.mod && (p === w.mod || p.startsWith(`${w.mod}/`));
|
|
161
|
+
const own = [
|
|
162
|
+
{ mod, dir },
|
|
163
|
+
...(p.split('/')[0].includes('.') && !inside({ mod, dir }) ? (work ??= goWork(root, path, read)) : []),
|
|
164
|
+
].find(inside);
|
|
165
|
+
const to = own
|
|
166
|
+
? posix.join(own.dir, p.slice(own.mod.length + 1))
|
|
167
|
+
: p.split('/')[0].includes('.') && !goRequired(text, p)
|
|
168
|
+
? null
|
|
169
|
+
: OUTSIDE;
|
|
170
|
+
add('*', m[1] ?? p.split('/').at(-1), to);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
function reexports(root, file, name, read) {
|
|
176
|
+
const N = esc(name);
|
|
177
|
+
const out = [];
|
|
178
|
+
if (name === '*')
|
|
179
|
+
return out;
|
|
180
|
+
if (file.lang === 'py') {
|
|
181
|
+
if (new RegExp(`^(?:(?:async\\s+)?def|class)\\s+${N}\\b|^${N}\\s*[:=]`, 'm').test(file.code))
|
|
182
|
+
return out;
|
|
183
|
+
for (const i of importsOf(root, file, read))
|
|
184
|
+
if (i.local === name || i.local === '*')
|
|
185
|
+
out.push([i.path, i.local === '*' ? '?' : i.name]);
|
|
186
|
+
return out.length > 1 ? [[null, '?']] : out;
|
|
187
|
+
}
|
|
188
|
+
if (file.lang !== 'ts')
|
|
189
|
+
return out;
|
|
190
|
+
if (new RegExp(`^export\\s+(?:default\\s+)?(?:async\\s+)?(?:function\\s*\\*?|class|const|let|var|enum)\\s*${N}\\b`, 'm').test(file.code))
|
|
191
|
+
return out;
|
|
192
|
+
for (const m of file.keep.matchAll(/\bexport\s+(?:type\s+)?(\*(?:\s+as\s+([\w$]+))?|\{([^}]*)\})\s+from\s+(["'])([^"']+)\4/g)) {
|
|
193
|
+
const p = resolveTs(root, file.path, m[5], read);
|
|
194
|
+
if (m[3] != null) {
|
|
195
|
+
for (const part of m[3].split(',')) {
|
|
196
|
+
const [a, b] = part
|
|
197
|
+
.trim()
|
|
198
|
+
.replace(/^type\s+/, '')
|
|
199
|
+
.split(/\s+as\s+/);
|
|
200
|
+
if (a && (b ?? a).trim() === name)
|
|
201
|
+
out.push([p, a.trim()]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (m[2] == null ? name !== 'default' : m[2] === name)
|
|
205
|
+
out.push([p, m[2] == null ? name : '*']);
|
|
206
|
+
}
|
|
207
|
+
for (const m of file.keep.matchAll(/\bexport\s*\{([^}]*)\}(?!\s*from)/g))
|
|
208
|
+
for (const part of m[1].split(',')) {
|
|
209
|
+
const [a, b] = part.trim().split(/\s+as\s+/);
|
|
210
|
+
if (!a || (b ?? a).trim() !== name)
|
|
211
|
+
continue;
|
|
212
|
+
const i = importsOf(root, file, read).find((x) => x.local === a.trim());
|
|
213
|
+
if (i)
|
|
214
|
+
out.push([i.path, i.name]);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
export function leadsTo(root, fromPath, toPath, name, read = readFile, cache = new Map(), depth = 0) {
|
|
219
|
+
if (fromPath === toPath)
|
|
220
|
+
return name;
|
|
221
|
+
if (langOf(fromPath) == null)
|
|
222
|
+
return posix.dirname(toPath) === fromPath ? name : null;
|
|
223
|
+
const file = depth < 5 ? codeFile(root, fromPath, read, cache) : null;
|
|
224
|
+
if (!file)
|
|
225
|
+
return null;
|
|
226
|
+
let star = false;
|
|
227
|
+
for (const [path, inner] of reexports(root, file, name, read)) {
|
|
228
|
+
const r = path == null || inner === '?' ? null : leadsTo(root, path, toPath, inner, read, cache, depth + 1);
|
|
229
|
+
if (r === '?' || inner === '?')
|
|
230
|
+
star = true;
|
|
231
|
+
else if (r != null)
|
|
232
|
+
return r;
|
|
233
|
+
}
|
|
234
|
+
return star ? '?' : null;
|
|
235
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Fragment, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { route } from './geometry.js';
|
|
4
|
+
import { arcRoom, avoidOf, route } from './geometry.js';
|
|
5
5
|
import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
6
6
|
import { textWidth } from './text.js';
|
|
7
7
|
import { checkScene, checkSpec, checkTheme } from './check.js';
|
|
8
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD,
|
|
8
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, EDGE_OFF, EDGE_ON, LIGHT, TONES, ON_ACCENT, toneFill, toneTint, decisions, edgeId, groupGap, isGroup, isRows, lanePlan as planLanes, laneEnd, laneBlock, STUB_ROOM, labelPillW, str, LANE_BLOCK_GAP, LANE_PAD, LANE_ROW_GAP, isLanesLayout, nodes, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, diamondLines, diamondRoom, tightCopies, nodeWidth, labelLines, fitCap, LABEL_LINE, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, edgeTip, } from './model.js';
|
|
9
9
|
export { LIGHT, DARK } from './model.js';
|
|
10
10
|
const DEFAULTS = { ...LIGHT, font: 'inherit' };
|
|
11
11
|
const v = (k) => `var(--fig-${k}, ${DEFAULTS[k]})`;
|
|
@@ -95,7 +95,9 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
95
95
|
: []),
|
|
96
96
|
]), [layout, tl, edges, timeline, today]);
|
|
97
97
|
const step = active == null ? null : steps[active];
|
|
98
|
+
const allBeats = useMemo(() => steps.flatMap((s) => s.flow.map(toBeat)), [steps]);
|
|
98
99
|
const beats = useMemo(() => (step?.flow ?? []).map(toBeat), [step]);
|
|
100
|
+
const cap = useMemo(() => fitCap({ layout, edges, steps, lanes, timeline }, 830), [layout, edges, steps, lanes, timeline]);
|
|
99
101
|
const carded = useMemo(() => {
|
|
100
102
|
const all = new Map();
|
|
101
103
|
for (const b of steps.flatMap((s) => s.flow.map(toBeat)))
|
|
@@ -155,25 +157,18 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
155
157
|
h: r.height / k,
|
|
156
158
|
};
|
|
157
159
|
});
|
|
158
|
-
const
|
|
160
|
+
const extra = [];
|
|
159
161
|
el.querySelectorAll('[data-fig-outside]').forEach((n) => {
|
|
160
162
|
const r = n.getBoundingClientRect();
|
|
161
|
-
|
|
163
|
+
extra.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
162
164
|
});
|
|
163
|
-
if (tl)
|
|
164
|
-
for (const n of nodes(layout))
|
|
165
|
-
if (rects[n.id])
|
|
166
|
-
avoid.push({ ...rects[n.id], box: true });
|
|
167
165
|
const stubs = lanePlan?.stubs ?? new Map();
|
|
168
|
-
if (stubs.size)
|
|
169
|
-
for (const n of nodes(layout))
|
|
170
|
-
if (rects[n.id])
|
|
171
|
-
avoid.push(rects[n.id]);
|
|
166
|
+
if (stubs.size)
|
|
172
167
|
el.querySelectorAll('[data-fig-gutter]').forEach((n) => {
|
|
173
168
|
const r = n.getBoundingClientRect();
|
|
174
|
-
|
|
169
|
+
extra.push({ x: (r.left - base.left) / k, y: (r.top - base.top) / k, w: r.width / k, h: r.height / k });
|
|
175
170
|
});
|
|
176
|
-
|
|
171
|
+
const avoid = avoidOf(nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])), extra, stubs.size > 0);
|
|
177
172
|
const bands = {};
|
|
178
173
|
el.querySelectorAll('[data-fig-copy]').forEach((n) => {
|
|
179
174
|
const r = n.getBoundingClientRect();
|
|
@@ -208,7 +203,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
208
203
|
stub: stubs.get(ids[i]).map(labelPillW),
|
|
209
204
|
bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)],
|
|
210
205
|
}),
|
|
211
|
-
...(
|
|
206
|
+
...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
212
207
|
})), rects, tips, avoid, pillArea, lanePlan ? { bands: Object.values(laneBands), boxes: nodes(layout).flatMap((n) => (rects[n.id] ? [rects[n.id]] : [])) } : undefined);
|
|
213
208
|
setRouted(next);
|
|
214
209
|
if (lanePlan) {
|
|
@@ -285,7 +280,17 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
285
280
|
const e = edges[ids.indexOf(r.id)];
|
|
286
281
|
if (r.stub)
|
|
287
282
|
return r.stub.pts.map((pts, j) => ({ id: r.id, from: e.from, to: e.to, curve: r.curve, pts, label: labels[`${r.id}:${j}`] }));
|
|
288
|
-
return [
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
id: r.id,
|
|
286
|
+
from: e.from,
|
|
287
|
+
to: e.to,
|
|
288
|
+
curve: r.curve,
|
|
289
|
+
label: labels[r.id],
|
|
290
|
+
...(tl && { behind: true }),
|
|
291
|
+
...(r.elbow && { elbow: r.elbow }),
|
|
292
|
+
},
|
|
293
|
+
];
|
|
289
294
|
}),
|
|
290
295
|
...(rail?.rows ?? []).flatMap((row) => {
|
|
291
296
|
const r = row.kind === 'message' ? labels[`rail:${row.n}`] : undefined;
|
|
@@ -458,7 +463,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
458
463
|
const inside = it.labelInside;
|
|
459
464
|
const outside = (_jsx("span", { "data-fig-outside": "", style: { position: 'absolute', left: '100%', marginLeft: 6, whiteSpace: 'nowrap', fontSize: 13, fontWeight: 500, color: v('fg') }, children: label }));
|
|
460
465
|
const top = LANE_PAD + it.row * (TL_BAR_H + TL_ROW_GAP);
|
|
461
|
-
return (_jsxs("div", { "data-fig": n.id, "data-diamond": it.milestone || undefined, onMouseEnter: () => setHover(n.id), onMouseLeave: () => setHover(null), style: {
|
|
466
|
+
return (_jsxs("div", { "data-fig": n.id, title: n.source, "data-diamond": it.milestone || undefined, onMouseEnter: () => setHover(n.id), onMouseLeave: () => setHover(null), style: {
|
|
462
467
|
position: 'absolute',
|
|
463
468
|
left: it.x,
|
|
464
469
|
top: it.milestone ? top + (TL_BAR_H - it.w) / 2 : top,
|
|
@@ -648,13 +653,15 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
648
653
|
const store = item.shape === 'store';
|
|
649
654
|
const card = carded.has(item.id);
|
|
650
655
|
const dl = diamond && !card ? diamondLines(item, nodeWidth(item, false) + 70) : null;
|
|
656
|
+
const w = diamond || card || item.width != null || str(item.label) ? nodeWidth(item, card, cap) + (diamond ? 70 : 0) : undefined;
|
|
657
|
+
const lines = w != null && !diamond && str(item.label) ? labelLines(item, w) : null;
|
|
651
658
|
const bt = item.tone && TONES[item.tone];
|
|
652
|
-
return (_jsxs("div", { "data-fig": item.id, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
659
|
+
return (_jsxs("div", { "data-fig": item.id, title: item.source, "data-diamond": diamond || undefined, onMouseEnter: () => setHover(item.id), onMouseLeave: () => setHover(null), style: {
|
|
653
660
|
position: 'relative',
|
|
654
661
|
isolation: 'isolate',
|
|
655
662
|
minWidth: 100,
|
|
656
|
-
maxWidth:
|
|
657
|
-
width:
|
|
663
|
+
maxWidth: w == null ? cap : undefined,
|
|
664
|
+
width: w,
|
|
658
665
|
minHeight: dl ? Math.max(dl.h, (minHeight(item.id) ?? 0) + 24) : minHeight(item.id),
|
|
659
666
|
boxSizing: 'border-box',
|
|
660
667
|
display: 'flex',
|
|
@@ -707,7 +714,7 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
707
714
|
border: `1.5px solid ${bt ?? v('accent')}`,
|
|
708
715
|
background: `radial-gradient(circle, ${bt ?? v('accent')} 0 4px, transparent 4.5px)`,
|
|
709
716
|
}),
|
|
710
|
-
} })), _jsx("div", { children: item.label }), dl
|
|
717
|
+
} })), lines ? (lines.map((l, i) => (_jsx("div", { style: { whiteSpace: 'nowrap', lineHeight: `${LABEL_LINE}px` }, children: l }, i)))) : (_jsx("div", { style: { lineHeight: `${LABEL_LINE}px` }, children: item.label })), dl
|
|
711
718
|
? dl.subs.map((l, i) => (_jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), lineHeight: '15px', whiteSpace: 'nowrap' }, children: l.text }, i)))
|
|
712
719
|
: item.sub != null && _jsx("div", { style: { fontSize: 12, fontWeight: 400, color: v('muted'), marginTop: 2 }, children: item.sub }), card && (_jsxs("div", { style: {
|
|
713
720
|
marginTop: 8,
|
|
@@ -780,11 +787,10 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
780
787
|
margin: '0 auto',
|
|
781
788
|
transform: fit.scale < 1 ? `scale(${fit.scale})` : undefined,
|
|
782
789
|
transformOrigin: 'top left',
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
: 4,
|
|
790
|
+
paddingTop: arcRoom(routed, 'above') || 4,
|
|
791
|
+
paddingBottom: arcRoom(routed, 'below') || 4,
|
|
792
|
+
paddingLeft: arcRoom(routed, 'left') || 4,
|
|
793
|
+
paddingRight: arcRoom(routed, 'right') || 4,
|
|
788
794
|
}, children: [renderItem(layout, 0), _jsxs("svg", { style: {
|
|
789
795
|
position: 'absolute',
|
|
790
796
|
inset: 0,
|
|
@@ -797,11 +803,12 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
797
803
|
const on = litEdges.has(r.id);
|
|
798
804
|
const tone = hopTone(r.id);
|
|
799
805
|
const hidden = !on && edges[ids.indexOf(r.id)].quiet;
|
|
806
|
+
const tip = edgeTip(r.id, edges[ids.indexOf(r.id)].source, allBeats);
|
|
800
807
|
const look = (d, key, guides = false) => (_jsx("path", { ref: guides
|
|
801
808
|
? (p) => {
|
|
802
809
|
paths.current[r.id] = p;
|
|
803
810
|
}
|
|
804
|
-
: undefined, d: d, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeOpacity: hidden ? 0 : focus && !on ? 0.35 : 1, markerEnd: hidden ? undefined : `url(#fig-arrow-${on ? 'on' : 'off'})`, style: { transition: 'stroke .25s, stroke-opacity .25s' } }, key));
|
|
811
|
+
: undefined, d: d, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeOpacity: hidden ? 0 : focus && !on ? 0.35 : 1, markerEnd: hidden ? undefined : `url(#fig-arrow-${on ? 'on' : 'off'})`, style: { transition: 'stroke .25s, stroke-opacity .25s', pointerEvents: tip ? 'stroke' : undefined }, children: tip && _jsx("title", { children: tip }) }, key));
|
|
805
812
|
const guide = (_jsx("path", { ref: (p) => {
|
|
806
813
|
paths.current[r.id] = p;
|
|
807
814
|
}, d: r.d, fill: "none", stroke: "none" }, r.id));
|
|
@@ -899,7 +906,8 @@ export function Flow({ layout, edges, steps: stepsIn = NONE, theme, speed = 900,
|
|
|
899
906
|
const ly = y + RAIL.row / 2;
|
|
900
907
|
const tone = state === 'now' && row.tone ? TONES[row.tone] : undefined;
|
|
901
908
|
const g = row.group != null ? rail.groups[row.group] : null;
|
|
902
|
-
|
|
909
|
+
const tip = edgeTip(row.edge, edges[ids.indexOf(row.edge)].source, allBeats);
|
|
910
|
+
return (_jsxs("g", { "data-rail-row": row.n, "data-state": state, opacity: state === 'next' && !on ? 0.45 : 1, style: { cursor: 'pointer' }, onClick: () => goTo(row.step, row.beat), onMouseEnter: () => setHoverEdge(row.edge), onMouseLeave: () => setHoverEdge(null), children: [tip && _jsx("title", { children: tip }), g && g.rows[0] === i && (_jsx("rect", { x: groupBox(rail, g).x, y: y + groupBox(rail, g).y, width: groupBox(rail, g).w, height: groupBox(rail, g).h, rx: 6, fill: `color-mix(in srgb, ${v('accent')} 8%, ${v('bg')})`, stroke: v('accent'), strokeDasharray: "3 3" })), _jsx("path", { d: `M ${x1} ${ly} H ${x2}`, fill: "none", stroke: on ? (tone ?? v('accent')) : v('muted'), strokeWidth: on ? EDGE_ON : EDGE_OFF, strokeDasharray: row.async ? '4 3' : undefined, markerEnd: "url(#flowfig-rail-arrow)" }), row.async &&
|
|
903
911
|
(() => {
|
|
904
912
|
const tagW = ASYNC_TAG_W;
|
|
905
913
|
const tx = !row.pill ? (x1 + x2) / 2 - tagW / 2 : x2 > x1 ? row.pill.x - tagW - 4 : row.pill.x + row.pill.w + 4;
|
package/dist/load.d.ts
CHANGED
|
@@ -8,3 +8,5 @@ export declare function svgWithSpec(props: FlowProps, opts?: SvgOptions, svg?: s
|
|
|
8
8
|
export declare function sortFindings(findings: Finding[], strict: boolean): Finding[];
|
|
9
9
|
/** One line per finding, the counts line (unless `withCounts` is false), and the `figure:` line. No color: the CLI adds it. */
|
|
10
10
|
export declare function reportLines(props: FlowProps, findings: Finding[], withCounts?: boolean): string[];
|
|
11
|
+
export declare function summaryLines(props: FlowProps): string[];
|
|
12
|
+
export declare function verifyLines(props: FlowProps, figure: string, root?: string): string[];
|
package/dist/load.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { counts } from './model.js';
|
|
2
|
+
import { counts, toBeat } from './model.js';
|
|
3
|
+
import { coverageLine, owners, unsureLines, verifyReport } from './verify.js';
|
|
3
4
|
import { toSvg } from './svg.js';
|
|
4
5
|
const SPEC_OPEN = '<metadata id="figure-spec"><![CDATA[';
|
|
5
6
|
const SPEC_CLOSE = ']]></metadata>';
|
|
@@ -54,3 +55,25 @@ export function reportLines(props, findings, withCounts = true) {
|
|
|
54
55
|
out.push(`figure: ${n(c.boxes, 'box').replace('boxs', 'boxes')}, ${n(c.groups, 'group')}, ${n(c.edges, 'edge')}, ${n(c.steps, 'step')}, ${n(c.messages, 'message')}`);
|
|
55
56
|
return out;
|
|
56
57
|
}
|
|
58
|
+
const text = (x) => (typeof x === 'string' ? x : '');
|
|
59
|
+
export function summaryLines(props) {
|
|
60
|
+
const edges = props.edges.map((e) => {
|
|
61
|
+
const label = text(e.label);
|
|
62
|
+
return `${e.from} -> ${e.to}${label ? `: ${label}` : ''}`;
|
|
63
|
+
});
|
|
64
|
+
const steps = (props.steps ?? []).map((s) => {
|
|
65
|
+
const hops = s.flow.reduce((k, b) => k + toBeat(b).hops.length, 0);
|
|
66
|
+
return `step "${text(s.label)}": ${hops} ${hops === 1 ? 'hop' : 'hops'}`;
|
|
67
|
+
});
|
|
68
|
+
return [...edges, ...steps];
|
|
69
|
+
}
|
|
70
|
+
export function verifyLines(props, figure, root = process.cwd()) {
|
|
71
|
+
if (!owners(props).some(([, source, via]) => source != null || via != null))
|
|
72
|
+
return [];
|
|
73
|
+
const { findings, coverage } = verifyReport(props, { root });
|
|
74
|
+
return [
|
|
75
|
+
...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${figure}: ${f.message}`),
|
|
76
|
+
coverageLine(figure, coverage),
|
|
77
|
+
...unsureLines(coverage),
|
|
78
|
+
];
|
|
79
|
+
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare const TOOLS: ({
|
|
|
13
13
|
inputSchema: {
|
|
14
14
|
type: string;
|
|
15
15
|
properties: {
|
|
16
|
+
topic: {
|
|
17
|
+
type: string;
|
|
18
|
+
enum: string[];
|
|
19
|
+
};
|
|
16
20
|
paths?: undefined;
|
|
17
21
|
root?: undefined;
|
|
18
22
|
strict?: undefined;
|
|
@@ -72,6 +76,7 @@ export declare const TOOLS: ({
|
|
|
72
76
|
type: string;
|
|
73
77
|
description: string;
|
|
74
78
|
};
|
|
79
|
+
topic?: undefined;
|
|
75
80
|
out: {
|
|
76
81
|
type: string;
|
|
77
82
|
description: string;
|
|
@@ -89,6 +94,7 @@ export declare const TOOLS: ({
|
|
|
89
94
|
inputSchema: {
|
|
90
95
|
type: string;
|
|
91
96
|
properties: {
|
|
97
|
+
topic?: undefined;
|
|
92
98
|
paths: {
|
|
93
99
|
type: string;
|
|
94
100
|
items: {
|
|
@@ -114,6 +120,7 @@ export declare const TOOLS: ({
|
|
|
114
120
|
inputSchema: {
|
|
115
121
|
type: string;
|
|
116
122
|
properties: {
|
|
123
|
+
topic?: undefined;
|
|
117
124
|
paths?: undefined;
|
|
118
125
|
root?: undefined;
|
|
119
126
|
strict?: undefined;
|
package/dist/mcp.js
CHANGED
|
@@ -2,11 +2,11 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
4
|
import { diff, formatDiff } from './diff.js';
|
|
5
|
-
import { GUIDE } from './guide.js';
|
|
6
|
-
import { loadSpec, reportLines, sortFindings, svgWithSpec } from './load.js';
|
|
5
|
+
import { GUIDE, TOPICS } from './guide.js';
|
|
6
|
+
import { loadSpec, reportLines, sortFindings, summaryLines, svgWithSpec, verifyLines } from './load.js';
|
|
7
7
|
import { check, render } from './svg.js';
|
|
8
8
|
import { checkRendered } from './check.js';
|
|
9
|
-
import {
|
|
9
|
+
import { coverageLine, unsureLines, verifyReport } from './verify.js';
|
|
10
10
|
import { VERSION } from './version.js';
|
|
11
11
|
const specSchema = {
|
|
12
12
|
description: 'The figure: the props object, { props }, or a path to a .json file or an SVG that flowfig wrote.',
|
|
@@ -19,7 +19,11 @@ const checkProps = {
|
|
|
19
19
|
minText: { type: 'number', description: 'The smallest text in px at that width. Default 10.' },
|
|
20
20
|
};
|
|
21
21
|
export const TOOLS = [
|
|
22
|
-
{
|
|
22
|
+
{
|
|
23
|
+
name: 'docs',
|
|
24
|
+
description: 'The flowfig guide. Read it before the first render. A topic gives the detail for one kind of figure.',
|
|
25
|
+
inputSchema: { type: 'object', properties: { topic: { type: 'string', enum: Object.keys(TOPICS) } } },
|
|
26
|
+
},
|
|
23
27
|
{
|
|
24
28
|
name: 'check',
|
|
25
29
|
description: 'List the faults of a figure: unknown ids, text overflow, crossings, label overlap, small text, low contrast. Fix every error before render.',
|
|
@@ -84,8 +88,14 @@ const types = (args, tool, want) => {
|
|
|
84
88
|
}
|
|
85
89
|
};
|
|
86
90
|
function run(name, args) {
|
|
87
|
-
if (name === 'docs')
|
|
88
|
-
|
|
91
|
+
if (name === 'docs') {
|
|
92
|
+
const topic = args.topic;
|
|
93
|
+
if (topic == null)
|
|
94
|
+
return ok(GUIDE);
|
|
95
|
+
if (typeof topic !== 'string' || !Object.hasOwn(TOPICS, topic))
|
|
96
|
+
throw new Bad(`docs: topic must be one of ${Object.keys(TOPICS).join(', ')}`);
|
|
97
|
+
return ok(TOPICS[topic]);
|
|
98
|
+
}
|
|
89
99
|
if (name === 'check' || name === 'render') {
|
|
90
100
|
need(args, name, name === 'render' ? ['spec', 'out'] : ['spec']);
|
|
91
101
|
types(args, name, { width: 'number', minText: 'number', out: 'string', strict: 'boolean' });
|
|
@@ -106,7 +116,7 @@ function run(name, args) {
|
|
|
106
116
|
catch (e) {
|
|
107
117
|
throw new Fault(`${out}: ${e.message}`);
|
|
108
118
|
}
|
|
109
|
-
return ok([...lines, `${out} — ${(svg.length / 1024).toFixed(1)} kB`].join('\n'));
|
|
119
|
+
return ok([...lines, ...summaryLines(props), ...verifyLines(props, out), `${out} — ${(svg.length / 1024).toFixed(1)} kB`].join('\n'));
|
|
110
120
|
}
|
|
111
121
|
if (name === 'verify') {
|
|
112
122
|
need(args, name, ['paths']);
|
|
@@ -115,6 +125,7 @@ function run(name, args) {
|
|
|
115
125
|
types(args, name, { root: 'string', strict: 'boolean' });
|
|
116
126
|
const root = args.root == null ? process.cwd() : String(args.root);
|
|
117
127
|
const lines = [];
|
|
128
|
+
const counts = [];
|
|
118
129
|
let errors = 0;
|
|
119
130
|
for (const p of args.paths) {
|
|
120
131
|
let props;
|
|
@@ -128,13 +139,15 @@ function run(name, args) {
|
|
|
128
139
|
lines.push(`${'error'.padEnd(8)} ${'missing-file'.padEnd(18)} ${e.message}`);
|
|
129
140
|
continue;
|
|
130
141
|
}
|
|
131
|
-
const
|
|
142
|
+
const report = verifyReport(props, { root });
|
|
143
|
+
counts.push(coverageLine(p, report.coverage), ...unsureLines(report.coverage));
|
|
144
|
+
const findings = sortFindings([...check(props), ...report.findings], args.strict === true);
|
|
132
145
|
errors += findings.filter((f) => f.severity === 'error').length;
|
|
133
146
|
lines.push(...findings.map((f) => `${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${p}: ${f.message}`));
|
|
134
147
|
}
|
|
135
148
|
const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`;
|
|
136
149
|
const total = lines.length;
|
|
137
|
-
lines.push(`${n(errors, 'error')}, ${n(total - errors, 'warning')}
|
|
150
|
+
lines.push(`${n(errors, 'error')}, ${n(total - errors, 'warning')}`, ...counts);
|
|
138
151
|
return ok(lines.join('\n'), errors > 0);
|
|
139
152
|
}
|
|
140
153
|
if (name === 'diff') {
|
package/dist/model.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export type FigEdge = {
|
|
|
58
58
|
quiet?: boolean;
|
|
59
59
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
60
60
|
source?: string;
|
|
61
|
+
/** The route, queue, topic or table that carries this edge. Checked by `flowfig verify`. */
|
|
62
|
+
via?: string;
|
|
61
63
|
};
|
|
62
64
|
/** One packet on one edge. A string is the edge `id`. */
|
|
63
65
|
export type FigHop = string | {
|
|
@@ -73,6 +75,8 @@ export type FigHop = string | {
|
|
|
73
75
|
async?: boolean;
|
|
74
76
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
75
77
|
source?: string;
|
|
78
|
+
/** The route, queue, topic or table that carries this edge. Checked by `flowfig verify`. */
|
|
79
|
+
via?: string;
|
|
76
80
|
};
|
|
77
81
|
/** One moment of a step. No edges means a pause. */
|
|
78
82
|
export type FigBeat = {
|
|
@@ -161,9 +165,11 @@ export type Beat = Omit<FigBeat, 'edges'> & {
|
|
|
161
165
|
tone?: FigTone;
|
|
162
166
|
async?: boolean;
|
|
163
167
|
source?: string;
|
|
168
|
+
via?: string;
|
|
164
169
|
}[];
|
|
165
170
|
};
|
|
166
171
|
export declare const toBeat: (b: FigHop | FigHop[] | FigBeat) => Beat;
|
|
172
|
+
export declare const edgeTip: (id: string, source: string | undefined, beats: Beat[]) => string | undefined;
|
|
167
173
|
export declare const isRows: (c: FigContent) => c is FigRow[];
|
|
168
174
|
export declare const edgeId: (e: FigEdge) => string;
|
|
169
175
|
export declare const isGroup: (x: FigNode | FigGroup) => x is FigGroup;
|
|
@@ -227,11 +233,17 @@ export declare const isLanesLayout: (g: FigGroup) => boolean;
|
|
|
227
233
|
export declare const validAt: (at: unknown) => at is number;
|
|
228
234
|
/** The time column of each box: first use in the steps, then layout order. `at` wins. */
|
|
229
235
|
export declare function laneColumns(fig: FlowProps): Map<string, number>;
|
|
236
|
+
export declare const LABEL_LINE = 18;
|
|
230
237
|
export declare const FRAME_SIDE = 18, NODE_MIN_W = 100, NODE_MAX_W = 190;
|
|
231
238
|
export declare const LANE_BLOCK_GAP = 40;
|
|
232
239
|
export declare const STUB = 12, STUB_CLEAR = 8, STUB_ROOM = 30;
|
|
233
240
|
/** The width of a box as the SVG draws it, before a diamond adds its 70 px. */
|
|
234
|
-
export declare function nodeWidth(item: FigNode, carded: boolean): number;
|
|
241
|
+
export declare function nodeWidth(item: FigNode, carded: boolean, cap?: number): number;
|
|
242
|
+
export declare function labelLines(item: FigNode, w: number): string[];
|
|
243
|
+
export declare function itemWidth(item: FigNode | FigGroup, carded: {
|
|
244
|
+
has(id: string): boolean;
|
|
245
|
+
}, edges: FigEdge[], cap?: number): number;
|
|
246
|
+
export declare function fitCap(fig: FlowProps, width: number, padding?: number): number;
|
|
235
247
|
export declare const diamondRoom: (w: number, h: number, far: number) => number;
|
|
236
248
|
/** The lines of a diamond with no card. The diamond grows 15 px for each sub line that wraps. */
|
|
237
249
|
export declare function diamondLines(item: FigNode, w: number): {
|
|
@@ -338,5 +350,4 @@ export declare const counts: (p: FlowProps) => {
|
|
|
338
350
|
steps: number;
|
|
339
351
|
messages: number;
|
|
340
352
|
};
|
|
341
|
-
/** A row's default gap grows to hold its widest edge label, plus 8 px each side. */
|
|
342
353
|
export declare function groupGap(g: FigGroup, edges: FigEdge[]): number;
|