code-node-graph 0.3.0__py3-none-any.whl → 0.5.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.
- code_node/__init__.py +1 -1
- code_node/analysis/graph.py +151 -8
- code_node/analysis/model.py +45 -0
- code_node/analysis/reader.py +20 -3
- code_node/cli.py +9 -1
- code_node/history.py +151 -0
- code_node/pages.py +6 -2
- code_node/report/build.py +5 -3
- code_node/report/changes.py +42 -0
- code_node/report/graph.py +336 -53
- code_node/report/style.py +40 -4
- code_node/report/walk.py +20 -5
- code_node/serve.py +2 -2
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.5.0.dist-info}/METADATA +2 -1
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.5.0.dist-info}/RECORD +18 -16
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.5.0.dist-info}/WHEEL +0 -0
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.5.0.dist-info}/entry_points.txt +0 -0
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.5.0.dist-info}/licenses/LICENSE +0 -0
code_node/report/graph.py
CHANGED
|
@@ -17,7 +17,9 @@ from ..kit.highlight import _python_html, _python_lines
|
|
|
17
17
|
from ..kit.render import _CALL_RE, _prose
|
|
18
18
|
from ..kit.text import _esc
|
|
19
19
|
|
|
20
|
-
from ..analysis.
|
|
20
|
+
from ..analysis.findings import _short_name
|
|
21
|
+
from ..analysis.graph import CodeGraph, GraphEdge, GraphNode, GraphView
|
|
22
|
+
from .changes import history_html
|
|
21
23
|
from .brands import AWS_LOGOS, SERVICE_LOGOS, fills, logo, small_logo
|
|
22
24
|
from .editor import editor_name, open_at
|
|
23
25
|
from .style import _ROOT
|
|
@@ -36,6 +38,9 @@ class _Graph:
|
|
|
36
38
|
empty: str = "(nothing to draw)"
|
|
37
39
|
sources: dict[str, str] = field(default_factory=dict)
|
|
38
40
|
editor: str = "" # a link to open a line in an editor, from editor_link(): each place in a panel links to it
|
|
41
|
+
views: list[GraphView] = field(default_factory=list) # with JavaScript, a box's line of calls not drawn opens the
|
|
42
|
+
# graph from that function, from these (graph_views())
|
|
43
|
+
everything: CodeGraph | None = None # ...and a button in the bar draws every function, this graph
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
_EDGE_WORDS = {
|
|
@@ -999,12 +1004,32 @@ def _g_calls(count: int) -> str:
|
|
|
999
1004
|
return f"{count} call" if count == 1 else f"{count} calls"
|
|
1000
1005
|
|
|
1001
1006
|
|
|
1007
|
+
def _g_prose_top(card: _GCard) -> float:
|
|
1008
|
+
"""
|
|
1009
|
+
Where a box's prose starts: under its header, its route's pill and its when pill.
|
|
1010
|
+
"""
|
|
1011
|
+
return card.head + (_GRAPH_ROUTE if card.route else 0) + (_GRAPH_ROUTE if card.node.when else 0)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _g_more_y(card: _GCard) -> float:
|
|
1015
|
+
"""
|
|
1016
|
+
The baseline of a box's line of calls not drawn.
|
|
1017
|
+
"""
|
|
1018
|
+
return _g_prose_top(card) + 13 + _GRAPH_PROSE * len(card.prose)
|
|
1019
|
+
|
|
1020
|
+
|
|
1002
1021
|
def _node_svg(
|
|
1003
|
-
card: _GCard,
|
|
1022
|
+
card: _GCard,
|
|
1023
|
+
box: tuple[float, float, float, float],
|
|
1024
|
+
neighbours: list[str],
|
|
1025
|
+
access: dict[str, str],
|
|
1026
|
+
panel: str = "",
|
|
1027
|
+
opens: bool = False,
|
|
1004
1028
|
) -> str:
|
|
1005
1029
|
"""
|
|
1006
1030
|
A box: its badge and name, what's under it, a route's pill, its listed lines (an operation's dot in its arrows'
|
|
1007
|
-
colour, a line a warning is about with its flag) or its docstring, and a warning badge.
|
|
1031
|
+
colour, a line a warning is about with its flag) or its docstring, and a warning badge. panel is the id of its
|
|
1032
|
+
code's panel, and opens says its line of calls not drawn opens a graph of its own (a link drawn over it).
|
|
1008
1033
|
"""
|
|
1009
1034
|
n = card.node
|
|
1010
1035
|
x, y, w, h = box
|
|
@@ -1071,15 +1096,18 @@ def _node_svg(
|
|
|
1071
1096
|
if _g_sans(f"{said}: {names}", 12.5) <= w - 28:
|
|
1072
1097
|
said = f"{said}: {names}"
|
|
1073
1098
|
break
|
|
1074
|
-
parts.append(
|
|
1075
|
-
f'<text class="pr mo" x="14" y="{top + 13 + _GRAPH_PROSE * len(card.prose):.1f}">{_esc(said)}</text>'
|
|
1076
|
-
)
|
|
1099
|
+
parts.append(f'<text class="pr mo md" x="14" y="{_g_more_y(card):.1f}">{_esc(said)}</text>')
|
|
1077
1100
|
if warn:
|
|
1078
1101
|
parts.append(
|
|
1079
1102
|
f'<g class="wb" transform="translate({w - 21:.0f},{badge_y + _G_BADGE / 2:.0f})"><circle r="8.5"/>'
|
|
1080
1103
|
'<path d="M0,-4.2V.6M0,3.9V4"/></g>'
|
|
1081
1104
|
)
|
|
1082
|
-
classes = " ".join(
|
|
1105
|
+
classes = " ".join(
|
|
1106
|
+
["cn", f"k-{n.kind}", f"n-{n.id}", *(f"a-{m}" for m in neighbours)]
|
|
1107
|
+
+ ([f"p-{panel}"] if panel else [])
|
|
1108
|
+
+ (["hd"] if opens else [])
|
|
1109
|
+
+ (["warn"] if warn else [])
|
|
1110
|
+
)
|
|
1083
1111
|
return (
|
|
1084
1112
|
f'<g class="{_esc(classes)}" transform="translate({x:.1f},{y:.1f})">'
|
|
1085
1113
|
f'<rect class="bg" width="{w:.0f}" height="{h:.0f}" rx="11"/>'
|
|
@@ -1114,11 +1142,11 @@ def _g_filled(line: str) -> bool:
|
|
|
1114
1142
|
return bool(re.sub(r"<[^>]*>", "", line).strip())
|
|
1115
1143
|
|
|
1116
1144
|
|
|
1117
|
-
def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "") -> str:
|
|
1145
|
+
def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "", panel: str = "") -> str:
|
|
1118
1146
|
"""
|
|
1119
|
-
A box's code, in a panel that slides in from the right when the box is clicked (its id
|
|
1120
|
-
it's CSS's :target that opens it): what the box is, then each place it's about with its
|
|
1121
|
-
worth pointing at lit. '' when none of its code is in the files read.
|
|
1147
|
+
A box's code, in a panel that slides in from the right when the box is clicked (its id, panel or the box's own, is
|
|
1148
|
+
the link's target, so it's CSS's :target that opens it): what the box is, then each place it's about with its
|
|
1149
|
+
lines numbered, the lines worth pointing at lit. '' when none of its code is in the files read.
|
|
1122
1150
|
"""
|
|
1123
1151
|
shown, lit_any = [], False
|
|
1124
1152
|
for span in n.code[:_G_PANEL_SPANS]:
|
|
@@ -1157,16 +1185,21 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
|
|
|
1157
1185
|
rest = len(n.code) - _G_PANEL_SPANS
|
|
1158
1186
|
if rest > 0:
|
|
1159
1187
|
shown.append(f'<div class="csm">+{_plural(rest, "more place")}: the tables below list them all</div>')
|
|
1160
|
-
said = "".join(
|
|
1188
|
+
said = "".join( # not the calls this graph leaves out: a function's panel is the same in every graph it's in
|
|
1189
|
+
f'<p class="csd">{_prose(line)}</p>'
|
|
1190
|
+
for line in (n.tip or "").split("\n")
|
|
1191
|
+
if line and not (n.more and line.startswith("It calls ") and "the graph leaves out" in line)
|
|
1192
|
+
)
|
|
1161
1193
|
if lit_any:
|
|
1162
1194
|
said += (
|
|
1163
1195
|
'<p class="csd csl">The lit lines are where it calls, reads or sends what its arrows show.</p>'
|
|
1164
1196
|
if n.kind in ("handler", "function", "start")
|
|
1165
1197
|
else '<p class="csd csl">The lit lines are where the code uses it.</p>'
|
|
1166
1198
|
)
|
|
1199
|
+
said += history_html(n.history)
|
|
1167
1200
|
mono = " mono" if n.kind in ("handler", "function") else ""
|
|
1168
1201
|
return (
|
|
1169
|
-
f'<aside class="cside" id="{_esc(_g_panel_id(n))}" aria-label="{_esc("The code of " + n.label)}">'
|
|
1202
|
+
f'<aside class="cside" id="{_esc(panel or _g_panel_id(n))}" aria-label="{_esc("The code of " + n.label)}">'
|
|
1170
1203
|
f'<div class="csh"><div class="cst"><span class="csk">{_esc(_G_PANEL_KINDS.get(n.kind, n.kind))}</span>'
|
|
1171
1204
|
f'<b class="csn{mono}">{_esc(n.label)}</b>'
|
|
1172
1205
|
+ (f'<span class="css">{_esc(n.sub)}</span>' if n.sub else "")
|
|
@@ -1269,6 +1302,10 @@ def _g_tip(n: GraphNode, lines_of: Callable[[str], list[str]], opens: bool) -> t
|
|
|
1269
1302
|
else:
|
|
1270
1303
|
body.append(f'<p class="ctp">{_g_tip_text(line)}</p>')
|
|
1271
1304
|
tall += 23 * math.ceil(len(line) / 42) + 10
|
|
1305
|
+
changed = history_html(n.history, short=True)
|
|
1306
|
+
if changed:
|
|
1307
|
+
body.append(changed)
|
|
1308
|
+
tall += 23 * math.ceil(len(re.sub(r"<[^>]*>", "", changed)) / 42) + 14
|
|
1272
1309
|
if warnings:
|
|
1273
1310
|
body.append(
|
|
1274
1311
|
'<div class="ctw"><b>Warning</b><ul>'
|
|
@@ -1368,12 +1405,178 @@ def _g_swatch(kind: str) -> str:
|
|
|
1368
1405
|
)
|
|
1369
1406
|
|
|
1370
1407
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1408
|
+
_G_SCRIPT = """<script>
|
|
1409
|
+
(() => {
|
|
1410
|
+
// A graph's views: a box's line of calls not drawn, the path over a view and the bar's All button link to
|
|
1411
|
+
// #<group>-v<k> (or -all, or -main), and each is a <template> the graph's box, path and key are swapped from.
|
|
1412
|
+
// Any other address (a box's code open, or closed) leaves the view as it is.
|
|
1413
|
+
const parts = ["cgc", "cg", "cgk"];
|
|
1414
|
+
const show = (wrap) => {
|
|
1415
|
+
const group = wrap.dataset.group, id = decodeURIComponent(location.hash.slice(1));
|
|
1416
|
+
const named = id === group + "-main" || id === group + "-all" || id.startsWith(group + "-v");
|
|
1417
|
+
if (id && !named) return;
|
|
1418
|
+
const view = named && id !== group + "-main" ? id : "";
|
|
1419
|
+
if ((wrap.dataset.view || "") === view) return;
|
|
1420
|
+
const from = view ? document.getElementById(view)?.content : wrap.main;
|
|
1421
|
+
if (!from) return;
|
|
1422
|
+
for (const part of parts) {
|
|
1423
|
+
wrap.querySelector(".cgb ." + part).innerHTML = from.querySelector(".x-" + part).innerHTML;
|
|
1424
|
+
}
|
|
1425
|
+
wrap.dataset.view = view;
|
|
1426
|
+
wrap.querySelector(".cgb").scrollIntoView({block: "nearest"});
|
|
1427
|
+
};
|
|
1428
|
+
for (const wrap of document.querySelectorAll(".cgw[data-group]")) {
|
|
1429
|
+
const main = document.createElement("template");
|
|
1430
|
+
for (const part of parts) {
|
|
1431
|
+
const kept = document.createElement("div");
|
|
1432
|
+
kept.className = "x-" + part;
|
|
1433
|
+
kept.innerHTML = wrap.querySelector(".cgb ." + part).innerHTML;
|
|
1434
|
+
main.content.append(kept);
|
|
1435
|
+
}
|
|
1436
|
+
wrap.main = main.content;
|
|
1437
|
+
wrap.classList.add("js");
|
|
1438
|
+
show(wrap);
|
|
1439
|
+
}
|
|
1440
|
+
addEventListener("hashchange", () => document.querySelectorAll(".cgw.js").forEach(show));
|
|
1441
|
+
// A box's code: a click anywhere outside its panel closes it, as its × does, and so does Escape. A link (another
|
|
1442
|
+
// box, a view) and the graph's bar do what they do with the panel left to them.
|
|
1443
|
+
const close = () => { if (document.querySelector(".cside:target")) location.hash = "cn-closed"; };
|
|
1444
|
+
document.addEventListener("click", (e) => {
|
|
1445
|
+
const open = document.querySelector(".cside:target");
|
|
1446
|
+
if (open && !open.contains(e.target) && !e.target.closest("a[href], label, input, .cgt")) close();
|
|
1447
|
+
});
|
|
1448
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
|
|
1449
|
+
// Moving about a graph as on a canvas: drag it to pan, Ctrl or ⌘ with the wheel (or a trackpad's pinch) to zoom
|
|
1450
|
+
// around the pointer, and + - 0 when it has focus. A zoom of its own unchecks the bar's buttons, so the plain rule
|
|
1451
|
+
// (the SVG's width times --z) draws it; picking a button again takes it back. Its box keeps one size, and Fit
|
|
1452
|
+
// (--fit) puts all of it in that box, both ways.
|
|
1453
|
+
for (const wrap of document.querySelectorAll(".cgw")) {
|
|
1454
|
+
const box = wrap.querySelector(".cgb .cg"), radios = wrap.querySelectorAll(":scope > .cgs[type=radio]");
|
|
1455
|
+
const said = wrap.querySelector(".cgtp"), svg = () => box.querySelector("svg");
|
|
1456
|
+
const scale = () => {
|
|
1457
|
+
const s = svg();
|
|
1458
|
+
return s && s.viewBox.baseVal.width ? s.getBoundingClientRect().width / s.viewBox.baseVal.width : 1;
|
|
1459
|
+
};
|
|
1460
|
+
const fit = () => {
|
|
1461
|
+
const s = svg(), w = s && s.viewBox.baseVal.width, h = s && s.viewBox.baseVal.height;
|
|
1462
|
+
if (w && h && box.clientWidth) {
|
|
1463
|
+
box.style.setProperty("--fit", Math.min((box.clientWidth - 24) / w, (box.clientHeight - 24) / h, 1.5));
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
const tell = () => { fit(); if (said) said.textContent = Math.round(scale() * 100) + "%"; };
|
|
1467
|
+
const pan = (dx, dy) => { // the graph's box first, then the page for what it can't scroll
|
|
1468
|
+
const top = box.scrollTop;
|
|
1469
|
+
box.scrollLeft += dx;
|
|
1470
|
+
box.scrollTop += dy;
|
|
1471
|
+
const left = dy - (box.scrollTop - top);
|
|
1472
|
+
if (Math.abs(left) > 0.5) scrollBy(0, left);
|
|
1473
|
+
};
|
|
1474
|
+
const zoom = (by, x, y) => { // keeps the point at (x, y) where it is
|
|
1475
|
+
const s = svg();
|
|
1476
|
+
if (!s) return;
|
|
1477
|
+
const from = scale(), to = Math.min(4, Math.max(0.1, from * by)), was = s.getBoundingClientRect();
|
|
1478
|
+
const px = (x - was.left) / from, py = (y - was.top) / from;
|
|
1479
|
+
radios.forEach((r) => { r.checked = false; });
|
|
1480
|
+
box.style.setProperty("--z", to);
|
|
1481
|
+
const now = s.getBoundingClientRect();
|
|
1482
|
+
pan(now.left + px * to - x, now.top + py * to - y);
|
|
1483
|
+
tell();
|
|
1484
|
+
};
|
|
1485
|
+
const auto = () => { // back to the Auto button
|
|
1486
|
+
const radio = wrap.querySelector(":scope > .cgz-auto");
|
|
1487
|
+
radio.checked = true;
|
|
1488
|
+
radio.dispatchEvent(new Event("change", {bubbles: true}));
|
|
1489
|
+
};
|
|
1490
|
+
const step = (by) => { // around the middle of what's in sight
|
|
1491
|
+
const r = box.getBoundingClientRect();
|
|
1492
|
+
zoom(by, r.left + r.width / 2, (Math.max(r.top, 0) + Math.min(r.bottom, innerHeight)) / 2);
|
|
1493
|
+
};
|
|
1494
|
+
wrap.querySelector(".cgb .zi")?.addEventListener("click", () => step(1.25));
|
|
1495
|
+
wrap.querySelector(".cgb .zo")?.addEventListener("click", () => step(0.8));
|
|
1496
|
+
let drag = null, moved = false;
|
|
1497
|
+
box.addEventListener("pointerdown", (e) => {
|
|
1498
|
+
moved = false;
|
|
1499
|
+
if (e.button === 0 && e.pointerType !== "touch") drag = {x: e.clientX, y: e.clientY, id: e.pointerId};
|
|
1500
|
+
});
|
|
1501
|
+
box.addEventListener("pointermove", (e) => {
|
|
1502
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
1503
|
+
const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
|
|
1504
|
+
if (!moved && Math.hypot(dx, dy) < 4) return; // a click on a box stays a click
|
|
1505
|
+
if (!moved) { moved = true; box.setPointerCapture(e.pointerId); box.classList.add("pan"); }
|
|
1506
|
+
pan(-dx, -dy);
|
|
1507
|
+
drag.x = e.clientX;
|
|
1508
|
+
drag.y = e.clientY;
|
|
1509
|
+
});
|
|
1510
|
+
const end = () => { drag = null; box.classList.remove("pan"); };
|
|
1511
|
+
box.addEventListener("pointerup", end);
|
|
1512
|
+
box.addEventListener("pointercancel", end);
|
|
1513
|
+
box.addEventListener("click", (e) => { if (moved) { e.preventDefault(); e.stopPropagation(); } }, true);
|
|
1514
|
+
box.addEventListener("dragstart", (e) => e.preventDefault());
|
|
1515
|
+
box.addEventListener("wheel", (e) => {
|
|
1516
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
1517
|
+
e.preventDefault();
|
|
1518
|
+
const d = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 400 : 1);
|
|
1519
|
+
zoom(Math.exp(-Math.max(-100, Math.min(100, d)) * 0.0015), e.clientX, e.clientY);
|
|
1520
|
+
}, {passive: false});
|
|
1521
|
+
box.tabIndex = 0;
|
|
1522
|
+
box.addEventListener("keydown", (e) => {
|
|
1523
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1524
|
+
if (e.key === "+" || e.key === "=") step(1.25);
|
|
1525
|
+
else if (e.key === "-" || e.key === "_") step(0.8);
|
|
1526
|
+
else if (e.key === "0") auto();
|
|
1527
|
+
else return;
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
});
|
|
1530
|
+
wrap.addEventListener("change", (e) => {
|
|
1531
|
+
if (!e.target.matches(".cgs")) return; // a zoom button, or full screen: its box's size changes
|
|
1532
|
+
if (e.target.type === "radio") box.style.removeProperty("--z");
|
|
1533
|
+
requestAnimationFrame(tell);
|
|
1534
|
+
});
|
|
1535
|
+
addEventListener("hashchange", () => requestAnimationFrame(tell));
|
|
1536
|
+
addEventListener("resize", tell);
|
|
1537
|
+
wrap.classList.add("pz");
|
|
1538
|
+
tell();
|
|
1539
|
+
}
|
|
1540
|
+
})();
|
|
1541
|
+
</script>"""
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _g_hidden(graph: CodeGraph, opens: bool) -> str:
|
|
1545
|
+
"""
|
|
1546
|
+
The legend's pill for the functions a graph leaves out, and why: without JavaScript where to find them, with it
|
|
1547
|
+
(when opens) that a box's line of them opens them.
|
|
1548
|
+
"""
|
|
1549
|
+
if not graph.hidden:
|
|
1550
|
+
return ""
|
|
1551
|
+
why = (
|
|
1552
|
+
"too deep"
|
|
1553
|
+
if graph.too_deep == graph.hidden
|
|
1554
|
+
else "too many to draw at once"
|
|
1555
|
+
if not graph.too_deep
|
|
1556
|
+
else "too deep or too many"
|
|
1557
|
+
)
|
|
1558
|
+
head = f"<b>+{graph.hidden}</b>{'more function' if graph.hidden == 1 else 'more functions'} not drawn ({why})"
|
|
1559
|
+
pill = f'<span class="kp kh{" nj" if opens else ""}">{head}: the Step by step tab has them all</span>'
|
|
1560
|
+
if opens:
|
|
1561
|
+
pill += f'<span class="kp kh oj">{head}: click a box\'s blue line of calls to open them</span>'
|
|
1562
|
+
return pill
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _g_canvas(
|
|
1566
|
+
graph: CodeGraph,
|
|
1567
|
+
block: _Graph,
|
|
1568
|
+
report: _Report,
|
|
1569
|
+
lines_of: Callable[[str], list[str]],
|
|
1570
|
+
panels: dict[str, tuple[str, str]],
|
|
1571
|
+
opens: dict[str, str],
|
|
1572
|
+
prefix: str,
|
|
1573
|
+
) -> tuple[str, str]:
|
|
1574
|
+
"""
|
|
1575
|
+
One graph drawn: its SVG and its legend. Its boxes' panels go into panels ({a function, or a panel without its
|
|
1576
|
+
id: (its id, the panel)}, one for a function or the same code wherever it's drawn, the first graph's; a new one's
|
|
1577
|
+
id starts with prefix), and a box whose function is in opens ({module.name: a
|
|
1578
|
+
link}) has its line of calls not drawn made that link.
|
|
1579
|
+
"""
|
|
1377
1580
|
cards = _g_cards(graph)
|
|
1378
1581
|
boxes, routes, width, tall, xs, widths, column_of = _graph_layout(graph, cards)
|
|
1379
1582
|
ends = _g_anchors(graph, cards, boxes)
|
|
@@ -1386,20 +1589,24 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1386
1589
|
for name in _g_names(e):
|
|
1387
1590
|
access[e.target].setdefault(name, e.kind)
|
|
1388
1591
|
root = report.root or _ROOT
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1592
|
+
panel_of: dict[str, str] = {} # a box -> its panel's id
|
|
1593
|
+
if block.sources:
|
|
1594
|
+
for n in graph.nodes:
|
|
1595
|
+
same = "\0" + n.name if n.name else ""
|
|
1596
|
+
if same not in panels:
|
|
1597
|
+
html = _g_panel(n, lines_of, block.editor, "\0")
|
|
1598
|
+
if not html:
|
|
1599
|
+
continue
|
|
1600
|
+
same = same or html
|
|
1601
|
+
if same not in panels:
|
|
1602
|
+
key = prefix + _g_panel_id(n)
|
|
1603
|
+
panels[same] = (key, html.replace('id="\0"', f'id="{key}"', 1))
|
|
1604
|
+
panel_of[n.id] = panels[same][0]
|
|
1605
|
+
for key in set(panel_of.values()): # the box whose code is open, outlined
|
|
1606
|
+
report.rules.add(
|
|
1607
|
+
f".{root} .cgw:has(#{key}:target) .cg .p-{key} .fg"
|
|
1608
|
+
"{stroke:light-dark(#2563eb,#60a5fa);stroke-opacity:1;stroke-width:2}"
|
|
1609
|
+
)
|
|
1403
1610
|
for n in graph.nodes: # pointing at a box leaves only it, its arrows and the boxes they join in sight
|
|
1404
1611
|
report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .ce:not(.s-{n.id}):not(.t-{n.id}){{opacity:.06}}")
|
|
1405
1612
|
report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .cn:not(.n-{n.id}):not(.a-{n.id}){{opacity:.28}}")
|
|
@@ -1446,16 +1653,33 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1446
1653
|
marked.add((e.source, e.target))
|
|
1447
1654
|
edges = "".join(body for body, _ in drawn)
|
|
1448
1655
|
ports = "".join(dots for _, dots in drawn)
|
|
1656
|
+
opened = {n.id: opens[n.name] for n in graph.nodes if n.more and n.name in opens}
|
|
1449
1657
|
nodes = "".join(
|
|
1450
1658
|
(
|
|
1451
|
-
f'<a class="cl" href="#{_esc(
|
|
1452
|
-
if
|
|
1659
|
+
f'<a class="cl" href="#{_esc(panel_of[n.id])}" aria-label="{_esc("Show the code of " + n.label)}">{box}</a>'
|
|
1660
|
+
if n.id in panel_of
|
|
1453
1661
|
else box
|
|
1454
1662
|
)
|
|
1455
1663
|
for n in graph.nodes
|
|
1456
|
-
for box in [
|
|
1664
|
+
for box in [
|
|
1665
|
+
_node_svg(
|
|
1666
|
+
cards[n.id],
|
|
1667
|
+
boxes[n.id],
|
|
1668
|
+
list(dict.fromkeys(near[n.id])),
|
|
1669
|
+
access[n.id],
|
|
1670
|
+
panel_of.get(n.id, ""),
|
|
1671
|
+
n.id in opened,
|
|
1672
|
+
)
|
|
1673
|
+
]
|
|
1674
|
+
)
|
|
1675
|
+
links = "".join( # over each line of calls not drawn that opens a graph of its own: drawn with JavaScript only
|
|
1676
|
+
f'<a class="dr" href="#{_esc(link)}" aria-label="{_esc(f"Open the calls {n.label} makes")}">'
|
|
1677
|
+
f'<rect x="{boxes[n.id][0] + 8:.1f}" y="{boxes[n.id][1] + _g_more_y(cards[n.id]) - 14:.1f}" '
|
|
1678
|
+
f'width="{boxes[n.id][2] - 16:.0f}" height="19" rx="5"/></a>'
|
|
1679
|
+
for n in graph.nodes
|
|
1680
|
+
if (link := opened.get(n.id))
|
|
1457
1681
|
)
|
|
1458
|
-
tips = "".join(_g_tip_svg(n, boxes[n.id], width, tall, lines_of,
|
|
1682
|
+
tips = "".join(_g_tip_svg(n, boxes[n.id], width, tall, lines_of, n.id in panel_of) for n in graph.nodes)
|
|
1459
1683
|
kinds = {e.kind for e in graph.edges}
|
|
1460
1684
|
pills = [f'<span class="kp">{_g_swatch(k)}{_esc(word)}</span>' for k, word in _G_LEGEND if k in kinds]
|
|
1461
1685
|
if any(e.guessed for e in graph.edges):
|
|
@@ -1474,22 +1698,71 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1474
1698
|
f'aria-hidden="true"><circle r="7"/><path transform="scale(.6) translate(-8,-8)" d="{_G_LOOP}"/>'
|
|
1475
1699
|
"</svg>in a loop: once per item</span>"
|
|
1476
1700
|
)
|
|
1477
|
-
hidden = (
|
|
1478
|
-
(
|
|
1479
|
-
f'<span class="kp kh"><b>+{graph.hidden}</b>{"more function" if graph.hidden == 1 else "more functions"} '
|
|
1480
|
-
"not drawn (too deep or too many): the Step by step tab has them all</span>"
|
|
1481
|
-
)
|
|
1482
|
-
if graph.hidden
|
|
1483
|
-
else ""
|
|
1484
|
-
)
|
|
1485
1701
|
hint = (
|
|
1486
1702
|
'<span class="kt">Point at a box to see only what it\'s joined to'
|
|
1487
|
-
+ ("; click it to read its code" if
|
|
1488
|
-
+ "</span>
|
|
1703
|
+
+ ("; click it to read its code" if panel_of else "")
|
|
1704
|
+
+ '</span><span class="kt pzo">Drag to move about; Ctrl or ⌘ and scroll, or pinch, to zoom</span>'
|
|
1489
1705
|
)
|
|
1706
|
+
svg = (
|
|
1707
|
+
f'<svg viewBox="0 0 {width:.0f} {tall:.0f}" width="{width:.0f}" height="{tall:.0f}" '
|
|
1708
|
+
f'style="--w:{width:.0f}px" role="img" aria-label="{_esc(block.title or "How the code fits together")}">'
|
|
1709
|
+
f"{_g_captions(graph, xs, widths, column_of)}{edges}{nodes}{links}{ports}{tips}</svg>"
|
|
1710
|
+
)
|
|
1711
|
+
return svg, "".join(pills) + _g_hidden(graph, bool(opened)) + hint
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def _g_path(path: list[str], links: dict[str, str], main: str, last: str = "") -> str:
|
|
1715
|
+
"""
|
|
1716
|
+
The calls from the entry point down to a view's function, over its graph: each one that has a view of its own a
|
|
1717
|
+
link to it, and a link back to the whole graph first. last, when given, is said after them in place of the last.
|
|
1718
|
+
"""
|
|
1719
|
+
names = [_esc(_short_name(name)) + "()" for name in path]
|
|
1720
|
+
steps = [
|
|
1721
|
+
f'<a href="#{_esc(links[name])}"><code>{said}</code></a>' if name in links else f"<code>{said}</code>"
|
|
1722
|
+
for name, said in zip(path[:-1], names[:-1])
|
|
1723
|
+
]
|
|
1724
|
+
steps.append(_esc(last) if last else f"<b><code>{names[-1]}</code></b>" if names else "")
|
|
1725
|
+
return f'<a class="cgcb" href="#{_esc(main)}">← The whole graph</a>' + "".join(
|
|
1726
|
+
f'<span class="cgcs">›</span>{step}' for step in steps
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
@_draws(_Graph, "html")
|
|
1731
|
+
def _graph_html(block: _Graph, report: _Report) -> str:
|
|
1732
|
+
title = f"<h4>{_prose(block.title)}</h4>" if block.title else ""
|
|
1733
|
+
graph = block.graph
|
|
1734
|
+
if not graph.nodes:
|
|
1735
|
+
return title + f'<div class="more">{_esc(block.empty)}</div>'
|
|
1736
|
+
highlighted: dict[str, list[str]] = {}
|
|
1737
|
+
|
|
1738
|
+
def lines_of(file: str) -> list[str]: # a file's lines in colour, each file coloured once
|
|
1739
|
+
if file not in highlighted:
|
|
1740
|
+
highlighted[file] = _python_lines(block.sources[file]) if file in block.sources else []
|
|
1741
|
+
return highlighted[file]
|
|
1742
|
+
|
|
1490
1743
|
made = report.shared("graphs", lambda: [0]) # graphs drawn so far, so each one's buttons have ids of their own
|
|
1491
1744
|
group = f"cn-graph{made[0]}"
|
|
1492
1745
|
made[0] += 1
|
|
1746
|
+
links = {view.name: f"{group}-v{k}" for k, view in enumerate(block.views, 1)}
|
|
1747
|
+
panels: dict[str, tuple[str, str]] = {}
|
|
1748
|
+
svg, legend = _g_canvas(graph, block, report, lines_of, panels, links, "")
|
|
1749
|
+
views = []
|
|
1750
|
+
for k, view in enumerate(block.views, 1):
|
|
1751
|
+
drawn, key = _g_canvas(view.graph, block, report, lines_of, panels, links, f"v{k}-")
|
|
1752
|
+
views.append(
|
|
1753
|
+
f'<template id="{group}-v{k}"><div class="x-cgc">{_g_path(view.path, links, group + "-main")}</div>'
|
|
1754
|
+
f'<div class="x-cg">{drawn}</div><div class="x-cgk">{key}</div></template>'
|
|
1755
|
+
)
|
|
1756
|
+
everything = ""
|
|
1757
|
+
if block.everything is not None and block.everything.nodes:
|
|
1758
|
+
count = sum(1 for n in block.everything.nodes if n.kind in ("handler", "function"))
|
|
1759
|
+
drawn, key = _g_canvas(block.everything, block, report, lines_of, panels, {}, "all-")
|
|
1760
|
+
views.append(
|
|
1761
|
+
f'<template id="{group}-all"><div class="x-cgc">'
|
|
1762
|
+
f"{_g_path([], links, group + '-main', f'Every function, all {count} of them')}</div>"
|
|
1763
|
+
f'<div class="x-cg">{drawn}</div><div class="x-cgk">{key}</div></template>'
|
|
1764
|
+
)
|
|
1765
|
+
everything = f'<a class="cgta oj" href="#{group}-all" title="Every function in one graph">All {count}</a>'
|
|
1493
1766
|
# its zoom and full screen: inputs before the box they change, so CSS reaches it with ~ (no :has(), no script)
|
|
1494
1767
|
switches = f'<input type="checkbox" class="cgs cgfs" id="{group}-full">' + "".join(
|
|
1495
1768
|
f'<input type="radio" class="cgs cgz-{key}" name="{group}-zoom" id="{group}-{key}"'
|
|
@@ -1497,7 +1770,7 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1497
1770
|
for key, _, _ in _GRAPH_ZOOMS
|
|
1498
1771
|
)
|
|
1499
1772
|
bar = (
|
|
1500
|
-
'<div class="cgt"
|
|
1773
|
+
f'<div class="cgt">{everything}<span class="cgtz" role="group" aria-label="Zoom">'
|
|
1501
1774
|
+ "".join(
|
|
1502
1775
|
f'<label class="zl-{key}" for="{group}-{key}" title="{_esc(tip)}">{_esc(word)}</label>'
|
|
1503
1776
|
for key, word, tip in _GRAPH_ZOOMS
|
|
@@ -1505,14 +1778,24 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1505
1778
|
+ f'</span><label class="cgtf" for="{group}-full"><span class="on">Full screen</span>'
|
|
1506
1779
|
'<span class="off">Close full screen</span></label></div>'
|
|
1507
1780
|
)
|
|
1781
|
+
zoom = ( # over the graph's corner, as a canvas has it: with JavaScript only
|
|
1782
|
+
'<span class="cgtm pzo" role="group" aria-label="Zoom out or in">'
|
|
1783
|
+
'<button type="button" class="zo" title="Zoom out (-, or Ctrl and scroll)">−</button>'
|
|
1784
|
+
'<span class="cgtp" title="How big the graph is drawn"></span>'
|
|
1785
|
+
'<button type="button" class="zi" title="Zoom in (+, or Ctrl and scroll)">+</button></span>'
|
|
1786
|
+
)
|
|
1787
|
+
script = ""
|
|
1788
|
+
if not report.shared("graph-script", lambda: [False])[0]: # once, for every graph: moving about and zooming
|
|
1789
|
+
report.shared("graph-script", lambda: [False])[0] = True
|
|
1790
|
+
script = _G_SCRIPT
|
|
1508
1791
|
# in a box of its own with its panels: the :has() rules that outline a box whose code is open look only in here,
|
|
1509
1792
|
# so a radio button elsewhere (the walk's steps) doesn't make the browser restyle the whole page
|
|
1793
|
+
data = f' data-group="{group}"' if views else ""
|
|
1510
1794
|
return (
|
|
1511
|
-
f'{title}<div class="cgw">{switches}<div class="cgb">{bar}
|
|
1512
|
-
f'<
|
|
1513
|
-
f'
|
|
1514
|
-
f"{
|
|
1515
|
-
f'<div class="cgk">{"".join(pills)}{hidden}{hint}</div></div>{"".join(panels.values())}</div>'
|
|
1795
|
+
f'{title}<div class="cgw"{data}>{switches}<div class="cgb">{bar}'
|
|
1796
|
+
f'<nav class="cgc" aria-label="Where this graph starts"></nav><div class="cgv"><div class="cg">{svg}</div>{zoom}</div>'
|
|
1797
|
+
f'<div class="cgk">{legend}</div></div>{"".join(html for _, html in panels.values())}{"".join(views)}</div>'
|
|
1798
|
+
f"{script}"
|
|
1516
1799
|
)
|
|
1517
1800
|
|
|
1518
1801
|
|
code_node/report/style.py
CHANGED
|
@@ -102,7 +102,8 @@ _EXTRA_CSS = """\
|
|
|
102
102
|
.cnd :is(.flow,.cside,.walk,.ctip) .pa{font-style:italic}
|
|
103
103
|
.cnd :is(.flow,.cside,.walk,.ctip) .pc{opacity:.6;font-style:italic}
|
|
104
104
|
.cnd .cgb{width:fit-content;max-width:min(100%,1180px)}
|
|
105
|
-
.cnd .
|
|
105
|
+
.cnd .cgv{position:relative;margin:6px 0 8px}
|
|
106
|
+
.cnd .cg{border:1px solid rgba(127,127,127,.2);border-radius:14px;overflow:auto;background-color:rgba(127,127,127,.035);background-attachment:local;background-size:18px 18px;
|
|
106
107
|
background-image:radial-gradient(circle at 1px 1px,rgba(127,127,127,.3) 1px,transparent 0);background-position:9px 9px}
|
|
107
108
|
.cnd .cg svg{display:block;width:calc(var(--w)*var(--z,1));max-width:none;height:auto;overflow:visible;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
|
|
108
109
|
.cnd .cgw{position:relative}
|
|
@@ -121,10 +122,38 @@ _EXTRA_CSS = """\
|
|
|
121
122
|
.cnd .cgz-auto:checked~.cgb .cg svg{width:var(--w);max-width:100%;min-width:calc(var(--w)*.8)}
|
|
122
123
|
.cnd .cgz-fit:checked~.cgb .cg svg{width:var(--w);max-width:100%}
|
|
123
124
|
.cnd .cgfs:checked~.cgb{position:fixed;inset:0;z-index:25;width:auto;max-width:none;display:flex;flex-direction:column;box-sizing:border-box;padding:12px clamp(12px,2vw,28px) 4px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas))}
|
|
124
|
-
.cnd .cgfs:checked~.cgb .
|
|
125
|
+
.cnd .cgfs:checked~.cgb .cgv{flex:1;min-height:0;display:flex;flex-direction:column;margin:0 0 8px}
|
|
126
|
+
.cnd .cgfs:checked~.cgb .cg{flex:1;min-height:0}
|
|
125
127
|
.cnd .cgfs:checked~.cgz-fit:checked~.cgb .cg svg{width:100%;height:100%;max-width:none}
|
|
126
128
|
.cnd .cgfs:checked~.cgz-auto:checked~.cgb .cg svg{min-width:0}
|
|
127
|
-
|
|
129
|
+
.cnd .cgw:not(.js) :is(.oj,.dr),.cnd .cgw.js .nj{display:none}
|
|
130
|
+
.cnd .cgw:not(.pz) .pzo{display:none}
|
|
131
|
+
.cnd .cgw.pz .cg{cursor:grab;user-select:none;-webkit-user-select:none}
|
|
132
|
+
.cnd .cgw.pz .cg.pan,.cnd .cgw.pz .cg.pan *{cursor:grabbing}
|
|
133
|
+
.cnd .cgw.pz .cg:focus-visible{outline:2px solid light-dark(#2563eb,#60a5fa);outline-offset:-2px}
|
|
134
|
+
.cnd .cgw.pz>.cgfs:not(:checked)~.cgb{width:100%;max-width:none}
|
|
135
|
+
.cnd .cgw.pz>.cgfs:not(:checked)~.cgb .cg{height:max(360px,72vh)}
|
|
136
|
+
.cnd .cgw.pz .cg{display:flex}
|
|
137
|
+
.cnd .cgw.pz .cg svg{flex:none;margin:auto}
|
|
138
|
+
.cnd .cgw.pz>.cgz-fit:checked~.cgb .cg svg{width:calc(var(--w)*var(--fit,1));height:auto;max-width:none}
|
|
139
|
+
.cnd .cgtm{position:absolute;right:14px;bottom:14px;z-index:2;display:inline-flex;align-items:center;font-size:12px;user-select:none;-webkit-user-select:none;border:1px solid rgba(127,127,127,.3);border-radius:8px;overflow:hidden;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));box-shadow:0 1px 2px rgba(0,0,0,.06),0 4px 12px rgba(15,23,42,.08)}
|
|
140
|
+
.cnd .cgtm button{font:inherit;font-size:16px;line-height:1;color:inherit;background:none;border:0;padding:6px 11px;cursor:pointer;opacity:.75}
|
|
141
|
+
.cnd .cgtm button:hover{opacity:1;background:rgba(127,127,127,.08)}
|
|
142
|
+
.cnd .cgtm button:focus-visible{outline:2px solid light-dark(#2563eb,#60a5fa);outline-offset:-2px}
|
|
143
|
+
.cnd .cgtp{min-width:3.4em;padding:3px 2px;text-align:center;opacity:.7;font-variant-numeric:tabular-nums;border-inline:1px solid rgba(127,127,127,.2)}
|
|
144
|
+
.cnd .cgta{padding:3px 10px;border:1px solid rgba(127,127,127,.28);border-radius:8px;color:inherit;text-decoration:none;opacity:.7}
|
|
145
|
+
.cnd .cgta:hover{opacity:.95;background:rgba(127,127,127,.08)}
|
|
146
|
+
.cnd .cgw[data-view$="-all"] .cgta{opacity:1;font-weight:650;background:rgba(127,127,127,.14)}
|
|
147
|
+
.cnd .cgc{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 7px;margin:0 0 6px;font-size:13px}
|
|
148
|
+
.cnd .cgc:empty{display:none}
|
|
149
|
+
.cnd .cgc a{color:light-dark(#2563eb,#60a5fa);text-decoration:none}
|
|
150
|
+
.cnd .cgc a:hover{text-decoration:underline}
|
|
151
|
+
.cnd .cgc code{font-family:var(--mono);font-size:12.5px}
|
|
152
|
+
.cnd .cgc .cgcs{opacity:.4}
|
|
153
|
+
.cnd .cg .dr rect{fill:transparent;cursor:pointer}
|
|
154
|
+
.cnd .cg .dr:hover rect{fill:rgba(37,99,235,.09)}
|
|
155
|
+
.cnd .cg .dr:focus-visible{outline:2px solid light-dark(#2563eb,#60a5fa)}
|
|
156
|
+
.cnd .cgw.js .cg .hd .md{fill:light-dark(#2563eb,#60a5fa);fill-opacity:1;font-style:normal}
|
|
128
157
|
.cnd .cg .cap{font-size:12px;font-weight:650;letter-spacing:1.2px;fill:currentColor;fill-opacity:.6}
|
|
129
158
|
.cnd .cg .capl{stroke:currentColor;stroke-opacity:.14;stroke-width:1;fill:none}
|
|
130
159
|
.cnd .cg .cn,.cnd .cg .ce{transition:opacity .15s}
|
|
@@ -270,7 +299,6 @@ _EXTRA_CSS = """\
|
|
|
270
299
|
.cnd .cside .sl.lit{background:rgba(245,158,11,.16);box-shadow:inset 3px 0 #f59e0b}
|
|
271
300
|
.cnd .cside .sn{display:inline-block;width:3.2em;padding-right:14px;text-align:right;opacity:.42;user-select:none;-webkit-user-select:none}
|
|
272
301
|
.cnd .cside .csm{font-size:12px;font-style:italic;opacity:.6;margin:6px 2px 0}
|
|
273
|
-
@media (min-width:1100px){.cnd .cgw:has(.cside:target){margin-right:min(660px,94vw)}}
|
|
274
302
|
@media (prefers-reduced-motion:reduce){.cnd .cside,.cnd .cside:target{transition:none}}
|
|
275
303
|
.cnd .tabs{position:relative;margin:0 0 8px}
|
|
276
304
|
.cnd .tbar{position:sticky;top:0;z-index:20;display:flex;flex-wrap:wrap;align-items:flex-end;gap:2px;padding-top:6px;margin:0 0 18px;border-bottom:1px solid rgba(127,127,127,.22);background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas))}
|
|
@@ -358,6 +386,14 @@ _EXTRA_CSS = """\
|
|
|
358
386
|
.cnd .walk .wsay p{margin:4px 0 0;opacity:.85}
|
|
359
387
|
.cnd .walk .wsay code{font-size:12.5px}
|
|
360
388
|
.cnd .walk .wdoc{font-style:italic;opacity:.7}
|
|
389
|
+
.cnd .cnh{margin:10px 0 0;padding:8px 0 0 10px;border-left:2px solid rgba(127,127,127,.3);font-size:12.5px;line-height:1.5}
|
|
390
|
+
.cnd .cnh p{margin:0 0 3px;opacity:.75;overflow-wrap:anywhere}
|
|
391
|
+
.cnd .cnh p:last-child{margin-bottom:0}
|
|
392
|
+
.cnd .cnh b{font-weight:600}
|
|
393
|
+
.cnd .cnh code{font:12px var(--mono)}
|
|
394
|
+
.cnd .cnh a{color:light-dark(#2563eb,#60a5fa);text-decoration:none}
|
|
395
|
+
.cnd .cnh a:hover{text-decoration:underline}
|
|
396
|
+
.cnd .cnh .cnhu{color:light-dark(#b45309,#fbbf24)}
|
|
361
397
|
.cnd .walk .wjump{user-select:none;-webkit-user-select:none;color:var(--blue);cursor:pointer;text-decoration:underline;text-underline-offset:2px}
|
|
362
398
|
.cnd .walk .wfacts{list-style:none;margin:10px 0 0;padding:10px 0 0;border-top:1px solid rgba(127,127,127,.14);display:flex;flex-direction:column;gap:5px}
|
|
363
399
|
.cnd .walk .wfacts li{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;font-size:12.5px;overflow-wrap:anywhere}
|