flowfig 0.11.0 → 0.12.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/dist/cli.js CHANGED
@@ -13,6 +13,7 @@ import { tmpdir } from 'node:os';
13
13
  import { basename, dirname, join, resolve } from 'node:path';
14
14
  import { pathToFileURL } from 'node:url';
15
15
  import { captureFrames, findBrowser, launch } from './browser.js';
16
+ import { keyFrames, sheetGrid, sheetSvg } from './frames.js';
16
17
  import { delays, encodeGif } from './gif.js';
17
18
  import { GUIDE, TOPICS } from './guide.js';
18
19
  import { runDraw } from './draw.js';
@@ -20,14 +21,14 @@ import { runInit } from './init.js';
20
21
  import { serve } from './mcp.js';
21
22
  import { runAtlas } from './atlas.js';
22
23
  import { runFromMermaid } from './mermaid.js';
23
- import { openedLine, openSvg, runOpen } from './open.js';
24
+ import { openedLine, openFile, openSvg, runOpen } from './open.js';
24
25
  import { pageHtml } from './page.js';
25
26
  import { decodePng } from './png.js';
26
27
  import { diff, formatDiff, mergeFigures } from './diff.js';
27
28
  import { loadSpec, reportLines, sortFindings, specOf, summaryLines, svgWithSpec, verifyLines } from './load.js';
28
29
  import { check, render, toSvg } from './svg.js';
29
30
  import { checkRendered } from './check.js';
30
- import { coverageLine, links, parseSource, unsureLines, verifyReport } from './verify.js';
31
+ import { coverageLine, levelLines, links, parseSource, unsureLines, verifyReport } from './verify.js';
31
32
  import { VERSION } from './version.js';
32
33
  import { trace, traceLines } from './trace.js';
33
34
  import { coverageLines, coverageReport } from './coverage.js';
@@ -46,12 +47,72 @@ const USAGE = `usage: flowfig <-|spec.json|figure.ts> [out.svg] [--open] rende
46
47
  flowfig open <figure.svg> [--html <path>] show the figure in the default browser
47
48
  flowfig from-mermaid <file.mmd|file.md|-> [--out <dir>] convert Mermaid diagrams to specs
48
49
  flowfig gif <figure.svg> [out.gif] [--step <n>] [--dark] [--fps <n>] [--scale <n>] [--mp4] write an animated GIF
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 frames <figure.svg> [out.png] [--step <n>] [--dark] [--fps <n>] [--every <ms>] [--scale <n>] [--open] write one PNG sheet of the key frames
51
+ flags for render and check: --strict (warnings are errors), --json, --width <px>, --min-text <px>, --no-check (render only), --no-verify (render only), --scene <id> (render one level), --script (add the script that opens a box on a click)
50
52
  flowfig --version, -v print the version`;
51
53
  function usage(message) {
52
54
  console.error(message);
53
55
  process.exit(2);
54
56
  }
57
+ async function withBrowser(name, work) {
58
+ const found = findBrowser({ platform: process.platform, env: process.env, exists: existsSync });
59
+ if (!found.path)
60
+ usage([`${name} needs Chrome, Edge, Chromium or Brave. Checked:`, ...found.checked, 'Set CHROME_PATH to the browser program.'].join('\n'));
61
+ const temp = mkdtempSync(join(tmpdir(), `flowfig-${name}-`));
62
+ let launching;
63
+ let cleaning;
64
+ const cleanup = () => (cleaning ??= (async () => {
65
+ await (await launching?.catch(() => undefined))?.close();
66
+ try {
67
+ // Windows can hold a file lock briefly after the exit, so retry.
68
+ rmSync(temp, { recursive: true, force: true, maxRetries: 10 });
69
+ }
70
+ catch (e) {
71
+ console.error(`warning: could not remove ${temp}: ${e.message}`);
72
+ }
73
+ })());
74
+ let stopped = 0;
75
+ const yieldLoop = () => new Promise((done) => setImmediate(done));
76
+ // A write comes right after this check, so a signal must run its handler first.
77
+ const run = {
78
+ written: [],
79
+ proceed: async () => {
80
+ await yieldLoop();
81
+ if (stopped)
82
+ throw new Error('stopped');
83
+ },
84
+ };
85
+ // Keep the handlers: a second signal must not end the process mid-cleanup.
86
+ const onSignal = (signal) => {
87
+ if (stopped)
88
+ return;
89
+ stopped = signal === 'SIGINT' ? 130 : 143;
90
+ void (run.onStop?.() ?? Promise.resolve())
91
+ .then(() => run.written.forEach((f) => rmSync(f, { force: true })))
92
+ .then(cleanup)
93
+ .finally(() => process.exit(stopped));
94
+ };
95
+ process.on('SIGINT', onSignal);
96
+ process.on('SIGTERM', onSignal);
97
+ let code = 0;
98
+ try {
99
+ launching = launch(found.path, join(temp, 'profile'));
100
+ await work((await launching).cdp, run);
101
+ }
102
+ catch (e) {
103
+ if (!stopped)
104
+ console.error(`${name}: ${e.message}`);
105
+ code = 1;
106
+ }
107
+ finally {
108
+ await cleanup();
109
+ }
110
+ // A late signal runs its handler in this await, before the exit.
111
+ await yieldLoop();
112
+ if (stopped)
113
+ run.written.forEach((f) => rmSync(f, { force: true }));
114
+ process.exit(stopped || code);
115
+ }
55
116
  const args = process.argv.slice(2);
56
117
  const flag = (name) => {
57
118
  const i = args.indexOf(name);
@@ -160,10 +221,12 @@ if (args[0] === 'verify') {
160
221
  else {
161
222
  const n = (k, word) => `${k} ${word}${k === 1 ? '' : 's'}`;
162
223
  for (const f of findings)
163
- console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.message}`);
224
+ console.log(`${f.severity.padEnd(8)} ${f.rule.padEnd(18)} ${f.figure}: ${f.scene ? `[${f.scene}] ` : ''}${f.message}`);
164
225
  console.log(`${n(errors, 'error')}, ${n(findings.length - errors, 'warning')}`);
165
226
  for (const c of coverage) {
166
227
  console.log(coverageLine(c.figure, c));
228
+ for (const l of levelLines(c))
229
+ console.log(l);
167
230
  for (const l of unsureLines(c))
168
231
  console.log(l);
169
232
  }
@@ -272,56 +335,20 @@ if (args[0] === 'gif') {
272
335
  usage(`${path}: no width and height on the <svg> element`);
273
336
  if (!existsSync(dirname(resolve(out))))
274
337
  usage(`${out}: the folder does not exist`);
275
- const found = findBrowser({ platform: process.platform, env: process.env, exists: existsSync });
276
- if (!found.path)
277
- usage(['gif needs Chrome, Edge, Chromium or Brave. Checked:', ...found.checked, 'Set CHROME_PATH to the browser program.'].join('\n'));
278
338
  const line = (file, frames, bytes) => `${file} — ${frames} frame${frames === 1 ? '' : 's'}, ${(frames / fps).toFixed(1)} s, ${(bytes / 1048576).toFixed(1)} MB`;
279
- const temp = mkdtempSync(join(tmpdir(), 'flowfig-gif-'));
280
- let launching;
281
- let cleaning;
282
- const cleanup = () => (cleaning ??= (async () => {
283
- await (await launching?.catch(() => undefined))?.close();
284
- try {
285
- // Windows can hold a file lock briefly after the exit, so retry.
286
- rmSync(temp, { recursive: true, force: true, maxRetries: 10 });
287
- }
288
- catch (e) {
289
- console.error(`warning: could not remove ${temp}: ${e.message}`);
290
- }
291
- })());
292
- const written = [];
293
- let ffmpeg, ffmpegDone = Promise.resolve(), stopped = 0;
294
- const yieldLoop = () => new Promise((done) => setImmediate(done));
295
- // Keep the handlers: a second signal must not end the process mid-cleanup.
296
- const onSignal = (signal) => {
297
- if (stopped)
298
- return;
299
- stopped = signal === 'SIGINT' ? 130 : 143;
300
- ffmpeg?.kill('SIGKILL');
301
- void ffmpegDone
302
- .then(() => written.forEach((f) => rmSync(f, { force: true })))
303
- .then(cleanup)
304
- .finally(() => process.exit(stopped));
305
- };
306
- process.on('SIGINT', onSignal);
307
- process.on('SIGTERM', onSignal);
308
- // A write comes right after this check, so a signal must run its handler first.
309
- const proceed = async () => {
310
- await yieldLoop();
311
- if (stopped)
312
- throw new Error('stopped');
313
- };
314
- let code = 0;
315
- try {
316
- launching = launch(found.path, join(temp, 'profile'));
317
- const browser = await launching;
339
+ await withBrowser('gif', async (cdp, run) => {
340
+ let ffmpeg, ffmpegDone = Promise.resolve();
341
+ run.onStop = () => {
342
+ ffmpeg?.kill('SIGKILL');
343
+ return ffmpegDone;
344
+ };
318
345
  const viewport = { width: Math.ceil(Number(size[1])), height: Math.ceil(Number(size[2])) };
319
- const { pngs } = await captureFrames(browser.cdp, pageHtml(svg, basename(path)), { ...viewport, scale, fps, dark });
346
+ const { pngs } = await captureFrames(cdp, pageHtml(svg, basename(path)), { ...viewport, scale, fps, dark });
320
347
  const wait = delays(pngs.length, fps);
321
348
  // The GIF keeps the PNG bytes: a decoded 2400 x 1600 frame is 15 MB.
322
349
  const gif = await encodeGif(pngs.map((png, i) => ({ load: () => decodePng(png), delay: wait[i], same: i > 0 && png.equals(pngs[i - 1]) })));
323
- await proceed();
324
- written.push(out);
350
+ await run.proceed();
351
+ run.written.push(out);
325
352
  writeFileSync(out, gif);
326
353
  console.log(line(out, pngs.length, gif.length));
327
354
  if (gif.length > 10485760)
@@ -332,8 +359,8 @@ if (args[0] === 'gif') {
332
359
  if (spawnSync(ffmpegBin, ['-version']).error)
333
360
  console.error('gif: no ffmpeg on the PATH, so no MP4. The GIF is written.');
334
361
  else {
335
- await proceed();
336
- written.push(mp4Path);
362
+ await run.proceed();
363
+ run.written.push(mp4Path);
337
364
  // yuv420p needs an even width and height, so the pad filter adds a pixel.
338
365
  ffmpeg = spawn(ffmpegBin, [
339
366
  '-y',
@@ -364,7 +391,7 @@ if (args[0] === 'gif') {
364
391
  ffmpeg.once('error', () => done(-1));
365
392
  ffmpeg.once('close', done);
366
393
  }));
367
- await proceed();
394
+ await run.proceed();
368
395
  if (status !== 0) {
369
396
  rmSync(mp4Path, { force: true });
370
397
  throw new Error(`ffmpeg failed: ${stderr.trim()}`);
@@ -372,26 +399,93 @@ if (args[0] === 'gif') {
372
399
  console.log(line(mp4Path, pngs.length, statSync(mp4Path).size));
373
400
  }
374
401
  }
375
- }
376
- catch (e) {
377
- if (!stopped)
378
- console.error(`gif: ${e.message}`);
379
- code = 1;
380
- }
381
- finally {
382
- await cleanup();
383
- }
384
- // A late signal runs its handler in this await, before the exit.
385
- await yieldLoop();
386
- if (stopped)
387
- written.forEach((f) => rmSync(f, { force: true }));
388
- process.exit(stopped || code);
402
+ });
403
+ }
404
+ if (args[0] === 'frames') {
405
+ args.shift();
406
+ const dark = flag('--dark'), opening = flag('--open');
407
+ const fps = value('--fps') ?? 20, scale = value('--scale') ?? 1, step = value('--step'), every = value('--every');
408
+ if (!Number.isInteger(fps) || fps > 50)
409
+ usage('--fps needs a whole number from 1 to 50');
410
+ const unknown = args.find((a) => a.startsWith('-'));
411
+ if (unknown)
412
+ usage(`unknown flag ${unknown}`);
413
+ const [path, outArg] = args;
414
+ if (!path)
415
+ usage('usage: flowfig frames <figure.svg> [out.png] [--step <n>] [--dark] [--fps <n>] [--every <ms>] [--scale <n>] [--open]');
416
+ if (!path.endsWith('.svg'))
417
+ usage(`${path}: expected a .svg path`);
418
+ const props = (() => {
419
+ try {
420
+ return loadSpec(path);
421
+ }
422
+ catch (e) {
423
+ return usage(e.message);
424
+ }
425
+ })();
426
+ const count = props.steps?.length ?? 0;
427
+ if (!count)
428
+ usage(`${path}: frames needs a figure with steps; this figure has 0`);
429
+ if (step !== undefined && (!Number.isInteger(step) || step > count))
430
+ usage(`--step needs a whole number from 1 to ${count}`);
431
+ const fig = step === undefined ? props : { ...props, steps: [props.steps[step - 1]] };
432
+ const svg = step === undefined ? readFileSync(path, 'utf8') : toSvg(fig);
433
+ const { tiles, frames } = keyFrames(fig, { fps, every, step });
434
+ if (tiles.length > 64)
435
+ usage(`${path}: ${tiles.length} tiles; the sheet holds 64 or fewer. Use --step <n> or a larger --every <ms>.`);
436
+ const out = outArg ?? path.replace(/\.svg$/, step === undefined ? '-frames.png' : `-step${step}-frames.png`);
437
+ const size = /<svg\b[^>]*?\swidth="([\d.]+)"[^>]*?\sheight="([\d.]+)"/.exec(svg);
438
+ if (!size)
439
+ usage(`${path}: no width and height on the <svg> element`);
440
+ if (!existsSync(dirname(resolve(out))))
441
+ usage(`${out}: the folder does not exist`);
442
+ const [w, h] = [Number(size[1]), Number(size[2])];
443
+ const grid = sheetGrid(tiles.length, w, h, scale);
444
+ const loopMs = Math.round((frames * 1000) / fps);
445
+ await withBrowser('frames', async (cdp, run) => {
446
+ // A fractional device scale draws paused opacity animations two ways on alternate frames.
447
+ const shot = await captureFrames(cdp, pageHtml(svg, basename(path), '', '', '<style>svg { width: 100% }</style>'), {
448
+ width: grid.tile,
449
+ height: grid.tileH,
450
+ scale: 1,
451
+ fps,
452
+ dark,
453
+ frames: tiles.map((t) => t.frame),
454
+ });
455
+ if (Math.abs(Math.round((shot.loopMs * fps) / 1000) - frames) > 1)
456
+ throw new Error(`${path}: the SVG loop is ${Math.round(shot.loopMs)} ms but its spec gives ${loopMs} ms. Render the figure again.`);
457
+ const header = `${basename(path)} · ${tiles.length} tiles of ${frames} frames at ${fps} fps · loop ${loopMs} ms`;
458
+ const sheet = await captureFrames(cdp, pageHtml(sheetSvg(tiles, shot.pngs, grid, { header, fps, dark, scale }), basename(out)), {
459
+ width: grid.width,
460
+ height: grid.height,
461
+ scale: 1,
462
+ fps,
463
+ dark,
464
+ });
465
+ const png = sheet.pngs[0];
466
+ await run.proceed();
467
+ run.written.push(out);
468
+ writeFileSync(out, png);
469
+ const rows = tiles.map((t) => [
470
+ `#${t.frame}`,
471
+ `${Math.round((t.frame * 1000) / fps)} ms`,
472
+ `step ${t.step}${t.beat ? ` beat ${t.beat}` : ''}`,
473
+ t.kinds.join('+'),
474
+ ]);
475
+ const widths = [0, 1, 2, 3].map((c) => Math.max(...rows.map((r) => r[c].length)));
476
+ tiles.forEach((t, i) => console.log([...rows[i].map((v, c) => v.padEnd(widths[c])), t.text].join(' ') + (t.say ? ` "${t.say}"` : '')));
477
+ console.log(`${out} — ${tiles.length} tiles of ${frames} frames at ${fps} fps, ${grid.cols} x ${grid.rows} grid, ${grid.width} x ${grid.height} px, ${(png.length / 1048576).toFixed(1)} MB`);
478
+ if (grid.warning)
479
+ console.error(grid.warning);
480
+ if (opening)
481
+ await openFile(out);
482
+ });
389
483
  }
390
484
  const command = args[0] === 'check' ? args.shift() : 'render';
391
485
  const json = flag('--json'), strict = flag('--strict'), skip = flag('--no-check'), skipVerify = flag('--no-verify'), open = flag('--open');
392
486
  if (command === 'check' && open)
393
487
  usage('--open works with a render, not with check');
394
- const opts = { width: value('--width'), minText: value('--min-text') };
488
+ const opts = { width: value('--width'), minText: value('--min-text'), scene: option('--scene'), script: flag('--script') };
395
489
  const unknown = args.find((a) => a.startsWith('-') && a !== '-');
396
490
  if (unknown)
397
491
  usage(`unknown flag ${unknown}`);
@@ -399,8 +493,18 @@ const [input, out] = args;
399
493
  if (!input)
400
494
  usage(USAGE);
401
495
  const props = await load(input);
402
- const rendered = skip ? undefined : render(props, opts);
403
- const findings = rendered ? sortFindings(checkRendered(props, opts, rendered.scene), strict) : [];
496
+ const drawn = () => {
497
+ try {
498
+ return render(props, opts);
499
+ }
500
+ catch (e) {
501
+ if (!e.message.endsWith('has no level to open'))
502
+ throw e;
503
+ return usage(e.message);
504
+ }
505
+ };
506
+ const rendered = skip ? undefined : drawn();
507
+ const findings = rendered ? sortFindings(checkRendered(props, opts, rendered.scene, rendered.levels), strict) : [];
404
508
  const errors = findings.filter((f) => f.severity === 'error').length;
405
509
  const report = (print, tty, extra = []) => {
406
510
  const paint = (s) => tty && !process.env.NO_COLOR ? s.replace(/^(error {3}|warning )/, (w) => `\x1b[${w[0] === 'e' ? 31 : 33}m${w}\x1b[0m`) : s;
@@ -418,7 +522,7 @@ const dest = out ?? (input === '-' ? 'figure.svg' : input.replace(/\.[^./\\]+$/,
418
522
  report(console.error, process.stderr.isTTY, errors ? [] : [...summaryLines(props), ...(skipVerify ? [] : verifyLines(props, dest))]);
419
523
  if (errors)
420
524
  process.exit(1);
421
- const withSpec = svgWithSpec(props, opts, rendered?.svg);
525
+ const withSpec = svgWithSpec(props, opts, (rendered ?? drawn()).svg);
422
526
  writeFileSync(dest, withSpec);
423
527
  console.log(`${dest} — ${(withSpec.length / 1024).toFixed(1)} kB`);
424
528
  if (open) {
package/dist/diff.d.ts CHANGED
@@ -4,6 +4,8 @@ export type Change = {
4
4
  op: 'added' | 'removed' | 'changed';
5
5
  id: string;
6
6
  detail?: string;
7
+ /** The path line of the level, for a change inside a box. */
8
+ path?: string;
7
9
  };
8
10
  export declare function diff(before: FlowProps, after: FlowProps): Change[];
9
11
  export declare function formatDiff(changes: Change[], as: 'text' | 'md'): string;
package/dist/diff.js CHANGED
@@ -1,49 +1,94 @@
1
- import { edgeId, isGroup, laneColumns, nodes, str, toBeat } from './model.js';
1
+ import { edgeId, isGroup, laneColumns, nodes, scenes, str, toBeat, } from './model.js';
2
2
  const show = (v) => (v == null ? '(none)' : `"${str(v)}"`);
3
3
  const fields = (a, b, names) => names.filter((k) => str(a[k]) !== str(b[k])).map((k) => `${String(k)} ${show(a[k])} -> ${show(b[k])}`);
4
4
  function compare(kind, before, after, fieldsOf) {
5
+ const at = (key, v) => ({ kind, op: 'added', id: v.id ?? key, ...(v.path && { path: v.path }) });
5
6
  const out = [];
6
- for (const [id, a] of before) {
7
- const b = after.get(id);
7
+ for (const [key, a] of before) {
8
+ const b = after.get(key);
8
9
  if (!b)
9
10
  continue;
10
11
  const d = fieldsOf(a, b);
11
12
  if (d.length)
12
- out.push({ kind, op: 'changed', id, detail: d.join('; ') });
13
+ out.push({ ...at(key, b), op: 'changed', detail: d.join('; ') });
13
14
  }
14
- for (const id of before.keys())
15
- if (!after.has(id))
16
- out.push({ kind, op: 'removed', id });
17
- for (const id of after.keys())
18
- if (!before.has(id))
19
- out.push({ kind, op: 'added', id });
15
+ for (const [key, a] of before)
16
+ if (!after.has(key))
17
+ out.push({ ...at(key, a), op: 'removed' });
18
+ for (const [key, b] of after)
19
+ if (!before.has(key))
20
+ out.push(at(key, b));
20
21
  return out;
21
22
  }
22
23
  const messages = (fig) => {
23
24
  const seen = new Map();
24
- const ids = (fig.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => {
25
+ return new Map(scenes(fig).flatMap((sc) => (sc.props.steps ?? []).flatMap((s) => s.flow.flatMap((b) => toBeat(b).hops.map((h) => {
25
26
  const key = `${str(s.label)}: ${h.edge}${h.back ? ' back' : ''}${h.data != null ? ` "${str(h.data)}"` : ''}${h.tone ? ` [${h.tone}]` : ''}${h.source ? ` <${h.source}>` : ''}${h.via ? ` via ${h.via}` : ''}`;
26
- const n = (seen.get(key) ?? 0) + 1;
27
- seen.set(key, n);
28
- return n > 1 ? `${key} #${n}` : key;
29
- })));
30
- return new Map(ids.map((id) => [id, id]));
27
+ const full = `${sc.path.join('>')}\n${key}`;
28
+ const n = (seen.get(full) ?? 0) + 1;
29
+ seen.set(full, n);
30
+ const id = n > 1 ? `${key} #${n}` : key;
31
+ return [`${sc.path.join('>')}\n${id}`, { id, path: sc.line }];
32
+ })))));
31
33
  };
32
34
  const boxes = (f) => {
33
35
  const m = new Map();
34
- const walk = (g, parent) => g.children.forEach((c) => (isGroup(c) ? walk(c, c.label != null ? str(c.label) : c.id) : m.set(c.id, { ...c, group: parent })));
35
- walk(f.layout);
36
+ for (const sc of scenes(f)) {
37
+ const ports = new Set(sc.ports.map((p) => p.id));
38
+ const walk = (g, parent) => g.children.forEach((c) => isGroup(c)
39
+ ? walk(c, c.label != null ? str(c.label) : c.id)
40
+ : ports.has(c.id) || m.set(c.id, { ...c, group: parent, path: sc.line, owner: sc.path.at(-1), top: sc.path[0] }));
41
+ walk(sc.props.layout);
42
+ }
36
43
  return m;
37
44
  };
45
+ const BOX_FIELDS = ['label', 'sub', 'shape', 'tone', 'mark', 'from', 'to', 'source', 'group'];
46
+ const touched = (a, b) => !a || !b || a.owner !== b.owner || fields(a, b, BOX_FIELDS).length > 0;
38
47
  export function diff(before, after) {
39
- const edges = (f) => new Map(f.edges.map((e) => [edgeId(e), e]));
40
- const steps = (f) => new Map((f.steps ?? []).map((s) => [str(s.label), s]));
48
+ const edges = (f) => new Map(scenes(f).flatMap((sc) => sc.props.edges.map((e) => [edgeId(e), { ...e, path: sc.line }])));
49
+ const steps = (f) => new Map(scenes(f).flatMap((sc) => (sc.props.steps ?? []).map((s) => [`${sc.path.join('>')}\n${str(s.label)}`, { ...s, id: str(s.label), path: sc.line }])));
50
+ const was = boxes(before);
51
+ const now = boxes(after);
41
52
  const out = [
42
- ...compare('box', boxes(before), boxes(after), (a, b) => fields(a, b, ['label', 'sub', 'shape', 'tone', 'mark', 'from', 'to', 'source', 'group'])),
53
+ ...compare('box', was, now, (a, b) => [
54
+ ...(a.owner !== b.owner ? [`moved from ${a.path || 'the root'} to ${b.path || 'the root'}`] : []),
55
+ ...fields(a, b, BOX_FIELDS),
56
+ ]),
43
57
  ...compare('edge', edges(before), edges(after), (a, b) => fields(a, b, ['from', 'to', 'label', 'source', 'via'])),
44
58
  ...compare('step', steps(before), steps(after), (a, b) => fields(a, b, ['caption'])),
45
59
  ...compare('message', messages(before), messages(after), () => []),
46
60
  ];
61
+ const parts = new Map();
62
+ const tally = (owner, k) => {
63
+ if (owner)
64
+ parts.set(owner, { added: 0, removed: 0, changed: 0, ...parts.get(owner), [k]: (parts.get(owner)?.[k] ?? 0) + 1 });
65
+ };
66
+ for (const id of new Set([...was.keys(), ...now.keys()])) {
67
+ const a = was.get(id);
68
+ const b = now.get(id);
69
+ if (!touched(a, b))
70
+ continue;
71
+ if (!a)
72
+ tally(b.owner, 'added');
73
+ else if (!b)
74
+ tally(a.owner, 'removed');
75
+ else {
76
+ tally(b.owner, 'changed');
77
+ if (a.owner !== b.owner)
78
+ tally(a.owner, 'removed');
79
+ }
80
+ }
81
+ for (const [owner, c] of parts) {
82
+ if (!was.has(owner) || !now.has(owner))
83
+ continue;
84
+ const text = `parts: ${c.added} added, ${c.removed} removed, ${c.changed} changed`;
85
+ const hit = out.find((x) => x.kind === 'box' && x.id === owner);
86
+ const path = now.get(owner).path;
87
+ if (hit)
88
+ hit.detail = hit.detail ? `${hit.detail}; ${text}` : text;
89
+ else
90
+ out.push({ kind: 'box', op: 'changed', id: owner, detail: text, ...(path && { path }) });
91
+ }
47
92
  // The rail is a boolean or 'only', so str() would hide it.
48
93
  if ((before.rail ?? false) !== (after.rail ?? false))
49
94
  out.push({ kind: 'rail', op: 'changed', id: 'rail', detail: `${before.rail ?? '(none)'} -> ${after.rail ?? '(none)'}` });
@@ -55,7 +100,7 @@ export function formatDiff(changes, as) {
55
100
  return changes
56
101
  .map((c) => {
57
102
  const id = as === 'md' ? `\`${c.id}\`` : c.id;
58
- const line = `${c.kind} ${c.op}: ${id}${c.detail ? ` (${c.detail})` : ''}`;
103
+ const line = `${c.path ? `[${c.path}] ` : ''}${c.kind} ${c.op}: ${id}${c.detail ? ` (${c.detail})` : ''}`;
59
104
  return as === 'md' ? `- ${line}` : line;
60
105
  })
61
106
  .join('\n');
@@ -73,8 +118,11 @@ function find(root, hit) {
73
118
  }
74
119
  }
75
120
  export function mergeFigures(before, after) {
76
- const was = boxes(before);
77
- const now = boxes(after);
121
+ const rootBoxes = (f) => new Map([...boxes(f)].filter(([, n]) => !n.path));
122
+ const was = rootBoxes(before);
123
+ const now = rootBoxes(after);
124
+ const innerBefore = boxes(before);
125
+ const innerAfter = boxes(after);
78
126
  const gone = (c) => ids(c).every((id) => !now.has(id));
79
127
  const layout = cloneGroup(after.layout);
80
128
  const placed = new Set();
@@ -119,6 +167,13 @@ export function mergeFigures(before, after) {
119
167
  for (const id of was.keys())
120
168
  if (!now.has(id))
121
169
  marks.boxes[id] = 'removed';
170
+ for (const id of new Set([...innerBefore.keys(), ...innerAfter.keys()])) {
171
+ const [a, b] = [innerBefore.get(id), innerAfter.get(id)];
172
+ if (touched(a, b))
173
+ for (const top of [a?.top, b?.top])
174
+ if (top && now.has(top) && !marks.boxes[top])
175
+ marks.boxes[top] = 'changed';
176
+ }
122
177
  const colAt = new Map();
123
178
  if (after.lanes) {
124
179
  const colsAfter = laneColumns(after);
@@ -0,0 +1,4 @@
1
+ var e=/[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]|\p{Extended_Pictographic}/u,t=/[ilj.,:;'|!()[\]ftrI ]/,n=/[WMmw@%]/,r=/[A-Z0-9#&?]/;function i(i,a,o=!1){let s=0;for(let a of i)s+=e.test(a)?1:o?.6:n.test(a)?.85:t.test(a)?.3:r.test(a)?.64:.53;return s*a}function a(e,t,n,r=!1){let a=[];for(let o of e.split(`
2
+ `)){let e=``;for(let s of o.split(` `))e?i(e+` `+s,n,r)<=t?e+=` `+s:(a.push(e),e=s):e=s;a.push(e)}return a}var o=e=>{let{edges:t,...n}=typeof e==`object`&&!Array.isArray(e)&&!(`edge`in e)?e:{edges:e},r=t==null?[]:Array.isArray(t)?t:[t];return{...n,hops:r.map(e=>typeof e==`string`?{edge:e,back:!1}:{back:!1,...e})}},s=(e,t,n)=>{let r=new Set(t?[t]:[]);for(let t of n)for(let n of t.hops)n.edge===e&&n.source&&r.add(n.source);return r.size?[...r].join(`
3
+ `):void 0},c=e=>Array.isArray(e)&&e.every(e=>typeof e==`object`&&!!e&&`text`in e),l=e=>e.id??`${e.from}->${e.to}`,u=e=>`children`in e,d=1.25,f={blue:`#3b82f6`,purple:`#8b5cf6`,green:`#10b981`,orange:`#f59e0b`,red:`#ef4444`,gray:`#8b949e`},p=e=>`color-mix(in srgb, ${e} 64%, #000)`,m=(e,t,n=8)=>`color-mix(in srgb, ${e} ${n}%, ${t})`,h=e=>i(e,11,!0)+14,g=i(`ASYNC`,9)+8,_=1.25,v={accent:`#0074d9`,fg:`#1c1e21`,muted:`#606770`,bg:`#ffffff`,surface:`#f5f7fa`,border:`#d0d7de`},y={accent:`#1f78c8`,fg:`#e3e3e3`,muted:`#9aa0a6`,bg:`#1b1b1d`,surface:`#242526`,border:`#3a3b3c`},b=`#ffffff`,x=e=>typeof e==`string`||typeof e==`number`?String(e):``,S=e=>{let t=x(e).split(/\s+/).filter(Boolean).length;return t?400+240*t:0},C=(e,t)=>e.ms??t+S(e.say),w=(e,t,n)=>{for(let r=Math.min(n,t.length-1);r>=0;r--){let n=(t[r].focus??[]).find(t=>e.some(e=>e.id===t));if(n!=null)return e.find(e=>e.id===n)}},T=(e,t)=>w(e,(t[0]?.flow??[]).slice(0,1).map(o),0),ee=(e,t)=>({x:e.x+e.w+6,y:e.y,w:i(t,13),h:e.h}),E=(e,t,n)=>[Math.min(e+n,t),t],D=(e,t,n,r)=>t!=null&&e+3<t-3&&e+3+n>t-3-r,O=2e3,k=e=>Math.round(e*10)/10,A=(e,t)=>{let n=t/1e3/d,r=O/1e3/d,i=[],a=0;return{beats:e.map(e=>{let o=e.map((i,o)=>{let s=a,c=C(i,t)/1e3/d;return a+=c+(o===e.length-1?r:0),{t0:s,t1:s+n,tEnd:s+c}});return i.push(a),o}),ends:i}},j=e=>e.children.flatMap(e=>u(e)?j(e):[e]),M=e=>e.way===`in`?`in: from ${e.name}`:e.way===`out`?`out: to ${e.name}`:`in and out: ${e.name}`;function N(e){let t={path:[],name:``,line:``,props:e,ports:[],parts:0};if(e.lanes||e.timeline||e.rail===`only`)return[t];let n=/* @__PURE__ */ new Map,r=(e,t)=>j(e).forEach(e=>{n.set(e.id,e),e.inside&&t<3&&r(e.inside.layout,t+1)});r(e.layout,1);let i=e=>x(n.get(e)?.label)||e,{theme:a,speed:o,rail:s}=e,c=[t],l=e=>{for(let t of j(e.props.layout)){let r=t.inside?j(t.inside.layout):[];if(!t.inside||!r.length)continue;let u=new Set(r.map(e=>e.id)),d=t.inside.edges??[],f=[],p=[];for(let e of d)u.has(e.from)!==u.has(e.to)&&(!u.has(e.from)&&!f.includes(e.from)&&f.push(e.from),!u.has(e.to)&&!p.includes(e.to)&&p.push(e.to));let m=[...f,...p.filter(e=>!f.includes(e))].map(e=>({id:e,way:f.includes(e)?p.includes(e)?`in and out`:`in`:`out`,name:i(e)})),h=e=>{let t=n.get(e.id)?.source;return{id:e.id,label:M(e),...t!=null&&{source:t}}},g=m.filter(e=>e.way!==`out`).map(h),_=m.filter(e=>e.way===`out`).map(h),v=t.inside.layout,y=v.auto?{...v,children:[...g,...v.children,..._]}:{direction:v.direction===`column`?`column`:`row`,gap:28,children:[...g,v,..._]},b=[...e.path,t.id],x={path:b,name:i(t.id),line:b.map(i).join(` > `),props:{layout:y,edges:d,steps:t.inside.steps,theme:a,speed:o,rail:s},ports:m,parts:u.size};c.push(x),b.length+1<3&&l(x)}};return l(t),c}var P={fade:120,grow:360,stagger:50,hide:120,shrink:300},F=(e,t)=>t?[e.to,e.from]:[e.from,e.to];function te(e,t){let n=new Map(e.props.edges.map(e=>[l(e),e])),r=(e.props.steps??[]).findIndex(e=>{let r=e.flow.length?o(e.flow[0]).hops[0]:void 0,i=r&&n.get(r.edge);return i!=null&&F(i,r.back)[0]===t});return Math.max(r,0)}function ne(e){let t=ae(e.props),n=j(e.props.layout).map(e=>e.id).sort((e,n)=>t.get(e)-t.get(n)),r=new Set(e.ports.map(e=>e.id));return[...n.filter(e=>r.has(e)),...n.filter(e=>!r.has(e))]}function re(e,t=e.speed??900){let n=N(e),r=new Map(n.map(e=>[e.path.join(`>`),e])),i=[],a=/* @__PURE__ */ new Set,s=(e,t,n)=>{let r=[],i=e;for(let[e,t]of n)e>i&&r.push([i,e]),i=t;return t>i&&r.push([i,t]),r},c=(e,n,i)=>{let s=n.map(e=>e.flow.map(o)),c=A(s,t),d=new Map(e.props.edges.map(e=>[l(e),e])),f=[],p=0,m=(t,n,i)=>{let o=[...e.path,t].join(`>`),s=r.get(o);if(!s||a.has(o))return;let c=u(s,n,i);f.push([i,i+c]),p+=c},h={beats:[],ends:[]};s.forEach((e,t)=>{h.beats.push(e.map((e,n)=>{let r=c.beats[t][n],a=i+r.t0*1e3+p,o=i+r.t1*1e3+p;for(let t of e.hops){let e=d.get(t.edge);e&&m(F(e,t.back)[1],F(e,t.back)[0],i+r.tEnd*1e3+p)}return{t0:a/1e3,t1:o/1e3,tEnd:(i+r.tEnd*1e3+p)/1e3}})),h.ends.push((i+c.ends[t]*1e3+p)/1e3)});let g=i+(c.ends.at(-1)??0)*1e3;for(let t of j(e.props.layout))m(t.id,void 0,g+p);return h.ends.length&&(h.ends[h.ends.length-1]=(g+p)/1e3),{times:h,end:g+p,spans:f}},u=(e,t,n)=>{a.add(e.path.join(`>`));let o=n+S(e.name)+P.fade+P.grow,l=j(e.props.layout).length,u=o+P.stagger*(l-1)+S([e.line,...e.ports.map(M)].join(` `)),d=e.props.steps??[],f=d.length?te(e,t):-1,p={path:e.path,step:f,open:[n,u],grown:o,loop:[],back:[0,0],times:{beats:[],ends:[]}};i.push(p);let m=c(e,f===-1?[]:[d[f]],u),h=e.path.length>1?`back to ${r.get(e.path.slice(0,-1).join(`>`)).name}`:`Back`,g=m.end+S(h)+P.hide+P.shrink+P.fade;return p.loop=s(u,m.end,m.spans),p.back=[m.end,g],p.times=m.times,g-n},d=c(n[0],e.steps??[],0);return{ms:d.end,loop:s(0,d.end,d.spans),times:d.times,opens:i}}var I=e=>e.direction===`column`&&e.children.every(e=>u(e)&&e.label!=null&&e.children.every(e=>!u(e))),ie=e=>typeof e==`number`&&Number.isInteger(e)&&e>=0;function ae(e){let t=new Map(e.edges.map(e=>[l(e),e])),n=/* @__PURE__ */ new Map,r=e=>{n.has(e)||n.set(e,n.size)};for(let n of e.steps??[])for(let e of n.flow.map(o))for(let n of e.hops){let e=t.get(n.edge);if(e)for(let t of n.back?[e.to,e.from]:[e.from,e.to])r(t)}for(let t of j(e.layout))r(t.id);for(let t of j(e.layout))ie(t.at)&&n.set(t.id,t.at);return n}function L(e,t,n=190){if(e.width!=null)return e.width;if(t)return 166;let r=i(x(e.label),14)+32,a=i(x(e.sub),12)+32;return Math.min(e.shape===`decision`?170:n,Math.max(100,r,a))}function R(e,t){let n=x(e.label);if(e.shape===`decision`)return a(n,t-102+.5,14);if(i(n,14)<=t-32+.5)return[n];let r=a(n,t-32,14);return r.every(e=>i(e,14)<=t-32)?r:[n]}function z(e,t,n,r=190){if(!u(e))return L(e,t.has(e.id),r)+(e.shape===`decision`?70:0);let i=e.children.map(e=>z(e,t,n,r));return(e.direction===`column`?Math.max(...i):i.reduce((e,t)=>e+t,0)+_e(e,n)*(i.length-1))+(e.label==null?0:36)}function oe(e,t,n=24){if((e.lanes||e.timeline)&&I(e.layout))return 190;let r=new Set((e.steps??[]).flatMap(e=>e.flow.flatMap(e=>Object.keys(o(e).show??{})))),a=(e,t)=>{let n=L(e,r.has(e.id),t);return Math.max(i(x(e.sub),12),...R(e,n).map(e=>i(e,14)))+32<=n+.5},s=j(e.layout).filter(e=>a(e,190)),c=190;for(;c>110&&z(e.layout,r,e.edges,c)+n*2>t&&s.every(e=>a(e,c-20));)c-=20;return c}var B=(e,t,n)=>e*Math.max(0,1-n/(t/2))-8;function se(e,t){let n=e.sub?x(e.sub):``,r=R(e,t),o=r.length-1,s=(e,t)=>Math.max(Math.abs(e-.75*t),e+.25*t);for(let e=+!!n,c=0;;){c=Math.max(c,62+15*e+18*o);let l=(e?5.5:5)-7.5*e-9*o,u=e?s(l+18*o+15*e,12):0,d=e?a(n,B(t,c,u),12):[],f=r.map((e,t)=>({text:e,dy:l+18*t,far:s(l+18*t,14)})),p=d.map((e,t)=>({text:e,dy:l+18*o+15*(t+1),far:s(l+18*o+15*(t+1),12)}));if(d.length>e&&e<6)e++;else if(c<t&&f.some(e=>i(e.text,14)>B(t,c,e.far)))c+=4;else return{h:c,label:f,subs:p}}}function ce(e,t,n,r){let i=t=>e.findIndex(e=>e.id===t||e.children.some(e=>e.id===t)),a=/* @__PURE__ */ new Set;for(let e of r){let r=n.find(t=>l(t)===e.id),[o,s]=t.get(e.id)??[];r&&e.stub&&(e.stub.tight[0]&&o!=null&&a.add(`${i(r.from)}@${o}`),e.stub.tight[1]&&s!=null&&a.add(`${i(r.to)}@${s}`))}return a}var le=e=>Math.max(0,...e.map(e=>i(x(e.label).toUpperCase(),12)))+36,ue=(e,t)=>{let n=0;for(;n<e.length-2&&t>=e[n+1];)n++;return n},de=(e,t,n)=>[`${x(e)?x(e)+` `:``}→ ${x(n)}`,`from ${x(t)}`,...x(e)?[`→ ${x(n)}`]:[]];function fe(e,{width:t=830,minText:n=10,padding:r=24,floor:i=0}={}){let a=ae(e),s=Math.max(-1,...a.values())+1,u=(e.steps??[]).flatMap(e=>e.flow.flatMap(e=>Object.entries(o(e).show??{}))),d=new Set(u.map(([e])=>e)),f=u.some(([,e])=>c(e)&&e.some(e=>e.mono)),p=Array.from({length:s},()=>0),m=new Map(j(e.layout).map(e=>[e.id,e]));for(let e of m.values()){let t=a.get(e.id);p[t]=Math.max(p[t],L(e,d.has(e.id))+(e.shape===`decision`?70:0))}let g=f?10.5:11,_=t*g/n,v=_-r*2,y=e.layout.children,b=new Map(y.filter(e=>e.id!=null).map(e=>[e.id,e])),S=le(y),C=t=>{let n=[0];for(let e=0;e<t;e++)n.push(n[e]+Math.floor(s/t)+ +(e<s%t));let r=e=>ue(n,e),i=e=>r(a.get(e)),o=[...new Set([...m.keys()].map(i))].sort((e,t)=>e-t),c=o.length?o:[0],u=c.length>1,d=e=>u?[...new Set(e.children.map(e=>i(e.id)))].sort((e,t)=>e-t):c,f=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new Map,_=[];e.edges.forEach(e=>{let t=l(e),[n,r]=[b.get(e.from),b.get(e.to)];if(!(m.has(e.from)||n)||!(m.has(e.to)||r))return;let a=!1,o=(e,t,n)=>{let r=d(e);return(n?r.find(e=>e>=t):r.findLast(e=>e<=t))??(a=!0,n?r.at(-1):r[0])},s,c;if(n||(s=i(e.from)),r||(c=i(e.to)),n&&r&&(s=d(n)[0]),n&&!r&&(s=o(n,c,!1)),r&&(c=o(r,s,!0)),s==null||c==null)return void(u&&_.push(t));if(a&&_.push(t),g.set(t,[s,c]),s===c)return;let p=de(e.label,(m.get(e.from)??n).label,(m.get(e.to)??r).label);f.set(t,p)});let C=Array.from({length:s},()=>0);for(let t of e.edges){let[e,n]=[a.get(t.from),a.get(t.to)];if(t.label==null||e==null||n==null||Math.abs(e-n)!==1||r(e)!==r(n))continue;let i=h(x(t.label));i-2>56&&(C[Math.min(e,n)]=Math.max(C[Math.min(e,n)],i+8))}let T=p.map((e,t)=>t===n[r(t)+1]-1?18:Math.max(56,C[t])),ee=Math.max(...n.slice(1).map((e,t)=>e-n[t])),E=Array.from({length:t},()=>0),D=e=>12+h(e)+8,O=c.every(e=>w(n[e],n[e+1],T,0)<=v),k=/* @__PURE__ */ new Map,A=/* @__PURE__ */ new Map,j=/* @__PURE__ */ new Set,M=/* @__PURE__ */ new Set;for(let[t,n]of f){let r=e.edges.find(e=>l(e)===t),[i,o]=[a.get(r.from),a.get(r.to)];i!=null&&k.set(i,Math.max(k.get(i)??0,D(n[0]))),o!=null&&A.set(o,Math.max(A.get(o)??0,D(n[1])))}for(let e of c){let[t,r]=[n[e],n[e+1]],i=v-w(t,r,T,0),a=(e,t)=>{e>0&&e<=i&&(i-=e,t())};a(A.get(t)??0,()=>E[e]=A.get(t)),(!A.has(t)||E[e])&&M.add(t);for(let e=t;e<r;e++){let t=(k.get(e)??0)+(e<r-1?A.get(e+1)??0:0);a(t-T[e],()=>T[e]=t),T[e]>=t&&j.add(e).add(-1-(e+1))}}let N=e=>y.findIndex(t=>t.id===e||t.children.some(t=>t.id===e)),P=/* @__PURE__ */ new Set;for(let[t]of f){let r=e.edges.find(e=>l(e)===t),[i,o]=[a.get(r.from),a.get(r.to)],[s,c]=g.get(t);(i==null||!j.has(i))&&P.add(`${N(r.from)}@${s}`),(o==null||!(o===n[c]?M.has(o):j.has(-1-o)))&&P.add(`${N(r.to)}@${c}`)}let F=new Map(y.flatMap(e=>e.children.map(t=>[t.id,e]))),te=new Set(e.edges.filter(e=>{let[t,n,i]=[a.get(e.from),a.get(e.to),F.get(e.from)];return e.around||t==null||n==null||!i||i!==F.get(e.to)||r(t)!==r(n)?!1:i.children.some(e=>a.get(e.id)>Math.min(t,n)&&a.get(e.id)<Math.max(t,n))}).map(l));return[{cols:a,starts:n,per:ee,blocks:c,gutter:S,gaps:T,lead:E,ends:g,stubs:f,around:te,tall:P,lost:_},O]},w=(e,t,n,r)=>S+r+p.slice(e,t).reduce((e,t)=>e+t,0)+n.slice(e,t).reduce((e,t)=>e+t,0);if(g<n||i>_)return C(1)[0];for(let e=1;e<=Math.max(1,Math.floor(s/2));e++){let[t,n]=C(e);if(n)return t}return C(1)[0]}function pe(e,t,n,r,i,a){let o=e.ends.get(t)?.[+!r],s=o==null?void 0:i(n,o);if(e.blocks.length<2||e.cols.has(n)||!s)return n;let c=e.stubs.get(t),l=c?`${t}@${r?`s`:`t`}`:`${n}@${o}`,u=r?s.x+s.w-(12+h(c?.[0]??``)+8):s.x+e.gutter+e.lead[o];return a[l]=c?{x:u,y:s.y,w:0,h:s.h}:s,l}var V=e=>{let t=typeof e==`string`?/^(\d{4})-(\d{2})-(\d{2})$/.exec(e):null;if(!t)return null;let n=Date.UTC(+t[1],t[2]-1,+t[3]);return new Date(n).toISOString().slice(0,10)===e?n/864e5:null},H=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],U=e=>/* @__PURE__ */ new Date(e*864e5),W=e=>((e+3)%7+7)%7,me=e=>{let t=e-W(e)+3,n=Date.UTC(U(t).getUTCFullYear(),0,1)/864e5;return Math.floor((t-n)/7)+1},G=e=>{let t=[];return e.layout.children.forEach((e,n)=>j(u(e)?e:{children:[e]}).forEach(e=>{let r=e.from==null?null:V(e.from);if(r==null)return;let i=e.to==null?null:V(e.to);t.push({id:e.id,track:n,from:r,to:i!=null&&i>=r?i:null,label:e})})),t};function he(e,t){let n=G(e),r=e.today==null?null:V(e.today),a=[...n.flatMap(e=>[e.from,e.to??e.from]),...r==null?[]:[r]],o=a.length?Math.min(...a):0,s=a.length?Math.max(...a):0,c=o-W(o),l=s+(6-W(s)),u=e=>(e-c)/(l-c+1)*t;for(let e=0;e<4;e++){let e=Math.max(0,...n.map(e=>{let n=e.to==null?u(e.from)-7:u(e.from),r=e.to==null?14:Math.max(24,u(e.to+1)-u(e.from)),a=i(x(e.label.label),13);return e.to!=null&&a+16<=r?0:n+r+6+a-t}));if(e<=0)break;l+=Math.ceil(e/(t/(l-c+1))/7)*7}let d=[];if((l-c+1)/7<=16)for(let e=c;e<=l;e+=7)d.push({x:u(e),label:`W${me(e)}`});else for(let e=c;e<=l;e++)U(e).getUTCDate()===1&&d.push({x:u(e),label:H[U(e).getUTCMonth()]});let f=e.layout.children.map(()=>1),p=[],m=e.layout.children.map(()=>[]);for(let e of[...n].sort((e,t)=>e.from-t.from)){let t=e.to!=null,n=t?u(e.from):u(e.from)-7,r=t?Math.max(24,u(e.to+1)-u(e.from)):14,a=i(x(e.label.label),13),o=t&&a+16<=r,s=m[e.track].findIndex(e=>e+8<=n);s<0&&(s=m[e.track].length),m[e.track][s]=o?n+r:n+r+6+a,f[e.track]=Math.max(f[e.track],s+1),p.push({id:e.id,track:e.track,row:s,x:n,w:r,milestone:!t,labelInside:o,date:K(e.from)})}let h=new Map(n.map((e,t)=>[e.id,t]));return p.sort((e,t)=>h.get(e.id)-h.get(t.id)),{start:c,end:l,ticks:d,items:p,rows:f,today:n.length&&r!=null?u(r):null,last:n.length?u(s):null,lastDate:K(s)}}var K=e=>`${U(e).getUTCDate()} ${H[U(e).getUTCMonth()]}`;function ge(e){let t=G(e).sort((e,t)=>e.from-t.from);return[{label:`timeline`,flow:t.map((e,n)=>({focus:[e.id],light:t.slice(0,n+1).map(e=>e.id),say:`${x(e.label.label)}, ${K(e.from)}${e.to==null?``:` to ${K(e.to)}`}${e.label.sub==null?``:` · ${x(e.label.sub)}`}`}))}]}var q=e=>u(e)?[...e.id?[e.id]:[],...e.children.flatMap(q)]:[e.id];function _e(e,t){let n=/* @__PURE__ */ new Map;e.children.forEach((e,t)=>q(e).forEach(e=>n.set(e,t)));let r=t.filter(t=>t.label!=null&&n.has(t.from)&&n.has(t.to)&&n.get(t.from)!==n.get(t.to)&&!(e.gap!=null&&t.around));if(e.gap!=null&&!r.length)return e.gap;let i=Math.max(0,...r.map(e=>h(x(e.label))+32));if(e.gap!=null&&e.direction!==`column`)return Math.max(e.gap,i);let a;if(e.direction===`column`){let t=r.map(e=>[n.get(e.from),n.get(e.to)].sort((e,t)=>e-t)),i=Math.max(0,...e.children.map((e,n)=>t.filter(([e,t])=>e<=n&&n<t).length));a=i?Math.min(i,3)*22+32-4:28}else a=Math.max(56,i);return Math.max(e.gap??0,a)}var ve=e=>{let t=/* @__PURE__ */ new Map,n=e=>e.children.forEach(e=>{e.id&&t.set(e.id,x(e.label)||e.id),u(e)&&n(e)});n(e.layout);let r=e=>t.get(e)??e,i=new Map(e.edges.map(e=>[l(e),e])),a=j(e.layout).map(e=>r(e.id)),s=e.steps??[],c=s.flatMap(e=>[`${x(e.label)}.`,...e.flow.map(o).flatMap(e=>x(e.say)?[x(e.say)]:e.hops.flatMap(e=>{let t=i.get(e.edge);return t?[e.back?`${r(t.to)} to ${r(t.from)}`:`${r(t.from)} to ${r(t.to)}`]:[]}))]),d=N(e).slice(1).map(e=>{let t=new Set(e.ports.map(e=>e.id)),n=j(e.props.layout).filter(e=>!t.has(e.id));return`${e.line}: ${n.map(e=>x(e.label)||e.id).join(`, `)}.${e.ports.length?` Ports: ${e.ports.map(M).join(`, `)}.`:``}`});return{title:`Flow figure: ${a.join(`, `)}.${s.length?` Steps: ${s.map(e=>x(e.label)).join(`, `)}.`:``}`,desc:[`The figure has these boxes: ${a.join(`, `)}.`,...c,...d].join(`
4
+ `)}},ye=6,J=e=>e.shape===`store`,Y=e=>e?.shape===`decision`,be=e=>e===`red`||e===`gray`,X=e=>!!e&&(e.shape===`decision`||!!e.check),xe=e=>e.check||(e.shape===`decision`?x(e.label):``),Z=(e,t)=>X(e.get(t.source))&&(be(e.get(t.target)?.tone)||t.tone===`red`),Se=e=>e.children.flatMap(e=>u(e)?e.label==null?Se(e):[e]:[]),Q=(e,t)=>{let n=new Map(e.edges.map(e=>[l(e),e]));return t.flow.map(e=>{let t=o(e);return{beat:t,hops:t.hops.flatMap(e=>{let t=n.get(e.edge);return t?[{...e,edge:t,source:e.back?t.to:t.from,target:e.back?t.from:t.to}]:[]})}})};function $(e){let t=j(e.layout),n=t.filter(J),r=new Map(t.map(e=>[e.id,e])),i=e.lanes&&I(e.layout)?e.layout.children.filter(u):Se(e.layout),a=[];for(let e of i){let t=j(e).filter(e=>!J(e));t.length&&a.push({name:x(e.label),look:e.look,boxes:t.map(e=>e.id)})}if(a.length){let n=new Set(a.flatMap(e=>e.boxes));for(let t of e.steps??[])for(let{hops:i}of Q(e,t))for(let e of i.flatMap(e=>[e.source,e.target])){let t=r.get(e);!t||n.has(e)||J(t)||Y(t)||(n.add(e),a.push({name:x(t.label),look:t.look,boxes:[e]}))}let i=new Map(t.map((e,t)=>[e.id,t]));a.sort((e,t)=>i.get(e.boxes[0])-i.get(t.boxes[0]))}if(!a.length){let t=/* @__PURE__ */ new Set;for(let n of e.steps??[])for(let{hops:i}of Q(e,n))for(let e of i)for(let n of[e.source,e.target])r.has(n)&&!J(r.get(n))&&!Y(r.get(n))&&t.add(n);a=[...t].slice(0,n.length?5:ye).map(e=>{let t=r.get(e);return{name:x(t.label),look:t.look,boxes:[e]}})}return n.length&&a.push({name:`Records`,look:n.find(e=>e.look)?.look,boxes:n.map(e=>e.id)}),a}function Ce(e,t){let n=e.steps?.find(e=>x(e.label)===t);if(!n)return[];let r=$(e),i=new Map(j(e.layout).map(e=>[e.id,e])),a=new Map(r.flatMap(e=>e.boxes.map(t=>[t,e.name]))),o=/* @__PURE__ */ new Map,s=e=>{for(let t of e.children)u(t)&&(t.id&&o.set(t.id,t.label),s(t))};s(e.layout);let c=e=>x(i.get(e)?.label??o.get(e)),l=Q(e,n);if(!l.length)return[];let d=l.flatMap(e=>e.hops).at(-1),f=d?c(d.target):``,p=r[0]?.name??``,m=l.map(({beat:e,hops:t},n)=>{let r=t.at(-1),o=e=>a.get(e)??c(e),s=r?a.has(r.target)||!Y(i.get(r.target))?o(r.target):n?p:o(r.source):p;p=s;let l=t.map(e=>x(e.edge.label)).filter(Boolean),u={actor:s,say:x(e.say)||(r?l.length?`${l.join(`, `)}: ${c(r.target)}`:c(r.target):``),note:x(e.note),records:[]},d=t.filter(e=>X(i.get(e.source))),f=d.find(e=>Z(i,e))??d.at(-1);f&&(u.stamp={mark:Z(i,f)?`✗`:`✓`,label:x(f.edge.label)},u.question=xe(i.get(f.source)));for(let e of t){let t=i.get(e.target);t&&J(t)&&u.records.push(x(e.data)||x(e.edge.label)||x(t.label))}return u}),h=new Set(l.flatMap(e=>e.hops.flatMap(e=>[e.source,e.target]))),g=l.flatMap(e=>e.hops).find(e=>Z(i,e)),_=[];if(g){let t=e.edges.filter(e=>e!==g.edge&&i.has(e.to)&&!be(i.get(e.to).tone)),n=/* @__PURE__ */ new Set([g.source]);for(let e=!0;e;){e=!1;for(let r of t)n.has(r.from)&&!n.has(r.to)&&(n.add(r.to),e=!0)}let r=t.filter(e=>n.has(e.from)&&n.has(e.to)&&e.to!==g.source),a=/* @__PURE__ */ new Map;for(let e of r)a.set(e.to,(a.get(e.to)??0)+1);for(let e=[g.source];e.length;){let t=e.shift();_.push(t);for(let n of r)n.from===t&&(a.set(n.to,a.get(n.to)-1),a.get(n.to)||e.push(n.to))}for(let e of n)_.includes(e)||_.push(e)}let v=_.slice(1).filter(e=>!h.has(e)).map(c);return m.forEach((e,t)=>{let n=m.slice(t+1).find(t=>t.actor!==e.actor);!e.note&&n&&(e.note=`${n.actor} acts next.`)}),m.push({actor:m.at(-1).actor,say:n.outcome||(/[.?!]$/.test(f)?f:`${f}.`),note:``,records:[],summary:{outcome:f,notReached:v}}),m}var we=[`person`,`shop`,`truck`],Te=e=>e.edges.some(e=>!!e.carries&&typeof e.carries==`object`)||$(e).some(e=>we.includes(e.look)),Ee={font:`system-ui, -apple-system, "Segoe UI", Roboto, sans-serif`},De={light:{...Ee,bg:`#fafbfd`,surface:`#ffffff`,border:`#dfe7f0`,ink:`#1d2433`,muted:`#5d687a`,accent:`#0074d9`,"on-accent":`#ffffff`,bad:`#c8352f`,good:`#23704e`,play:`#1d2433`},dark:{...Ee,bg:`#131820`,surface:`#1c2330`,border:`#2a3343`,ink:`#e9eef6`,muted:`#9aa6b8`,accent:`#4da3f0`,"on-accent":`#0b1220`,bad:`#ee5d57`,good:`#5cc48f`,play:`#6b778c`}},Oe={wide:880,roomy:1200,panel:376,slim:300,pad:16,chips:52,sheet:160,short:560,tall:760};export{p as $,q as A,le as B,se as C,oe as D,s as E,R as F,ee as G,T as H,h as I,x as J,w as K,E as L,I as M,c as N,ne as O,z as P,o as Q,ue as R,D as S,l as T,k as U,fe as V,j as W,ge as X,ce as Y,he as Z,O as _,Q as a,C as b,xe as c,d,m as et,y as f,P as g,b as h,Z as i,u as j,_e as k,Te as l,v as m,De as n,i as nt,X as o,_ as p,N as q,$ as r,a as rt,Ce as s,Oe as t,re as tt,g as u,f as v,B as w,A as x,ve as y,pe as z};