pymap-cli 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,822 @@
1
+ <!doctype html>
2
+ <html lang="en"><meta charset="utf-8">
3
+ <meta name="viewport" content="width=device-width,initial-scale=1">
4
+ <title>__PYMAP_NAME__ — data flow</title>
5
+ <style>
6
+ :root {
7
+ --bg:#F4F5F6; --card:#FFFFFF; --line:#D7DADD; --rail:#9AA0A6;
8
+ --accent:#1F4E79; --alert:#8C2F22;
9
+ --text:#191C1F; --grey:#6E757C;
10
+ --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace;
11
+ }
12
+ * { box-sizing:border-box }
13
+ html,body { height:100% }
14
+ body {
15
+ margin:0; display:flex; flex-direction:column; background:var(--bg);
16
+ color:var(--text); font:13px/1.5 var(--mono);
17
+ }
18
+ header { border-bottom:1px solid var(--line); padding:13px 22px 12px }
19
+ .banner { display:flex; gap:15px; align-items:baseline; flex-wrap:wrap }
20
+ .banner h1 { margin:0; font-size:16px; font-weight:600 }
21
+ .banner p { margin:0; color:var(--grey); font-size:12px }
22
+ #q {
23
+ margin-top:10px; width:100%; max-width:430px; padding:8px 12px;
24
+ border:1px solid var(--line); border-radius:3px; background:var(--card);
25
+ color:var(--text); font:inherit;
26
+ }
27
+ #q:focus { outline:none; border-color:var(--accent); box-shadow:0 0 0 3px rgba(31,78,121,.12) }
28
+ #q::placeholder { color:var(--grey) }
29
+ main { flex:1; display:flex; min-height:0 }
30
+ #nav {
31
+ width:370px; min-width:220px; flex-shrink:0; overflow:auto;
32
+ padding:8px 0 50px; background:var(--card);
33
+ }
34
+ #handle {
35
+ width:5px; flex-shrink:0; cursor:col-resize; background:var(--line);
36
+ border-left:1px solid var(--card); border-right:1px solid var(--card);
37
+ }
38
+ #handle:hover, #handle.active { background:var(--accent) }
39
+ #scene { flex:1; display:flex; flex-direction:column; min-width:0; background:var(--bg) }
40
+ #canvas { flex:1; overflow:auto; padding:26px 34px 34px }
41
+ .plan { display:flex; justify-content:center; min-width:min-content; margin-top:6px }
42
+
43
+ .section {
44
+ display:flex; align-items:baseline; gap:9px; padding:15px 16px 7px;
45
+ border-top:1px solid var(--line); margin-top:10px; cursor:default;
46
+ }
47
+ .section:first-child { border-top:none; margin-top:2px }
48
+ .section h2 { margin:0; font-size:11.5px; font-weight:600; letter-spacing:.02em }
49
+ .section .n { color:var(--grey); font-size:11px; margin-left:auto }
50
+ .section.collapsible { cursor:pointer }
51
+ .section.collapsible:hover { background:#EDEFF1 }
52
+ .section .arrow { color:var(--grey); font-size:9px }
53
+ .section p { margin:0; color:var(--grey); font-size:11px; font-weight:400 }
54
+ .group { padding:11px 16px 3px; color:var(--grey); font-size:11px }
55
+ .alias {
56
+ display:inline-block; margin:0 4px 4px 0; padding:1px 7px; font-size:11px;
57
+ border:1px solid var(--line); border-radius:2px; background:var(--card);
58
+ color:var(--grey); cursor:pointer; font-family:inherit;
59
+ }
60
+ .alias:hover { border-color:var(--accent); color:var(--text) }
61
+ .aliases { padding:3px 16px 7px 34px }
62
+ .count {
63
+ font-size:10.5px; color:var(--grey); border:1px solid var(--line);
64
+ border-radius:2px; padding:0 5px; flex-shrink:0;
65
+ }
66
+ .node[aria-current="true"] .count { color:#A8ADB2; border-color:#4A4F54 }
67
+ .branch { padding-left:14px }
68
+ .children { margin-left:9px; padding-left:14px; border-left:1px solid var(--line) }
69
+ .node {
70
+ position:relative; display:block; width:100%; text-align:left; border:0;
71
+ background:none; color:var(--text); font:inherit; padding:4px 8px 4px 6px;
72
+ cursor:pointer; border-radius:3px;
73
+ }
74
+ .children > .branch > .node::before {
75
+ content:""; position:absolute; left:-14px; top:13px; width:11px;
76
+ border-top:1px solid var(--line);
77
+ }
78
+ .node:hover { background:var(--card) }
79
+ .node[aria-current="true"] { background:var(--text); color:#FFFFFF }
80
+ .node[aria-current="true"] .mod, .node[aria-current="true"] .num { color:#A8ADB2 }
81
+ .node[aria-current="true"] .gauge i { background:#A8ADB2 }
82
+ .head { display:flex; align-items:center; gap:8px; min-width:0 }
83
+ .twist { width:10px; flex-shrink:0; font-size:9px; color:var(--grey) }
84
+ .node[aria-current="true"] .twist { color:var(--bg) }
85
+ .name { font-weight:600; white-space:nowrap; overflow:hidden;
86
+ text-overflow:ellipsis; min-width:34px }
87
+ .mod { color:var(--grey); font-size:11px; white-space:nowrap;
88
+ overflow:hidden; text-overflow:ellipsis; min-width:0 }
89
+ .gap { flex:1 1 auto; min-width:6px }
90
+ .gauge { display:flex; align-items:center; gap:6px; flex-shrink:0; padding-right:2px }
91
+ .gauge i { display:block; height:4px; border-radius:1px; background:var(--rail) }
92
+ .num { color:var(--grey); font-size:10.5px; min-width:16px; text-align:right }
93
+ mark { background:#FBEFB8; color:var(--text); border-radius:1px }
94
+
95
+ .flow-title { margin:0 0 4px; font-size:19px; font-weight:600 }
96
+ .flow-sub { color:var(--grey); font-size:12px; margin:0 0 8px }
97
+ .trail { color:var(--grey); font-size:11.5px; margin:0 0 20px }
98
+ .trail b { color:var(--text) }
99
+ .trail i { color:var(--rail); font-style:normal }
100
+ .empty { color:var(--grey); max-width:56ch; line-height:1.75 }
101
+ .empty b { color:var(--text); font-weight:600 }
102
+ #legend {
103
+ flex-shrink:0; display:flex; gap:20px; flex-wrap:wrap; align-items:center;
104
+ padding:11px 34px; border-top:1px solid var(--line); background:var(--card);
105
+ color:var(--grey); font-size:11px;
106
+ }
107
+ #legend:empty { display:none }
108
+ #legend span i { display:inline-block; width:9px; height:9px; border-radius:2px;
109
+ margin-right:6px; vertical-align:-1px }
110
+
111
+ /* --- hover tooltip --- */
112
+ #tip {
113
+ position:fixed; z-index:60; max-width:430px; display:none;
114
+ background:var(--card); border:1px solid var(--text); border-radius:2px;
115
+ padding:11px 14px 12px; box-shadow:0 6px 20px rgba(0,0,0,.13);
116
+ pointer-events:none; font-size:12.5px;
117
+ }
118
+ #tip .t-name { font-weight:600; font-size:13.5px }
119
+ #tip .t-where { color:var(--grey); font-size:11px; margin-left:7px }
120
+ #tip .t-sig { color:var(--accent); font-size:11.5px; margin-top:3px; word-break:break-word }
121
+ #tip .t-doc {
122
+ font-family:Charter,Georgia,serif; font-size:13.5px; line-height:1.5;
123
+ margin-top:8px; padding-left:11px; border-left:2px solid var(--line);
124
+ }
125
+ #tip .t-doc.none { color:var(--alert); font-style:italic; border-color:var(--alert) }
126
+ #tip .t-meta { color:var(--grey); font-size:11px; margin-top:9px }
127
+ #tip .t-raw { word-break:break-word; line-height:1.5 }
128
+
129
+ /* --- progress --- */
130
+ .node.seen .name { font-weight:400; color:var(--grey) }
131
+ .node.seen[aria-current="true"] .name { color:#FFF }
132
+ .check { color:var(--accent); font-size:10px; flex-shrink:0 }
133
+ .node[aria-current="true"] .check { color:#A8ADB2 }
134
+ .progress {
135
+ display:flex; align-items:center; gap:9px; padding:8px 34px;
136
+ border-bottom:1px solid var(--line); background:var(--card);
137
+ color:var(--grey); font-size:11.5px; flex-shrink:0;
138
+ }
139
+ .progress .bar { flex:1; max-width:200px; height:4px; background:var(--line); border-radius:2px }
140
+ .progress .bar i { display:block; height:4px; background:var(--accent); border-radius:2px }
141
+ .progress b { color:var(--text) }
142
+ .progress .link { margin-left:auto; display:flex; gap:14px; align-items:center }
143
+ a.edit {
144
+ color:var(--accent); text-decoration:none; border-bottom:1px solid transparent;
145
+ }
146
+ a.edit:hover { border-bottom-color:var(--accent) }
147
+ svg .step-live rect, svg .step-live path { stroke:var(--accent); stroke-width:2.4 }
148
+
149
+ /* --- home --- */
150
+ .home { max-width:620px }
151
+ .home h3 { margin:0 0 5px; font-size:15px }
152
+ .home > p { color:var(--grey); margin:0 0 22px; line-height:1.7 }
153
+ .track {
154
+ display:flex; align-items:center; gap:14px; width:100%; text-align:left;
155
+ padding:13px 16px; margin-bottom:9px; cursor:pointer;
156
+ background:var(--card); border:1px solid var(--line); border-radius:2px;
157
+ font:inherit; color:inherit;
158
+ }
159
+ .track:hover { border-color:var(--accent) }
160
+ .track .r { font-size:19px; font-weight:600; color:var(--line); min-width:22px }
161
+ .track .t { flex:1; min-width:0 }
162
+ .track .t b { font-size:14px }
163
+ .track .t span { display:block; color:var(--grey); font-size:11.5px; margin-top:2px }
164
+ .track .m { color:var(--grey); font-size:11px; text-align:right; flex-shrink:0 }
165
+ .keys { margin-top:26px; padding-top:16px; border-top:1px solid var(--line);
166
+ color:var(--grey); font-size:11.5px; line-height:2 }
167
+ .keys kbd { font-family:var(--mono); border:1px solid var(--line); border-radius:2px;
168
+ padding:1px 5px; background:var(--bg); color:var(--text); font-size:11px }
169
+ svg .hit { cursor:pointer }
170
+ svg .hit:hover rect { stroke:var(--accent); stroke-width:1.8 }
171
+ </style>
172
+ <header>
173
+ <div class="banner">
174
+ <h1>__PYMAP_NAME__</h1>
175
+ <p><b id="nr">0</b> entry points · __PYMAP_COVERAGE__% documented</p>
176
+ </div>
177
+ <input id="q" placeholder="Search a symbol — the tree opens down to it" autocomplete="off">
178
+ </header>
179
+ <main>
180
+ <nav id="nav"></nav>
181
+ <div id="handle" title="drag to resize · double-click to reset"></div>
182
+ <section id="scene">
183
+ <div class="progress" id="progress"></div>
184
+ <div id="canvas"></div>
185
+ <div id="legend"></div>
186
+ </section>
187
+ </main>
188
+ <div id="tip" role="tooltip"></div>
189
+ <script>
190
+ const SYM = __PYMAP_DATA__;
191
+ const REL = __PYMAP_RELATIONS__;
192
+ const byKey = Object.fromEntries(SYM.map(s => [s.key, s]));
193
+ const OUT = {}, IN = {};
194
+ for (const [k, v] of Object.entries(REL)) {
195
+ OUT[k] = (v.calls || []).slice(); IN[k] = (v.called_by || []).slice();
196
+ }
197
+ for (const k of Object.keys(OUT)) if (!(k in byKey)) byKey[k] = null;
198
+ const $ = id => document.getElementById(id);
199
+ const esc = t => (t ?? '').replace(/[&<>"]/g, c =>
200
+ ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
201
+ const shortName = k => (byKey[k] ? byKey[k].name : k.split('::').pop());
202
+ const moduleOf = k => k.split('::')[0];
203
+
204
+ const reachMemo = {};
205
+ /* How many distinct symbols this one can reach, transitively. Drives the
206
+ ordering everywhere: the widest entry point is the best place to start. */
207
+ function reach(key) {
208
+ if (reachMemo[key] !== undefined) return reachMemo[key];
209
+ const seen = new Set(), stack = [key];
210
+ while (stack.length) {
211
+ const node = stack.pop();
212
+ for (const next of (OUT[node] || [])) if (!seen.has(next)) { seen.add(next); stack.push(next); }
213
+ }
214
+ return reachMemo[key] = seen.size;
215
+ }
216
+ const everyKey = new Set([...Object.keys(OUT), ...Object.keys(IN)]);
217
+ const roots = [...everyKey].filter(k => !(IN[k] || []).length && (OUT[k] || []).length)
218
+ .sort((a, b) => reach(b) - reach(a) || a.localeCompare(b));
219
+
220
+ const ROOT = __PYMAP_ROOT__;
221
+ const EDITOR = __PYMAP_EDITOR__; // URI template, {f} = absolute file, {l} = line
222
+ let expanded = new Set(), sel = null, selPath = [], highlight = '';
223
+ let seen = new Set(), steps = [], iStep = -1;
224
+
225
+ /* --- editor link --- */
226
+ function editorUrl(s) {
227
+ if (!EDITOR || !s) return '';
228
+ return EDITOR.replace('{f}', encodeURI(ROOT + '/' + s.file))
229
+ .replace('{l}', s.line);
230
+ }
231
+ function editorLink(s) {
232
+ if (!s) return '';
233
+ const where = `${esc(s.file)}:${s.line}`, url = editorUrl(s);
234
+ return url ? `<a class="edit" href="${url}" title="open in the editor">${where} ↗</a>` : where;
235
+ }
236
+
237
+ /* --- walkable steps of the current symbol, in diagram order --- */
238
+ function flatten(block, acc) {
239
+ for (const step of block) {
240
+ if (step.k === 'branch') { flatten(step.then || [], acc); flatten(step.alt || [], acc); }
241
+ else if (step.k === 'loop' || step.k === 'try') {
242
+ flatten(step.body || [], acc); flatten(step.fallback || [], acc);
243
+ } else if (step.k === 'call') {
244
+ const key = resolved[step.name] || resolved[step.name.replace(/^_+/, '')];
245
+ if (key) acc.push(key);
246
+ }
247
+ }
248
+ return acc;
249
+ }
250
+
251
+ /* --- progress --- */
252
+ function progress() {
253
+ const bar = $('progress');
254
+ if (!sel) { bar.innerHTML = ''; return; }
255
+ const reachable = new Set([sel]), stack = [sel];
256
+ while (stack.length) {
257
+ const node = stack.pop();
258
+ for (const next of (OUT[node] || [])) if (!reachable.has(next)) {
259
+ reachable.add(next); stack.push(next);
260
+ }
261
+ }
262
+ const done = [...reachable].filter(k => seen.has(k)).length, total = reachable.size;
263
+ const percent = Math.round(100 * done / total);
264
+ bar.innerHTML = `<span>walkthrough: <b>${done}</b> / ${total} symbols seen</span>`
265
+ + `<span class="bar"><i style="width:${percent}%"></i></span><span>${percent}%</span>`
266
+ + `<span class="link">${editorLink(byKey[sel])}`
267
+ + `<span>${seen.has(sel) ? 'seen' : 'not seen'} · <kbd>m</kbd></span></span>`;
268
+ }
269
+ const pathKey = path => path.join('|');
270
+
271
+ /* ================= DIAGRAM ================= */
272
+ /* BOX/ROW: node size. VGAP/HGAP: spacing. TALL: decision and frame height.
273
+ PARAMS: header height of the input-data card. */
274
+ const BOX = 250, ROW = 46, VGAP = 34, HGAP = 30, TALL = 62, PARAMS = 30;
275
+ let resolved = {};
276
+
277
+ function measure(block) {
278
+ let w = BOX, h = 0;
279
+ for (const step of block) {
280
+ if (step.k === 'branch') {
281
+ const a = measure(step.then || []), b = measure(step.alt || []);
282
+ const side = (a.h || b.h) ? Math.max(a.w, BOX) + HGAP + Math.max(b.w, 0) : BOX;
283
+ w = Math.max(w, Math.max(side, BOX));
284
+ h += TALL + VGAP + Math.max(a.h, b.h) + (Math.max(a.h, b.h) ? VGAP : 0);
285
+ } else if (step.k === 'loop' || step.k === 'try') {
286
+ const body = measure(step.body || []), fallback = measure(step.fallback || []);
287
+ w = Math.max(w, body.w + 26, fallback.w + 26);
288
+ h += TALL + VGAP + body.h + (body.h ? VGAP : 0) + (fallback.h ? fallback.h + VGAP : 0);
289
+ } else { h += ROW + VGAP; }
290
+ }
291
+ return { w, h };
292
+ }
293
+
294
+ function place(block, cx, y, out, depth) {
295
+ for (let i = 0; i < block.length; i++) {
296
+ const step = block[i], isLast = i === block.length - 1;
297
+ if (step.k === 'branch') {
298
+ const then = step.then || [], alt = step.alt || [];
299
+ const mThen = measure(then), mAlt = measure(alt);
300
+ out.push(diamond(cx, y, step.test));
301
+ const bottom = y + TALL;
302
+ if (!then.length && !alt.length) { y = bottom + VGAP; edge(out, cx, bottom, cx, y); continue; }
303
+ const xThen = then.length ? cx - (mAlt.h ? (mThen.w / 2 + HGAP / 2) : 0) : cx;
304
+ const xAlt = alt.length ? cx + (mThen.h ? (mAlt.w / 2 + HGAP / 2) : 0) : cx;
305
+ if (then.length) { elbow(out, cx, bottom, xThen, bottom + VGAP, 'then', 'var(--accent)');
306
+ place(then, xThen, bottom + VGAP, out, depth + 1); }
307
+ if (alt.length) { elbow(out, cx, bottom, xAlt, bottom + VGAP, 'else', 'var(--alert)');
308
+ place(alt, xAlt, bottom + VGAP, out, depth + 1); }
309
+ const end = bottom + VGAP + Math.max(mThen.h, mAlt.h);
310
+ const closes = b => b.length && ['return', 'raise'].includes(b[b.length - 1].k);
311
+ if (then.length && alt.length) {
312
+ if (!closes(then)) elbow(out, xThen, bottom + mThen.h, cx, end, '', 'var(--rail)');
313
+ if (!closes(alt)) elbow(out, xAlt, bottom + mAlt.h, cx, end, '', 'var(--rail)');
314
+ }
315
+ y = end;
316
+ } else if (step.k === 'loop' || step.k === 'try') {
317
+ const isLoop = step.k === 'loop';
318
+ out.push(frame(cx, y, step.test || 'try / recover', isLoop));
319
+ const body = step.body || [], fallback = step.fallback || [];
320
+ let yy = y + TALL;
321
+ if (body.length) { edge(out, cx, y + TALL, cx, yy + VGAP);
322
+ place(body, cx, yy + VGAP, out, depth + 1);
323
+ yy += VGAP + measure(body).h; }
324
+ if (fallback.length) { elbow(out, cx, y + TALL, cx + BOX / 2 + HGAP, yy, 'error', 'var(--alert)');
325
+ place(fallback, cx + BOX / 2 + HGAP, yy, out, depth + 1);
326
+ yy += measure(fallback).h; }
327
+ y = yy || y + TALL + VGAP;
328
+ } else {
329
+ out.push(box(cx, y, step));
330
+ const next = y + ROW + VGAP;
331
+ const terminal = step.k === 'return' || step.k === 'raise';
332
+ if (!terminal && !isLast) edge(out, cx, y + ROW, cx, next, step.out || '');
333
+ y = (terminal || isLast) ? y + ROW + 12 : next;
334
+ }
335
+ }
336
+ return y;
337
+ }
338
+
339
+ function edge(out, x1, y1, x2, y2, label) {
340
+ out.push(`<path d="M${x1},${y1} L${x2},${y2}" stroke="var(--rail)" `
341
+ + `stroke-width="1.3" fill="none" marker-end="url(#arrow)"/>`);
342
+ if (label) {
343
+ const w = label.length * 6.5 + 14, my = (y1 + y2) / 2;
344
+ out.push(`<g><rect x="${x1 + 8}" y="${my - 9}" width="${w}" height="18" rx="2" `
345
+ + `fill="var(--card)" stroke="var(--line)" stroke-width="1"/>`
346
+ + `<rect x="${x1 + 8}" y="${my + 8}" width="${w}" height="1" fill="var(--accent)"/>`
347
+ + `<text x="${x1 + 15}" y="${my + 4}" fill="var(--text)" font-size="10.5" `
348
+ + `font-family="var(--mono)">${esc(label)}</text></g>`);
349
+ }
350
+ }
351
+
352
+ function elbow(out, x1, y1, x2, y2, label, color) {
353
+ const my = y1 + (y2 - y1) / 2;
354
+ out.push(`<path d="M${x1},${y1} L${x1},${my} L${x2},${my} L${x2},${y2}" `
355
+ + `stroke="${color}" stroke-width="1.3" fill="none" marker-end="url(#arrow)"/>`);
356
+ if (label) {
357
+ const lx = x1 === x2 ? x1 + 9 : (x1 + x2) / 2;
358
+ out.push(`<text x="${lx}" y="${my - 6}" fill="${color}" font-size="10.5" `
359
+ + `text-anchor="${x1 === x2 ? 'start' : 'middle'}" `
360
+ + `font-family="var(--mono)">${label}</text>`);
361
+ }
362
+ }
363
+
364
+ function diamond(cx, y, test) {
365
+ const label = trunc(test, 34);
366
+ const w = Math.max(168, label.length * 7.2 + 76), h = TALL;
367
+ return `<g class="hint" data-raw="${esc(test)}" data-kind="decision">`
368
+ + `<path d="M${cx},${y} L${cx + w / 2},${y + h / 2} L${cx},${y + h} `
369
+ + `L${cx - w / 2},${y + h / 2} Z" fill="var(--card)" `
370
+ + `stroke="var(--text)" stroke-width="1.3"/>`
371
+ + `<text x="${cx}" y="${y + h / 2 + 4}" fill="var(--text)" font-size="10.5" `
372
+ + `text-anchor="middle" font-family="var(--mono)">${esc(label)}</text></g>`;
373
+ }
374
+
375
+ function frame(cx, y, test, isLoop) {
376
+ const color = isLoop ? 'var(--text)' : 'var(--alert)';
377
+ return `<g class="hint" data-raw="${esc(test)}" data-kind="${isLoop ? 'loop' : 'error block'}">`
378
+ + `<rect x="${cx - BOX / 2}" y="${y}" width="${BOX}" height="${TALL}" rx="2" `
379
+ + `fill="var(--card)" stroke="${color}" stroke-width="1.2" stroke-dasharray="6 3"/>`
380
+ + `<text x="${cx}" y="${y + TALL / 2 + 4}" fill="${color}" font-size="10.5" `
381
+ + `text-anchor="middle" font-family="var(--mono)">${esc(trunc(test, 34))}</text></g>`;
382
+ }
383
+
384
+ function box(cx, y, step) {
385
+ const x = cx - BOX / 2;
386
+ if (step.k === 'return' || step.k === 'raise') {
387
+ const raising = step.k === 'raise';
388
+ const color = raising ? 'var(--alert)' : 'var(--text)';
389
+ return `<g class="hint" data-raw="${esc(step.expr)}" `
390
+ + `data-kind="${raising ? 'raised exception' : 'return value'}">`
391
+ + `<rect x="${x + 30}" y="${y}" width="${BOX - 60}" height="${ROW}" rx="${ROW / 2}" `
392
+ + `fill="var(--card)" stroke="${color}" stroke-width="2"/>`
393
+ + `<text x="${cx}" y="${y + 19}" fill="${color}" font-size="10" text-anchor="middle" `
394
+ + `font-family="var(--mono)">${raising ? 'raises' : 'returns'}</text>`
395
+ + `<text x="${cx}" y="${y + 33}" fill="var(--text)" font-size="11.5" `
396
+ + `text-anchor="middle" font-family="var(--mono)">${esc(trunc(step.expr, 26))}</text></g>`;
397
+ }
398
+ const key = resolved[step.name];
399
+ const inside = !!key;
400
+ const args = (step.args || []).concat((step.kw || []).map(k => k + '=')).join(', ');
401
+ const hover = inside ? ` data-sym="${esc(key)}"`
402
+ : ` data-raw="${esc(step.name + '(' + args + ')')}" data-kind="external call"`;
403
+ return `<g class="${inside ? 'hit' : 'hint'}"${hover}>`
404
+ + `<rect x="${x}" y="${y}" width="${BOX}" height="${ROW}" rx="2" fill="var(--card)" `
405
+ + `stroke="${inside ? 'var(--line)' : '#E4E6E8'}" stroke-width="1.1"`
406
+ + `${inside ? '' : ' stroke-dasharray="4 3"'}/>`
407
+ + (inside ? `<rect x="${x}" y="${y}" width="3" height="${ROW}" fill="var(--accent)"/>` : '')
408
+ + `<text x="${x + 14}" y="${y + 19}" fill="${inside ? 'var(--text)' : 'var(--grey)'}" `
409
+ + `font-size="12.5" font-weight="600" font-family="var(--mono)">${esc(trunc(step.name, 24))}</text>`
410
+ + `<text x="${x + 14}" y="${y + 34}" fill="var(--grey)" font-size="10" `
411
+ + `font-family="var(--mono)">${esc(trunc(args || (inside ? moduleOf(key) : 'external'), 34))}</text>`
412
+ + (inside ? `<text x="${x + BOX - 12}" y="${y + 27}" fill="var(--grey)" font-size="10" `
413
+ + `text-anchor="end" font-family="var(--mono)">${reach(key)}</text>` : '')
414
+ + `</g>`;
415
+ }
416
+
417
+ const trunc = (t, n) => (t || '').length > n ? (t || '').slice(0, n - 1) + '…' : (t || '');
418
+
419
+ /* Map the names appearing in the flow onto the call-graph keys, so a box can
420
+ be clicked through. A constructor also answers to its class name. */
421
+ function resolveNames(key) {
422
+ resolved = {};
423
+ (OUT[key] || []).forEach(target => {
424
+ const qualified = target.split('::').pop(); // e.g. Request.__init__
425
+ const leaf = qualified.split('.').pop(); // e.g. __init__
426
+ resolved[leaf] = target;
427
+ resolved['_' + leaf] = target;
428
+ if (leaf.startsWith('__') && qualified.includes('.'))
429
+ resolved[qualified.split('.')[0]] = target;
430
+ });
431
+ }
432
+
433
+ function diagram(key) {
434
+ const s = byKey[key];
435
+ if (!s || !s.flow || !s.flow.length) return null;
436
+ resolveNames(key);
437
+ const size = measure(s.flow);
438
+ const cx = Math.max(size.w / 2, BOX / 2) + 40;
439
+ const out = [];
440
+ const params = s.params || [];
441
+ let y = 0;
442
+ if (params.length) {
443
+ const text = params.map(p => p.t ? `${p.n}: ${p.t}` : p.n).join(', ');
444
+ const rows = [];
445
+ let line = '';
446
+ for (const word of text.split(', ')) {
447
+ if ((line + word).length > 38) { rows.push(line); line = ''; }
448
+ line += (line ? ', ' : '') + word;
449
+ }
450
+ if (line) rows.push(line);
451
+ const h = PARAMS + 8 + rows.length * 14;
452
+ const widest = Math.max(...rows.map(r => r.length), 18);
453
+ const w = Math.max(BOX + 40, widest * 7.3 + 34);
454
+ out.push(`<g><rect x="${cx - w / 2}" y="0" width="${w}" height="${h}" rx="2" `
455
+ + `fill="var(--card)" stroke="var(--text)" stroke-width="1.3"/>`
456
+ + `<rect x="${cx - w / 2 + 1}" y="1" width="${w - 2}" height="23" fill="#EDEFF1"/>`
457
+ + `<rect x="${cx - w / 2 + 1}" y="24" width="${w - 2}" height="1" fill="var(--line)"/>`
458
+ + `<text x="${cx}" y="16" fill="var(--grey)" font-size="10" text-anchor="middle" `
459
+ + `font-family="var(--mono)">input data</text>`
460
+ + rows.map((r, i) => `<text x="${cx}" y="${41 + i * 14}" fill="var(--text)" font-size="11" `
461
+ + `text-anchor="middle" font-family="var(--mono)">${esc(r)}</text>`).join('')
462
+ + `</g>`);
463
+ edge(out, cx, h, cx, h + VGAP);
464
+ y = h + VGAP;
465
+ }
466
+ y = place(s.flow, cx, y, out, 0);
467
+ const W = Math.max(size.w + 120, BOX + 160), H = y + 30;
468
+ return `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">`
469
+ + `<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" `
470
+ + `markerHeight="5" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="var(--rail)"/>`
471
+ + `</marker></defs>${out.join('')}</svg>`;
472
+ }
473
+
474
+ /* ================= TREE ================= */
475
+ function renderTree() {
476
+ $('nr').textContent = roots.length;
477
+ const out = [];
478
+ renderGroup(out, entries, "Public surface",
479
+ "documented functions nobody calls: the entry points", false);
480
+ renderGroup(out, orphans, "Not called inside the package",
481
+ "special methods Python invokes, or code with no caller", true);
482
+ $('nav').innerHTML = out.join('');
483
+ for (const button of $('nav').querySelectorAll('.node')) {
484
+ button.onclick = () => {
485
+ const path = button.dataset.path.split('|'), key = path[path.length - 1];
486
+ if ((OUT[key] || []).length && !path.slice(0, -1).includes(key))
487
+ expanded.has(button.dataset.path) ? expanded.delete(button.dataset.path)
488
+ : expanded.add(button.dataset.path);
489
+ sel = key; selPath = path; renderTree(); renderScene();
490
+ };
491
+ }
492
+ for (const header of $('nav').querySelectorAll('[data-section]'))
493
+ header.onclick = () => {
494
+ const title = header.dataset.section;
495
+ collapsed.has(title) ? collapsed.delete(title) : collapsed.add(title);
496
+ renderTree();
497
+ };
498
+ for (const alias of $('nav').querySelectorAll('[data-jump]'))
499
+ alias.onclick = event => {
500
+ event.stopPropagation();
501
+ const key = alias.dataset.jump;
502
+ sel = key; selPath = [key]; expanded.add(pathKey([key]));
503
+ renderTree(); renderScene();
504
+ };
505
+ for (const button of $('nav').querySelectorAll('.node'))
506
+ button.dataset.sym = button.dataset.path.split('|').pop();
507
+ bindTips($('nav'));
508
+ const current = $('nav').querySelector('[aria-current="true"]');
509
+ if (current) current.scrollIntoView({ block: 'nearest' });
510
+ }
511
+
512
+ /* --- sorting: real entry points vs orphan symbols --- */
513
+ const isEntry = key => {
514
+ const s = byKey[key];
515
+ return !!(s && !s.name.startsWith('_') && s.doc);
516
+ };
517
+ const entries = roots.filter(isEntry);
518
+ const orphans = roots.filter(key => !isEntry(key));
519
+ const MAX_REACH = Math.max(1, ...roots.map(reach));
520
+
521
+ /* --- group the roots that unfold exactly the same flow --- */
522
+ function groupBy(keys) {
523
+ const groups = new Map();
524
+ for (const key of keys) {
525
+ const signature = moduleOf(key) + '##' + (OUT[key] || []).slice().sort().join('|');
526
+ if (!groups.has(signature)) groups.set(signature, []);
527
+ groups.get(signature).push(key);
528
+ }
529
+ return [...groups.values()];
530
+ }
531
+
532
+ let collapsed = new Set(["Not called inside the package"]);
533
+
534
+ function renderGroup(out, keys, title, hint, collapsible) {
535
+ if (!keys.length) return;
536
+ const shut = collapsed.has(title);
537
+ out.push(`<div class="section${collapsible ? ' collapsible' : ''}" `
538
+ + `${collapsible ? `data-section="${esc(title)}"` : ''}>`
539
+ + (collapsible ? `<span class="arrow">${shut ? '▸' : '▾'}</span>` : '')
540
+ + `<h2>${esc(title)}</h2><span class="n">${keys.length}</span></div>`
541
+ + (shut ? '' : `<p class="group" style="padding-top:0">${esc(hint)}</p>`));
542
+ if (shut) return;
543
+ let lastModule = null;
544
+ for (const group of groupBy(keys)) {
545
+ const lead = group[0];
546
+ if (moduleOf(lead) !== lastModule) {
547
+ lastModule = moduleOf(lead);
548
+ out.push(`<div class="group">${esc(lastModule)}</div>`);
549
+ }
550
+ branch(lead, [lead], out, MAX_REACH, group.length > 1 ? group : null);
551
+ }
552
+ }
553
+
554
+ function branch(key, path, out, widest, siblings) {
555
+ const id = pathKey(path), recursive = path.slice(0, -1).includes(key);
556
+ const kids = recursive ? [] : (OUT[key] || []).slice()
557
+ .sort((a, b) => reach(b) - reach(a) || a.localeCompare(b));
558
+ const isOpen = expanded.has(id), span = reach(key);
559
+ const width = widest > 0 ? Math.max(3, Math.round(34 * Math.sqrt(span / widest))) : 0;
560
+ out.push(`<div class="branch"><button class="node${seen.has(key) ? ' seen' : ''}" `
561
+ + `data-path="${esc(id)}" `
562
+ + `aria-current="${key === sel && id === pathKey(selPath)}"><span class="head">`
563
+ + `<span class="twist">${recursive ? '↺' : kids.length ? (isOpen ? '▾' : '▸') : ''}</span>`
564
+ + `<span class="name">${mark(shortName(key))}</span>`
565
+ + (seen.has(key) ? `<span class="check">✓</span>` : '')
566
+ + `<span class="mod">${esc(moduleOf(key))}</span><span class="gap"></span>`
567
+ + (siblings ? `<span class="count">+${siblings.length - 1}</span>` : '')
568
+ + (span ? `<span class="gauge"><i style="width:${width}px"></i>`
569
+ + `<span class="num">${span}</span></span>` : '')
570
+ + `</span></button>`);
571
+ if (siblings) out.push(`<div class="aliases">` + siblings.slice(1).map(other =>
572
+ `<button class="alias" data-jump="${esc(other)}">${esc(shortName(other))}</button>`)
573
+ .join('') + `</div>`);
574
+ if (isOpen && kids.length) {
575
+ const widestKid = reach(kids[0]) || 1;
576
+ out.push('<div class="children">');
577
+ kids.forEach(kid => branch(kid, path.concat(kid), out, widestKid));
578
+ out.push('</div>');
579
+ }
580
+ out.push('</div>');
581
+ }
582
+
583
+ function mark(text) {
584
+ if (!highlight) return esc(text);
585
+ const at = text.toLowerCase().indexOf(highlight);
586
+ return at < 0 ? esc(text) : esc(text.slice(0, at)) + '<mark>'
587
+ + esc(text.slice(at, at + highlight.length)) + '</mark>' + esc(text.slice(at + highlight.length));
588
+ }
589
+
590
+ /* Breadth-first search for a path from any root down to `goal`, expanding the
591
+ tree along the way so the symbol becomes visible. */
592
+ function revealSymbol(goal) {
593
+ for (const root of roots) {
594
+ const visited = new Set([root]), queue = [[root]];
595
+ while (queue.length) {
596
+ const path = queue.shift(), node = path[path.length - 1];
597
+ if (node === goal) {
598
+ for (let i = 1; i < path.length; i++) expanded.add(pathKey(path.slice(0, i)));
599
+ sel = goal; selPath = path; return path;
600
+ }
601
+ if (path.length > 14) continue;
602
+ for (const next of (OUT[node] || []))
603
+ if (!visited.has(next)) { visited.add(next); queue.push(path.concat(next)); }
604
+ }
605
+ }
606
+ return null;
607
+ }
608
+
609
+ /* --- hover tooltip --- */
610
+ const TIP = $('tip');
611
+ function tipForSymbol(key) {
612
+ const s = byKey[key];
613
+ if (!s) return `<div class="t-name">${esc(shortName(key))}</div>`
614
+ + `<div class="t-where">${esc(moduleOf(key))} · outside the analysed package</div>`;
615
+ const doc = s.doc ? s.doc.trim().split(/\n\s*\n/)[0].replace(/\s+/g, ' ') : '';
616
+ return `<div><span class="t-name">${esc(s.name)}</span>`
617
+ + `<span class="t-where">${esc(s.module)} · ${esc(s.kind)}</span></div>`
618
+ + (s.sig ? `<div class="t-sig">${esc(trunc(s.sig, 150))}</div>` : '')
619
+ + `<div class="t-doc${doc ? '' : ' none'}">`
620
+ + `${doc ? esc(trunc(doc, 260)) : 'No docstring.'}</div>`
621
+ + `<div class="t-meta">${esc(s.file)}:${s.line} · reaches ${reach(key)} symbols · `
622
+ + `${(OUT[key] || []).length} calls · ${(IN[key] || []).length} callers</div>`;
623
+ }
624
+ function tipRaw(text, kind) {
625
+ return `<div class="t-where" style="margin:0 0 5px">${esc(kind || '')}</div>`
626
+ + `<div class="t-raw">${esc(text)}</div>`;
627
+ }
628
+ function tipPlace(event) {
629
+ const margin = 14, rect = TIP.getBoundingClientRect();
630
+ let x = event.clientX + margin, y = event.clientY + margin;
631
+ if (x + rect.width > window.innerWidth - 8) x = event.clientX - rect.width - margin;
632
+ if (y + rect.height > window.innerHeight - 8) y = event.clientY - rect.height - margin;
633
+ TIP.style.left = Math.max(8, x) + 'px';
634
+ TIP.style.top = Math.max(8, y) + 'px';
635
+ }
636
+ function tipHide() { TIP.style.display = 'none'; }
637
+ function bindTips(scope) {
638
+ for (const el of scope.querySelectorAll('[data-sym],[data-raw]')) {
639
+ el.addEventListener('mouseenter', event => {
640
+ TIP.innerHTML = el.dataset.sym ? tipForSymbol(el.dataset.sym)
641
+ : tipRaw(el.dataset.raw, el.dataset.kind);
642
+ TIP.style.display = 'block';
643
+ tipPlace(event);
644
+ });
645
+ el.addEventListener('mousemove', tipPlace);
646
+ el.addEventListener('mouseleave', tipHide);
647
+ }
648
+ }
649
+
650
+ function renderScene() {
651
+ const canvas = $('canvas');
652
+ $('legend').innerHTML = '';
653
+ tipHide();
654
+ steps = []; iStep = -1;
655
+ if (!sel) {
656
+ const best = groupBy(entries).map(group => group[0])
657
+ .sort((a, b) => reach(b) - reach(a)).slice(0, 3);
658
+ canvas.innerHTML = `<div class="home">
659
+ <h3>Where to start</h3>
660
+ <p>These walkthroughs begin at the functions nobody calls — the package's
661
+ entry points. The first one already covers most of the useful code.</p>`
662
+ + best.map((key, i) => {
663
+ const s = byKey[key];
664
+ const doc = s && s.doc ? s.doc.trim().split('\n')[0] : 'no docstring';
665
+ return `<button class="track" data-track="${esc(key)}">`
666
+ + `<span class="r">${i + 1}</span><span class="t">`
667
+ + `<b>${esc(shortName(key))}</b><span>${esc(doc.slice(0, 74))}</span></span>`
668
+ + `<span class="m">${reach(key)} symbols<br>~${Math.max(5, Math.round(reach(key) / 4))} min</span>`
669
+ + `</button>`;
670
+ }).join('')
671
+ + `<div class="keys">
672
+ <kbd>↓</kbd><kbd>↑</kbd> walk the steps of the flow ·
673
+ <kbd>↵</kbd> step into the highlighted call ·
674
+ <kbd>←</kbd> go back up<br>
675
+ <kbd>m</kbd> mark seen / unseen ·
676
+ <kbd>o</kbd> open in the editor ·
677
+ <kbd>/</kbd> search · <kbd>Esc</kbd> clear
678
+ </div></div>`;
679
+ for (const button of canvas.querySelectorAll('[data-track]'))
680
+ button.onclick = () => {
681
+ const key = button.dataset.track;
682
+ sel = key; selPath = [key]; expanded.add(pathKey([key]));
683
+ renderTree(); renderScene();
684
+ };
685
+ progress();
686
+ return;
687
+ }
688
+ const s = byKey[sel];
689
+ const trail = selPath.map((key, i) => i === selPath.length - 1
690
+ ? `<b>${esc(shortName(key))}</b>` : `${esc(shortName(key))}<i> ▸ </i>`).join('');
691
+ const svg = diagram(sel);
692
+ canvas.innerHTML = `<h2 class="flow-title">${s ? esc(s.name) : esc(sel)}</h2>`
693
+ + `<p class="flow-sub">${s ? editorLink(s) + ' · '
694
+ + (s.doc ? esc(s.doc.split('\n')[0].slice(0, 90)) : 'no docstring')
695
+ : 'outside the package'}</p>`
696
+ + `<p class="trail">${trail}</p>`
697
+ + (svg ? `<div class="plan">${svg}</div>`
698
+ : '<p class="empty">This function orchestrates nothing: no call to unfold.</p>');
699
+ // The diagram has just resolved the flow names, so the walkable steps line up
700
+ // with the boxes actually drawn.
701
+ if (svg) steps = flatten(s.flow, []);
702
+ if (svg) $('legend').innerHTML = `
703
+ <span><i style="border:1px solid var(--line);border-bottom:2px solid var(--accent);background:var(--card)"></i>data passing through</span>
704
+ <span><i style="background:var(--accent)"></i>internal call</span>
705
+ <span><i style="border:1px dashed var(--rail);background:var(--card)"></i>external call</span>
706
+ <span><i style="background:var(--text)"></i>branch, loop, exit</span>
707
+ <span><i style="background:var(--alert)"></i>error path</span>
708
+ <span style="margin-left:auto;color:var(--rail)">hover any element for details</span>`;
709
+ bindTips(canvas);
710
+ for (const node of canvas.querySelectorAll('[data-sym]'))
711
+ node.onclick = () => {
712
+ const goal = node.dataset.sym;
713
+ for (let i = 1; i <= selPath.length; i++) expanded.add(pathKey(selPath.slice(0, i)));
714
+ if ((OUT[sel] || []).includes(goal)) { selPath = selPath.concat(goal); sel = goal; }
715
+ else if (!revealSymbol(goal)) return;
716
+ renderTree(); renderScene();
717
+ };
718
+ progress();
719
+ }
720
+
721
+ /* --- panel resizing --- */
722
+ (function () {
723
+ const handle = $('handle'), nav = $('nav');
724
+ let dragging = false;
725
+ const setWidth = x =>
726
+ nav.style.width = Math.min(Math.max(x, 220), window.innerWidth - 340) + 'px';
727
+ handle.addEventListener('mousedown', event => {
728
+ dragging = true; handle.classList.add('active');
729
+ document.body.style.userSelect = 'none';
730
+ document.body.style.cursor = 'col-resize';
731
+ event.preventDefault();
732
+ });
733
+ document.addEventListener('mousemove', event => { if (dragging) setWidth(event.clientX); });
734
+ document.addEventListener('mouseup', () => {
735
+ if (!dragging) return;
736
+ dragging = false; handle.classList.remove('active');
737
+ document.body.style.userSelect = ''; document.body.style.cursor = '';
738
+ });
739
+ handle.addEventListener('dblclick', () => nav.style.width = '370px');
740
+ })();
741
+
742
+ let searchTimer = null;
743
+ $('q').addEventListener('input', event => {
744
+ clearTimeout(searchTimer);
745
+ searchTimer = setTimeout(() => {
746
+ const needle = event.target.value.trim().toLowerCase();
747
+ highlight = needle;
748
+ if (!needle) { renderTree(); return; }
749
+ const hits = [...everyKey].filter(k => shortName(k).toLowerCase().includes(needle))
750
+ .sort((a, b) => shortName(a).length - shortName(b).length);
751
+ if (hits.length && revealSymbol(hits[0])) { renderTree(); renderScene(); } else renderTree();
752
+ }, 160);
753
+ });
754
+
755
+ /* Highlight the nth walkable step of the current diagram. Several boxes can
756
+ point at the same symbol, so we count occurrences to land on the right one. */
757
+ function focusStep(delta) {
758
+ if (!steps.length) return;
759
+ iStep = Math.min(Math.max(iStep + delta, 0), steps.length - 1);
760
+ const boxes = [...$('canvas').querySelectorAll('.hit')];
761
+ for (const node of boxes) node.classList.remove('step-live');
762
+ const goal = steps[iStep];
763
+ let nth = -1;
764
+ for (const node of boxes) {
765
+ if (node.dataset.sym === goal && ++nth === occurrence(goal, iStep)) {
766
+ node.classList.add('step-live');
767
+ node.scrollIntoView({ block: 'nearest' });
768
+ break;
769
+ }
770
+ }
771
+ if (byKey[goal]) {
772
+ TIP.innerHTML = tipForSymbol(goal);
773
+ TIP.style.display = 'block';
774
+ const rect = $('canvas').getBoundingClientRect();
775
+ TIP.style.left = (rect.left + 24) + 'px';
776
+ TIP.style.top = (rect.bottom - 210) + 'px';
777
+ }
778
+ }
779
+ const occurrence = (key, upTo) => steps.slice(0, upTo).filter(k => k === key).length;
780
+
781
+ function stepInto() {
782
+ if (iStep < 0 || !steps[iStep]) return;
783
+ const goal = steps[iStep];
784
+ for (let i = 1; i <= selPath.length; i++) expanded.add(pathKey(selPath.slice(0, i)));
785
+ selPath = selPath.concat(goal); sel = goal;
786
+ tipHide(); renderTree(); renderScene();
787
+ }
788
+ function stepOut() {
789
+ if (selPath.length < 2) return;
790
+ selPath = selPath.slice(0, -1);
791
+ sel = selPath[selPath.length - 1];
792
+ tipHide(); renderTree(); renderScene();
793
+ }
794
+
795
+ document.addEventListener('keydown', event => {
796
+ const search = $('q');
797
+ if (document.activeElement === search) {
798
+ if (event.key === 'Escape') { search.value = ''; highlight = ''; renderTree(); search.blur(); }
799
+ return;
800
+ }
801
+ const key = event.key;
802
+ if (key === '/') { event.preventDefault(); search.focus(); search.select(); }
803
+ else if (key === 'Escape') { tipHide(); search.value = ''; highlight = ''; renderTree(); }
804
+ else if (key === 'ArrowDown' || key === 'j') { event.preventDefault(); focusStep(1); }
805
+ else if (key === 'ArrowUp' || key === 'k' || key === 'K') { event.preventDefault(); focusStep(-1); }
806
+ else if (key === 'Enter' || key === 'ArrowRight') { event.preventDefault(); stepInto(); }
807
+ else if (key === 'Backspace' || key === 'ArrowLeft') { event.preventDefault(); stepOut(); }
808
+ else if (key === 'm' && sel) {
809
+ seen.has(sel) ? seen.delete(sel) : seen.add(sel);
810
+ renderTree(); progress();
811
+ }
812
+ else if (key === 'o' && sel && byKey[sel]) {
813
+ const url = editorUrl(byKey[sel]);
814
+ if (url) window.location.href = url;
815
+ }
816
+ });
817
+
818
+ if (entries.length) expanded.add(pathKey([entries[0]]));
819
+ renderTree();
820
+ renderScene();
821
+ </script>
822
+ </html>