@flyingrobots/bijou 1.1.0 → 1.3.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 +35 -5
- package/dist/adapters/test/io.d.ts +2 -0
- package/dist/adapters/test/io.d.ts.map +1 -1
- package/dist/adapters/test/io.js +9 -0
- package/dist/adapters/test/io.js.map +1 -1
- package/dist/core/components/dag-edges.d.ts +78 -0
- package/dist/core/components/dag-edges.d.ts.map +1 -0
- package/dist/core/components/dag-edges.js +127 -0
- package/dist/core/components/dag-edges.js.map +1 -0
- package/dist/core/components/dag-layout.d.ts +37 -0
- package/dist/core/components/dag-layout.d.ts.map +1 -0
- package/dist/core/components/dag-layout.js +172 -0
- package/dist/core/components/dag-layout.js.map +1 -0
- package/dist/core/components/dag-render.d.ts +51 -0
- package/dist/core/components/dag-render.d.ts.map +1 -0
- package/dist/core/components/dag-render.js +440 -0
- package/dist/core/components/dag-render.js.map +1 -0
- package/dist/core/components/dag.d.ts +4 -4
- package/dist/core/components/dag.d.ts.map +1 -1
- package/dist/core/components/dag.js +6 -651
- package/dist/core/components/dag.js.map +1 -1
- package/dist/core/components/markdown-parse.d.ts +69 -0
- package/dist/core/components/markdown-parse.d.ts.map +1 -0
- package/dist/core/components/markdown-parse.js +272 -0
- package/dist/core/components/markdown-parse.js.map +1 -0
- package/dist/core/components/markdown-render.d.ts +20 -0
- package/dist/core/components/markdown-render.d.ts.map +1 -0
- package/dist/core/components/markdown-render.js +135 -0
- package/dist/core/components/markdown-render.js.map +1 -0
- package/dist/core/components/markdown.d.ts.map +1 -1
- package/dist/core/components/markdown.js +6 -371
- package/dist/core/components/markdown.js.map +1 -1
- package/dist/core/detect/tty.d.ts +2 -3
- package/dist/core/detect/tty.d.ts.map +1 -1
- package/dist/core/detect/tty.js +3 -4
- package/dist/core/detect/tty.js.map +1 -1
- package/dist/core/forms/filter-interactive.d.ts +66 -0
- package/dist/core/forms/filter-interactive.d.ts.map +1 -0
- package/dist/core/forms/filter-interactive.js +241 -0
- package/dist/core/forms/filter-interactive.js.map +1 -0
- package/dist/core/forms/filter.d.ts +2 -32
- package/dist/core/forms/filter.d.ts.map +1 -1
- package/dist/core/forms/filter.js +2 -167
- package/dist/core/forms/filter.js.map +1 -1
- package/dist/core/forms/select.js +29 -5
- package/dist/core/forms/select.js.map +1 -1
- package/dist/core/forms/textarea-editor.d.ts +39 -0
- package/dist/core/forms/textarea-editor.d.ts.map +1 -0
- package/dist/core/forms/textarea-editor.js +210 -0
- package/dist/core/forms/textarea-editor.js.map +1 -0
- package/dist/core/forms/textarea.d.ts +2 -19
- package/dist/core/forms/textarea.d.ts.map +1 -1
- package/dist/core/forms/textarea.js +2 -195
- package/dist/core/forms/textarea.js.map +1 -1
- package/dist/core/forms/types.d.ts +2 -0
- package/dist/core/forms/types.d.ts.map +1 -1
- package/dist/core/theme/resolve.d.ts +7 -0
- package/dist/core/theme/resolve.d.ts.map +1 -1
- package/dist/core/theme/resolve.js +6 -2
- package/dist/core/theme/resolve.js.map +1 -1
- package/dist/ports/io.d.ts +2 -0
- package/dist/ports/io.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,657 +1,13 @@
|
|
|
1
1
|
import { resolveCtx } from '../resolve-ctx.js';
|
|
2
2
|
import { isDagSource, isSlicedDagSource, arraySource, materialize, sliceSource } from './dag-source.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Compute the visible display width of a string in terminal columns.
|
|
7
|
-
* Delegates to `graphemeWidth` which handles ANSI escapes and wide characters.
|
|
8
|
-
*
|
|
9
|
-
* @param str - The string to measure.
|
|
10
|
-
* @returns The visible column width.
|
|
11
|
-
*/
|
|
12
|
-
function visibleLength(str) {
|
|
13
|
-
return graphemeWidth(str);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Truncate a label to fit within a maximum visible width.
|
|
17
|
-
* Strips ANSI escapes before measuring, truncates by grapheme cluster,
|
|
18
|
-
* and appends an ellipsis character when truncation occurs.
|
|
19
|
-
*
|
|
20
|
-
* @param text - The label text to truncate.
|
|
21
|
-
* @param maxLen - Maximum visible width in terminal columns.
|
|
22
|
-
* @returns The possibly truncated label string.
|
|
23
|
-
*/
|
|
24
|
-
function truncateLabel(text, maxLen) {
|
|
25
|
-
if (maxLen <= 0)
|
|
26
|
-
return '';
|
|
27
|
-
if (visibleLength(text) <= maxLen)
|
|
28
|
-
return text;
|
|
29
|
-
// Truncate by grapheme clusters, not code units
|
|
30
|
-
const clean = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
31
|
-
const graphemes = segmentGraphemes(clean);
|
|
32
|
-
let width = 0;
|
|
33
|
-
let result = '';
|
|
34
|
-
for (const g of graphemes) {
|
|
35
|
-
const gw = graphemeWidth(g);
|
|
36
|
-
if (width + gw > maxLen - 1)
|
|
37
|
-
break;
|
|
38
|
-
result += g;
|
|
39
|
-
width += gw;
|
|
40
|
-
}
|
|
41
|
-
return result + '\u2026';
|
|
42
|
-
}
|
|
43
|
-
// ── Layout: Layer Assignment ───────────────────────────────────────
|
|
44
|
-
/**
|
|
45
|
-
* Assign each node to a layer using longest-path layer assignment.
|
|
46
|
-
*
|
|
47
|
-
* Performs Kahn's topological sort to detect cycles, then assigns each
|
|
48
|
-
* node to the layer one past its deepest parent. Root nodes (in-degree 0)
|
|
49
|
-
* are placed on layer 0.
|
|
50
|
-
*
|
|
51
|
-
* @param nodes - The graph nodes to lay out.
|
|
52
|
-
* @returns Map from node ID to its zero-based layer index.
|
|
53
|
-
* @throws If the graph contains a cycle.
|
|
54
|
-
*/
|
|
55
|
-
function assignLayers(nodes) {
|
|
56
|
-
const children = new Map();
|
|
57
|
-
const parents = new Map();
|
|
58
|
-
const inDegree = new Map();
|
|
59
|
-
const nodeIds = new Set(nodes.map(n => n.id));
|
|
60
|
-
for (const n of nodes) {
|
|
61
|
-
// Filter edges to only include targets that exist in the graph
|
|
62
|
-
children.set(n.id, (n.edges ?? []).filter(e => nodeIds.has(e)));
|
|
63
|
-
inDegree.set(n.id, 0);
|
|
64
|
-
if (!parents.has(n.id))
|
|
65
|
-
parents.set(n.id, []);
|
|
66
|
-
}
|
|
67
|
-
for (const n of nodes) {
|
|
68
|
-
for (const childId of children.get(n.id) ?? []) {
|
|
69
|
-
if (!parents.has(childId))
|
|
70
|
-
parents.set(childId, []);
|
|
71
|
-
parents.get(childId).push(n.id);
|
|
72
|
-
inDegree.set(childId, (inDegree.get(childId) ?? 0) + 1);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Kahn's topological sort
|
|
76
|
-
const queue = [];
|
|
77
|
-
for (const [id, deg] of inDegree) {
|
|
78
|
-
if (deg === 0)
|
|
79
|
-
queue.push(id);
|
|
80
|
-
}
|
|
81
|
-
const topoOrder = [];
|
|
82
|
-
const visited = new Set();
|
|
83
|
-
while (queue.length > 0) {
|
|
84
|
-
const id = queue.shift();
|
|
85
|
-
if (visited.has(id))
|
|
86
|
-
continue;
|
|
87
|
-
visited.add(id);
|
|
88
|
-
topoOrder.push(id);
|
|
89
|
-
for (const childId of children.get(id) ?? []) {
|
|
90
|
-
const newDeg = (inDegree.get(childId) ?? 1) - 1;
|
|
91
|
-
inDegree.set(childId, newDeg);
|
|
92
|
-
if (newDeg === 0)
|
|
93
|
-
queue.push(childId);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (topoOrder.length !== nodes.length) {
|
|
97
|
-
throw new Error('[bijou] dag(): cycle detected in graph');
|
|
98
|
-
}
|
|
99
|
-
// Longest-path layer assignment
|
|
100
|
-
const layerMap = new Map();
|
|
101
|
-
for (const id of topoOrder) {
|
|
102
|
-
const pars = parents.get(id) ?? [];
|
|
103
|
-
if (pars.length === 0) {
|
|
104
|
-
layerMap.set(id, 0);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
let maxParent = 0;
|
|
108
|
-
for (const p of pars) {
|
|
109
|
-
maxParent = Math.max(maxParent, layerMap.get(p) ?? 0);
|
|
110
|
-
}
|
|
111
|
-
layerMap.set(id, maxParent + 1);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return layerMap;
|
|
115
|
-
}
|
|
116
|
-
// ── Layout: Column Ordering ────────────────────────────────────────
|
|
117
|
-
/**
|
|
118
|
-
* Group node IDs into layer arrays indexed by layer number.
|
|
119
|
-
*
|
|
120
|
-
* @param nodes - All graph nodes.
|
|
121
|
-
* @param layerMap - Map from node ID to layer index (from `assignLayers`).
|
|
122
|
-
* @returns Array of layers, where each layer is an array of node IDs.
|
|
123
|
-
*/
|
|
124
|
-
function buildLayerArrays(nodes, layerMap) {
|
|
125
|
-
let maxLayer = 0;
|
|
126
|
-
for (const v of layerMap.values()) {
|
|
127
|
-
if (v > maxLayer)
|
|
128
|
-
maxLayer = v;
|
|
129
|
-
}
|
|
130
|
-
const layers = Array.from({ length: maxLayer + 1 }, () => []);
|
|
131
|
-
for (const n of nodes) {
|
|
132
|
-
const l = layerMap.get(n.id);
|
|
133
|
-
if (l !== undefined)
|
|
134
|
-
layers[l].push(n.id);
|
|
135
|
-
}
|
|
136
|
-
return layers;
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Reorder nodes within each layer to minimize edge crossings.
|
|
140
|
-
*
|
|
141
|
-
* Uses the barycenter heuristic with one top-down pass followed by
|
|
142
|
-
* one bottom-up pass. Mutates the `layers` arrays in place.
|
|
143
|
-
*
|
|
144
|
-
* @param layers - Layer arrays from `buildLayerArrays`, mutated in place.
|
|
145
|
-
* @param nodes - All graph nodes (used to build adjacency maps).
|
|
146
|
-
*/
|
|
147
|
-
function orderColumns(layers, nodes) {
|
|
148
|
-
const childrenMap = new Map();
|
|
149
|
-
const parentsMap = new Map();
|
|
150
|
-
for (const n of nodes) {
|
|
151
|
-
childrenMap.set(n.id, n.edges ?? []);
|
|
152
|
-
if (!parentsMap.has(n.id))
|
|
153
|
-
parentsMap.set(n.id, []);
|
|
154
|
-
}
|
|
155
|
-
for (const n of nodes) {
|
|
156
|
-
for (const c of n.edges ?? []) {
|
|
157
|
-
if (!parentsMap.has(c))
|
|
158
|
-
parentsMap.set(c, []);
|
|
159
|
-
parentsMap.get(c).push(n.id);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Top-down pass
|
|
163
|
-
for (let l = 1; l < layers.length; l++) {
|
|
164
|
-
const prevLayer = layers[l - 1];
|
|
165
|
-
const curLayer = layers[l];
|
|
166
|
-
const prevIndex = new Map();
|
|
167
|
-
for (let i = 0; i < prevLayer.length; i++) {
|
|
168
|
-
prevIndex.set(prevLayer[i], i);
|
|
169
|
-
}
|
|
170
|
-
const bary = new Map();
|
|
171
|
-
for (const id of curLayer) {
|
|
172
|
-
const pars = (parentsMap.get(id) ?? []).filter(p => prevIndex.has(p));
|
|
173
|
-
if (pars.length === 0) {
|
|
174
|
-
bary.set(id, Infinity);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
const avg = pars.reduce((s, p) => s + (prevIndex.get(p) ?? 0), 0) / pars.length;
|
|
178
|
-
bary.set(id, avg);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
curLayer.sort((a, b) => (bary.get(a) ?? Infinity) - (bary.get(b) ?? Infinity));
|
|
182
|
-
}
|
|
183
|
-
// Bottom-up pass
|
|
184
|
-
for (let l = layers.length - 2; l >= 0; l--) {
|
|
185
|
-
const nextLayer = layers[l + 1];
|
|
186
|
-
const curLayer = layers[l];
|
|
187
|
-
const nextIndex = new Map();
|
|
188
|
-
for (let i = 0; i < nextLayer.length; i++) {
|
|
189
|
-
nextIndex.set(nextLayer[i], i);
|
|
190
|
-
}
|
|
191
|
-
const bary = new Map();
|
|
192
|
-
for (const id of curLayer) {
|
|
193
|
-
const chlds = (childrenMap.get(id) ?? []).filter(c => nextIndex.has(c));
|
|
194
|
-
if (chlds.length === 0) {
|
|
195
|
-
bary.set(id, Infinity);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const avg = chlds.reduce((s, c) => s + (nextIndex.get(c) ?? 0), 0) / chlds.length;
|
|
199
|
-
bary.set(id, avg);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
curLayer.sort((a, b) => (bary.get(a) ?? Infinity) - (bary.get(b) ?? Infinity));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Lookup table mapping sorted direction-set keys to Unicode box-drawing characters.
|
|
207
|
-
* For example, `'DR'` maps to `\u250c` (top-left corner).
|
|
208
|
-
*/
|
|
209
|
-
const JUNCTION = {
|
|
210
|
-
'D': '\u2502', 'U': '\u2502', 'DU': '\u2502',
|
|
211
|
-
'L': '\u2500', 'R': '\u2500', 'LR': '\u2500',
|
|
212
|
-
'DR': '\u250c', 'DL': '\u2510', 'RU': '\u2514', 'LU': '\u2518',
|
|
213
|
-
'DRU': '\u251c', 'DLU': '\u2524', 'DLR': '\u252c', 'LRU': '\u2534',
|
|
214
|
-
'DLRU': '\u253c',
|
|
215
|
-
};
|
|
216
|
-
/**
|
|
217
|
-
* Select the Unicode box-drawing character for a cell based on its edge directions.
|
|
218
|
-
*
|
|
219
|
-
* @param dirs - Set of cardinal directions passing through this cell.
|
|
220
|
-
* @returns The appropriate box-drawing character, or `\u253c` (cross) as fallback.
|
|
221
|
-
*/
|
|
222
|
-
function junctionChar(dirs) {
|
|
223
|
-
const key = [...dirs].sort().join('');
|
|
224
|
-
return JUNCTION[key] ?? '\u253c';
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Allocate an empty edge-routing grid.
|
|
228
|
-
*
|
|
229
|
-
* @param rows - Number of rows in the grid.
|
|
230
|
-
* @param cols - Number of columns in the grid.
|
|
231
|
-
* @returns A fresh `GridState` with empty direction sets for every cell.
|
|
232
|
-
*/
|
|
233
|
-
function createGrid(rows, cols) {
|
|
234
|
-
const dirs = [];
|
|
235
|
-
for (let r = 0; r < rows; r++) {
|
|
236
|
-
const row = [];
|
|
237
|
-
for (let c = 0; c < cols; c++) {
|
|
238
|
-
row.push(new Set());
|
|
239
|
-
}
|
|
240
|
-
dirs.push(row);
|
|
241
|
-
}
|
|
242
|
-
return { dirs, arrows: new Set(), rows, cols };
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Add direction markers to a single grid cell. Bounds-checked.
|
|
246
|
-
*
|
|
247
|
-
* @param g - The grid state to mutate.
|
|
248
|
-
* @param r - Row index.
|
|
249
|
-
* @param c - Column index.
|
|
250
|
-
* @param ds - One or more directions to mark in this cell.
|
|
251
|
-
*/
|
|
252
|
-
function markDir(g, r, c, ...ds) {
|
|
253
|
-
if (r >= 0 && r < g.rows && c >= 0 && c < g.cols) {
|
|
254
|
-
const cell = g.dirs[r][c];
|
|
255
|
-
for (const d of ds)
|
|
256
|
-
cell.add(d);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Route a single edge through the grid between two node positions.
|
|
261
|
-
*
|
|
262
|
-
* Draws a vertical segment from the source, an optional horizontal jog
|
|
263
|
-
* if the columns differ, then a vertical segment down to the target.
|
|
264
|
-
* Records an arrowhead position just above the destination node.
|
|
265
|
-
*
|
|
266
|
-
* @param g - The grid state to mutate.
|
|
267
|
-
* @param fromCol - Column index of the source node.
|
|
268
|
-
* @param fromLayer - Layer index of the source node.
|
|
269
|
-
* @param toCol - Column index of the destination node.
|
|
270
|
-
* @param toLayer - Layer index of the destination node.
|
|
271
|
-
* @param RS - Row stride (number of grid rows per layer).
|
|
272
|
-
* @param colCenter - Function mapping a column index to its center grid column.
|
|
273
|
-
*/
|
|
274
|
-
function markEdge(g, fromCol, fromLayer, toCol, toLayer, RS, colCenter) {
|
|
275
|
-
const srcC = colCenter(fromCol);
|
|
276
|
-
const dstC = colCenter(toCol);
|
|
277
|
-
const sRow = fromLayer * RS + 3;
|
|
278
|
-
const dRow = toLayer * RS - 1; // one row above dest box
|
|
279
|
-
const mid = sRow + 1;
|
|
280
|
-
if (srcC === dstC) {
|
|
281
|
-
for (let r = sRow; r < dRow; r++)
|
|
282
|
-
markDir(g, r, srcC, 'D', 'U');
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
markDir(g, sRow, srcC, 'D', 'U');
|
|
286
|
-
markDir(g, mid, srcC, srcC < dstC ? 'R' : 'L', 'U');
|
|
287
|
-
const minC = Math.min(srcC, dstC);
|
|
288
|
-
const maxC = Math.max(srcC, dstC);
|
|
289
|
-
for (let c = minC + 1; c < maxC; c++)
|
|
290
|
-
markDir(g, mid, c, 'L', 'R');
|
|
291
|
-
markDir(g, mid, dstC, srcC < dstC ? 'L' : 'R', 'D');
|
|
292
|
-
for (let r = mid + 1; r < dRow; r++)
|
|
293
|
-
markDir(g, r, dstC, 'D', 'U');
|
|
294
|
-
}
|
|
295
|
-
g.arrows.add(dRow * 10000 + dstC);
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Render a single node as a three-line Unicode box.
|
|
299
|
-
*
|
|
300
|
-
* Ghost nodes use dashed borders. The label is truncated to fit and a
|
|
301
|
-
* badge (if present) is right-aligned within the box.
|
|
302
|
-
*
|
|
303
|
-
* @param label - The node label text.
|
|
304
|
-
* @param badgeText - Optional badge text displayed to the right of the label.
|
|
305
|
-
* @param width - Total character width of the box (including borders).
|
|
306
|
-
* @param ghost - Whether to render with dashed (ghost) border characters.
|
|
307
|
-
* @returns The rendered box lines and per-character type map.
|
|
308
|
-
*/
|
|
309
|
-
function renderNodeBox(label, badgeText, width, ghost) {
|
|
310
|
-
const h = ghost ? '\u254c' : '\u2500';
|
|
311
|
-
const v = ghost ? '\u254e' : '\u2502';
|
|
312
|
-
const innerW = width - 2;
|
|
313
|
-
const contentW = innerW - 2;
|
|
314
|
-
let content;
|
|
315
|
-
let midTypes;
|
|
316
|
-
if (badgeText) {
|
|
317
|
-
const maxLabelW = contentW - visibleLength(badgeText) - 1;
|
|
318
|
-
const tLabel = truncateLabel(label, maxLabelW);
|
|
319
|
-
const gap = Math.max(1, contentW - visibleLength(tLabel) - visibleLength(badgeText));
|
|
320
|
-
content = tLabel + ' '.repeat(gap) + badgeText;
|
|
321
|
-
// Build char-type map for mid line: border + pad + label + gap + badge + pad + border
|
|
322
|
-
// Use segmentGraphemes for correct grapheme cluster counting.
|
|
323
|
-
midTypes = ['border']; // v
|
|
324
|
-
midTypes.push('pad'); // space
|
|
325
|
-
for (let i = 0; i < segmentGraphemes(tLabel).length; i++)
|
|
326
|
-
midTypes.push('label');
|
|
327
|
-
for (let i = 0; i < gap; i++)
|
|
328
|
-
midTypes.push('pad');
|
|
329
|
-
for (let i = 0; i < segmentGraphemes(badgeText).length; i++)
|
|
330
|
-
midTypes.push('badge');
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
content = truncateLabel(label, contentW);
|
|
334
|
-
// Build char-type map for mid line: border + pad + label + pad + border
|
|
335
|
-
midTypes = ['border']; // v
|
|
336
|
-
midTypes.push('pad'); // space
|
|
337
|
-
for (let i = 0; i < segmentGraphemes(content).length; i++)
|
|
338
|
-
midTypes.push('label');
|
|
339
|
-
}
|
|
340
|
-
const padRight = Math.max(0, contentW - visibleLength(content));
|
|
341
|
-
for (let i = 0; i < padRight; i++)
|
|
342
|
-
midTypes.push('pad');
|
|
343
|
-
midTypes.push('pad'); // trailing space
|
|
344
|
-
midTypes.push('border'); // v
|
|
345
|
-
const top = '\u256d' + h.repeat(innerW) + '\u256e';
|
|
346
|
-
const mid = v + ' ' + content + ' '.repeat(padRight) + ' ' + v;
|
|
347
|
-
const bot = '\u2570' + h.repeat(innerW) + '\u256f';
|
|
348
|
-
const borderLine = Array.from({ length: width }, () => 'border');
|
|
349
|
-
return {
|
|
350
|
-
lines: [top, mid, bot],
|
|
351
|
-
charTypes: [borderLine, midTypes, borderLine],
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
// ── Interactive Renderer ───────────────────────────────────────────
|
|
355
|
-
/**
|
|
356
|
-
* Render the full interactive (styled) DAG layout.
|
|
357
|
-
*
|
|
358
|
-
* Performs the complete layout pipeline: layer assignment, column ordering,
|
|
359
|
-
* edge routing, node box rendering, highlight/selection styling, and ANSI
|
|
360
|
-
* serialization into a final string.
|
|
361
|
-
*
|
|
362
|
-
* @param nodes - The graph nodes to render.
|
|
363
|
-
* @param options - Rendering options (tokens, selection, sizing).
|
|
364
|
-
* @param ctx - The resolved bijou context.
|
|
365
|
-
* @returns The rendered output string, node position map, and grid dimensions.
|
|
366
|
-
*/
|
|
367
|
-
function renderInteractiveLayout(nodes, options, ctx) {
|
|
368
|
-
if (nodes.length === 0)
|
|
369
|
-
return { output: '', nodes: new Map(), width: 0, height: 0 };
|
|
370
|
-
const nodeMap = new Map();
|
|
371
|
-
for (const n of nodes)
|
|
372
|
-
nodeMap.set(n.id, n);
|
|
373
|
-
const layerMap = assignLayers(nodes);
|
|
374
|
-
const layers = buildLayerArrays(nodes, layerMap);
|
|
375
|
-
orderColumns(layers, nodes);
|
|
376
|
-
const colIndex = new Map();
|
|
377
|
-
for (const layer of layers) {
|
|
378
|
-
for (let i = 0; i < layer.length; i++) {
|
|
379
|
-
colIndex.set(layer[i], i);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
let maxNodesPerLayer = 1;
|
|
383
|
-
for (const layer of layers) {
|
|
384
|
-
if (layer.length > maxNodesPerLayer)
|
|
385
|
-
maxNodesPerLayer = layer.length;
|
|
386
|
-
}
|
|
387
|
-
const maxWidth = options.maxWidth ?? ctx.runtime.columns;
|
|
388
|
-
let nodeWidth = options.nodeWidth ?? Math.max(...nodes.map(n => visibleLength(n.label) + (n.badge ? visibleLength(n.badge) + 2 : 0) + 4), 16);
|
|
389
|
-
let gap = 4;
|
|
390
|
-
let colStride = nodeWidth + gap;
|
|
391
|
-
let totalWidth = maxNodesPerLayer * colStride;
|
|
392
|
-
if (totalWidth > maxWidth && !options.nodeWidth) {
|
|
393
|
-
gap = 2;
|
|
394
|
-
colStride = nodeWidth + gap;
|
|
395
|
-
totalWidth = maxNodesPerLayer * colStride;
|
|
396
|
-
}
|
|
397
|
-
if (totalWidth > maxWidth && !options.nodeWidth) {
|
|
398
|
-
nodeWidth = Math.max(16, Math.floor((maxWidth - gap) / maxNodesPerLayer) - gap);
|
|
399
|
-
colStride = nodeWidth + gap;
|
|
400
|
-
totalWidth = maxNodesPerLayer * colStride;
|
|
401
|
-
}
|
|
402
|
-
const RS = 6;
|
|
403
|
-
const gridRows = layers.length * RS;
|
|
404
|
-
const gridCols = totalWidth;
|
|
405
|
-
const colCenter = (c) => c * colStride + Math.floor(nodeWidth / 2);
|
|
406
|
-
const g = createGrid(gridRows, gridCols);
|
|
407
|
-
// Mark edges
|
|
408
|
-
for (const n of nodes) {
|
|
409
|
-
const fLayer = layerMap.get(n.id);
|
|
410
|
-
const fCol = colIndex.get(n.id);
|
|
411
|
-
if (fLayer === undefined || fCol === undefined)
|
|
412
|
-
continue;
|
|
413
|
-
for (const childId of n.edges ?? []) {
|
|
414
|
-
const tLayer = layerMap.get(childId);
|
|
415
|
-
const tCol = colIndex.get(childId);
|
|
416
|
-
if (tLayer === undefined || tCol === undefined)
|
|
417
|
-
continue;
|
|
418
|
-
markEdge(g, fCol, fLayer, tCol, tLayer, RS, colCenter);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// Build output grids
|
|
422
|
-
const charGrid = [];
|
|
423
|
-
const tokenGrid = [];
|
|
424
|
-
for (let r = 0; r < gridRows; r++) {
|
|
425
|
-
const charRow = [];
|
|
426
|
-
const tokenRow = [];
|
|
427
|
-
for (let c = 0; c < gridCols; c++) {
|
|
428
|
-
charRow.push(' ');
|
|
429
|
-
tokenRow.push(null);
|
|
430
|
-
}
|
|
431
|
-
charGrid.push(charRow);
|
|
432
|
-
tokenGrid.push(tokenRow);
|
|
433
|
-
}
|
|
434
|
-
// Write edge characters
|
|
435
|
-
const edgeToken = options.edgeToken ?? ctx.theme.theme.border.muted;
|
|
436
|
-
for (let r = 0; r < gridRows; r++) {
|
|
437
|
-
for (let c = 0; c < gridCols; c++) {
|
|
438
|
-
const cell = g.dirs[r][c];
|
|
439
|
-
if (cell.size > 0) {
|
|
440
|
-
charGrid[r][c] = junctionChar(cell);
|
|
441
|
-
tokenGrid[r][c] = edgeToken;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
// Write arrowheads
|
|
446
|
-
for (const encoded of g.arrows) {
|
|
447
|
-
const r = Math.floor(encoded / 10000);
|
|
448
|
-
const c = encoded % 10000;
|
|
449
|
-
if (r >= 0 && r < gridRows && c >= 0 && c < gridCols) {
|
|
450
|
-
charGrid[r][c] = '\u25bc';
|
|
451
|
-
tokenGrid[r][c] = edgeToken;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// Highlight edges
|
|
455
|
-
const highlightSet = new Set(options.highlightPath ?? []);
|
|
456
|
-
if (options.highlightPath && options.highlightToken) {
|
|
457
|
-
const hlToken = options.highlightToken;
|
|
458
|
-
const path = options.highlightPath;
|
|
459
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
460
|
-
const fromId = path[i];
|
|
461
|
-
const toId = path[i + 1];
|
|
462
|
-
const fLayer = layerMap.get(fromId);
|
|
463
|
-
const tLayer = layerMap.get(toId);
|
|
464
|
-
const fCol = colIndex.get(fromId);
|
|
465
|
-
const tCol = colIndex.get(toId);
|
|
466
|
-
if (fLayer === undefined || tLayer === undefined || fCol === undefined || tCol === undefined)
|
|
467
|
-
continue;
|
|
468
|
-
const srcC = colCenter(fCol);
|
|
469
|
-
const dstC = colCenter(tCol);
|
|
470
|
-
const sRow = fLayer * RS + 3;
|
|
471
|
-
const dRow = tLayer * RS - 1;
|
|
472
|
-
const midRow = sRow + 1;
|
|
473
|
-
if (srcC === dstC) {
|
|
474
|
-
for (let r = sRow; r <= dRow && r < gridRows; r++) {
|
|
475
|
-
if (srcC < gridCols)
|
|
476
|
-
tokenGrid[r][srcC] = hlToken;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
if (sRow < gridRows && srcC < gridCols)
|
|
481
|
-
tokenGrid[sRow][srcC] = hlToken;
|
|
482
|
-
const minC = Math.min(srcC, dstC);
|
|
483
|
-
const maxC2 = Math.max(srcC, dstC);
|
|
484
|
-
if (midRow < gridRows) {
|
|
485
|
-
for (let c = minC; c <= maxC2 && c < gridCols; c++) {
|
|
486
|
-
tokenGrid[midRow][c] = hlToken;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
for (let r = midRow; r <= dRow && r < gridRows; r++) {
|
|
490
|
-
if (dstC < gridCols)
|
|
491
|
-
tokenGrid[r][dstC] = hlToken;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
// Write node boxes
|
|
497
|
-
const positions = new Map();
|
|
498
|
-
for (const n of nodes) {
|
|
499
|
-
const layer = layerMap.get(n.id);
|
|
500
|
-
const col = colIndex.get(n.id);
|
|
501
|
-
if (layer === undefined || col === undefined)
|
|
502
|
-
continue;
|
|
503
|
-
const startCol = col * colStride;
|
|
504
|
-
const startRow = layer * RS;
|
|
505
|
-
positions.set(n.id, { row: startRow, col: startCol, width: nodeWidth, height: 3 });
|
|
506
|
-
const box = renderNodeBox(n.label, n.badge, nodeWidth, n._ghost === true);
|
|
507
|
-
let nToken;
|
|
508
|
-
if (options.selectedId === n.id) {
|
|
509
|
-
nToken = options.selectedToken ?? ctx.theme.theme.ui.cursor;
|
|
510
|
-
}
|
|
511
|
-
else if (highlightSet.has(n.id) && options.highlightToken) {
|
|
512
|
-
nToken = options.highlightToken;
|
|
513
|
-
}
|
|
514
|
-
else if (n.token) {
|
|
515
|
-
nToken = n.token;
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
nToken = options.nodeToken ?? ctx.theme.theme.border.primary;
|
|
519
|
-
}
|
|
520
|
-
for (let lineIdx = 0; lineIdx < box.lines.length; lineIdx++) {
|
|
521
|
-
const row = startRow + lineIdx;
|
|
522
|
-
if (row >= gridRows)
|
|
523
|
-
continue;
|
|
524
|
-
const line = box.lines[lineIdx];
|
|
525
|
-
const types = box.charTypes[lineIdx];
|
|
526
|
-
const chars = segmentGraphemes(line);
|
|
527
|
-
for (let ci = 0; ci < chars.length; ci++) {
|
|
528
|
-
const gc = startCol + ci;
|
|
529
|
-
if (gc < gridCols) {
|
|
530
|
-
charGrid[row][gc] = chars[ci];
|
|
531
|
-
const charType = types[ci];
|
|
532
|
-
if (charType === 'label' && n.labelToken) {
|
|
533
|
-
tokenGrid[row][gc] = n.labelToken;
|
|
534
|
-
}
|
|
535
|
-
else if (charType === 'badge' && n.badgeToken) {
|
|
536
|
-
tokenGrid[row][gc] = n.badgeToken;
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
tokenGrid[row][gc] = nToken;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
// Serialize
|
|
546
|
-
const lines = [];
|
|
547
|
-
for (let r = 0; r < gridRows; r++) {
|
|
548
|
-
let line = '';
|
|
549
|
-
let prevToken = null;
|
|
550
|
-
let run = '';
|
|
551
|
-
for (let c = 0; c < gridCols; c++) {
|
|
552
|
-
const ch = charGrid[r][c];
|
|
553
|
-
const tk = tokenGrid[r][c];
|
|
554
|
-
if (tk === prevToken || (tk === null && prevToken === null)) {
|
|
555
|
-
run += ch;
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
558
|
-
if (run) {
|
|
559
|
-
line += prevToken ? ctx.style.styled(prevToken, run) : run;
|
|
560
|
-
}
|
|
561
|
-
run = ch;
|
|
562
|
-
prevToken = tk;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (run) {
|
|
566
|
-
line += prevToken ? ctx.style.styled(prevToken, run) : run;
|
|
567
|
-
}
|
|
568
|
-
lines.push(line.replace(/\s+$/, ''));
|
|
569
|
-
}
|
|
570
|
-
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
571
|
-
lines.pop();
|
|
572
|
-
}
|
|
573
|
-
return { output: lines.join('\n'), nodes: positions, width: gridCols, height: gridRows };
|
|
574
|
-
}
|
|
575
|
-
// ── Pipe Renderer ──────────────────────────────────────────────────
|
|
576
|
-
/**
|
|
577
|
-
* Render the graph as plain text for piped (non-TTY) output.
|
|
578
|
-
*
|
|
579
|
-
* Produces one line per node in the format `Label -> Target1, Target2`
|
|
580
|
-
* with no ANSI styling or box-drawing characters.
|
|
581
|
-
*
|
|
582
|
-
* @param nodes - The graph nodes to render.
|
|
583
|
-
* @returns Plain text representation of the graph.
|
|
584
|
-
*/
|
|
585
|
-
function renderPipe(nodes) {
|
|
586
|
-
if (nodes.length === 0)
|
|
587
|
-
return '';
|
|
588
|
-
const lines = [];
|
|
589
|
-
for (const n of nodes) {
|
|
590
|
-
const edges = n.edges ?? [];
|
|
591
|
-
const badgePart = n.badge ? ` (${n.badge})` : '';
|
|
592
|
-
if (edges.length > 0) {
|
|
593
|
-
const targets = edges
|
|
594
|
-
.map(id => {
|
|
595
|
-
const target = nodes.find(t => t.id === id);
|
|
596
|
-
return target ? target.label : id;
|
|
597
|
-
})
|
|
598
|
-
.join(', ');
|
|
599
|
-
lines.push(`${n.label}${badgePart} -> ${targets}`);
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
lines.push(`${n.label}${badgePart}`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return lines.join('\n');
|
|
606
|
-
}
|
|
607
|
-
// ── Accessible Renderer ────────────────────────────────────────────
|
|
608
|
-
/**
|
|
609
|
-
* Render the graph as structured accessible text.
|
|
610
|
-
*
|
|
611
|
-
* Produces a summary header ("Graph: N nodes, M edges") followed by
|
|
612
|
-
* layer-grouped node listings with edge descriptions.
|
|
613
|
-
*
|
|
614
|
-
* @param nodes - The graph nodes to render.
|
|
615
|
-
* @param layerMap - Map from node ID to layer index.
|
|
616
|
-
* @returns Accessible text representation of the graph.
|
|
617
|
-
*/
|
|
618
|
-
function renderAccessible(nodes, layerMap) {
|
|
619
|
-
if (nodes.length === 0)
|
|
620
|
-
return 'Graph: 0 nodes, 0 edges';
|
|
621
|
-
const totalEdges = nodes.reduce((s, n) => s + (n.edges?.length ?? 0), 0);
|
|
622
|
-
const lines = [`Graph: ${nodes.length} nodes, ${totalEdges} edges`, ''];
|
|
623
|
-
const layers = buildLayerArrays(nodes, layerMap);
|
|
624
|
-
const nodeMap = new Map();
|
|
625
|
-
for (const n of nodes)
|
|
626
|
-
nodeMap.set(n.id, n);
|
|
627
|
-
for (let l = 0; l < layers.length; l++) {
|
|
628
|
-
lines.push(`Layer ${l + 1}:`);
|
|
629
|
-
for (const id of layers[l]) {
|
|
630
|
-
const n = nodeMap.get(id);
|
|
631
|
-
if (!n)
|
|
632
|
-
continue;
|
|
633
|
-
const badgePart = n.badge ? ` (${n.badge})` : '';
|
|
634
|
-
const edges = (n.edges ?? []).filter(e => nodeMap.has(e));
|
|
635
|
-
if (edges.length > 0) {
|
|
636
|
-
const targets = edges.map(e => nodeMap.get(e)?.label ?? e).join(', ');
|
|
637
|
-
lines.push(` ${n.label}${badgePart} -> ${targets}`);
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
lines.push(` ${n.label}${badgePart} (end)`);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
lines.push('');
|
|
644
|
-
}
|
|
645
|
-
while (lines.length > 0 && lines[lines.length - 1] === '')
|
|
646
|
-
lines.pop();
|
|
647
|
-
return lines.join('\n');
|
|
648
|
-
}
|
|
3
|
+
import { assignLayers } from './dag-layout.js';
|
|
4
|
+
import { renderInteractiveLayout, renderPipe, renderAccessible } from './dag-render.js';
|
|
649
5
|
export function dagSlice(input, focus, opts) {
|
|
650
6
|
if (isDagSource(input)) {
|
|
651
7
|
return sliceSource(input, focus, opts);
|
|
652
8
|
}
|
|
653
9
|
// Array path: wrap, slice, materialize back for backward compat
|
|
654
|
-
const source = arraySource(input);
|
|
10
|
+
const source = arraySource([...input]);
|
|
655
11
|
return materialize(sliceSource(source, focus, opts));
|
|
656
12
|
}
|
|
657
13
|
export function dagLayout(input, options = {}) {
|
|
@@ -659,11 +15,10 @@ export function dagLayout(input, options = {}) {
|
|
|
659
15
|
throw new Error('[bijou] dagLayout(): received an unbounded DagSource. Use dagSlice() to produce a SlicedDagSource first.');
|
|
660
16
|
}
|
|
661
17
|
const ctx = resolveCtx(options.ctx);
|
|
662
|
-
const nodes = isSlicedDagSource(input) ? materialize(input) : input;
|
|
18
|
+
const nodes = isSlicedDagSource(input) ? materialize(input) : [...input];
|
|
663
19
|
if (nodes.length === 0)
|
|
664
20
|
return { output: '', nodes: new Map(), width: 0, height: 0 };
|
|
665
|
-
|
|
666
|
-
return { output: result.output, nodes: result.nodes, width: result.width, height: result.height };
|
|
21
|
+
return renderInteractiveLayout(nodes, options, ctx);
|
|
667
22
|
}
|
|
668
23
|
export function dag(input, options = {}) {
|
|
669
24
|
if (isDagSource(input) && !isSlicedDagSource(input)) {
|
|
@@ -671,7 +26,7 @@ export function dag(input, options = {}) {
|
|
|
671
26
|
}
|
|
672
27
|
const ctx = resolveCtx(options.ctx);
|
|
673
28
|
const mode = ctx.mode;
|
|
674
|
-
const nodes = isSlicedDagSource(input) ? materialize(input) : input;
|
|
29
|
+
const nodes = isSlicedDagSource(input) ? materialize(input) : [...input];
|
|
675
30
|
if (nodes.length === 0)
|
|
676
31
|
return '';
|
|
677
32
|
if (mode === 'pipe') {
|