code-node-graph 0.1.0__py3-none-any.whl → 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.
code_node/__init__.py CHANGED
@@ -13,7 +13,7 @@ from .libraries import Library, register_library
13
13
  from .platforms import Platform, get_platform, register_platform
14
14
  from .project import EntryPoint, Project, entry_points, read_project
15
15
 
16
- __version__ = "0.1.0"
16
+ __version__ = "0.3.0"
17
17
 
18
18
  __all__ = [
19
19
  "CodeGraph",
code_node/report/graph.py CHANGED
@@ -13,8 +13,8 @@ from typing import Any, Callable
13
13
 
14
14
  from ..kit.blocks import _Report, _draws
15
15
  from ..kit.fmt import _plural
16
- from ..kit.highlight import _python_lines
17
- from ..kit.render import _prose
16
+ from ..kit.highlight import _python_html, _python_lines
17
+ from ..kit.render import _CALL_RE, _prose
18
18
  from ..kit.text import _esc
19
19
 
20
20
  from ..analysis.graph import CodeGraph, GraphEdge, GraphNode
@@ -1081,7 +1081,7 @@ def _node_svg(
1081
1081
  )
1082
1082
  classes = " ".join(["cn", f"k-{n.kind}", f"n-{n.id}", *(f"a-{m}" for m in neighbours)]) + (" warn" if warn else "")
1083
1083
  return (
1084
- f'<g class="{_esc(classes)}" transform="translate({x:.1f},{y:.1f})"><title>{_esc(n.tip or n.label)}</title>'
1084
+ f'<g class="{_esc(classes)}" transform="translate({x:.1f},{y:.1f})">'
1085
1085
  f'<rect class="bg" width="{w:.0f}" height="{h:.0f}" rx="11"/>'
1086
1086
  f'<rect class="fg" x=".5" y=".5" width="{w - 1:.0f}" height="{h - 1:.0f}" rx="10.5"/>{"".join(parts)}</g>'
1087
1087
  )
@@ -1175,6 +1175,153 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
1175
1175
  )
1176
1176
 
1177
1177
 
1178
+ _G_TIP_W = 420 # a box's hover card, wide
1179
+
1180
+ _G_TIP_SIGNATURE = 6 # lines of a def its hover card shows, at most
1181
+
1182
+ _G_TIP_WARNING = "A warning is about it: "
1183
+
1184
+ # a name in the card's sentences worth setting as code: a dotted or keyed path (Records[].s3.bucket.name,
1185
+ # book:{book_id}, s3:GetObject, orders/handler.py) or a variable in capitals (DB_HOST)
1186
+ _G_CODE_RE = re.compile(
1187
+ r"(?<![\w.\-/])([A-Za-z_][\w\-]*(?:[.:/]+[\w{}\-]+|\[\]|\{\w*\})+|[A-Z][A-Z0-9]*_[A-Z0-9_]*[A-Z0-9])(?![\w{])"
1188
+ )
1189
+
1190
+
1191
+ def _g_tip_text(text: str) -> str:
1192
+ """
1193
+ One of the card's sentences as HTML, its calls coloured as Python and its names set as code, every piece escaped.
1194
+ """
1195
+ out = []
1196
+ for i, piece in enumerate(_CALL_RE.split(text)):
1197
+ if i % 2:
1198
+ out.append(f'<code class="ctk">{_python_html(piece)}</code>')
1199
+ continue
1200
+ for k, part in enumerate(_G_CODE_RE.split(piece)):
1201
+ out.append(f'<code class="ctk ctn">{_esc(part)}</code>' if k % 2 else _esc(part))
1202
+ return "".join(out)
1203
+
1204
+
1205
+ def _g_tip_item(item: str) -> str:
1206
+ """
1207
+ One thing a service box lists, for its card: 'PutItem (dynamodb:PutItem)' as the operation in code and what it
1208
+ needs beside it.
1209
+ """
1210
+ m = re.fullmatch(r"(.+?) \((.+)\)", item)
1211
+ if not m:
1212
+ return f'<li><code class="ctk ctn">{_esc(item)}</code></li>'
1213
+ return (
1214
+ f'<li><code class="ctk ctn">{_esc(m.group(1))}</code> <span class="ctm">{_g_tip_text(m.group(2))}</span></li>'
1215
+ )
1216
+
1217
+
1218
+ def _g_tip_signature(n: GraphNode, lines_of: Callable[[str], list[str]]) -> list[str]:
1219
+ """
1220
+ A function's def as it's written, in colour: its decorators and the lines up to the colon that opens its body.
1221
+ [] when its file wasn't read.
1222
+ """
1223
+ if not n.code:
1224
+ return []
1225
+ span = n.code[0]
1226
+ lines = lines_of(span.file)
1227
+ if not lines or span.start > len(lines):
1228
+ return []
1229
+ start = span.start
1230
+ while start > 1 and lines[start - 2].lstrip().startswith("@"):
1231
+ start -= 1
1232
+ shown = []
1233
+ for k in range(start, min(len(lines), span.end, start + _G_TIP_SIGNATURE - 1) + 1):
1234
+ shown.append(lines[k - 1])
1235
+ if k >= span.start and re.sub(r"<[^>]*>", "", lines[k - 1]).rstrip().endswith(":"):
1236
+ break
1237
+ indent = min((len(line) - len(line.lstrip(" ")) for line in shown if line.strip()), default=0)
1238
+ return [line[indent:] for line in shown]
1239
+
1240
+
1241
+ def _g_tip(n: GraphNode, lines_of: Callable[[str], list[str]], opens: bool) -> tuple[str, float]:
1242
+ """
1243
+ What a box says on hover, as a card: what kind of box it is and its name, a function's def in colour and its
1244
+ docstring, then the rest of its words with the code in them set as code, its warnings apart, and when it has a
1245
+ panel (opens), that a click shows its code. With how tall it is likely to be, to pick whether it goes under the
1246
+ box or over it.
1247
+ """
1248
+ said = [line for line in (n.tip or n.label).split("\n") if line]
1249
+ warnings = [w for line in said if line.startswith(_G_TIP_WARNING) for w in line[len(_G_TIP_WARNING) :].split("; ")]
1250
+ said = [line for line in said if not line.startswith(_G_TIP_WARNING)]
1251
+ mono = n.kind in ("handler", "function")
1252
+ body, tall, sub = [], 80.0 + (20 if n.sub else 0), n.sub
1253
+ if mono and said and _CALL_RE.match(said[0]) and " in " in said[0]: # 'name(args) in file, 3 lines: its doc'
1254
+ head = said.pop(0)
1255
+ signature = _g_tip_signature(n, lines_of) or [_python_html(head.split(" in ", 1)[0])]
1256
+ body.append(f'<pre class="ctc">{chr(10).join(signature)}</pre>')
1257
+ tall += 21 * len(signature) + 34
1258
+ lines = re.search(r", (\d+ lines?)", head)
1259
+ sub = f"{n.sub} · {lines.group(1)}" if lines and n.sub else sub
1260
+ if n.lines and n.lines[0]:
1261
+ body.append(f'<p class="ctd">{_g_tip_text(n.lines[0])}</p>')
1262
+ tall += 23 * math.ceil(len(n.lines[0]) / 42) + 10
1263
+ for line in said:
1264
+ lead, _, rest = line.partition(": ")
1265
+ if rest and lead == n.label: # 'S3: GetObject (s3:GetObject), PutObject (s3:PutObject)': a list
1266
+ items = rest.split(", ")
1267
+ body.append(f'<ul class="ctl">{"".join(_g_tip_item(item) for item in items)}</ul>')
1268
+ tall += 30 * len(items) + 8
1269
+ else:
1270
+ body.append(f'<p class="ctp">{_g_tip_text(line)}</p>')
1271
+ tall += 23 * math.ceil(len(line) / 42) + 10
1272
+ if warnings:
1273
+ body.append(
1274
+ '<div class="ctw"><b>Warning</b><ul>'
1275
+ + "".join(f"<li>{_g_tip_text(w)}</li>" for w in warnings)
1276
+ + "</ul></div>"
1277
+ )
1278
+ tall += 50 + sum(23 * math.ceil(len(w) / 36) + 4 for w in warnings)
1279
+ kind = _G_PANEL_KINDS.get(n.kind, n.kind)
1280
+ name = _python_html(n.label) if mono else _esc(n.label)
1281
+ card = (
1282
+ f'<div class="ctt k-{_esc(n.kind)}{" warn" if warnings else ""}"><div class="cth">'
1283
+ f'<span class="ctkd">{_esc(kind)}</span><b class="ctn{" mono" if mono else ""}">{name}</b>'
1284
+ + (f'<span class="cts{" mono" if mono else ""}">{_esc(sub)}</span>' if sub else "")
1285
+ + f"</div>{''.join(body)}"
1286
+ + ('<div class="cto">Click it to read its code</div>' if opens else "")
1287
+ + "</div>"
1288
+ )
1289
+ return card, tall + (40 if opens else 0)
1290
+
1291
+
1292
+ def _g_tip_svg(
1293
+ n: GraphNode,
1294
+ box: tuple[float, float, float, float],
1295
+ width: float,
1296
+ tall: float,
1297
+ lines_of: Callable[[str], list[str]],
1298
+ opens: bool,
1299
+ ) -> str:
1300
+ """
1301
+ A box's hover card, in the graph so it zooms with it: under the box when it fits there, else over it, else beside
1302
+ it, and inside the graph from left to right. Hidden until the box is pointed at.
1303
+ """
1304
+ card, high = _g_tip(n, lines_of, opens)
1305
+ x, y, w, h = box
1306
+ card_w = min(_G_TIP_W, max(width - 16, 220))
1307
+ left = max(8.0, min(x, width - card_w - 8))
1308
+ below, above = tall - (y + h + 10) - 8, y - 10 - 8
1309
+ if high <= below:
1310
+ top, room, side = y + h + 10, below, "dn"
1311
+ elif high <= above:
1312
+ top, room, side = 8.0, above, "up"
1313
+ else: # beside the box, as high up as keeps all of it in the graph; over the box only when there's no room beside
1314
+ if width - (x + w + 12) - 8 >= card_w:
1315
+ left = x + w + 12
1316
+ elif x - 12 - 8 >= card_w:
1317
+ left = x - 12 - card_w
1318
+ top, room, side = max(8.0, min(y, tall - high - 8)), high, "dn"
1319
+ return (
1320
+ f'<foreignObject class="ctip tp-{_esc(n.id)} {side}" x="{left:.0f}" y="{top:.0f}" width="{card_w:.0f}" '
1321
+ f'height="{room:.0f}" aria-hidden="true"><div class="ctx">{card}</div></foreignObject>'
1322
+ )
1323
+
1324
+
1178
1325
  def _g_captions(graph: CodeGraph, xs: dict[int, float], widths: dict[int, float], column_of: dict[str, int]) -> str:
1179
1326
  """
1180
1327
  The words over the columns: COMES IN, HANDLER, CALLS, REACHES, each over the columns it covers.
@@ -1256,6 +1403,7 @@ def _graph_html(block: _Graph, report: _Report) -> str:
1256
1403
  for n in graph.nodes: # pointing at a box leaves only it, its arrows and the boxes they join in sight
1257
1404
  report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .ce:not(.s-{n.id}):not(.t-{n.id}){{opacity:.06}}")
1258
1405
  report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .cn:not(.n-{n.id}):not(.a-{n.id}){{opacity:.28}}")
1406
+ report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .tp-{n.id}{{display:inline}}")
1259
1407
  report.rules.add(
1260
1408
  f".{root} .cg:has(.n-{n.id}:hover) :is(.s-{n.id},.t-{n.id}) .ln{{stroke-width:2.4;stroke-opacity:1}}"
1261
1409
  )
@@ -1307,6 +1455,7 @@ def _graph_html(block: _Graph, report: _Report) -> str:
1307
1455
  for n in graph.nodes
1308
1456
  for box in [_node_svg(cards[n.id], boxes[n.id], list(dict.fromkeys(near[n.id])), access[n.id])]
1309
1457
  )
1458
+ tips = "".join(_g_tip_svg(n, boxes[n.id], width, tall, lines_of, bool(panels.get(n.id))) for n in graph.nodes)
1310
1459
  kinds = {e.kind for e in graph.edges}
1311
1460
  pills = [f'<span class="kp">{_g_swatch(k)}{_esc(word)}</span>' for k, word in _G_LEGEND if k in kinds]
1312
1461
  if any(e.guessed for e in graph.edges):
@@ -1362,7 +1511,7 @@ def _graph_html(block: _Graph, report: _Report) -> str:
1362
1511
  f'{title}<div class="cgw">{switches}<div class="cgb">{bar}<div class="cg">'
1363
1512
  f'<svg viewBox="0 0 {width:.0f} {tall:.0f}" width="{width:.0f}" height="{tall:.0f}" '
1364
1513
  f'style="--w:{width:.0f}px" role="img" aria-label="{_esc(block.title or "How the code fits together")}">'
1365
- f"{_g_captions(graph, xs, widths, column_of)}{edges}{nodes}{ports}</svg></div>"
1514
+ f"{_g_captions(graph, xs, widths, column_of)}{edges}{nodes}{ports}{tips}</svg></div>"
1366
1515
  f'<div class="cgk">{"".join(pills)}{hidden}{hint}</div></div>{"".join(panels.values())}</div>'
1367
1516
  )
1368
1517
 
code_node/report/style.py CHANGED
@@ -95,12 +95,12 @@ _EXTRA_CSS = """\
95
95
  .cnd .flow .oq{font-style:italic}
96
96
  .cnd .flow .os{display:inline-flex;flex-wrap:wrap;gap:3px 5px}
97
97
  .cnd .flow .ob{margin-left:22px}
98
- .cnd :is(.flow,.cside,.walk) .pk{color:var(--jp-mirror-editor-keyword-color,light-dark(#7c3aed,#c792ea))}
99
- .cnd :is(.flow,.cside,.walk) .pf{color:var(--jp-mirror-editor-def-color,light-dark(#2563eb,#82aaff))}
100
- .cnd :is(.flow,.cside,.walk) :is(.js,.jk){color:var(--jp-mirror-editor-string-color,light-dark(#047857,#a5d6a7))}
101
- .cnd :is(.flow,.cside,.walk) :is(.jn,.jl){color:var(--jp-mirror-editor-number-color,light-dark(#c2410c,#f78c6c))}
102
- .cnd :is(.flow,.cside,.walk) .pa{font-style:italic}
103
- .cnd :is(.flow,.cside,.walk) .pc{opacity:.6;font-style:italic}
98
+ .cnd :is(.flow,.cside,.walk,.ctip) .pk{color:var(--jp-mirror-editor-keyword-color,light-dark(#7c3aed,#c792ea))}
99
+ .cnd :is(.flow,.cside,.walk,.ctip) .pf{color:var(--jp-mirror-editor-def-color,light-dark(#2563eb,#82aaff))}
100
+ .cnd :is(.flow,.cside,.walk,.ctip) :is(.js,.jk){color:var(--jp-mirror-editor-string-color,light-dark(#047857,#a5d6a7))}
101
+ .cnd :is(.flow,.cside,.walk,.ctip) :is(.jn,.jl){color:var(--jp-mirror-editor-number-color,light-dark(#c2410c,#f78c6c))}
102
+ .cnd :is(.flow,.cside,.walk,.ctip) .pa{font-style:italic}
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
105
  .cnd .cg{border:1px solid rgba(127,127,127,.2);border-radius:14px;margin:6px 0 8px;overflow:auto;background-color:rgba(127,127,127,.035);background-attachment:local;background-size:18px 18px;
106
106
  background-image:radial-gradient(circle at 1px 1px,rgba(127,127,127,.3) 1px,transparent 0);background-position:9px 9px}
@@ -221,6 +221,34 @@ _EXTRA_CSS = """\
221
221
  .cnd .cgk .kl path{fill:none;stroke:currentColor;stroke-opacity:.8;stroke-width:2.1;stroke-linecap:round;stroke-linejoin:round}
222
222
  .cnd .cg a.cl{cursor:pointer;outline:none}
223
223
  .cnd .cg a.cl:focus-visible .fg{stroke:light-dark(#2563eb,#60a5fa);stroke-opacity:1;stroke-width:2}
224
+ .cnd .cg .ctip{display:none;overflow:visible;pointer-events:none}
225
+ .cnd .cg .ctip .ctx{display:flex;flex-direction:column;height:100%}
226
+ .cnd .cg .ctip.up .ctx{justify-content:flex-end}
227
+ .cnd .cg .ctt{position:relative;box-sizing:border-box;padding:16px 18px;border:1px solid rgba(127,127,127,.3);border-radius:12px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));color:CanvasText;box-shadow:0 2px 6px rgba(15,23,42,.08),0 14px 34px rgba(15,23,42,.18);font-size:14px;line-height:1.6;text-align:left;animation:cnTip .14s ease-out .18s both}
228
+ @keyframes cnTip{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
229
+ @media (prefers-reduced-motion:reduce){.cnd .cg .ctt{animation:none}}
230
+ .cnd .cg .ctt.k-function,.cnd .cg .ctt.k-handler{--c:light-dark(#2563eb,#82aaff)}
231
+ .cnd .cg .cth{display:flex;flex-direction:column;gap:3px;margin:0 0 14px}
232
+ .cnd .cg .ctkd{font-size:10.5px;font-weight:650;letter-spacing:.08em;text-transform:uppercase;color:var(--c,currentColor);opacity:.9}
233
+ .cnd .cg .ctt b.ctn{font-size:16px;font-weight:650;overflow-wrap:anywhere}
234
+ .cnd .cg .ctt b.ctn.mono{font:650 15px var(--mono)}
235
+ .cnd .cg .cts{font-size:12.5px;opacity:.65;overflow-wrap:anywhere}
236
+ .cnd .cg .cts.mono{font-family:var(--mono)}
237
+ .cnd .cg .ctt pre.ctc{margin:0 0 12px;padding:10px 12px;border:1px solid rgba(127,127,127,.2);border-radius:8px;background:rgba(127,127,127,.06);font:13px/1.55 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere}
238
+ .cnd .cg .cto{margin:16px -18px -16px;padding:9px 18px;border-top:1px solid rgba(127,127,127,.16);border-radius:0 0 12px 12px;background:rgba(127,127,127,.05);font-size:12px;opacity:.7}
239
+ .cnd .cg .ctt p{margin:0 0 10px;overflow-wrap:anywhere}
240
+ .cnd .cg .ctt p:last-child{margin-bottom:0}
241
+ .cnd .cg .ctt p.ctd{font-style:italic;opacity:.85}
242
+ .cnd .cg .ctt p.ctp{opacity:.88}
243
+ .cnd .cg .ctt code.ctk{font:12.5px var(--mono);padding:1px 5px;border-radius:4px;background:rgba(127,127,127,.12);overflow-wrap:anywhere}
244
+ .cnd .cg .ctt code.ctn{color:light-dark(#0f766e,#7dd3c0)}
245
+ .cnd .cg .ctt ul{margin:0;padding:0;list-style:none}
246
+ .cnd .cg .ctt ul.ctl li{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;padding:6px 0;border-top:1px solid rgba(127,127,127,.14)}
247
+ .cnd .cg .ctt ul.ctl li:first-child{border-top:none}
248
+ .cnd .cg .ctt .ctm{font-size:12px;opacity:.7}
249
+ .cnd .cg .ctt .ctw{margin:12px 0 0;padding:10px 14px;border-radius:8px;background:rgba(245,158,11,.12);border-left:3px solid #f59e0b}
250
+ .cnd .cg .ctt .ctw b{display:block;margin:0 0 4px;font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:light-dark(#b45309,#fbbf24)}
251
+ .cnd .cg .ctt .ctw li{margin:4px 0 0;overflow-wrap:anywhere}
224
252
  .cnd .cside{position:fixed;top:0;right:0;bottom:0;z-index:30;box-sizing:border-box;width:min(660px,94vw);overflow:auto;padding:0 20px 28px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));color:CanvasText;border-left:1px solid rgba(127,127,127,.3);box-shadow:-14px 0 36px rgba(15,23,42,.14);transform:translateX(104%);visibility:hidden;transition:transform .22s ease,visibility 0s linear .22s}
225
253
  .cnd .cside:target{transform:none;visibility:visible;transition:transform .22s ease}
226
254
  .cnd .cside .csh{display:flex;align-items:flex-start;gap:12px;position:sticky;top:0;z-index:1;margin:0 -20px 12px;padding:16px 20px 12px;background:var(--jp-layout-color0,var(--vscode-editor-background,Canvas));border-bottom:1px solid rgba(127,127,127,.2)}
@@ -270,13 +298,27 @@ _EXTRA_CSS = """\
270
298
  .cnd .walk .ws{border-left:1px solid rgba(127,127,127,.2)}
271
299
  .cnd .walk .wch{position:sticky;top:0;z-index:2;padding:12px 16px 9px;font-size:12.5px;font-weight:650;background:var(--bg);border-bottom:1px solid rgba(127,127,127,.14)}
272
300
  .cnd .walk .wr{display:block;width:0;height:0;margin:0;border:0;padding:0;opacity:0;appearance:none;-webkit-appearance:none;pointer-events:none}
273
- .cnd .walk .wil{display:flex;align-items:center;gap:10px;min-height:30px;padding:0 14px 0 4px;cursor:pointer;white-space:nowrap;user-select:none;-webkit-user-select:none;transition:background-color .15s}
301
+ .cnd .walk .wil{display:flex;align-items:center;gap:10px;min-height:30px;padding:0 14px 0 4px;cursor:pointer;white-space:nowrap;user-select:none;-webkit-user-select:none;transition:background-color .2s,box-shadow .2s;animation:cnd-row .22s ease-out}
302
+ @keyframes cnd-row{from{opacity:0;transform:translateY(-3px)}}
303
+ .cnd .walk .wcx{position:absolute;width:0;height:0;margin:0;opacity:0;pointer-events:none}
304
+ .cnd .walk .wtg{position:relative;z-index:1;display:block;height:0}
305
+ .cnd .walk .wtg i{position:absolute;top:0;left:calc(40px + var(--d)*16px);width:16px;height:30px;border-radius:4px;cursor:pointer;opacity:.45;transition:opacity .15s,background-color .15s}
306
+ .cnd .walk .wtg i::before{content:"";position:absolute;left:5px;top:12px;width:5px;height:5px;border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg);transition:transform .2s cubic-bezier(.3,.7,.3,1)}
307
+ .cnd .walk .wtg:hover i{opacity:1;background:rgba(127,127,127,.14)}
308
+ .cnd .walk .wcx:checked+.wtg i{opacity:.8}
309
+ .cnd .walk .wcx:checked+.wtg i::before{transform:translate(-1px,1px) rotate(-45deg)}
310
+ .cnd .walk .wcnt{display:none;flex:none;font-size:11px;padding:0 7px;border-radius:9px;line-height:17px;background:rgba(127,127,127,.14);opacity:.75;animation:cnd-in .2s ease}
311
+ .cnd .walk .wcx:checked+.wtg+.wr+.wil .wcnt{display:inline-block}
274
312
  .cnd .walk .wil:hover{background-color:rgba(127,127,127,.08)}
275
313
  .cnd .walk .wr:checked+.wil{background-color:rgba(245,158,11,.16);box-shadow:inset 3px 0 #f59e0b}
276
314
  .cnd .walk .wr:focus-visible+.wil{outline:2px solid var(--blue);outline-offset:-2px}
277
315
  .cnd .walk .wno{flex:0 0 26px;text-align:right;font:11px var(--mono);opacity:.4;font-variant-numeric:tabular-nums}
278
- .cnd .walk .wr:checked+.wil .wno{opacity:.8}
279
- .cnd .walk .wx{display:flex;align-items:center;align-self:stretch;gap:8px;min-width:0;flex:1;padding-left:calc(var(--d)*16px);background:repeating-linear-gradient(90deg,rgba(127,127,127,.18) 0 1px,transparent 1px 16px) 5px 0/calc(var(--d)*16px) 100% no-repeat}
316
+ .cnd .walk .wr:checked+.wil .wno{opacity:.9;color:#d97706;font-weight:600}
317
+ .cnd .walk .wtx{transition:transform .2s ease}
318
+ .cnd .walk .wr:checked+.wil .wtx{transform:translateX(2px)}
319
+ .cnd .walk .wr:checked+.wil .wdot{animation:cnd-ping 1s ease-out 2}
320
+ @keyframes cnd-ping{from{box-shadow:0 0 0 0 var(--c,rgba(127,127,127,.6))}to{box-shadow:0 0 0 6px transparent}}
321
+ .cnd .walk .wx{display:flex;align-items:center;align-self:stretch;gap:8px;min-width:0;flex:1;padding-left:calc(var(--d)*16px + 18px);background:repeating-linear-gradient(90deg,rgba(127,127,127,.18) 0 1px,transparent 1px 16px) 8px 0/calc(var(--d)*16px) 100% no-repeat}
280
322
  .cnd .walk .wtx{min-width:0;overflow:hidden;text-overflow:ellipsis}
281
323
  .cnd .walk .wtx code{font:12px var(--mono);background:none;padding:0}
282
324
  .cnd .walk :is(.k-def,.k-module) .wtx b{font:650 12.5px var(--mono)}
@@ -284,7 +326,10 @@ _EXTRA_CSS = """\
284
326
  .cnd .walk .wdot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--c,rgba(127,127,127,.6))}
285
327
  .cnd .walk :is(.d-aws-reads,.d-lib-reads){--c:#3b82f6}.cnd .walk :is(.d-aws-writes,.d-lib-writes){--c:#ea580c}
286
328
  .cnd .walk :is(.d-aws-sends,.d-lib-sends,.d-http){--c:#db2777}.cnd .walk :is(.d-aws-runs,.d-lib-runs){--c:#9333ea}.cnd .walk .d-aws-signs{--c:#64748b}
287
- .cnd .walk .wc{display:none;position:absolute;left:var(--lw);right:0;top:0;height:var(--ch);box-sizing:border-box;overflow:auto;padding:14px 22px 12px;white-space:normal;cursor:auto;border-left:1px solid rgba(127,127,127,.2);border-bottom:1px solid rgba(127,127,127,.18);background:var(--bg);animation:cnd-in .18s ease}
329
+ .cnd .walk .wc{display:none;position:absolute;left:var(--lw);right:0;top:0;height:var(--ch);box-sizing:border-box;overflow:auto;padding:14px 22px 12px;white-space:normal;cursor:auto;border-left:1px solid rgba(127,127,127,.2);border-bottom:1px solid rgba(127,127,127,.18);background:var(--bg)}
330
+ .cnd .walk .wc>*{animation:cnd-in .24s ease both}
331
+ .cnd .walk .wc>.whead{animation-delay:.03s}.cnd .walk .wc>.wstk{animation-delay:.06s}.cnd .walk .wc>.wsay{animation-delay:.09s}.cnd .walk .wc>.wthen{animation-delay:.12s}
332
+ .cnd .walk .wfacts li{animation:cnd-in .24s ease both;animation-delay:.12s}.cnd .walk .wfacts li:nth-child(2){animation-delay:.15s}.cnd .walk .wfacts li:nth-child(n+3){animation-delay:.18s}
288
333
  .cnd .walk .wr:checked+.wil+.wc{display:block}
289
334
  .cnd .walk .wnav{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
290
335
  .cnd .walk .wbt{display:inline-flex;align-items:center;gap:5px;padding:4px 14px;border:1px solid rgba(127,127,127,.32);border-radius:8px;font-size:13px;font-weight:600;line-height:20px;cursor:pointer;user-select:none;-webkit-user-select:none;transition:background-color .12s,transform .06s}
@@ -320,7 +365,9 @@ _EXTRA_CSS = """\
320
365
  .cnd .walk :is(.wfm,.wfv){opacity:.6}
321
366
  .cnd .walk .wthen{margin-top:10px;font-size:12px;opacity:.6}
322
367
  .cnd .walk .wf{display:none;position:absolute;left:var(--lw);right:0;top:var(--ch);bottom:0;flex-direction:column;border-left:1px solid rgba(127,127,127,.2);background:var(--bg)}
323
- .cnd .walk .wfn>.wr:checked~.wf{display:flex}
368
+ .cnd .walk .wfn>.wr:checked~.wf{display:flex;animation:cnd-fade .25s ease}
369
+ .cnd .walk .wfh{animation:cnd-in .25s ease}
370
+ @keyframes cnd-fade{from{opacity:0}}
324
371
  .cnd .walk .wfh{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;padding:8px 22px;font-size:12px;border-bottom:1px solid rgba(127,127,127,.12)}
325
372
  .cnd .walk .wfh b{font:650 12.5px var(--mono)}
326
373
  .cnd .walk .wfh span{opacity:.62}
@@ -328,15 +375,16 @@ _EXTRA_CSS = """\
328
375
  .cnd .walk .wfw{position:relative;flex:1;min-height:0;overflow-x:auto;overflow-y:hidden;container-type:size;background:rgba(127,127,127,.03)}
329
376
  .cnd .walk .wfp{position:relative;box-sizing:border-box;width:max-content;min-width:100%;padding:var(--pad) 0;font:12.5px/var(--lh) var(--mono);
330
377
  translate:0 min(0px,max(calc(100cqh - var(--n)*var(--lh) - 2*var(--pad)),calc(36cqh - var(--k,0)*var(--lh))));transition:translate .32s cubic-bezier(.25,.8,.25,1)}
331
- .cnd .walk .wbar{position:absolute;left:0;right:0;top:calc(var(--pad) + var(--k,-9)*var(--lh));height:var(--lh);background:rgba(245,158,11,.2);box-shadow:inset 3px 0 #f59e0b;transition:top .32s cubic-bezier(.25,.8,.25,1)}
378
+ .cnd .walk .wbar{position:absolute;left:0;right:0;top:calc(var(--pad) + var(--k,-9)*var(--lh));height:var(--lh);background:rgba(245,158,11,.2);box-shadow:inset 3px 0 #f59e0b;transition:top .32s cubic-bezier(.25,.8,.25,1);animation:cnd-glow 1.2s ease-in-out 2}
379
+ @keyframes cnd-glow{50%{background:rgba(245,158,11,.3)}}
332
380
  .cnd .walk .sl{position:relative;display:block;padding-right:16px;white-space:pre}
333
381
  .cnd .walk span.sl{opacity:.6}
334
- .cnd .walk label.sl{cursor:pointer}
382
+ .cnd .walk label.sl{cursor:pointer;transition:background-color .15s}
335
383
  .cnd .walk label.sl:hover{background:rgba(127,127,127,.1)}
336
384
  .cnd .walk .sn{display:inline-block;width:3.2em;padding-right:14px;text-align:right;opacity:.42;user-select:none;-webkit-user-select:none}
337
385
  .cnd .walk label.sl .sn{opacity:.8}
338
386
  @media (max-width:860px){.cnd .walk{--lt:40vh;grid-template-columns:minmax(0,1fr);grid-template-rows:var(--lt) 1fr;height:calc(var(--lt) + var(--ch) + 380px)}.cnd .walk .wl{max-height:none}.cnd .walk :is(.wc,.wf){left:0;border-left:0}.cnd .walk .wc{top:var(--lt);border-top:1px solid rgba(127,127,127,.2);padding:12px 16px}.cnd .walk .wf{top:calc(var(--lt) + var(--ch))}.cnd .walk .ws{border-left:0}}
339
- @media (prefers-reduced-motion:reduce){.cnd .walk .wfp,.cnd .walk .wbar,.cnd .walk .wc,.cnd .tabs>.tp{transition:none;animation:none}}"""
387
+ @media (prefers-reduced-motion:reduce){.cnd .walk :is(.wfp,.wbar,.wc,.wc>*,.wfacts li,.wil,.wtx,.wdot,.wcnt,.wf,.wfh,.wtg i,.wtg i::before),.cnd .tabs>.tp{transition:none;animation:none}}"""
340
388
 
341
389
  _CSS = _base_css(_ROOT, _EXTRA_CSS)
342
390
 
code_node/report/walk.py CHANGED
@@ -7,6 +7,11 @@ No JavaScript: each step is a radio button (one group, so once one has focus the
7
7
  Each function's steps sit in a box of their own, with the function's code drawn once at the end of it; the CSS shows
8
8
  the card after the checked button and the code of its box with sibling selectors, so the light slides from line to
9
9
  line as you step, and a step costs the same however many there are.
10
+
11
+ The list folds like a tree: a step with steps under it has a checkbox before it, and its arrow folds them away. Each
12
+ step under it carries the class `wu<step>`, so one rule per fold hides them, whether they sit beside it or inside the
13
+ box of a function it calls (`#cx5:checked ~ .wu5, #cx5:checked ~ * .wu5`). Only the steps' rows are hidden: the
14
+ current one's card and code still show when it's folded away.
10
15
  """
11
16
 
12
17
  from __future__ import annotations
@@ -523,11 +528,29 @@ def _frame_html(index: int, frame: _Frame, rows: list[tuple[int, str]]) -> str:
523
528
  )
524
529
 
525
530
 
526
- def _item_html(stops: list[_Stop], i: int, card: str) -> str:
531
+ def _folds(stops: list[_Stop]) -> tuple[list[int], list[list[int]]]:
532
+ """
533
+ How many steps sit under each stop in the list, and the stops each one sits under, outermost first.
534
+ """
535
+ under = [0] * len(stops)
536
+ above: list[list[int]] = []
537
+ opened: list[int] = []
538
+ for i, stop in enumerate(stops):
539
+ while opened and (stops[opened[-1]].depth >= stop.depth or stops[opened[-1]].chapter != stop.chapter):
540
+ opened.pop()
541
+ for j in opened:
542
+ under[j] += 1
543
+ above.append(list(opened))
544
+ opened.append(i)
545
+ return under, above
546
+
547
+
548
+ def _item_html(stops: list[_Stop], i: int, card: str, under: int = 0, above: tuple[int, ...] = ()) -> str:
527
549
  """
528
550
  A step in the list: its radio button, then its number and what it does in a few words (or, for a function it goes
529
551
  into, the function's name and file, as a heading over its steps) as the button's label, then its card, which shows
530
- (beside the list) while the button is checked.
552
+ (beside the list) while the button is checked. under: how many steps sit under it, folded away by the checkbox and
553
+ arrow before it; above: the steps it sits under, whose folds hide it.
531
554
  """
532
555
  stop = stops[i]
533
556
  step = stop.step
@@ -543,10 +566,18 @@ def _item_html(stops: list[_Stop], i: int, card: str) -> str:
543
566
  dot = f'<span class="wdot d-{said}" title="{_esc(_NOTE_TIPS.get(said, ""))}"></span>' if said in _DOTS else ""
544
567
  text = f'<span class="wtx">{headline}</span>{dot}'
545
568
  checked = " checked" if i == 0 else ""
569
+ hidden_by = "".join(f" wu{j}" for j in above)
570
+ fold = ""
571
+ if under:
572
+ fold = (
573
+ f'<input type="checkbox" class="wcx" id="cx{i}" tabindex="-1"><label class="wtg{hidden_by}" for="cx{i}" '
574
+ f'style="--d:{stop.depth}" title="Fold or unfold the {_plural(under, "step")} under it"><i></i></label>'
575
+ )
576
+ text += f'<span class="wcnt">{_plural(under, "step")}</span>'
546
577
  return (
547
- f'<input type="radio" class="wr" name="{_GROUP}" id="cw{i}"{checked}><label class="wil k-{_esc(step.kind)}" '
548
- f'for="cw{i}" style="--d:{stop.depth}"><span class="wno">{i + 1}</span><span class="wx">{text}</span>'
549
- f"</label>{card}"
578
+ f'{fold}<input type="radio" class="wr" name="{_GROUP}" id="cw{i}"{checked}><label class="wil '
579
+ f'k-{_esc(step.kind)}{hidden_by}" for="cw{i}" style="--d:{stop.depth}"><span class="wno">{i + 1}</span>'
580
+ f'<span class="wx">{text}</span></label>{card}'
550
581
  )
551
582
 
552
583
 
@@ -576,6 +607,7 @@ def _walk_html(block: _Walk, report: _Report) -> str:
576
607
  rows = _frame_rows(frame, highlighted.get(frame.file, []), stops)
577
608
  rows_of.append({number: n for n, (number, _) in enumerate(rows) if number})
578
609
  drawn.append(_frame_html(k, frame, rows))
610
+ under, above = _folds(stops)
579
611
  out: list[str] = []
580
612
  opened: list[int] = [] # the frames whose boxes are open, innermost last
581
613
  chapter = -1
@@ -593,7 +625,9 @@ def _walk_html(block: _Walk, report: _Report) -> str:
593
625
  if not opened or opened[-1] != stop.frame: # into a call
594
626
  out.append('<div class="wfn">')
595
627
  opened.append(stop.frame)
596
- out.append(_item_html(stops, i, _card_html(stops, i, first, docs, block.editor)))
628
+ out.append(_item_html(stops, i, _card_html(stops, i, first, docs, block.editor), under[i], tuple(above[i])))
629
+ if under[i]: # its fold hides the steps under it, beside it or in the boxes of what it calls
630
+ report.rules.add(f".{root} #cx{i}:checked~.wu{i},.{root} #cx{i}:checked~* .wu{i}{{display:none}}")
597
631
  at = rows_of[stop.frame].get(stop.lit)
598
632
  if at is not None: # the line its code lights
599
633
  report.rules.add(f".{root} #cw{i}:checked~.wf{stop.frame}{{--k:{at}}}")
@@ -601,7 +635,8 @@ def _walk_html(block: _Walk, report: _Report) -> str:
601
635
  out.append(drawn[opened.pop()] + "</div>")
602
636
  sub = (
603
637
  f'<div class="wsub">{_plural(len(stops), "step")}, in the order the code runs. Press Next or click a step to '
604
- "light its line in the code; after a click, <kbd>↑</kbd> <kbd>↓</kbd> move through them.</div>"
638
+ "light its line in the code; after a click, <kbd>↑</kbd> <kbd>↓</kbd> move through them. The arrow beside a "
639
+ "step folds the steps under it away.</div>"
605
640
  )
606
641
  return (
607
642
  f'{title}{sub}<div class="walk"><div class="wl" role="radiogroup" aria-label="The steps">{"".join(out)}'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-node-graph
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: See how Python code works without running it: a graph of who calls whom from an entry point, and what each path reaches.
5
5
  Project-URL: Homepage, https://github.com/utkarsh5026/code-node
6
6
  Project-URL: Documentation, https://utkarsh5026.github.io/code-node/
@@ -1,4 +1,4 @@
1
- code_node/__init__.py,sha256=P00enhUnqmeAPpAajqOLb8MJQ7ljxA1RS4mSfcurHPU,1106
1
+ code_node/__init__.py,sha256=RGdvFZ4nq5pe5UXLWQMZaZvneTOwgLS-aGK6TqTBTG4,1106
2
2
  code_node/__main__.py,sha256=0W2bmerF1S96rjlTNJZKVzO4FvVhN6ENzAl451A08sQ,101
3
3
  code_node/cli.py,sha256=GI0vZY1t4Pf7qtZI2coTpWZi-ulQEWNREDJPkTtmxG0,12302
4
4
  code_node/pages.py,sha256=2UOrHUz3G_vAjPO89phJxhF4652FysVLgrLtJ-Iw6p4,3234
@@ -34,11 +34,11 @@ code_node/report/__init__.py,sha256=l3dfZFbLEo-7Fl7O_Rk2lvyAElQnyugSZPI_PzHrG_Q,
34
34
  code_node/report/brands.py,sha256=n32BfFz2d02ZVUHvy8L4wPHBtiD_1FCTKzdR4NyvU88,8372
35
35
  code_node/report/build.py,sha256=F0wdC0ibu3WSAenUsovUWuKwLC0vVxnNYKJVXtU5jRk,4644
36
36
  code_node/report/editor.py,sha256=TRUX6BTB-oe-m5Vf3xM9PppnC99sCLL4PObXkYAGKoU,1466
37
- code_node/report/graph.py,sha256=bLbsB8YxByX7RWH9Hmyc76aq6VHsllgcPyIKQpszW6Y,60090
37
+ code_node/report/graph.py,sha256=YbrVWbymYUL8-KOdsDSRhD2RxCMWbGVEhHD0-MSjX1I,66832
38
38
  code_node/report/outline.py,sha256=3RRxU4FMUZKBVjFDsscKZkvDlj_W4ZeW4DwUztg4wd0,8539
39
- code_node/report/style.py,sha256=rpubo57Qbgp1m4zsn4sfBikkzNSJuRTNXC__07qylEs,31021
39
+ code_node/report/style.py,sha256=a5JesVQaDnqLqmqnuh-ZeayA-VZKTlUoDCPDcoUrK3M,35883
40
40
  code_node/report/tabs.py,sha256=K2XR16owfgEL1kfux61PI9nBcRsgjQurTx1ptMM5Mac,3016
41
- code_node/report/walk.py,sha256=jumSV-x1X0D614V7K0zECCSWk69OkHNR7IuDt8K4ggg,27516
41
+ code_node/report/walk.py,sha256=LWJDGJ3efF0fPWlMO4G4HBQqnNVn2RHXgLeuLrC-WnA,29466
42
42
  code_node/report/logos/README.md,sha256=8Evw8eApl_56QJrwY7LAtGgJMZE0mp0-HIpRz9bu1ME,613
43
43
  code_node/report/logos/anthropic-icon.svg,sha256=-_25zx86lfDTm-ksVXkLE4uQ_ldzLbTd3U0z-wjT-JA,302
44
44
  code_node/report/logos/aws-amplify.svg,sha256=62WXM0XCn4WrQYWcVZFz4ln62xidiqF8eO4ELlgO8hg,531
@@ -171,8 +171,8 @@ code_node/report/logos/small/aws-systems-manager.svg,sha256=9VzzFdTjaRZO9ulR8Ulp
171
171
  code_node/report/logos/small/aws-timestream.svg,sha256=yKdKIj7pbooFqDKClEQxW9h7dYV5mnDn0RCHnLUDf8Q,639
172
172
  code_node/report/logos/small/aws-waf.svg,sha256=l9ZzBZAl2g8bK1-m6A50ga37l5g1t-9Mj70bxWDykWE,591
173
173
  code_node/report/logos/small/aws-xray.svg,sha256=VKg_QtKGZKDCbZvKBRDn7eReuNVbJpduBUVV8MQrZZU,591
174
- code_node_graph-0.1.0.dist-info/METADATA,sha256=h488K0OzyyEL7CfcJxU8_HOogMK3qIP2Qm_tfK3hwuk,7619
175
- code_node_graph-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
176
- code_node_graph-0.1.0.dist-info/entry_points.txt,sha256=eEArp-uk7ZzODIWr6l62rI0-mIA0-mLDaqJEyKikPJw,49
177
- code_node_graph-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
178
- code_node_graph-0.1.0.dist-info/RECORD,,
174
+ code_node_graph-0.3.0.dist-info/METADATA,sha256=N1ODeR5ZEpH4roWDSuYXxMNjQu_q31V2uCuYsQQyTig,7619
175
+ code_node_graph-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
176
+ code_node_graph-0.3.0.dist-info/entry_points.txt,sha256=eEArp-uk7ZzODIWr6l62rI0-mIA0-mLDaqJEyKikPJw,49
177
+ code_node_graph-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
178
+ code_node_graph-0.3.0.dist-info/RECORD,,