flowfig 0.8.4 → 0.10.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 +375 -41
- package/dist/atlas.d.ts +6 -0
- package/dist/atlas.js +122 -0
- package/dist/auto.d.ts +3 -0
- package/dist/auto.js +302 -0
- package/dist/browser.js +3 -1
- package/dist/check.js +54 -17
- package/dist/cli.js +69 -6
- package/dist/code.d.ts +2 -0
- package/dist/code.js +9 -4
- package/dist/coverage.d.ts +27 -0
- package/dist/coverage.js +116 -0
- package/dist/diff.d.ts +10 -0
- package/dist/diff.js +137 -7
- package/dist/edges.d.ts +3 -0
- package/dist/edges.js +3 -3
- package/dist/geometry.d.ts +4 -0
- package/dist/geometry.js +45 -17
- package/dist/imports.d.ts +1 -0
- package/dist/imports.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +161 -81
- package/dist/load.js +6 -5
- package/dist/mermaid.d.ts +19 -0
- package/dist/mermaid.js +617 -0
- package/dist/model.d.ts +15 -3
- package/dist/model.js +56 -1
- 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 +73 -22
- package/dist/trace.d.ts +30 -0
- package/dist/trace.js +278 -0
- package/dist/verify.d.ts +5 -2
- package/dist/verify.js +39 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/model.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export type FigNode = {
|
|
|
26
26
|
mark?: 'start' | 'end';
|
|
27
27
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
28
28
|
source?: string;
|
|
29
|
+
/** A figure with more detail: an SVG path from the repo root. */
|
|
30
|
+
detail?: string;
|
|
29
31
|
};
|
|
30
32
|
/** A frame that lays out its children in a row or a column. */
|
|
31
33
|
export type FigGroup = {
|
|
@@ -37,10 +39,12 @@ export type FigGroup = {
|
|
|
37
39
|
direction?: 'row' | 'column';
|
|
38
40
|
/** Space between children in px. Default: 28 for a column, fitted for a row. */
|
|
39
41
|
gap?: number;
|
|
40
|
-
/** Where children line up across the group's direction. Default: center
|
|
42
|
+
/** Where children line up across the group's direction. Default: `center`, also for a column. */
|
|
41
43
|
align?: 'start' | 'center' | 'end';
|
|
42
44
|
/** The boxes and groups inside, in order. */
|
|
43
45
|
children: (FigNode | FigGroup)[];
|
|
46
|
+
/** On the root layout: flowfig places the boxes. A labeled group is a frame. */
|
|
47
|
+
auto?: true;
|
|
44
48
|
};
|
|
45
49
|
/** An arrow from one box to another. */
|
|
46
50
|
export type FigEdge = {
|
|
@@ -52,8 +56,8 @@ export type FigEdge = {
|
|
|
52
56
|
to: string;
|
|
53
57
|
/** Text on the edge, drawn as a small pill. */
|
|
54
58
|
label?: ReactNode;
|
|
55
|
-
/** Routes the edge
|
|
56
|
-
around?: 'above' | 'below';
|
|
59
|
+
/** Routes the edge around the boxes in between (loops, skip-ahead edges). */
|
|
60
|
+
around?: 'above' | 'below' | 'left' | 'right';
|
|
57
61
|
/** Draws the edge only while a step uses it. */
|
|
58
62
|
quiet?: boolean;
|
|
59
63
|
/** Code this draws: `path` or `path#symbol` from the repo root. Checked by `flowfig verify`. */
|
|
@@ -350,4 +354,12 @@ export declare const counts: (p: FlowProps) => {
|
|
|
350
354
|
steps: number;
|
|
351
355
|
messages: number;
|
|
352
356
|
};
|
|
357
|
+
export declare const idsIn: (c: FigNode | FigGroup) => string[];
|
|
353
358
|
export declare function groupGap(g: FigGroup, edges: FigEdge[]): number;
|
|
359
|
+
export declare const altText: (fig: FlowProps) => {
|
|
360
|
+
title: string;
|
|
361
|
+
desc: string;
|
|
362
|
+
};
|
|
363
|
+
export declare const specKey: (...parts: unknown[]) => string;
|
|
364
|
+
export declare const fitScale: (wide: number, box: number, map: number) => number;
|
|
365
|
+
export declare const nextWide: (wide: number, box: number, map: number) => number;
|
package/dist/model.js
CHANGED
|
@@ -451,7 +451,7 @@ export const counts = (p) => {
|
|
|
451
451
|
messages: st.reduce((n, s) => n + s.flow.reduce((m, b) => m + toBeat(b).hops.length, 0), 0),
|
|
452
452
|
};
|
|
453
453
|
};
|
|
454
|
-
const idsIn = (c) => (isGroup(c) ? [...(c.id ? [c.id] : []), ...c.children.flatMap(idsIn)] : [c.id]);
|
|
454
|
+
export const idsIn = (c) => (isGroup(c) ? [...(c.id ? [c.id] : []), ...c.children.flatMap(idsIn)] : [c.id]);
|
|
455
455
|
const LINE_CLEAR = 16;
|
|
456
456
|
export function groupGap(g, edges) {
|
|
457
457
|
const side = new Map();
|
|
@@ -472,3 +472,58 @@ export function groupGap(g, edges) {
|
|
|
472
472
|
auto = Math.max(56, need);
|
|
473
473
|
return Math.max(g.gap ?? 0, auto);
|
|
474
474
|
}
|
|
475
|
+
export const altText = (fig) => {
|
|
476
|
+
const names = new Map();
|
|
477
|
+
const walk = (g) => g.children.forEach((c) => {
|
|
478
|
+
if (c.id)
|
|
479
|
+
names.set(c.id, str(c.label) || c.id);
|
|
480
|
+
if (isGroup(c))
|
|
481
|
+
walk(c);
|
|
482
|
+
});
|
|
483
|
+
walk(fig.layout);
|
|
484
|
+
const name = (id) => names.get(id) ?? id;
|
|
485
|
+
const ends = new Map(fig.edges.map((e) => [edgeId(e), e]));
|
|
486
|
+
const boxes = nodes(fig.layout).map((n) => name(n.id));
|
|
487
|
+
const steps = fig.steps ?? [];
|
|
488
|
+
const lines = steps.flatMap((s) => [
|
|
489
|
+
`${str(s.label)}.`,
|
|
490
|
+
...s.flow.map(toBeat).flatMap((b) => {
|
|
491
|
+
if (str(b.say))
|
|
492
|
+
return [str(b.say)];
|
|
493
|
+
return b.hops.flatMap((h) => {
|
|
494
|
+
const e = ends.get(h.edge);
|
|
495
|
+
if (!e)
|
|
496
|
+
return [];
|
|
497
|
+
return [h.back ? `${name(e.to)} to ${name(e.from)}` : `${name(e.from)} to ${name(e.to)}`];
|
|
498
|
+
});
|
|
499
|
+
}),
|
|
500
|
+
]);
|
|
501
|
+
return {
|
|
502
|
+
title: `Flow figure: ${boxes.join(', ')}.${steps.length ? ` Steps: ${steps.map((s) => str(s.label)).join(', ')}.` : ''}`,
|
|
503
|
+
desc: [`The figure has these boxes: ${boxes.join(', ')}.`, ...lines].join('\n'),
|
|
504
|
+
};
|
|
505
|
+
};
|
|
506
|
+
const keyOf = (v, path) => {
|
|
507
|
+
if (typeof v === 'function' || typeof v === 'symbol')
|
|
508
|
+
return undefined;
|
|
509
|
+
if (typeof v === 'bigint')
|
|
510
|
+
return `${v}n`;
|
|
511
|
+
if (typeof v !== 'object' || v === null)
|
|
512
|
+
return v;
|
|
513
|
+
if (path.includes(v))
|
|
514
|
+
return '[cycle]';
|
|
515
|
+
const at = [...path, v];
|
|
516
|
+
if (Array.isArray(v))
|
|
517
|
+
return v.map((x) => keyOf(x, at));
|
|
518
|
+
const el = v;
|
|
519
|
+
const proto = Object.getPrototypeOf(v);
|
|
520
|
+
if (!el.$$typeof && proto !== Object.prototype && proto !== null)
|
|
521
|
+
return '[object]';
|
|
522
|
+
const src = el.$$typeof ? { type: typeof el.type === 'string' ? el.type : el.type?.name, key: el.key, props: el.props } : v;
|
|
523
|
+
return Object.fromEntries(Object.entries(src).flatMap(([k, x]) => (k === 'ref' || k.startsWith('_') ? [] : [[k, keyOf(x, at)]])));
|
|
524
|
+
};
|
|
525
|
+
export const specKey = (...parts) => JSON.stringify(keyOf(parts, []));
|
|
526
|
+
export const fitScale = (wide, box, map) => Math.max(wide ? 1 : 0.5, Math.min(1, box / map));
|
|
527
|
+
const PHONE = 480;
|
|
528
|
+
const folds = (box, map) => box < map / 2 || (box < PHONE && box < map);
|
|
529
|
+
export const nextWide = (wide, box, map) => (folds(box, wide || map) ? wide || map : 0);
|
package/dist/open.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare function openerFor(platform: NodeJS.Platform, file: string): Open
|
|
|
7
7
|
/** The temp page name: each character other than a letter, digit, - or _ becomes -. */
|
|
8
8
|
export declare function pageName(svgPath: string): string;
|
|
9
9
|
export declare const openedLine: (page: string) => string;
|
|
10
|
+
export declare function openFile(path: string): Promise<void>;
|
|
10
11
|
/** Write the page to a new temp folder, then start the opener. Returns the written paths. */
|
|
11
12
|
export declare function openSvg(svgPath: string, html?: string): Promise<string[]>;
|
|
12
13
|
export declare function runOpen(argv: string[]): Promise<number>;
|
package/dist/open.js
CHANGED
|
@@ -20,6 +20,16 @@ export function pageName(svgPath) {
|
|
|
20
20
|
.replace(/[^\w-]/g, '-') + '.html');
|
|
21
21
|
}
|
|
22
22
|
export const openedLine = (page) => `${page} — opened in the default browser`;
|
|
23
|
+
export async function openFile(path) {
|
|
24
|
+
const o = process.env.FLOWFIG_OPENER
|
|
25
|
+
? { cmd: process.env.FLOWFIG_OPENER, args: [path], verbatim: false }
|
|
26
|
+
: openerFor(process.platform, path);
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const child = spawn(o.cmd, o.args, { detached: true, windowsHide: true, stdio: 'ignore', windowsVerbatimArguments: o.verbatim });
|
|
29
|
+
child.once('spawn', () => (child.unref(), resolve()));
|
|
30
|
+
child.once('error', (e) => reject(new Error(`${e.message}. Open ${path} in a browser.`)));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
23
33
|
/** Write the page to a new temp folder, then start the opener. Returns the written paths. */
|
|
24
34
|
export async function openSvg(svgPath, html) {
|
|
25
35
|
const page = pageHtml(readFileSync(svgPath, 'utf8'), basename(svgPath));
|
|
@@ -30,14 +40,7 @@ export async function openSvg(svgPath, html) {
|
|
|
30
40
|
writeFileSync(temp, page);
|
|
31
41
|
if (html)
|
|
32
42
|
writeFileSync(html, page);
|
|
33
|
-
|
|
34
|
-
? { cmd: process.env.FLOWFIG_OPENER, args: [temp], verbatim: false }
|
|
35
|
-
: openerFor(process.platform, temp);
|
|
36
|
-
await new Promise((resolve, reject) => {
|
|
37
|
-
const child = spawn(o.cmd, o.args, { detached: true, windowsHide: true, stdio: 'ignore', windowsVerbatimArguments: o.verbatim });
|
|
38
|
-
child.once('spawn', () => (child.unref(), resolve()));
|
|
39
|
-
child.once('error', (e) => reject(new Error(`${e.message}. Open ${temp} in a browser.`)));
|
|
40
|
-
});
|
|
43
|
+
await openFile(temp);
|
|
41
44
|
return html ? [temp, html] : [temp];
|
|
42
45
|
}
|
|
43
46
|
export async function runOpen(argv) {
|
package/dist/page.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare const esc: (s: string) => string;
|
|
2
|
+
export declare function pageHtml(svg: string, title: string, before?: string, after?: string, head?: string): string;
|
package/dist/page.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2
|
-
export function pageHtml(svg, title) {
|
|
1
|
+
export const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
2
|
+
export function pageHtml(svg, title, before = '', after = '', head = '') {
|
|
3
3
|
// Use element selectors only: figure styles use class names.
|
|
4
4
|
return `<!doctype html>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
7
|
<title>${esc(title)}</title>
|
|
7
|
-
<style>
|
|
8
|
+
${head}<style>
|
|
8
9
|
:root { color-scheme: light dark }
|
|
9
10
|
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: Canvas }
|
|
10
11
|
svg { display: block; max-width: 100%; height: auto }
|
|
11
12
|
</style>
|
|
12
|
-
${svg}
|
|
13
|
+
${before}${svg}${after}
|
|
13
14
|
`;
|
|
14
15
|
}
|
package/dist/svg.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type CheckOptions } from './check.ts';
|
|
2
2
|
import type { Finding, Scene } from './scene.ts';
|
|
3
|
+
import type { DiffMarks } from './diff.ts';
|
|
3
4
|
export type { CheckOptions } from './check.ts';
|
|
4
5
|
export type { Finding, Scene } from './scene.ts';
|
|
5
6
|
export type * from './model.ts';
|
|
6
7
|
export { parseSource, links, type Link } from './source.ts';
|
|
7
|
-
export { diff, formatDiff, type Change } from './diff.ts';
|
|
8
|
+
export { diff, formatDiff, mergeFigures, type Change, type DiffMark, type DiffMarks } from './diff.ts';
|
|
8
9
|
import { type FigTheme, type FlowProps } from './model.ts';
|
|
9
10
|
export { LIGHT, DARK } from './model.ts';
|
|
10
11
|
/** Options for `toSvg`, `render` and `check`. */
|
|
@@ -18,9 +19,13 @@ export type SvgOptions = {
|
|
|
18
19
|
/** Lanes: page width and smallest text size that set the wrap. Default: 830 and 10. */
|
|
19
20
|
width?: number;
|
|
20
21
|
minText?: number;
|
|
22
|
+
/** Draws a still diff with a legend. `mergeFigures` makes the marks. */
|
|
23
|
+
marks?: DiffMarks;
|
|
24
|
+
/** Links a box to an href, by box id. `flowfig atlas` sets it. */
|
|
25
|
+
links?: Record<string, string>;
|
|
21
26
|
};
|
|
22
27
|
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
23
|
-
export declare function render(
|
|
28
|
+
export declare function render(spec: FlowProps, opts?: SvgOptions): {
|
|
24
29
|
svg: string;
|
|
25
30
|
scene: Scene;
|
|
26
31
|
};
|
package/dist/svg.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Wrapping is approximate: text is measured by character class, not by a browser.
|
|
2
|
-
import { arcRoom, avoidOf, route } from './geometry.js';
|
|
2
|
+
import { arcRoom, avoidOf, markSide, route } from './geometry.js';
|
|
3
3
|
import { foldedLabel, groupBox, layoutRail, railState, RAIL } from './rail.js';
|
|
4
4
|
import { textWidth, wrap } from './text.js';
|
|
5
5
|
import { checkRendered, planFor } from './check.js';
|
|
6
|
+
import { autoLayout, rowBreaks } from './auto.js';
|
|
6
7
|
export { parseSource, links } from './source.js';
|
|
7
|
-
export { diff, formatDiff } from './diff.js';
|
|
8
|
-
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, edgeTip, } from './model.js';
|
|
8
|
+
export { diff, formatDiff, mergeFigures } from './diff.js';
|
|
9
|
+
import { ASYNC_TAG_W, BASE_RATE, CARD_LINE, CARD_PAD, CARD_WIDTH, DARK, EDGE_OFF, EDGE_ON, LIGHT, TONES, toneFill, toneTint, labelPillW, ON_ACCENT, str, edgeId, groupGap, isGroup, isRows, LANE_PAD, LANE_ROW_GAP, LANE_BLOCK_GAP, FRAME_SIDE, laneGutter as gutterOf, STUB_ROOM, laneBlock, laneEnd, tightCopies, LABEL_LINE, itemWidth, labelLines, fitCap, diamondLines, diamondRoom, isLanesLayout, timelineBeats, timelineLayout, TL_AXIS_H, TL_AXIS_W, TL_BAR_H, TL_ROW_GAP, toBeat, altText, beatMs, STEP_HOLD_MS, playheadItem, loopStartItem, dateLabelRaised, labelSpan, outsideLabelRect, edgeTip, } from './model.js';
|
|
9
10
|
export { LIGHT, DARK } from './model.js';
|
|
10
11
|
const LINE = CARD_LINE, CARD_SIDE = 8, ROW_GAP = 4;
|
|
11
12
|
const SUB_LINE = 15;
|
|
13
|
+
const MIN_W = 320;
|
|
12
14
|
const FRAME_TOP = 37, FRAME_BOTTOM = 18;
|
|
13
15
|
// The card-on fills approximate the player's 8 % accent tint.
|
|
14
16
|
const vars = (t, cardOn) => `--accent:${t.accent}; --fg:${t.fg}; --muted:${t.muted}; --bg:${t.bg}; --surface:${t.surface}; --border:${t.border}; --card-on:${cardOn}; --tint:color-mix(in srgb, var(--accent) 10%, var(--surface));`;
|
|
@@ -159,13 +161,39 @@ function place(item, x, y, s, out) {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
const MARK_COLOR = { added: TONES.green, removed: TONES.red, changed: TONES.orange };
|
|
165
|
+
const marked = (mark, inner) => mark === 'removed' ? `<g opacity="0.5" stroke-dasharray="5 4">${inner}</g>` : mark ? `<g stroke-width="2">${inner}</g>` : inner;
|
|
166
|
+
function legendOf(m) {
|
|
167
|
+
const count = (kind) => Object.values(m.boxes).filter((k) => k === kind).length + Object.values(m.edges).filter((k) => k === kind).length;
|
|
168
|
+
const items = ['added', 'removed', 'changed'].filter(count).map((k) => ({ color: MARK_COLOR[k], text: `${k} ${count(k)}` }));
|
|
169
|
+
if (m.steps)
|
|
170
|
+
items.push({ color: 'none', text: 'steps: see the text diff' });
|
|
171
|
+
return items.length ? items : [{ color: 'none', text: 'no change in boxes and edges' }];
|
|
172
|
+
}
|
|
173
|
+
const legendWidths = (items) => items.map((i) => textWidth(i.text, 12) + (i.color === 'none' ? 0 : 16));
|
|
174
|
+
const legendWidth = (items) => legendWidths(items).reduce((a, b) => a + b + 20, -20);
|
|
175
|
+
function legendSvg(items, W, H) {
|
|
176
|
+
if (!items.length)
|
|
177
|
+
return '';
|
|
178
|
+
const widths = legendWidths(items);
|
|
179
|
+
let x = (W - legendWidth(items)) / 2;
|
|
180
|
+
return items
|
|
181
|
+
.map((i, k) => {
|
|
182
|
+
const sq = i.color === 'none' ? '' : `<rect x="${n2(x)}" y="${n2(H - 24)}" width="10" height="10" rx="2" fill="${i.color}"/>`;
|
|
183
|
+
const out = `${sq}<text x="${n2(x + (i.color === 'none' ? 0 : 16))}" y="${n2(H - 15)}" font-size="12" fill="var(--muted)">${esc(i.text)}</text>\n`;
|
|
184
|
+
x += widths[k] + 20;
|
|
185
|
+
return out;
|
|
186
|
+
})
|
|
187
|
+
.join('');
|
|
188
|
+
}
|
|
162
189
|
const SYSTEM_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
|
|
163
190
|
/** The SVG string and the scene it drew. The scene shape may change; use `toSvg` for the SVG only. */
|
|
164
|
-
export function render(
|
|
191
|
+
export function render(spec, opts = {}) {
|
|
192
|
+
const fig = autoLayout(spec);
|
|
165
193
|
const speed = (opts.speed ?? fig.speed ?? 900) / 1000 / BASE_RATE;
|
|
166
194
|
const pad = opts.padding ?? 24;
|
|
167
195
|
const tl = fig.timeline && isLanesLayout(fig.layout) ? timelineLayout(fig, TL_AXIS_W) : null;
|
|
168
|
-
const synthetic = tl != null && !fig.steps?.length;
|
|
196
|
+
const synthetic = tl != null && !fig.steps?.length && !opts.marks;
|
|
169
197
|
const steps = synthetic ? timelineBeats(fig) : (fig.steps ?? []);
|
|
170
198
|
const beats = steps.map((s) => s.flow.map(toBeat));
|
|
171
199
|
const cards = new Map();
|
|
@@ -233,6 +261,7 @@ export function render(fig, opts = {}) {
|
|
|
233
261
|
to: end(ids[i], e.to, false),
|
|
234
262
|
around: e.around ?? (plan?.around.has(ids[i]) ? 'below' : undefined),
|
|
235
263
|
...(tl && { sides: ['r', 'l'], elbow: true }),
|
|
264
|
+
...(rowBreaks.has(e) && { drop: true }),
|
|
236
265
|
...(stubs.has(ids[i]) && { stub: stubs.get(ids[i]).map(labelPillW), bands: [bandOf(ids[i], e.from, 0), bandOf(ids[i], e.to, 1)] }),
|
|
237
266
|
...(!tl && e.label != null && { labelW: labelPillW(str(e.label)) }),
|
|
238
267
|
})), rects, tips, avoidOf(placed.filter((p) => !isGroup(p.item)), stubs.size
|
|
@@ -451,8 +480,13 @@ export function render(fig, opts = {}) {
|
|
|
451
480
|
cls(p.lane && 'lane', anim(on, 'stroke: var(--accent)', 'stroke: var(--border)', 'n')) +
|
|
452
481
|
`/><text x="${n2(p.x + FRAME_SIDE)}" y="${n2(p.lane ? p.y + p.h / 2 + 4 : p.y + 20)}" class="frame">${esc(str(item.label).toUpperCase())}</text>`);
|
|
453
482
|
}
|
|
454
|
-
const
|
|
455
|
-
const
|
|
483
|
+
const diffMark = opts.marks?.boxes[item.id];
|
|
484
|
+
const href = opts.links?.[item.id];
|
|
485
|
+
const tip = (inner) => {
|
|
486
|
+
const body = marked(diffMark, item.source ? `<g><title>${esc(item.source)}</title>${inner}</g>` : inner);
|
|
487
|
+
return href == null ? body : `<a href="${esc(href)}" tabindex="-1">${body}</a>`;
|
|
488
|
+
};
|
|
489
|
+
const bt = diffMark ? MARK_COLOR[diffMark] : item.tone && TONES[item.tone];
|
|
456
490
|
const stroke = cls(boxAnim(item.id, true, bt));
|
|
457
491
|
if (p.tl) {
|
|
458
492
|
const fill0 = bt ? toneTint(bt, 'var(--bg)') : 'var(--bg)';
|
|
@@ -490,11 +524,13 @@ export function render(fig, opts = {}) {
|
|
|
490
524
|
? `<text x="${n2(cx)}" y="${n2(firstY + extra + SUB_LINE)}" class="sub">${esc(str(item.sub))}</text>`
|
|
491
525
|
: '';
|
|
492
526
|
const dot = bt ?? 'var(--accent)';
|
|
493
|
-
const
|
|
527
|
+
const side = item.mark && markSide(item.id, item.mark, routed);
|
|
528
|
+
const mx = n2(side === 'l' ? p.x - 12 : side === 'r' ? p.x + p.w + 12 : cx);
|
|
529
|
+
const my = n2(side === 't' ? p.y - 12 : side === 'b' ? p.y + p.h + 12 : p.y + p.h / 2);
|
|
494
530
|
const mark = item.mark === 'start'
|
|
495
|
-
? `<circle cx="${
|
|
531
|
+
? `<circle cx="${mx}" cy="${my}" r="5" fill="${dot}"/>`
|
|
496
532
|
: item.mark === 'end'
|
|
497
|
-
? `<circle cx="${
|
|
533
|
+
? `<circle cx="${mx}" cy="${my}" r="6.25" fill="none" stroke="${dot}" stroke-width="1.5"/><circle cx="${mx}" cy="${my}" r="4" fill="${dot}"/>`
|
|
498
534
|
: '';
|
|
499
535
|
return tip(shape + label + sub + mark + (contents ? card(p, cardTop, contents) : ''));
|
|
500
536
|
});
|
|
@@ -562,18 +598,21 @@ export function render(fig, opts = {}) {
|
|
|
562
598
|
const lit = cls(toned
|
|
563
599
|
? frames(on.map((x, i) => (x ? `stroke: ${col[i]}; stroke-width: ${EDGE_ON}` : off)), 'e', off)
|
|
564
600
|
: anim(on, `stroke: var(--accent); stroke-width: ${EDGE_ON}`, off, 'e'));
|
|
601
|
+
const em = opts.marks?.edges[r.id];
|
|
602
|
+
const line = em && (em !== 'changed' || e.label == null) ? MARK_COLOR[em] : 'var(--muted)';
|
|
565
603
|
const tip = edgeTip(r.id, e.source, beats.flat());
|
|
566
604
|
const path0 = r.stub
|
|
567
605
|
? r.stub.parts
|
|
568
|
-
.map((d) => `<path d="${d}" fill="none" stroke="
|
|
606
|
+
.map((d) => `<path d="${d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`)
|
|
569
607
|
.join('') + `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="none"/>`
|
|
570
|
-
: `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="
|
|
608
|
+
: `<path id="p-${esc(r.id)}" d="${r.d}" fill="none" stroke="${line}" stroke-width="${EDGE_OFF}" marker-end="url(#arrow)"${lit}/>`;
|
|
571
609
|
const path = tip ? `<g><title>${esc(tip)}</title>${path0}</g>` : path0;
|
|
572
|
-
const
|
|
610
|
+
const pillColor = em ? MARK_COLOR[em] : undefined;
|
|
611
|
+
const pill = (x, y, lw, text) => `<rect x="${n2(x - lw / 2)}" y="${n2(y - 9)}" width="${n2(lw)}" height="18" rx="9" fill="${pillColor ? toneTint(pillColor, 'var(--bg)', 14) : 'var(--bg)'}" stroke="${pillColor ?? 'var(--border)'}"` +
|
|
573
612
|
cls(toned
|
|
574
613
|
? frames(on.map((x, i) => x ? `fill: ${tone[i] ? toneFill(col[i]) : col[i]}; stroke: ${col[i]}` : 'fill: var(--bg); stroke: var(--border)'), 'l', 'fill: var(--bg); stroke: var(--border)')
|
|
575
614
|
: anim(on, 'fill: var(--accent); stroke: var(--accent)', 'fill: var(--bg); stroke: var(--border)', 'l')) +
|
|
576
|
-
`/><text x="${n2(x)}" y="${n2(y + 4)}"${cls('edgelabel', anim(on, `fill: ${ON_ACCENT}`, 'fill: var(--muted)', 'x'))}>${esc(text)}</text>`;
|
|
615
|
+
`/><text x="${n2(x)}" y="${n2(y + 4)}"${cls('edgelabel', anim(on, `fill: ${ON_ACCENT}`, 'fill: var(--muted)', 'x'))}${pillColor ? ' style="fill: var(--fg)"' : ''}>${esc(text)}</text>`;
|
|
577
616
|
let label = '';
|
|
578
617
|
if (r.stub) {
|
|
579
618
|
fonts.push(11);
|
|
@@ -586,7 +625,7 @@ export function render(fig, opts = {}) {
|
|
|
586
625
|
fonts.push(11);
|
|
587
626
|
label = pill(r.mid.x, r.mid.y, lw, str(e.label));
|
|
588
627
|
}
|
|
589
|
-
return hidden ? `<g opacity="0"${shown}>${path}${label}</g>` : path + label;
|
|
628
|
+
return marked(em === 'removed' ? em : undefined, hidden ? `<g opacity="0"${shown}>${path}${label}</g>` : path + label);
|
|
590
629
|
});
|
|
591
630
|
const packets = hops.map((h, i) => {
|
|
592
631
|
const t0 = h.t0 / total, t1 = h.t1 / total, tEnd = h.tEnd / total;
|
|
@@ -683,16 +722,21 @@ export function render(fig, opts = {}) {
|
|
|
683
722
|
}
|
|
684
723
|
const bounds = placed[0];
|
|
685
724
|
const [arcT, arcB, arcL, arcR] = ['above', 'below', 'left', 'right'].map((w) => arcRoom(routed, w));
|
|
686
|
-
const
|
|
687
|
-
const
|
|
725
|
+
const legend = opts.marks ? legendOf(opts.marks) : [];
|
|
726
|
+
const stepLabels = fig.rail || synthetic ? [] : steps.map((s) => textWidth(str(s.label), 13, true));
|
|
727
|
+
const floor = Math.max(MIN_W, ...[...stepLabels, legend.length ? legendWidth(legend) : 0].map((w) => w + 16));
|
|
728
|
+
const mapW = Math.max(bounds.w + pad * 2 + arcL + arcR, floor);
|
|
688
729
|
const rail = fig.rail ? layoutRail(fig, fig.rail === 'only' ? 560 : mapW) : null;
|
|
689
730
|
const only = fig.rail === 'only' && rail != null;
|
|
731
|
+
const wrapW = Math.max(only ? 560 : MIN_W, bounds.w);
|
|
732
|
+
const capLines = [...new Set(captions)].flatMap((c) => wrap(c, wrapW, 13.5).length);
|
|
690
733
|
const capTop = rail ? 20 : 26;
|
|
691
734
|
const capH = steps.length ? capTop + 4 + Math.max(0, ...capLines) * 20 : 0;
|
|
692
|
-
const W = only ? Math.max(560, rail.width) : Math.max(mapW, rail?.width ?? 0);
|
|
735
|
+
const W = only ? Math.max(560, floor, rail.width) : Math.max(mapW, rail?.width ?? 0);
|
|
693
736
|
const mapH = only ? 0 : bounds.h + pad * 2 + arcT + arcB;
|
|
694
737
|
const top = only ? pad : mapH + RAIL.gap;
|
|
695
|
-
const
|
|
738
|
+
const legendH = opts.marks ? 34 : 0;
|
|
739
|
+
const H = (only ? pad + rail.height + capH : mapH + (rail ? RAIL.gap + rail.height : 0) + capH) + legendH;
|
|
696
740
|
const shift = (W - (bounds.w + pad * 2 + arcL + arcR)) / 2 + arcL;
|
|
697
741
|
const railX = rail ? (W - rail.width) / 2 : 0;
|
|
698
742
|
const labels = rail || synthetic
|
|
@@ -703,7 +747,7 @@ export function render(fig, opts = {}) {
|
|
|
703
747
|
});
|
|
704
748
|
const said = [...new Set(captions)].map((text) => {
|
|
705
749
|
const on = captions.map((c) => c === text);
|
|
706
|
-
const lines = wrap(text,
|
|
750
|
+
const lines = wrap(text, wrapW, 13.5);
|
|
707
751
|
return (`<g opacity="0"${cls(anim(on, 'opacity: 1', 'opacity: 0', 'y'))}>` +
|
|
708
752
|
lines.map((l, li) => `<text x="${n2(W / 2)}" y="${n2(H - capH + capTop + 16 + li * 20)}" class="caption">${esc(l)}</text>`).join('') +
|
|
709
753
|
`</g>`);
|
|
@@ -782,7 +826,11 @@ export function render(fig, opts = {}) {
|
|
|
782
826
|
return `<g transform="translate(${n2(railX)} 0)">${parts.join('')}</g>`;
|
|
783
827
|
})();
|
|
784
828
|
const { font, ...t0 } = { ...LIGHT, ...fig.theme, ...opts.theme };
|
|
785
|
-
const
|
|
829
|
+
const alt = altText(fig);
|
|
830
|
+
const holdAt = beats[0]?.length ? n2(segs[beats[0].length - 1].t1 - 0.1) : 0;
|
|
831
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${n2(W)}" height="${n2(H)}" viewBox="0 0 ${n2(W)} ${n2(H)}" font-family="${esc(font ?? SYSTEM_FONT)}" role="img">
|
|
832
|
+
<title>${esc(alt.title)}</title>
|
|
833
|
+
<desc>${esc(alt.desc)}</desc>
|
|
786
834
|
<style>
|
|
787
835
|
svg { ${vars(t0, '#eef5fd')} }
|
|
788
836
|
@media (prefers-color-scheme: dark) { svg { ${vars(DARK, '#1d2733')} } }
|
|
@@ -806,6 +854,7 @@ svg { ${vars(t0, '#eef5fd')} }
|
|
|
806
854
|
.railphase.muted { fill: var(--muted); }
|
|
807
855
|
.end { text-anchor: end; }
|
|
808
856
|
${css.join('\n')}
|
|
857
|
+
@media (prefers-reduced-motion: reduce) { * { animation-play-state: paused !important; animation-delay: -${holdAt}s !important; } }
|
|
809
858
|
</style>
|
|
810
859
|
<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 1 L 9 5 L 0 9 z" fill="context-stroke"/></marker></defs>
|
|
811
860
|
<rect width="100%" height="100%" fill="var(--bg)"/>
|
|
@@ -823,10 +872,12 @@ ${todaySvg}
|
|
|
823
872
|
`}${railSvg}
|
|
824
873
|
${labels.join('\n')}
|
|
825
874
|
${said.join('\n')}
|
|
826
|
-
</svg>
|
|
875
|
+
${legendSvg(legend, W, H)}</svg>
|
|
827
876
|
`;
|
|
828
877
|
if (rail)
|
|
829
878
|
fonts.push(12, 13);
|
|
879
|
+
if (legend.length)
|
|
880
|
+
fonts.push(12);
|
|
830
881
|
const scene = {
|
|
831
882
|
width: n2(W),
|
|
832
883
|
...(!only && { area: { x: -shift, y: -arcT, w: W, h: mapH } }),
|
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Read } from './code.ts';
|
|
2
|
+
export type TraceEdge = {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
at: string;
|
|
6
|
+
};
|
|
7
|
+
export type TraceNote = {
|
|
8
|
+
at: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
};
|
|
11
|
+
export type TraceResult = {
|
|
12
|
+
start: string;
|
|
13
|
+
edges: TraceEdge[];
|
|
14
|
+
unsure: TraceNote[];
|
|
15
|
+
open: TraceNote[];
|
|
16
|
+
stops: {
|
|
17
|
+
reason: 'depth' | 'max';
|
|
18
|
+
count: number;
|
|
19
|
+
}[];
|
|
20
|
+
outside: number;
|
|
21
|
+
ms: number;
|
|
22
|
+
};
|
|
23
|
+
export type TraceOptions = {
|
|
24
|
+
root?: string;
|
|
25
|
+
depth?: number;
|
|
26
|
+
max?: number;
|
|
27
|
+
read?: Read;
|
|
28
|
+
};
|
|
29
|
+
export declare function trace(start: string, { root, depth, max, read }?: TraceOptions): TraceResult;
|
|
30
|
+
export declare function traceLines(r: TraceResult, depth: number, max: number): string[];
|