flowfig 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -61
- package/dist/atlas.d.ts +6 -0
- package/dist/atlas.js +122 -0
- package/dist/auto.d.ts +3 -0
- package/dist/auto.js +397 -0
- package/dist/browser.js +3 -1
- package/dist/check.d.ts +2 -1
- package/dist/check.js +61 -19
- package/dist/cli.js +29 -6
- package/dist/coverage.js +10 -7
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +137 -7
- package/dist/geometry.d.ts +4 -0
- package/dist/geometry.js +45 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +177 -83
- package/dist/load.js +4 -3
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +19 -6
- package/dist/model.js +60 -17
- package/dist/open.d.ts +1 -0
- package/dist/open.js +11 -8
- package/dist/page.d.ts +2 -1
- package/dist/page.js +5 -4
- package/dist/svg.d.ts +7 -2
- package/dist/svg.js +76 -26
- package/dist/trace.js +10 -3
- package/dist/verify.d.ts +2 -0
- package/dist/verify.js +36 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/auto.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { edgeId, fitCap, idsIn, isGroup, isRows, itemWidth, nodes, toBeat, NODE_MAX_W, } from './model.js';
|
|
2
|
+
const PAGE = 830, PAD = 24, SWEEPS = 8, TALL = 2.5,
|
|
3
|
+
// A 20-box chain measures 105 px for each rank.
|
|
4
|
+
RANK_H = 105, MIN_TEXT = 10;
|
|
5
|
+
const flat = (it) => (isGroup(it) && it.label == null && it.id == null ? it.children.flatMap(flat) : [it]);
|
|
6
|
+
export const rowBreaks = new WeakSet();
|
|
7
|
+
const less = (p, q) => {
|
|
8
|
+
const k = p.findIndex((v, i) => v !== q[i]);
|
|
9
|
+
return k >= 0 && p[k] < q[k];
|
|
10
|
+
};
|
|
11
|
+
function arcFold(run, long, pos, carded, edges) {
|
|
12
|
+
const m = run.length;
|
|
13
|
+
const spans = long.map((e) => [pos.get(e.from), pos.get(e.to)].sort((p, q) => p - q));
|
|
14
|
+
const wide = new Map();
|
|
15
|
+
const width = (i, j) => wide.get(i * m + j) ?? wide.set(i * m + j, itemWidth({ direction: 'row', children: run.slice(i, j) }, carded, edges)).get(i * m + j);
|
|
16
|
+
let pick;
|
|
17
|
+
for (let code = 0; long.length <= 6 && code < 3 ** long.length; code++) {
|
|
18
|
+
const sides = spans.map((_, k) => Math.floor(code / 3 ** k) % 3);
|
|
19
|
+
const need = new Set(), full = new Set(), ban = new Set();
|
|
20
|
+
spans.forEach(([a, b], k) => {
|
|
21
|
+
if (!sides[k])
|
|
22
|
+
for (let c = a + 1; c <= b; c++)
|
|
23
|
+
ban.add(c);
|
|
24
|
+
else
|
|
25
|
+
for (const c of sides[k] === 1 ? [a + 1, b + 1] : [a, b]) {
|
|
26
|
+
need.add(c);
|
|
27
|
+
if (sides[k] === 1)
|
|
28
|
+
full.add(c);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const plans = [{ cost: [0, 0], from: -1 }];
|
|
32
|
+
for (let j = 1; j <= m; j++)
|
|
33
|
+
for (let i = j - 1; i >= Math.max(0, j - 4); i--) {
|
|
34
|
+
if ((i < j - 1 && need.has(i + 1)) || width(i, j) > PAGE - 2 * PAD)
|
|
35
|
+
break;
|
|
36
|
+
const prev = plans[i];
|
|
37
|
+
if (!prev || (i && ban.has(i)) || (full.has(j) && j - i < 4))
|
|
38
|
+
continue;
|
|
39
|
+
const cost = [prev.cost[0] + 1, Math.max(prev.cost[1], width(i, j))];
|
|
40
|
+
if (!plans[j] || less(cost, plans[j].cost))
|
|
41
|
+
plans[j] = { cost, from: i };
|
|
42
|
+
}
|
|
43
|
+
if (!plans[m] || (pick && !less(plans[m].cost, pick.cost)))
|
|
44
|
+
continue;
|
|
45
|
+
const cuts = [];
|
|
46
|
+
for (let j = m; j > 0; j = plans[j].from)
|
|
47
|
+
cuts.unshift(plans[j].from);
|
|
48
|
+
const line = (k) => cuts.findLastIndex((c) => c <= k);
|
|
49
|
+
const rowW = (r) => width(cuts[r], cuts[r + 1] ?? m);
|
|
50
|
+
const clash = spans.some(([a, b], k) => {
|
|
51
|
+
const [ra, rb] = [line(a), line(b)];
|
|
52
|
+
const inner = Array.from({ length: rb - ra + 1 }, (_, r) => rowW(ra + r));
|
|
53
|
+
return ((!sides[k] && ra > 0 && ra < cuts.length - 1) ||
|
|
54
|
+
(sides[k] === 1 && Math.min(rowW(ra), rowW(rb)) < Math.max(...inner)) ||
|
|
55
|
+
spans.some(([p, q], l) => l > k && sides[k] && sides[k] === sides[l] && a < q && p < b));
|
|
56
|
+
});
|
|
57
|
+
if (!clash)
|
|
58
|
+
pick = { cost: plans[m].cost, cuts, sides };
|
|
59
|
+
}
|
|
60
|
+
if (!pick)
|
|
61
|
+
return undefined;
|
|
62
|
+
const top = pick.cuts[1] ?? m;
|
|
63
|
+
long.forEach((e, k) => (e.around = [pos.get(e.from) < top ? 'above' : 'below', 'right', 'left'][pick.sides[k]]));
|
|
64
|
+
const rows = pick.cuts.map((c, k) => ({ direction: 'row', children: run.slice(c, pick.cuts[k + 1] ?? m) }));
|
|
65
|
+
const line = (id) => rows.findIndex((l) => idsIn(l).includes(id));
|
|
66
|
+
for (const e of edges)
|
|
67
|
+
if (!long.includes(e) && line(e.from) >= 0 && line(e.from) < line(e.to))
|
|
68
|
+
rowBreaks.add(e);
|
|
69
|
+
return { direction: 'column', align: 'start', children: rows };
|
|
70
|
+
}
|
|
71
|
+
function seenOrder(fig) {
|
|
72
|
+
const at = new Map();
|
|
73
|
+
const byId = new Map(fig.edges.map((e) => [edgeId(e), e]));
|
|
74
|
+
for (const s of fig.steps ?? [])
|
|
75
|
+
for (const b of s.flow)
|
|
76
|
+
for (const h of toBeat(b).hops) {
|
|
77
|
+
const e = byId.get(h.edge);
|
|
78
|
+
if (e)
|
|
79
|
+
for (const id of h.back ? [e.to, e.from] : [e.from, e.to])
|
|
80
|
+
if (!at.has(id))
|
|
81
|
+
at.set(id, at.size);
|
|
82
|
+
}
|
|
83
|
+
for (const n of nodes(fig.layout))
|
|
84
|
+
if (!at.has(n.id))
|
|
85
|
+
at.set(n.id, at.size);
|
|
86
|
+
return at;
|
|
87
|
+
}
|
|
88
|
+
function level(items, dir, ctx) {
|
|
89
|
+
const n = items.length;
|
|
90
|
+
const unit = new Map();
|
|
91
|
+
items.forEach((it, k) => idsIn(it).forEach((id) => unit.set(id, k)));
|
|
92
|
+
const first = items.map((it) => Math.min(...idsIn(it).map((id) => ctx.at.get(id) ?? Infinity)));
|
|
93
|
+
const pairs = ctx.edges.flatMap((e) => {
|
|
94
|
+
const a = unit.get(e.from), b = unit.get(e.to);
|
|
95
|
+
return a == null || b == null || a === b ? [] : [[a, b, e]];
|
|
96
|
+
});
|
|
97
|
+
const keys = [...items.keys()].sort((p, q) => first[p] - first[q]);
|
|
98
|
+
const state = Array.from({ length: n }, () => 0);
|
|
99
|
+
const back = new Set();
|
|
100
|
+
const dfs = (u) => {
|
|
101
|
+
state[u] = 1;
|
|
102
|
+
const out = [...pairs.keys()].filter((i) => pairs[i][0] === u).sort((i, j) => first[pairs[i][1]] - first[pairs[j][1]]);
|
|
103
|
+
for (const i of out) {
|
|
104
|
+
const v = pairs[i][1];
|
|
105
|
+
if (state[v] === 1)
|
|
106
|
+
back.add(i);
|
|
107
|
+
else if (state[v] === 0)
|
|
108
|
+
dfs(v);
|
|
109
|
+
}
|
|
110
|
+
state[u] = 2;
|
|
111
|
+
};
|
|
112
|
+
for (const k of keys)
|
|
113
|
+
if (!state[k])
|
|
114
|
+
dfs(k);
|
|
115
|
+
const dag = pairs.map(([a, b], i) => (back.has(i) ? [b, a] : [a, b]));
|
|
116
|
+
const rank = Array.from({ length: n }, () => 0);
|
|
117
|
+
for (let pass = 0; pass < n; pass++)
|
|
118
|
+
for (const [a, b] of dag)
|
|
119
|
+
rank[b] = Math.max(rank[b], rank[a] + 1);
|
|
120
|
+
const ranks = Math.max(0, ...rank) + 1;
|
|
121
|
+
const layers = Array.from({ length: ranks }, () => []);
|
|
122
|
+
const layerOf = [...rank];
|
|
123
|
+
for (const k of keys)
|
|
124
|
+
layers[rank[k]].push(k);
|
|
125
|
+
const links = [];
|
|
126
|
+
const via = dag.map(() => []);
|
|
127
|
+
for (const [i, [a, b]] of dag.entries()) {
|
|
128
|
+
let prev = a;
|
|
129
|
+
for (let r = rank[a] + 1; r < rank[b]; r++) {
|
|
130
|
+
const v = layerOf.length;
|
|
131
|
+
layerOf.push(r);
|
|
132
|
+
layers[r].push(v);
|
|
133
|
+
via[i].push(v);
|
|
134
|
+
links.push([prev, v]);
|
|
135
|
+
prev = v;
|
|
136
|
+
}
|
|
137
|
+
links.push([prev, b]);
|
|
138
|
+
}
|
|
139
|
+
const pos = [];
|
|
140
|
+
const index = () => layers.forEach((l) => l.forEach((x, i) => (pos[x] = i)));
|
|
141
|
+
const crossings = () => {
|
|
142
|
+
index();
|
|
143
|
+
let c = 0;
|
|
144
|
+
for (let i = 0; i < links.length; i++)
|
|
145
|
+
for (let j = i + 1; j < links.length; j++) {
|
|
146
|
+
const [a, b] = links[i], [p, q] = links[j];
|
|
147
|
+
if (layerOf[a] === layerOf[p] && (pos[a] - pos[p]) * (pos[b] - pos[q]) < 0)
|
|
148
|
+
c++;
|
|
149
|
+
}
|
|
150
|
+
return c;
|
|
151
|
+
};
|
|
152
|
+
let best = layers.map((l) => [...l]);
|
|
153
|
+
let fewest = crossings();
|
|
154
|
+
for (let it = 0; it < SWEEPS; it++) {
|
|
155
|
+
const down = it % 2 === 0;
|
|
156
|
+
const order = [...layers.keys()].slice(1).map((r) => (down ? r : ranks - 1 - r));
|
|
157
|
+
for (const r of order) {
|
|
158
|
+
index();
|
|
159
|
+
const bary = (x) => {
|
|
160
|
+
const ns = links.filter(([a, b]) => (down ? b : a) === x).map(([a, b]) => pos[down ? a : b]);
|
|
161
|
+
return ns.length ? ns.reduce((s, v) => s + v, 0) / ns.length : pos[x];
|
|
162
|
+
};
|
|
163
|
+
const w = new Map(layers[r].map((x) => [x, bary(x)]));
|
|
164
|
+
layers[r].sort((x, y) => w.get(x) - w.get(y));
|
|
165
|
+
}
|
|
166
|
+
const c = crossings();
|
|
167
|
+
if (c < fewest)
|
|
168
|
+
[fewest, best] = [c, layers.map((l) => [...l])];
|
|
169
|
+
}
|
|
170
|
+
const cross = dir === 'row' ? 'column' : 'row';
|
|
171
|
+
const built = items.map((it) => (isGroup(it) ? { ...it, direction: dir, children: level(it.children.flatMap(flat), dir, ctx) } : it));
|
|
172
|
+
const beside = new Map();
|
|
173
|
+
const unitOf = (k) => {
|
|
174
|
+
const leaf = beside.get(k);
|
|
175
|
+
if (leaf == null)
|
|
176
|
+
return built[k];
|
|
177
|
+
const real = best.find((l) => l.includes(k)).filter((x) => x < n);
|
|
178
|
+
const pair = [built[k], built[leaf]];
|
|
179
|
+
return { direction: 'row', children: real.indexOf(k) < (real.length - 1) / 2 ? pair.reverse() : pair };
|
|
180
|
+
};
|
|
181
|
+
const rankRow = (l) => {
|
|
182
|
+
const real = l.filter((x) => x < n);
|
|
183
|
+
return real.length === 1 ? unitOf(real[0]) : { direction: cross, children: real.map(unitOf) };
|
|
184
|
+
};
|
|
185
|
+
if (dir === 'column') {
|
|
186
|
+
const fits = (l) => itemWidth(rankRow(l), ctx.carded, ctx.edges, NODE_MAX_W) <= PAGE - 2 * PAD;
|
|
187
|
+
for (const l of best) {
|
|
188
|
+
const leaves = l
|
|
189
|
+
.filter((k) => k < n && !dag.some(([a]) => a === k) && dag.filter(([, b]) => b === k).length === 1)
|
|
190
|
+
.sort((p, q) => first[q] - first[p]);
|
|
191
|
+
for (const k of leaves) {
|
|
192
|
+
if (fits(l))
|
|
193
|
+
break;
|
|
194
|
+
const src = dag.find(([, b]) => b === k)[0];
|
|
195
|
+
if (beside.has(src))
|
|
196
|
+
continue;
|
|
197
|
+
const home = best.find((m) => m.includes(src));
|
|
198
|
+
const i = l.indexOf(k);
|
|
199
|
+
l.splice(i, 1);
|
|
200
|
+
beside.set(src, k);
|
|
201
|
+
if (!fits(home)) {
|
|
202
|
+
beside.delete(src);
|
|
203
|
+
l.splice(i, 0, k);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const outer = (vs, side) => vs.every((v) => {
|
|
209
|
+
const l = best[layerOf[v]];
|
|
210
|
+
const k = l.indexOf(v);
|
|
211
|
+
return (side > 0 ? l.slice(k + 1) : l.slice(0, k)).every((x) => x >= n);
|
|
212
|
+
});
|
|
213
|
+
const hints = () => via.map((vs, i) => {
|
|
214
|
+
const [a, b] = dag[i];
|
|
215
|
+
const stacked = [a, b].every((k) => best[rank[k]].filter((x) => x < n).length === 1);
|
|
216
|
+
return !back.has(i) && (!vs.length || stacked) ? 0 : outer([a, ...vs, b], 1) ? 1 : outer([a, ...vs, b], -1) ? -1 : 0;
|
|
217
|
+
});
|
|
218
|
+
const turn = (o, u, w) => Math.sign((u[0] - o[0]) * (w[1] - o[1]) - (u[1] - o[1]) * (w[0] - o[0]));
|
|
219
|
+
const drawn = (ws) => {
|
|
220
|
+
const at = [];
|
|
221
|
+
best.forEach((l, r) => l.filter((x) => x < n).forEach((x, i, real) => (at[x] = [r, i - (real.length - 1) / 2])));
|
|
222
|
+
let c = 0;
|
|
223
|
+
for (let i = 0; i < dag.length; i++)
|
|
224
|
+
for (let j = i + 1; j < dag.length; j++) {
|
|
225
|
+
const [a, b] = dag[i], [p, q] = dag[j];
|
|
226
|
+
if (a === p || a === q || b === p || b === q || ![a, b, p, q].every((x) => at[x]))
|
|
227
|
+
continue;
|
|
228
|
+
if (ws[i] || ws[j])
|
|
229
|
+
c += +(ws[i] === ws[j] && rank[a] < rank[q] && rank[p] < rank[b]);
|
|
230
|
+
else
|
|
231
|
+
c += +(turn(at[a], at[b], at[p]) * turn(at[a], at[b], at[q]) < 0 && turn(at[p], at[q], at[a]) * turn(at[p], at[q], at[b]) < 0);
|
|
232
|
+
}
|
|
233
|
+
return c;
|
|
234
|
+
};
|
|
235
|
+
let ways = hints();
|
|
236
|
+
for (const i of ways.keys())
|
|
237
|
+
for (const j of ways.keys()) {
|
|
238
|
+
const wi = ways[i];
|
|
239
|
+
const [ai, bi] = dag[i], [aj, bj] = dag[j];
|
|
240
|
+
if (j <= i || !wi || wi !== ways[j] || rank[ai] >= rank[bj] || rank[aj] >= rank[bi])
|
|
241
|
+
continue;
|
|
242
|
+
for (const k of via[i].length <= via[j].length ? [i, j] : [j, i]) {
|
|
243
|
+
const units = [dag[k][0], ...via[k], dag[k][1]];
|
|
244
|
+
if (units.some((v) => !best[layerOf[v]].includes(v)))
|
|
245
|
+
continue;
|
|
246
|
+
const kept = best.map((l) => [...l]);
|
|
247
|
+
const was = drawn(ways);
|
|
248
|
+
for (const v of units) {
|
|
249
|
+
const l = best[layerOf[v]];
|
|
250
|
+
l.splice(l.indexOf(v), 1);
|
|
251
|
+
if (wi > 0)
|
|
252
|
+
l.unshift(v);
|
|
253
|
+
else
|
|
254
|
+
l.push(v);
|
|
255
|
+
}
|
|
256
|
+
const now = hints();
|
|
257
|
+
if (now[k] === -wi && now[i + j - k] === wi && drawn(now) < was) {
|
|
258
|
+
ways = now;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
best = kept;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
ways.forEach((way, i) => {
|
|
265
|
+
if (way)
|
|
266
|
+
pairs[i][2].around = dir === 'column' ? (way > 0 ? 'right' : 'left') : way > 0 ? 'below' : 'above';
|
|
267
|
+
});
|
|
268
|
+
const kept = best.filter((l) => l.some((x) => x < n));
|
|
269
|
+
const rows = kept.map(rankRow);
|
|
270
|
+
if (dir === 'row' || kept.length * RANK_H <= TALL * PAGE)
|
|
271
|
+
return rows;
|
|
272
|
+
const out = [];
|
|
273
|
+
let run = [];
|
|
274
|
+
let runAt = [];
|
|
275
|
+
const owner = new Map(via.flatMap((vs, i) => vs.map((v) => [v, pairs[i][2]])));
|
|
276
|
+
const lines = (part, per) => {
|
|
277
|
+
const k = Math.ceil(part.length / per), q = Math.ceil(part.length / k);
|
|
278
|
+
return Array.from({ length: k }, (_, i) => ({ direction: 'row', children: part.slice(i * q, (i + 1) * q) }));
|
|
279
|
+
};
|
|
280
|
+
const plain = (part) => {
|
|
281
|
+
const folded = part.length < 4
|
|
282
|
+
? undefined
|
|
283
|
+
: [4, 3].map((per) => lines(part, per)).find((ls) => ls.every((l) => itemWidth(l, ctx.carded, ctx.edges) <= PAGE - 2 * PAD));
|
|
284
|
+
out.push(...(folded ?? part));
|
|
285
|
+
};
|
|
286
|
+
const flush = () => {
|
|
287
|
+
const pos = new Map(run.map((r, k) => [r.id, k]));
|
|
288
|
+
const long = ctx.edges.filter((e) => pos.has(e.from) && pos.has(e.to) && Math.abs(pos.get(e.from) - pos.get(e.to)) > 1);
|
|
289
|
+
const local = runAt.every((i) => kept[i].every((x) => x < n || long.includes(owner.get(x))));
|
|
290
|
+
const folded = run.length >= 4 && long.length && local ? arcFold(run, long, pos, ctx.carded, ctx.edges) : undefined;
|
|
291
|
+
if (folded)
|
|
292
|
+
out.push(folded);
|
|
293
|
+
else {
|
|
294
|
+
let part = [];
|
|
295
|
+
run.forEach((r, k) => {
|
|
296
|
+
if (kept[runAt[k]].length === 1)
|
|
297
|
+
part.push(r);
|
|
298
|
+
else {
|
|
299
|
+
plain(part);
|
|
300
|
+
part = [];
|
|
301
|
+
out.push(r);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
plain(part);
|
|
305
|
+
}
|
|
306
|
+
run = [];
|
|
307
|
+
runAt = [];
|
|
308
|
+
};
|
|
309
|
+
rows.forEach((r, i) => {
|
|
310
|
+
if (kept[i].filter((x) => x < n).length === 1 && !isGroup(r)) {
|
|
311
|
+
run.push(r);
|
|
312
|
+
runAt.push(i);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
flush();
|
|
316
|
+
out.push(r);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
flush();
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
const pruned = (g) => ({
|
|
323
|
+
...g,
|
|
324
|
+
children: g.children.map((c) => (isGroup(c) ? pruned(c) : c)).filter((c) => !isGroup(c) || c.children.length),
|
|
325
|
+
});
|
|
326
|
+
const titled = (it) => isGroup(it) && (it.label != null || it.children.some(titled));
|
|
327
|
+
function smallFont(f, carded) {
|
|
328
|
+
const shown = (f.steps ?? []).flatMap((s) => s.flow.flatMap((b) => Object.values(toBeat(b).show ?? {})));
|
|
329
|
+
if (shown.some((c) => isRows(c) && c.some((r) => r.mono)))
|
|
330
|
+
return 10.5;
|
|
331
|
+
if (carded.size || f.edges.some((e) => e.label != null) || titled(f.layout))
|
|
332
|
+
return 11;
|
|
333
|
+
if ((f.steps ?? []).some((s) => s.flow.some((b) => toBeat(b).hops.some((h) => h.data != null))))
|
|
334
|
+
return 11.5;
|
|
335
|
+
if (nodes(f.layout).some((n) => n.sub != null))
|
|
336
|
+
return 12;
|
|
337
|
+
return f.steps?.length ? 13.5 : 14;
|
|
338
|
+
}
|
|
339
|
+
function fold(f, carded) {
|
|
340
|
+
const room = (PAGE * smallFont(f, carded)) / MIN_TEXT - 2 * PAD;
|
|
341
|
+
if (f.rail || itemWidth(f.layout, carded, f.edges, fitCap(f, PAGE)) <= room)
|
|
342
|
+
return f;
|
|
343
|
+
const ranks = f.layout.children;
|
|
344
|
+
const n = ranks.length;
|
|
345
|
+
const unit = new Map(ranks.flatMap((r, k) => idsIn(r).map((id) => [id, k])));
|
|
346
|
+
const ends = f.edges.filter((e) => unit.has(e.from) && unit.has(e.to)).map((e) => [unit.get(e.from), unit.get(e.to)]);
|
|
347
|
+
const across = (c) => ends.filter(([a, b]) => a < c !== b < c).length;
|
|
348
|
+
const last = Math.min(n - 1, ...f.edges.filter((e) => e.around).flatMap((e) => [e.from, e.to].map((id) => unit.get(id) ?? n)));
|
|
349
|
+
const better = (p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2];
|
|
350
|
+
const plans = [{ cost: [0, 0, 0], from: -1 }];
|
|
351
|
+
for (let j = 1; j <= n; j++)
|
|
352
|
+
for (let i = j - 1; i >= 0; i--) {
|
|
353
|
+
const w = itemWidth({ direction: 'row', children: ranks.slice(i, j) }, carded, f.edges);
|
|
354
|
+
if (w > room)
|
|
355
|
+
break;
|
|
356
|
+
const prev = plans[i];
|
|
357
|
+
if (!prev || (j === n && i > last))
|
|
358
|
+
continue;
|
|
359
|
+
const cost = [prev.cost[0] + 1, prev.cost[1] + (i ? across(i) : 0), Math.max(prev.cost[2], w)];
|
|
360
|
+
if (!plans[j] || better(cost, plans[j].cost) < 0)
|
|
361
|
+
plans[j] = { cost, from: i };
|
|
362
|
+
}
|
|
363
|
+
if (!plans[n] || plans[n].cost[0] < 2) {
|
|
364
|
+
const long = f.edges.filter((e) => unit.has(e.from) && unit.has(e.to) && (e.around || Math.abs(unit.get(e.from) - unit.get(e.to)) > 1));
|
|
365
|
+
const layout = n >= 4 && long.some((e) => e.around) && !ranks.some(isGroup) ? arcFold(ranks, long, unit, carded, f.edges) : undefined;
|
|
366
|
+
return layout ? { ...f, layout } : f;
|
|
367
|
+
}
|
|
368
|
+
const cuts = [];
|
|
369
|
+
for (let j = n; j > 0; j = plans[j].from)
|
|
370
|
+
cuts.unshift(plans[j].from);
|
|
371
|
+
const rows = cuts.map((c, k) => ranks.slice(c, cuts[k + 1] ?? n));
|
|
372
|
+
for (const e of f.edges)
|
|
373
|
+
if (cuts.some((c) => c && unit.get(e.from) < c && unit.get(e.to) >= c))
|
|
374
|
+
rowBreaks.add(e);
|
|
375
|
+
const children = rows.map((rs) => (rs.length === 1 ? rs[0] : { direction: 'row', children: rs }));
|
|
376
|
+
return { ...f, layout: { direction: 'column', children } };
|
|
377
|
+
}
|
|
378
|
+
export function autoLayout(spec) {
|
|
379
|
+
if (spec.lanes || spec.timeline)
|
|
380
|
+
return spec;
|
|
381
|
+
const fig = { ...spec, layout: pruned(spec.layout) };
|
|
382
|
+
if (!fig.layout.auto)
|
|
383
|
+
return fig;
|
|
384
|
+
const carded = new Set((fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => Object.keys(toBeat(b).show ?? {}))));
|
|
385
|
+
const at = seenOrder(fig);
|
|
386
|
+
const items = fig.layout.children.flatMap(flat);
|
|
387
|
+
const make = (direction) => {
|
|
388
|
+
const edges = fig.edges.map((e) => ({ ...e, around: undefined }));
|
|
389
|
+
return { ...fig, edges, layout: { direction, children: level(items, direction, { edges, at, carded }) } };
|
|
390
|
+
};
|
|
391
|
+
if (fig.layout.direction === 'row')
|
|
392
|
+
return fold(make('row'), carded);
|
|
393
|
+
if (fig.layout.direction)
|
|
394
|
+
return make(fig.layout.direction);
|
|
395
|
+
const lr = make('row');
|
|
396
|
+
return itemWidth(lr.layout, carded, lr.edges, fitCap(lr, PAGE)) + 2 * PAD <= PAGE ? lr : make('column');
|
|
397
|
+
}
|
package/dist/browser.js
CHANGED
|
@@ -36,6 +36,8 @@ export function findBrowser(o) {
|
|
|
36
36
|
}
|
|
37
37
|
return { path: null, checked };
|
|
38
38
|
}
|
|
39
|
+
// Measured: 2 parallel starts on a loaded 4-CPU Linux runner took over 30 s.
|
|
40
|
+
const START_MS = 120_000;
|
|
39
41
|
export function launch(path, profile, timeoutMs = 30_000) {
|
|
40
42
|
const args = [
|
|
41
43
|
'--headless=new',
|
|
@@ -147,7 +149,7 @@ export function launch(path, profile, timeoutMs = 30_000) {
|
|
|
147
149
|
killGroup();
|
|
148
150
|
await closed;
|
|
149
151
|
};
|
|
150
|
-
return cdp.send('Browser.getVersion').then(() => ({ cdp, close }), async (e) => {
|
|
152
|
+
return cdp.send('Browser.getVersion', {}, undefined, START_MS).then(() => ({ cdp, close }), async (e) => {
|
|
151
153
|
await close();
|
|
152
154
|
throw e;
|
|
153
155
|
});
|
package/dist/check.d.ts
CHANGED
|
@@ -6,8 +6,9 @@ export declare function checkSpec(fig: FlowProps): Finding[];
|
|
|
6
6
|
export type CheckOptions = {
|
|
7
7
|
width?: number;
|
|
8
8
|
minText?: number;
|
|
9
|
+
auto?: boolean;
|
|
9
10
|
};
|
|
10
|
-
export declare function checkScene(scene: Scene, { width, minText }?: CheckOptions): Finding[];
|
|
11
|
+
export declare function checkScene(scene: Scene, { width, minText, auto }?: CheckOptions): Finding[];
|
|
11
12
|
/** The WCAG contrast ratio of two colors, rounded to 2 places; null if either cannot be read. */
|
|
12
13
|
export declare function contrast(a: string, b: string): number | null;
|
|
13
14
|
/** Text the reader cannot read: each text and background pair, in light, dark and any custom theme, needs 4.5:1 (WCAG AA). */
|
package/dist/check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DARK, LIGHT, ON_ACCENT, TONES, edgeId, isGroup, dayOf, isLanesLayout, lanePlan, validAt, laneColumns, nodes, str, toBeat, } from './model.js';
|
|
2
2
|
import { layoutRail } from './rail.js';
|
|
3
|
-
import { crosses,
|
|
3
|
+
import { crosses, samples } from './geometry.js';
|
|
4
4
|
import { textWidth } from './text.js';
|
|
5
5
|
import { owners, parseSource } from './source.js';
|
|
6
6
|
const err = (rule, ids, message) => ({ rule, severity: 'error', ids, message });
|
|
@@ -75,10 +75,22 @@ export function checkSpec(fig) {
|
|
|
75
75
|
twice('edges', edges);
|
|
76
76
|
const known = new Set([...boxIds, ...groups]);
|
|
77
77
|
const knownEdges = new Set(edges);
|
|
78
|
+
const empty = new Set();
|
|
79
|
+
const walk = (g) => {
|
|
80
|
+
for (const c of g.children)
|
|
81
|
+
if (isGroup(c)) {
|
|
82
|
+
if (c.id != null && !nodes(c).length)
|
|
83
|
+
empty.add(c.id);
|
|
84
|
+
walk(c);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk(fig.layout);
|
|
78
88
|
fig.edges.forEach((e, i) => {
|
|
79
89
|
for (const end of [e.from, e.to])
|
|
80
90
|
if (!known.has(end))
|
|
81
91
|
out.push(err('unknown-id', [edges[i], end], `edge "${edges[i]}" names box "${end}", which does not exist`));
|
|
92
|
+
else if (empty.has(end))
|
|
93
|
+
out.push(err('unknown-id', [edges[i], end], `edge "${edges[i]}" ends at group "${end}", which has no boxes`));
|
|
82
94
|
});
|
|
83
95
|
(fig.steps ?? []).forEach((s, si) => {
|
|
84
96
|
const where = `step ${si + 1} ("${str(s.label)}")`;
|
|
@@ -96,6 +108,17 @@ export function checkSpec(fig) {
|
|
|
96
108
|
out.push(err('unknown-id', [id], `${where}: show, light or focus names box "${id}", which does not exist`));
|
|
97
109
|
}
|
|
98
110
|
});
|
|
111
|
+
const form = fig.timeline ? 'timeline' : fig.lanes ? 'lanes' : null;
|
|
112
|
+
if (fig.layout.auto && form)
|
|
113
|
+
out.push(warn('auto-ignored', [], `the figure sets ${form}; ${form === 'lanes' ? 'lanes place the boxes and ignore' : 'the timeline places the boxes and ignores'} layout.auto`));
|
|
114
|
+
if (fig.layout.auto && !form)
|
|
115
|
+
fig.edges.forEach((e, i) => {
|
|
116
|
+
if (e.around)
|
|
117
|
+
out.push(warn('auto-ignored', [edges[i]], `auto routes edge "${edges[i]}"; remove around`));
|
|
118
|
+
});
|
|
119
|
+
const nestedAuto = (g) => g.children.some((c) => isGroup(c) && (c.auto || nestedAuto(c)));
|
|
120
|
+
if (nestedAuto(fig.layout))
|
|
121
|
+
out.push(warn('auto-ignored', [], 'auto works on the root layout only'));
|
|
99
122
|
const used = new Set((fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => h.edge))));
|
|
100
123
|
fig.edges.forEach((e, i) => {
|
|
101
124
|
if (e.quiet && !used.has(edges[i]))
|
|
@@ -107,6 +130,9 @@ export function checkSpec(fig) {
|
|
|
107
130
|
if (via != null && (typeof via !== 'string' || !/^\S+$/.test(via)))
|
|
108
131
|
out.push(warn('bad-source', [], `${who}: via ${JSON.stringify(via)} is not a string of one word`));
|
|
109
132
|
}
|
|
133
|
+
for (const n of nodes(fig.layout))
|
|
134
|
+
if (n.detail != null && !(typeof n.detail === 'string' && n.detail.endsWith('.svg') && !/^([\\/]|[A-Za-z]+:)/.test(n.detail)))
|
|
135
|
+
out.push(warn('bad-detail', [n.id], `box "${n.id}": detail ${JSON.stringify(n.detail)} is not a relative path to an .svg file`));
|
|
110
136
|
const marks = nodes(fig.layout).map((n) => n.mark);
|
|
111
137
|
const starts = marks.filter((m) => m === 'start').length;
|
|
112
138
|
const ends = marks.filter((m) => m === 'end').length;
|
|
@@ -130,7 +156,10 @@ export function checkSpec(fig) {
|
|
|
130
156
|
if (f != null && t != null && t < f)
|
|
131
157
|
out.push(err('bad-date', [n.id], `box "${n.id}": to ${n.to} is before from ${n.from}`));
|
|
132
158
|
}
|
|
133
|
-
const span = new Map(nodes(fig.layout).map((n) =>
|
|
159
|
+
const span = new Map(nodes(fig.layout).map((n) => {
|
|
160
|
+
const f = n.from == null ? null : dayOf(n.from);
|
|
161
|
+
return [n.id, [f, n.to ? dayOf(n.to) : f == null ? null : f - 1]];
|
|
162
|
+
}));
|
|
134
163
|
for (const e of fig.edges) {
|
|
135
164
|
const start = span.get(e.to)?.[0];
|
|
136
165
|
const end = span.get(e.from)?.[1];
|
|
@@ -164,17 +193,23 @@ export function checkSpec(fig) {
|
|
|
164
193
|
out.push(...plainText(fig));
|
|
165
194
|
return out;
|
|
166
195
|
}
|
|
167
|
-
const at = ([p0, p1, p2, p3], t) => {
|
|
168
|
-
const u = 1 - t;
|
|
169
|
-
const [a, b, c, d] = [u * u * u, 3 * u * u * t, 3 * u * t * t, t * t * t];
|
|
170
|
-
return { x: a * p0.x + b * p1.x + c * p2.x + d * p3.x, y: a * p0.y + b * p1.y + c * p2.y + d * p3.y };
|
|
171
|
-
};
|
|
172
196
|
const lerp = (p, q, t) => ({ x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t });
|
|
197
|
+
const path = (e) => {
|
|
198
|
+
const line = e.elbow ?? e.pts;
|
|
199
|
+
if (!line)
|
|
200
|
+
return samples(e.curve);
|
|
201
|
+
return line.slice(1).flatMap((q, k) => {
|
|
202
|
+
const n = Math.max(16, Math.ceil(Math.hypot(q.x - line[k].x, q.y - line[k].y) / 4));
|
|
203
|
+
return Array.from({ length: n + 1 }, (_, i) => lerp(line[k], q, i / n));
|
|
204
|
+
});
|
|
205
|
+
};
|
|
173
206
|
// Margins stop edges along a border and pills edge to edge from counting as touches.
|
|
174
207
|
const inside = (p, r, pad) => p.x > r.x + pad && p.x < r.x + r.w - pad && p.y > r.y + pad && p.y < r.y + r.h - pad;
|
|
175
208
|
const overlap = (a, b, pad) => a.x + pad < b.x + b.w - pad && b.x + pad < a.x + a.w - pad && a.y + pad < b.y + b.h - pad && b.y + pad < a.y + a.h - pad;
|
|
176
209
|
const px = (n) => Math.round(n * 10) / 10;
|
|
177
|
-
|
|
210
|
+
// Measured corner touches go 3.5 px deep; real crossings go 24 px or more.
|
|
211
|
+
const TOUCH = 6;
|
|
212
|
+
export function checkScene(scene, { width = 830, minText = 10, auto } = {}) {
|
|
178
213
|
const out = [];
|
|
179
214
|
for (const b of scene.boxes)
|
|
180
215
|
for (const t of b.texts) {
|
|
@@ -185,29 +220,31 @@ export function checkScene(scene, { width = 830, minText = 10 } = {}) {
|
|
|
185
220
|
for (const e of scene.edges) {
|
|
186
221
|
if (e.behind && !e.elbow)
|
|
187
222
|
continue;
|
|
188
|
-
const
|
|
189
|
-
const pts = line?.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(line[k], q, i / 16)));
|
|
223
|
+
const pts = path(e);
|
|
190
224
|
for (const b of scene.boxes)
|
|
191
|
-
if (b.id !== e.from && b.id !== e.to &&
|
|
192
|
-
out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}"`));
|
|
225
|
+
if (b.id !== e.from && b.id !== e.to && pts.some((p) => inside(p, b.rect, TOUCH)))
|
|
226
|
+
out.push(err('edge-crosses-box', [e.id, b.id], `edge "${e.id}" passes through box "${b.id}": move a box${auto ? '' : ', or set "auto": true on the root layout'}`));
|
|
227
|
+
}
|
|
228
|
+
for (const e of scene.edges) {
|
|
229
|
+
const pts = path(e);
|
|
230
|
+
const drawn = pts.slice(1).reduce((s, q, k) => s + Math.hypot(q.x - pts[k].x, q.y - pts[k].y), 0);
|
|
231
|
+
const ratio = drawn / Math.max(1, Math.hypot(pts.at(-1).x - pts[0].x, pts.at(-1).y - pts[0].y));
|
|
232
|
+
// The README figures have a good edge at ratio 1.77 and 425 px.
|
|
233
|
+
if (ratio >= 1.6 && drawn >= 600)
|
|
234
|
+
out.push(warn('long-edge', [e.id], `edge "${e.id}" is ${px(ratio)} times the straight distance between its ends: put its ends in one row or column${auto ? ', or route it with around' : ', route it with around, or set "auto": true on the root layout'}`));
|
|
193
235
|
}
|
|
194
236
|
const pills = scene.edges.filter((e) => e.pts && e.label);
|
|
195
237
|
const under = new Set();
|
|
196
238
|
for (const e of scene.edges) {
|
|
197
239
|
if (e.behind)
|
|
198
240
|
continue;
|
|
199
|
-
const pts = e
|
|
200
|
-
? e.pts.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(e.pts[k], q, i / 16)))
|
|
201
|
-
: Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
|
|
241
|
+
const pts = path(e);
|
|
202
242
|
for (const f of pills)
|
|
203
243
|
if (f.id !== e.id && !under.has(e.id + ' ' + f.id) && pts.some((p) => inside(p, f.label, 2))) {
|
|
204
244
|
under.add(e.id + ' ' + f.id);
|
|
205
245
|
out.push(err('label-overlap', [e.id, f.id], `edge "${e.id}" passes under the pill of edge "${f.id}"`));
|
|
206
246
|
}
|
|
207
247
|
}
|
|
208
|
-
const path = (e) => e.pts
|
|
209
|
-
? e.pts.slice(1).flatMap((q, k) => Array.from({ length: 17 }, (_, i) => lerp(e.pts[k], q, i / 16)))
|
|
210
|
-
: Array.from({ length: 33 }, (_, i) => at(e.curve, i / 32));
|
|
211
248
|
const stubs = scene.edges.filter((e) => e.pts && e.pts.length > 1);
|
|
212
249
|
const hit = new Set();
|
|
213
250
|
for (const f of stubs)
|
|
@@ -346,5 +383,10 @@ export function checkRendered(fig, opts, scene) {
|
|
|
346
383
|
ids: [id],
|
|
347
384
|
message: `edge "${id}" ends at a lane that no block on its side shows; the edge uses the nearest block`,
|
|
348
385
|
}));
|
|
349
|
-
return [
|
|
386
|
+
return [
|
|
387
|
+
...checkSpec(fig),
|
|
388
|
+
...checkScene(scene, { ...opts, auto: opts.auto ?? fig.layout.auto }),
|
|
389
|
+
...lost,
|
|
390
|
+
...checkTheme({ ...fig.theme, ...opts.theme }),
|
|
391
|
+
];
|
|
350
392
|
}
|
package/dist/cli.js
CHANGED
|
@@ -18,30 +18,36 @@ import { GUIDE, TOPICS } from './guide.js';
|
|
|
18
18
|
import { runDraw } from './draw.js';
|
|
19
19
|
import { runInit } from './init.js';
|
|
20
20
|
import { serve } from './mcp.js';
|
|
21
|
+
import { runAtlas } from './atlas.js';
|
|
22
|
+
import { runFromMermaid } from './mermaid.js';
|
|
21
23
|
import { openedLine, openSvg, runOpen } from './open.js';
|
|
22
24
|
import { pageHtml } from './page.js';
|
|
23
25
|
import { decodePng } from './png.js';
|
|
24
|
-
import { diff, formatDiff } from './diff.js';
|
|
26
|
+
import { diff, formatDiff, mergeFigures } from './diff.js';
|
|
25
27
|
import { loadSpec, reportLines, sortFindings, specOf, summaryLines, svgWithSpec, verifyLines } from './load.js';
|
|
26
28
|
import { check, render, toSvg } from './svg.js';
|
|
27
29
|
import { checkRendered } from './check.js';
|
|
28
30
|
import { coverageLine, links, parseSource, unsureLines, verifyReport } from './verify.js';
|
|
31
|
+
import { VERSION } from './version.js';
|
|
29
32
|
import { trace, traceLines } from './trace.js';
|
|
30
33
|
import { coverageLines, coverageReport } from './coverage.js';
|
|
31
34
|
const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] render a figure; a spec on stdin with -
|
|
32
35
|
flowfig check <-|spec.json|figure.ts|figure.svg> list the faults; the input can be an SVG this wrote
|
|
33
36
|
flowfig --spec figure.svg print the spec the SVG carries
|
|
34
37
|
flowfig verify <input>... [--root <dir>] [--json] [--strict] check the code links of one or more figures
|
|
35
|
-
flowfig diff <old> <new> [--json|--md] list the spec changes between two figures
|
|
38
|
+
flowfig diff <old> <new> [--json|--md] [--svg <out.svg>] list the spec changes between two figures, or draw them in one SVG
|
|
36
39
|
flowfig trace <file#symbol> [--depth <n>] [--max <n>] [--root <dir>] [--json] list the calls under a symbol, each one checked
|
|
37
40
|
flowfig coverage [--figures <glob>] [--entries <glob>] [--root <dir>] [--strict] [--json] list failing and stale figures, and code with no figure
|
|
38
41
|
flowfig docs [topic] print the guide, or one topic (Markdown)
|
|
39
42
|
flowfig mcp serve check, render, verify, diff and docs over MCP (stdio)
|
|
40
|
-
flowfig init [dir] [--agents <ids>] [-y] [--global] [--dry-run] [--no-mcp] write flowfig instructions for the coding agents of a repo
|
|
43
|
+
flowfig init [dir] [--agents <ids> | --all-agents | --list-agents] [-y] [--global] [--dry-run] [--no-mcp] write flowfig instructions for the coding agents of a repo
|
|
41
44
|
flowfig draw "<question>" [--out <path>] [--model <alias>] [--max-turns <n>] [--json] [--open] ask Claude Code for a figure, then check it
|
|
45
|
+
flowfig atlas [--figures <glob>] [--root <dir>] [--out <dir>] [--open] write one linked web page for each figure, and an index
|
|
42
46
|
flowfig open <figure.svg> [--html <path>] show the figure in the default browser
|
|
47
|
+
flowfig from-mermaid <file.mmd|file.md|-> [--out <dir>] convert Mermaid diagrams to specs
|
|
43
48
|
flowfig gif <figure.svg> [out.gif] [--step <n>] [--dark] [--fps <n>] [--scale <n>] [--mp4] write an animated GIF
|
|
44
|
-
flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only)
|
|
49
|
+
flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only)
|
|
50
|
+
flowfig --version, -v print the version`;
|
|
45
51
|
function usage(message) {
|
|
46
52
|
console.error(message);
|
|
47
53
|
process.exit(2);
|
|
@@ -69,6 +75,10 @@ if (!args[0] || ['help', '--help', '-h'].includes(args[0])) {
|
|
|
69
75
|
usage(USAGE);
|
|
70
76
|
process.exit(0);
|
|
71
77
|
}
|
|
78
|
+
if (['--version', '-v'].includes(args[0])) {
|
|
79
|
+
console.log(VERSION);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
72
82
|
if (args[0] === 'docs') {
|
|
73
83
|
const topic = args[1];
|
|
74
84
|
if (topic && !Object.hasOwn(TOPICS, topic))
|
|
@@ -86,8 +96,12 @@ if (args[0] === 'init')
|
|
|
86
96
|
process.exit(await runInit(args.slice(1)));
|
|
87
97
|
if (args[0] === 'draw')
|
|
88
98
|
process.exit(await runDraw(args.slice(1)));
|
|
99
|
+
if (args[0] === 'atlas')
|
|
100
|
+
process.exit(await runAtlas(args.slice(1)));
|
|
89
101
|
if (args[0] === 'open')
|
|
90
102
|
process.exit(await runOpen(args.slice(1)));
|
|
103
|
+
if (args[0] === 'from-mermaid')
|
|
104
|
+
process.exit(runFromMermaid(args.slice(1)));
|
|
91
105
|
if (args[0] === '--spec') {
|
|
92
106
|
if (!args[1])
|
|
93
107
|
usage('--spec needs the path of an SVG');
|
|
@@ -197,12 +211,21 @@ if (args[0] === 'coverage') {
|
|
|
197
211
|
if (args[0] === 'diff') {
|
|
198
212
|
args.shift();
|
|
199
213
|
const json = flag('--json'), md = flag('--md');
|
|
214
|
+
const svgOut = option('--svg');
|
|
200
215
|
const unknown = args.find((a) => a.startsWith('-') && a !== '-');
|
|
201
216
|
if (unknown)
|
|
202
217
|
usage(`unknown flag ${unknown}`);
|
|
203
218
|
if (args.length !== 2)
|
|
204
|
-
usage('diff needs two figures: flowfig diff <old> <new> [--json|--md]');
|
|
205
|
-
const
|
|
219
|
+
usage('diff needs two figures: flowfig diff <old> <new> [--json|--md] [--svg <out.svg>]');
|
|
220
|
+
const [before, after] = [await load(args[0]), await load(args[1])];
|
|
221
|
+
const changes = diff(before, after);
|
|
222
|
+
if (svgOut) {
|
|
223
|
+
const { figure, marks } = mergeFigures(before, after);
|
|
224
|
+
const { svg, scene } = render(figure, { marks });
|
|
225
|
+
for (const f of sortFindings(checkRendered(figure, {}, scene), false))
|
|
226
|
+
console.error(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.message}`);
|
|
227
|
+
writeFileSync(svgOut, svg);
|
|
228
|
+
}
|
|
206
229
|
console.log(json ? JSON.stringify(changes, null, 2) : formatDiff(changes, md ? 'md' : 'text'));
|
|
207
230
|
process.exit(0);
|
|
208
231
|
}
|