jupyter-python-tutor 0.2.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.
- jupyter_python_tutor/__init__.py +16 -0
- jupyter_python_tutor/magics.py +109 -0
- jupyter_python_tutor/renderer.py +837 -0
- jupyter_python_tutor/tracer.py +591 -0
- jupyter_python_tutor-0.2.0.dist-info/METADATA +151 -0
- jupyter_python_tutor-0.2.0.dist-info/RECORD +9 -0
- jupyter_python_tutor-0.2.0.dist-info/WHEEL +5 -0
- jupyter_python_tutor-0.2.0.dist-info/licenses/LICENSE +21 -0
- jupyter_python_tutor-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
"""Render a trace as interactive HTML inside an iframe srcdoc.
|
|
2
|
+
|
|
3
|
+
JupyterLab 4 sanitizes HTML output from untrusted notebooks, stripping
|
|
4
|
+
``<style>``, ``<script>``, ``<iframe>``, ``<button>``, ``<input>``, etc.
|
|
5
|
+
However, when a notebook is **trusted** (either run live by the user, or
|
|
6
|
+
signed with ``jupyter trust``), all HTML passes through unsanitized.
|
|
7
|
+
|
|
8
|
+
This renderer wraps the entire visualization in ``<iframe srcdoc="...">``,
|
|
9
|
+
which:
|
|
10
|
+
- Works in trusted notebooks (live execution + signed notebooks)
|
|
11
|
+
- Contains full HTML with ``<style>``, ``<script>``, ``<input type="range">``
|
|
12
|
+
- Is fully isolated from JupyterLab's CSS/JS
|
|
13
|
+
|
|
14
|
+
Layout (OPT_Mentor / Python Tutor style):
|
|
15
|
+
|
|
16
|
+
┌──────────────────────────────────────────────────────────┐
|
|
17
|
+
│ ⏮ ◀ [====slider====] ▶ ⏭ Step 3 of 15 │ ← nav bar
|
|
18
|
+
├──────────────────────────────────────────────────────────┤
|
|
19
|
+
│ Program Output │
|
|
20
|
+
├──────────────┬───────────────────────────────────────────┤
|
|
21
|
+
│ Code │ Frames │ Objects (heap) │
|
|
22
|
+
│ (current │ (Global frame │ classes, instances, │
|
|
23
|
+
│ line ►) │ + call stack) │ functions, containers │
|
|
24
|
+
│ │ ┌──┐ │ ┌──┐ │
|
|
25
|
+
│ │ │c │───────────│→│ │ Circle instance │ ← SVG arrows
|
|
26
|
+
│ │ └──┘ │ └──┘ │
|
|
27
|
+
└──────────────┴───────────────────────────────────────────┘
|
|
28
|
+
|
|
29
|
+
Key design:
|
|
30
|
+
- Slider navigation (``<input type="range">``) — not number buttons
|
|
31
|
+
- SVG arrows from frame variables to heap objects (like OPT_Mentor/jsPlumb)
|
|
32
|
+
- Single-step view — only current step shown, JS swaps content
|
|
33
|
+
- Resizable code panel (draggable divider)
|
|
34
|
+
- Functions show TRUE name (co_name), not __name__ (functools.wraps-safe)
|
|
35
|
+
- Heap only shows objects reachable from the current call stack
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import html
|
|
40
|
+
import json
|
|
41
|
+
|
|
42
|
+
__all__ = ["render_trace", "render_trace_widget"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _generate_inner_html(steps: list[dict], source_code: str, height: int = 500) -> str:
|
|
46
|
+
"""Generate the full HTML page that goes inside the iframe srcdoc.
|
|
47
|
+
|
|
48
|
+
This is a self-contained HTML page with:
|
|
49
|
+
- CSS for the visualization layout
|
|
50
|
+
- All step data embedded as JSON
|
|
51
|
+
- JavaScript for step navigation, SVG arrows, resizing
|
|
52
|
+
"""
|
|
53
|
+
if not steps:
|
|
54
|
+
return '<!DOCTYPE html><html><body><p style="color:red;">No trace steps to display.</p></body></html>'
|
|
55
|
+
|
|
56
|
+
# Determine source lines
|
|
57
|
+
if source_code:
|
|
58
|
+
source_lines = source_code.splitlines()
|
|
59
|
+
else:
|
|
60
|
+
max_line = max((s.get("line", 0) for s in steps), default=0)
|
|
61
|
+
source_lines = [""] * max_line
|
|
62
|
+
for s in steps:
|
|
63
|
+
ln = s.get("line", 0)
|
|
64
|
+
if 1 <= ln <= len(source_lines) and s.get("source"):
|
|
65
|
+
source_lines[ln - 1] = s["source"]
|
|
66
|
+
|
|
67
|
+
n_steps = len(steps)
|
|
68
|
+
|
|
69
|
+
# Prepare step data as JSON — strip any non-serializable fields
|
|
70
|
+
clean_steps = []
|
|
71
|
+
for step in steps:
|
|
72
|
+
clean_step = {
|
|
73
|
+
"line": step.get("line", 0),
|
|
74
|
+
"stdout": step.get("stdout", ""),
|
|
75
|
+
"call_stack": [],
|
|
76
|
+
"return_value": step.get("return_value"),
|
|
77
|
+
"event": step.get("event", ""),
|
|
78
|
+
}
|
|
79
|
+
for frame_info in step.get("call_stack", []):
|
|
80
|
+
clean_frame = {
|
|
81
|
+
"func": frame_info.get("func", "<module>"),
|
|
82
|
+
"line": frame_info.get("line", 0),
|
|
83
|
+
"simple_vars": frame_info.get("simple_vars", {}),
|
|
84
|
+
"object_refs": [],
|
|
85
|
+
}
|
|
86
|
+
for ref in frame_info.get("object_refs", []):
|
|
87
|
+
clean_ref = {
|
|
88
|
+
"name": ref.get("name", "?"),
|
|
89
|
+
"heap_id": ref.get("heap_id", ""),
|
|
90
|
+
"type": ref.get("type", "object"),
|
|
91
|
+
"repr": ref.get("repr", ""),
|
|
92
|
+
"is_function": ref.get("is_function", False),
|
|
93
|
+
"is_class": ref.get("is_class", False),
|
|
94
|
+
"is_module": ref.get("is_module", False),
|
|
95
|
+
"is_instance": ref.get("is_instance", False),
|
|
96
|
+
"true_name": ref.get("true_name", ""),
|
|
97
|
+
"display_name": ref.get("display_name", ""),
|
|
98
|
+
"details": ref.get("details", {"attrs": [], "items": [], "bases": []}),
|
|
99
|
+
}
|
|
100
|
+
clean_frame["object_refs"].append(clean_ref)
|
|
101
|
+
clean_step["call_stack"].append(clean_frame)
|
|
102
|
+
clean_steps.append(clean_step)
|
|
103
|
+
|
|
104
|
+
steps_json = json.dumps(clean_steps, ensure_ascii=False)
|
|
105
|
+
source_json = json.dumps(source_lines, ensure_ascii=False)
|
|
106
|
+
|
|
107
|
+
return f"""<!DOCTYPE html>
|
|
108
|
+
<html lang="en">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="utf-8">
|
|
111
|
+
<style>
|
|
112
|
+
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
113
|
+
body {{
|
|
114
|
+
font-family: 'DejaVu Sans', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
color: #1f2937;
|
|
117
|
+
background: #fff;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
}}
|
|
120
|
+
.jpt-widget {{
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
border: 1px solid #d4d4d4;
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}}
|
|
127
|
+
/* Nav bar */
|
|
128
|
+
.jpt-navbar {{
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
padding: 4px 10px;
|
|
132
|
+
background: #f9fafb;
|
|
133
|
+
border-bottom: 1px solid #d4d4d4;
|
|
134
|
+
gap: 6px;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
}}
|
|
137
|
+
.jpt-navbar button {{
|
|
138
|
+
padding: 2px 8px;
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
border: 1px solid #d4d4d4;
|
|
141
|
+
border-radius: 3px;
|
|
142
|
+
background: #fff;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
}}
|
|
146
|
+
.jpt-navbar button:hover {{ background: #f0f0f0; }}
|
|
147
|
+
.jpt-navbar button:disabled {{ opacity: 0.4; cursor: default; }}
|
|
148
|
+
.jpt-slider {{
|
|
149
|
+
flex: 1;
|
|
150
|
+
min-width: 100px;
|
|
151
|
+
height: 6px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
accent-color: #3b82f6;
|
|
154
|
+
}}
|
|
155
|
+
.jpt-step-counter {{
|
|
156
|
+
font-size: 11px;
|
|
157
|
+
color: #6b7280;
|
|
158
|
+
min-width: 90px;
|
|
159
|
+
text-align: right;
|
|
160
|
+
}}
|
|
161
|
+
/* Print output */
|
|
162
|
+
.jpt-output-section {{
|
|
163
|
+
border-bottom: 1px solid #d4d4d4;
|
|
164
|
+
background: #fafafa;
|
|
165
|
+
flex-shrink: 0;
|
|
166
|
+
}}
|
|
167
|
+
.jpt-output-label {{
|
|
168
|
+
font-size: 10px;
|
|
169
|
+
color: #6b7280;
|
|
170
|
+
padding: 3px 10px 1px;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
text-transform: uppercase;
|
|
173
|
+
}}
|
|
174
|
+
.jpt-stdout {{
|
|
175
|
+
background: #1e1e1e;
|
|
176
|
+
color: #d4d4d4;
|
|
177
|
+
font-family: 'DejaVu Sans Mono', monospace;
|
|
178
|
+
font-size: 11px;
|
|
179
|
+
padding: 4px 10px;
|
|
180
|
+
margin: 0 10px 4px;
|
|
181
|
+
border-radius: 4px;
|
|
182
|
+
white-space: pre-wrap;
|
|
183
|
+
max-height: 80px;
|
|
184
|
+
overflow-y: auto;
|
|
185
|
+
min-height: 16px;
|
|
186
|
+
}}
|
|
187
|
+
/* Main area */
|
|
188
|
+
.jpt-main {{
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: stretch;
|
|
191
|
+
}}
|
|
192
|
+
.jpt-code-section {{
|
|
193
|
+
flex: 0 0 40%;
|
|
194
|
+
min-width: 30px;
|
|
195
|
+
overflow: auto;
|
|
196
|
+
background: #fafafa;
|
|
197
|
+
position: relative;
|
|
198
|
+
}}
|
|
199
|
+
.jpt-code-section::-webkit-scrollbar {{ width: 8px; height: 8px; }}
|
|
200
|
+
.jpt-code-section::-webkit-scrollbar-thumb {{ background: #cbd5e1; border-radius: 4px; }}
|
|
201
|
+
/* Draggable vertical dividers between columns */
|
|
202
|
+
.jpt-divider {{
|
|
203
|
+
flex: 0 0 6px;
|
|
204
|
+
cursor: col-resize;
|
|
205
|
+
background: #d4d4d4;
|
|
206
|
+
position: relative;
|
|
207
|
+
user-select: none;
|
|
208
|
+
-webkit-user-select: none;
|
|
209
|
+
transition: background 0.15s;
|
|
210
|
+
}}
|
|
211
|
+
.jpt-divider:hover {{ background: #3b82f6; }}
|
|
212
|
+
.jpt-divider.jpt-dragging {{ background: #2563eb; }}
|
|
213
|
+
.jpt-divider.jpt-collapsed {{ background: #9ca3af; cursor: pointer; }}
|
|
214
|
+
.jpt-divider.jpt-collapsed:hover {{ background: #3b82f6; }}
|
|
215
|
+
/* Right panel holds frames-divider-heap, no overflow so SVG arrows stay aligned */
|
|
216
|
+
.jpt-right-panel {{
|
|
217
|
+
flex: 1;
|
|
218
|
+
display: flex;
|
|
219
|
+
min-width: 0;
|
|
220
|
+
position: relative;
|
|
221
|
+
}}
|
|
222
|
+
.jpt-frames-section {{
|
|
223
|
+
flex: 0 0 35%;
|
|
224
|
+
min-width: 30px;
|
|
225
|
+
padding: 6px;
|
|
226
|
+
background: #fafafa;
|
|
227
|
+
}}
|
|
228
|
+
.jpt-heap-section {{
|
|
229
|
+
flex: 1;
|
|
230
|
+
padding: 6px;
|
|
231
|
+
background: #f5f5f5;
|
|
232
|
+
position: relative;
|
|
233
|
+
min-width: 30px;
|
|
234
|
+
}}
|
|
235
|
+
/* Code */
|
|
236
|
+
.jpt-code-line {{
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: baseline;
|
|
239
|
+
padding: 0 4px;
|
|
240
|
+
white-space: pre-wrap;
|
|
241
|
+
word-break: break-all;
|
|
242
|
+
min-height: 18px;
|
|
243
|
+
border-left: 3px solid transparent;
|
|
244
|
+
}}
|
|
245
|
+
.jpt-code-line.jpt-current {{
|
|
246
|
+
background: #fef9c3;
|
|
247
|
+
border-left: 3px solid #facc15;
|
|
248
|
+
}}
|
|
249
|
+
.jpt-arrow {{
|
|
250
|
+
width: 16px; flex: 0 0 16px; text-align: center;
|
|
251
|
+
color: transparent; font-weight: bold; user-select: none;
|
|
252
|
+
}}
|
|
253
|
+
.jpt-current .jpt-arrow {{ color: #dc2626; }}
|
|
254
|
+
.jpt-lineno {{
|
|
255
|
+
width: 28px; flex: 0 0 28px; text-align: right;
|
|
256
|
+
padding-right: 8px; color: #6b7280; font-size: 11px; user-select: none;
|
|
257
|
+
}}
|
|
258
|
+
.jpt-code-text {{ flex: 1; padding-left: 4px; }}
|
|
259
|
+
/* Frames */
|
|
260
|
+
.jpt-frame {{
|
|
261
|
+
border: 1px solid #bae6fd; border-radius: 4px; background: #f0f9ff;
|
|
262
|
+
margin-bottom: 4px; overflow: hidden;
|
|
263
|
+
}}
|
|
264
|
+
.jpt-frame-header {{
|
|
265
|
+
background: #e0f2fe; padding: 3px 8px; font-size: 12px;
|
|
266
|
+
font-weight: 600; color: #0369a1;
|
|
267
|
+
display: flex; justify-content: space-between;
|
|
268
|
+
}}
|
|
269
|
+
.jpt-frame-line {{ font-size: 10px; font-weight: normal; color: #64748b; }}
|
|
270
|
+
.jpt-frame-body {{ padding: 3px 8px; }}
|
|
271
|
+
.jpt-var-row {{
|
|
272
|
+
display: flex; align-items: center; padding: 1px 0; font-size: 12px; gap: 2px;
|
|
273
|
+
}}
|
|
274
|
+
.jpt-var-name {{
|
|
275
|
+
font-family: 'DejaVu Sans Mono', monospace; color: #3b82f6;
|
|
276
|
+
font-weight: 600; max-width: 40%; overflow: hidden;
|
|
277
|
+
text-overflow: ellipsis; white-space: nowrap;
|
|
278
|
+
}}
|
|
279
|
+
.jpt-var-eq {{ color: #6b7280; }}
|
|
280
|
+
.jpt-var-value {{
|
|
281
|
+
font-family: 'DejaVu Sans Mono', monospace; color: #1f2937;
|
|
282
|
+
word-break: break-all; flex: 1; overflow: hidden; text-overflow: ellipsis;
|
|
283
|
+
}}
|
|
284
|
+
.jpt-var-pointer {{
|
|
285
|
+
display: inline-flex; align-items: center; background: #eef2ff;
|
|
286
|
+
border: 1px solid #818cf8; border-radius: 3px; padding: 0 5px;
|
|
287
|
+
font-family: 'DejaVu Sans Mono', monospace; font-size: 11px;
|
|
288
|
+
cursor: pointer; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
|
289
|
+
white-space: nowrap; position: relative;
|
|
290
|
+
}}
|
|
291
|
+
.jpt-pointer-arrow {{ color: #4f46e5; font-weight: bold; margin-right: 3px; }}
|
|
292
|
+
.jpt-pointer-label {{ color: #3730a3; overflow: hidden; text-overflow: ellipsis; }}
|
|
293
|
+
.jpt-frame.jpt-global {{
|
|
294
|
+
border-color: #fde047; background: #fefce8;
|
|
295
|
+
}}
|
|
296
|
+
.jpt-global .jpt-frame-header {{ background: #fef9c3; color: #854d0e; }}
|
|
297
|
+
.jpt-return-badge {{
|
|
298
|
+
display: inline-block; background: #dcfce7; color: #16a34a;
|
|
299
|
+
padding: 1px 6px; border-radius: 3px; font-size: 11px;
|
|
300
|
+
font-family: 'DejaVu Sans Mono', monospace; margin-top: 2px;
|
|
301
|
+
}}
|
|
302
|
+
/* Heap objects */
|
|
303
|
+
.jpt-heap-header {{
|
|
304
|
+
font-size: 12px; font-weight: 600; color: #374151;
|
|
305
|
+
padding: 2px 4px 6px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
306
|
+
}}
|
|
307
|
+
.jpt-heap-object {{
|
|
308
|
+
border: 1px solid #e5e5e5; border-radius: 4px; background: #fff;
|
|
309
|
+
padding: 4px 8px; margin-bottom: 6px; font-size: 12px; position: relative;
|
|
310
|
+
}}
|
|
311
|
+
.jpt-heap-class {{ border-color: #7c3aed; background: #faf5ff; }}
|
|
312
|
+
.jpt-heap-instance {{ border-color: #059669; background: #ecfdf5; }}
|
|
313
|
+
.jpt-heap-module {{ border-color: #d97706; background: #fffbeb; }}
|
|
314
|
+
.jpt-heap-func {{ border-color: #2563eb; background: #eff6ff; }}
|
|
315
|
+
.jpt-heap-type-label {{ font-size: 11px; font-weight: 600; color: #6b7280; margin-bottom: 2px; }}
|
|
316
|
+
.jpt-heap-name {{ color: #3b82f6; font-weight: 600; }}
|
|
317
|
+
.jpt-heap-extends {{ color: #6b7280; font-size: 11px; font-style: italic; }}
|
|
318
|
+
.jpt-attr-row {{ display: flex; padding: 1px 0; font-size: 11px; }}
|
|
319
|
+
.jpt-attr-key {{
|
|
320
|
+
font-family: 'DejaVu Sans Mono', monospace; color: #3b82f6;
|
|
321
|
+
font-weight: 500; min-width: 70px; flex: 0 0 auto; padding-right: 4px;
|
|
322
|
+
}}
|
|
323
|
+
.jpt-attr-val {{
|
|
324
|
+
font-family: 'DejaVu Sans Mono', monospace; color: #1f2937; flex: 1;
|
|
325
|
+
}}
|
|
326
|
+
.jpt-heap-renamed {{
|
|
327
|
+
font-size: 10px; color: #9ca3af; font-style: italic; margin-top: 2px;
|
|
328
|
+
}}
|
|
329
|
+
.jpt-heap-empty {{
|
|
330
|
+
color: #9ca3af; font-style: italic; font-size: 12px; padding: 8px; text-align: center;
|
|
331
|
+
}}
|
|
332
|
+
/* SVG arrow overlay */
|
|
333
|
+
.jpt-svg-overlay {{
|
|
334
|
+
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
335
|
+
pointer-events: none; z-index: 10;
|
|
336
|
+
}}
|
|
337
|
+
.jpt-arrow-path {{ stroke: #6b7280; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }}
|
|
338
|
+
.jpt-arrow-path-highlight {{ stroke: #3b82f6; stroke-width: 2; }}
|
|
339
|
+
</style>
|
|
340
|
+
</head>
|
|
341
|
+
<body>
|
|
342
|
+
<div class="jpt-widget" id="jptWidget">
|
|
343
|
+
<div class="jpt-navbar">
|
|
344
|
+
<button id="btnFirst" title="First">⏮</button>
|
|
345
|
+
<button id="btnPrev" title="Previous">◀</button>
|
|
346
|
+
<input type="range" class="jpt-slider" id="slider" min="0" max="{n_steps - 1}" value="0">
|
|
347
|
+
<button id="btnNext" title="Next">▶</button>
|
|
348
|
+
<button id="btnLast" title="Last">⏭</button>
|
|
349
|
+
<span class="jpt-step-counter" id="stepCounter">Step 1 of {n_steps}</span>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="jpt-output-section">
|
|
352
|
+
<div class="jpt-output-label">Program Output</div>
|
|
353
|
+
<div class="jpt-stdout" id="stdout"></div>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="jpt-main">
|
|
356
|
+
<div class="jpt-code-section" id="codeSection">
|
|
357
|
+
<div id="codeDisplay"></div>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="jpt-divider" id="divider1" title="Drag to resize, double-click to collapse"></div>
|
|
360
|
+
<div class="jpt-right-panel" id="rightPanel">
|
|
361
|
+
<div class="jpt-svg-overlay">
|
|
362
|
+
<svg width="100%" height="100%" id="svgOverlay">
|
|
363
|
+
<defs>
|
|
364
|
+
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
|
365
|
+
<polygon points="0 0, 8 3, 0 6" fill="#6b7280"/>
|
|
366
|
+
</marker>
|
|
367
|
+
</defs>
|
|
368
|
+
</svg>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="jpt-frames-section" id="framesSection"></div>
|
|
371
|
+
<div class="jpt-divider" id="divider2" title="Drag to resize, double-click to collapse"></div>
|
|
372
|
+
<div class="jpt-heap-section" id="heapSection">
|
|
373
|
+
<div id="heapContent"></div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<script>
|
|
379
|
+
(function() {{
|
|
380
|
+
var steps = {steps_json};
|
|
381
|
+
var sourceLines = {source_json};
|
|
382
|
+
var nSteps = steps.length;
|
|
383
|
+
var curStep = 0;
|
|
384
|
+
|
|
385
|
+
var slider = document.getElementById('slider');
|
|
386
|
+
var stepCounter = document.getElementById('stepCounter');
|
|
387
|
+
var stdout = document.getElementById('stdout');
|
|
388
|
+
var codeDisplay = document.getElementById('codeDisplay');
|
|
389
|
+
var framesSection = document.getElementById('framesSection');
|
|
390
|
+
var heapContent = document.getElementById('heapContent');
|
|
391
|
+
var svgOverlay = document.getElementById('svgOverlay');
|
|
392
|
+
var btnFirst = document.getElementById('btnFirst');
|
|
393
|
+
var btnPrev = document.getElementById('btnPrev');
|
|
394
|
+
var btnNext = document.getElementById('btnNext');
|
|
395
|
+
var btnLast = document.getElementById('btnLast');
|
|
396
|
+
|
|
397
|
+
function escapeHtml(s) {{
|
|
398
|
+
if (s === null || s === undefined) return '';
|
|
399
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
400
|
+
}}
|
|
401
|
+
|
|
402
|
+
function funcDisplayName(ref) {{
|
|
403
|
+
if (ref.true_name) return ref.true_name;
|
|
404
|
+
return ref.name || 'func';
|
|
405
|
+
}}
|
|
406
|
+
|
|
407
|
+
function renderCode(lineNo) {{
|
|
408
|
+
if (!lineNo || lineNo < 1) lineNo = 1;
|
|
409
|
+
var html = '';
|
|
410
|
+
for (var i = 0; i < sourceLines.length; i++) {{
|
|
411
|
+
var ln = i + 1;
|
|
412
|
+
var line = sourceLines[i] || '';
|
|
413
|
+
var escaped = line.trim() ? escapeHtml(line) : ' ';
|
|
414
|
+
var cur = (ln === lineNo) ? ' jpt-current' : '';
|
|
415
|
+
html += '<div class="jpt-code-line' + cur + '">' +
|
|
416
|
+
'<span class="jpt-arrow">▶</span>' +
|
|
417
|
+
'<span class="jpt-lineno">' + ln + '</span>' +
|
|
418
|
+
'<span class="jpt-code-text">' + escaped + '</span>' +
|
|
419
|
+
'</div>';
|
|
420
|
+
}}
|
|
421
|
+
codeDisplay.innerHTML = html;
|
|
422
|
+
}}
|
|
423
|
+
|
|
424
|
+
function renderFrames(step) {{
|
|
425
|
+
var html = '<div>';
|
|
426
|
+
var stack = step.call_stack || [];
|
|
427
|
+
if (stack.length === 0) {{
|
|
428
|
+
html += '<div class="jpt-frame jpt-global"><div class="jpt-frame-header"><span>Global frame</span></div>' +
|
|
429
|
+
'<div class="jpt-frame-body"><div style="color:#9ca3af;font-style:italic;">(no variables)</div></div></div>';
|
|
430
|
+
}} else {{
|
|
431
|
+
for (var fi = 0; fi < stack.length; fi++) {{
|
|
432
|
+
var frame = stack[fi];
|
|
433
|
+
var isGlobal = (frame.func === '<module>');
|
|
434
|
+
var frameClass = isGlobal ? 'jpt-frame jpt-global' : 'jpt-frame';
|
|
435
|
+
var headerText = isGlobal ? 'Global frame' : escapeHtml(frame.func) + ' frame';
|
|
436
|
+
html += '<div class="' + frameClass + '">';
|
|
437
|
+
html += '<div class="jpt-frame-header"><span>' + headerText + '</span>' +
|
|
438
|
+
'<span class="jpt-frame-line">line ' + frame.line + '</span></div>';
|
|
439
|
+
html += '<div class="jpt-frame-body">';
|
|
440
|
+
var sv = frame.simple_vars || {{}};
|
|
441
|
+
var keys = Object.keys(sv).sort();
|
|
442
|
+
for (var ki = 0; ki < keys.length; ki++) {{
|
|
443
|
+
var name = keys[ki];
|
|
444
|
+
html += '<div class="jpt-var-row">' +
|
|
445
|
+
'<span class="jpt-var-name">' + escapeHtml(name) + '</span>' +
|
|
446
|
+
'<span class="jpt-var-eq">=</span>' +
|
|
447
|
+
'<span class="jpt-var-value">' + escapeHtml(sv[name]) + '</span></div>';
|
|
448
|
+
}}
|
|
449
|
+
var refs = frame.object_refs || [];
|
|
450
|
+
for (var ri = 0; ri < refs.length; ri++) {{
|
|
451
|
+
var ref = refs[ri];
|
|
452
|
+
var heapId = escapeHtml(ref.heap_id || '');
|
|
453
|
+
var typeLabel;
|
|
454
|
+
if (ref.is_function) {{
|
|
455
|
+
typeLabel = 'func ' + escapeHtml(funcDisplayName(ref));
|
|
456
|
+
}} else if (ref.is_class) {{
|
|
457
|
+
typeLabel = 'class ' + escapeHtml(ref.name);
|
|
458
|
+
}} else if (ref.is_module) {{
|
|
459
|
+
typeLabel = 'module ' + escapeHtml(ref.name);
|
|
460
|
+
}} else if (ref.is_instance) {{
|
|
461
|
+
typeLabel = escapeHtml(ref.type || 'instance');
|
|
462
|
+
}} else {{
|
|
463
|
+
typeLabel = escapeHtml(ref.type || 'object');
|
|
464
|
+
}}
|
|
465
|
+
html += '<div class="jpt-var-row">' +
|
|
466
|
+
'<span class="jpt-var-name">' + escapeHtml(ref.name) + '</span>' +
|
|
467
|
+
'<span class="jpt-var-eq">=</span>' +
|
|
468
|
+
'<span class="jpt-var-pointer" data-heap-id="' + heapId + '" title="' + escapeHtml(ref.repr) + '">' +
|
|
469
|
+
'<span class="jpt-pointer-arrow">→</span>' +
|
|
470
|
+
'<span class="jpt-pointer-label">' + typeLabel + '</span></span></div>';
|
|
471
|
+
}}
|
|
472
|
+
if (keys.length === 0 && refs.length === 0) {{
|
|
473
|
+
html += '<div class="jpt-var-row" style="color:#9ca3af;font-style:italic;">(no variables)</div>';
|
|
474
|
+
}}
|
|
475
|
+
html += '</div></div>';
|
|
476
|
+
}}
|
|
477
|
+
}}
|
|
478
|
+
if (step.return_value !== null && step.return_value !== undefined) {{
|
|
479
|
+
html += '<div class="jpt-return-badge">Return: ' + escapeHtml(step.return_value) + '</div>';
|
|
480
|
+
}}
|
|
481
|
+
html += '</div>';
|
|
482
|
+
framesSection.innerHTML = html;
|
|
483
|
+
}}
|
|
484
|
+
|
|
485
|
+
function renderHeap(step) {{
|
|
486
|
+
var html = '<div class="jpt-heap-header">Objects</div>';
|
|
487
|
+
var allRefs = [];
|
|
488
|
+
var stack = step.call_stack || [];
|
|
489
|
+
for (var fi = 0; fi < stack.length; fi++) {{
|
|
490
|
+
var refs = stack[fi].object_refs || [];
|
|
491
|
+
for (var ri = 0; ri < refs.length; ri++) {{
|
|
492
|
+
allRefs.push(refs[ri]);
|
|
493
|
+
}}
|
|
494
|
+
}}
|
|
495
|
+
if (allRefs.length === 0) {{
|
|
496
|
+
html += '<div class="jpt-heap-empty">No objects</div>';
|
|
497
|
+
heapContent.innerHTML = html;
|
|
498
|
+
return;
|
|
499
|
+
}}
|
|
500
|
+
var seenIds = {{}};
|
|
501
|
+
var unique = [];
|
|
502
|
+
for (var i = 0; i < allRefs.length; i++) {{
|
|
503
|
+
var id = allRefs[i].heap_id;
|
|
504
|
+
if (id && !seenIds[id]) {{
|
|
505
|
+
seenIds[id] = 1;
|
|
506
|
+
unique.push(allRefs[i]);
|
|
507
|
+
}}
|
|
508
|
+
}}
|
|
509
|
+
for (var i = 0; i < unique.length; i++) {{
|
|
510
|
+
var ref = unique[i];
|
|
511
|
+
var heapId = escapeHtml(ref.heap_id || '');
|
|
512
|
+
var details = ref.details || {{attrs:[], items:[], bases:[]}};
|
|
513
|
+
if (ref.is_class) {{
|
|
514
|
+
var bases = details.bases || [];
|
|
515
|
+
var extendsStr = bases.length ? ' <span class="jpt-heap-extends">extends ' + escapeHtml(bases.join(', ')) + '</span>' : '';
|
|
516
|
+
var attrs = details.attrs || [];
|
|
517
|
+
var attrHtml = '';
|
|
518
|
+
for (var ai = 0; ai < Math.min(attrs.length, 10); ai++) {{
|
|
519
|
+
attrHtml += '<div class="jpt-attr-row"><span class="jpt-attr-key">' + escapeHtml(attrs[ai].name) + '</span><span class="jpt-attr-val">method</span></div>';
|
|
520
|
+
}}
|
|
521
|
+
html += '<div class="jpt-heap-object jpt-heap-class" data-heap-id="' + heapId + '">' +
|
|
522
|
+
'<div class="jpt-heap-type-label"><span class="jpt-heap-name">' + escapeHtml(ref.name) + '</span> class' + extendsStr + '</div>' +
|
|
523
|
+
attrHtml + '</div>';
|
|
524
|
+
}} else if (ref.is_module) {{
|
|
525
|
+
html += '<div class="jpt-heap-object jpt-heap-module" data-heap-id="' + heapId + '">' +
|
|
526
|
+
'<div class="jpt-heap-type-label">module ' + escapeHtml(ref.name) + '</div></div>';
|
|
527
|
+
}} else if (ref.is_function) {{
|
|
528
|
+
var tn = ref.true_name || ref.name;
|
|
529
|
+
var dn = ref.display_name || ref.name;
|
|
530
|
+
var attrs = details.attrs || [];
|
|
531
|
+
var attrHtml = '';
|
|
532
|
+
for (var ai = 0; ai < attrs.length; ai++) {{
|
|
533
|
+
attrHtml += '<div class="jpt-attr-row"><span class="jpt-attr-key">' + escapeHtml(attrs[ai].name) + '</span><span class="jpt-attr-val">' + escapeHtml(attrs[ai].value_repr || '') + '</span></div>';
|
|
534
|
+
}}
|
|
535
|
+
var renamedNote = '';
|
|
536
|
+
if (dn && tn && dn !== tn) {{
|
|
537
|
+
renamedNote = '<div class="jpt-heap-renamed">Renamed by functools.wraps: ' + escapeHtml(tn) + ' → ' + escapeHtml(dn) + '</div>';
|
|
538
|
+
}}
|
|
539
|
+
html += '<div class="jpt-heap-object jpt-heap-func" data-heap-id="' + heapId + '">' +
|
|
540
|
+
'<div class="jpt-heap-type-label"><span class="jpt-heap-name">' + escapeHtml(tn) + '</span> function</div>' +
|
|
541
|
+
attrHtml + renamedNote + '</div>';
|
|
542
|
+
}} else if (ref.is_instance) {{
|
|
543
|
+
var attrs = details.attrs || [];
|
|
544
|
+
var attrHtml = '';
|
|
545
|
+
for (var ai = 0; ai < attrs.length; ai++) {{
|
|
546
|
+
attrHtml += '<div class="jpt-attr-row"><span class="jpt-attr-key">' + escapeHtml(attrs[ai].name) + '</span><span class="jpt-attr-val">' + escapeHtml(attrs[ai].value_repr || '') + '</span></div>';
|
|
547
|
+
}}
|
|
548
|
+
html += '<div class="jpt-heap-object jpt-heap-instance" data-heap-id="' + heapId + '">' +
|
|
549
|
+
'<div class="jpt-heap-type-label"><span class="jpt-heap-name">' + escapeHtml(ref.type || 'instance') + '</span> instance</div>' +
|
|
550
|
+
attrHtml + '</div>';
|
|
551
|
+
}} else {{
|
|
552
|
+
var items = details.items || [];
|
|
553
|
+
var itemHtml = '';
|
|
554
|
+
for (var ii = 0; ii < items.length; ii++) {{
|
|
555
|
+
itemHtml += '<div class="jpt-attr-row"><span class="jpt-attr-key">' + escapeHtml(items[ii].key) + '</span><span class="jpt-attr-val">' + escapeHtml(items[ii].value_repr || '') + '</span></div>';
|
|
556
|
+
}}
|
|
557
|
+
html += '<div class="jpt-heap-object" data-heap-id="' + heapId + '">' +
|
|
558
|
+
'<div class="jpt-heap-type-label">' + escapeHtml(ref.type || 'object') + '</div>' +
|
|
559
|
+
itemHtml + '</div>';
|
|
560
|
+
}}
|
|
561
|
+
}}
|
|
562
|
+
heapContent.innerHTML = html;
|
|
563
|
+
}}
|
|
564
|
+
|
|
565
|
+
function drawArrows() {{
|
|
566
|
+
// Clear existing arrows (keep defs)
|
|
567
|
+
while (svgOverlay.lastChild && svgOverlay.lastChild.tagName !== 'defs') {{
|
|
568
|
+
svgOverlay.removeChild(svgOverlay.lastChild);
|
|
569
|
+
}}
|
|
570
|
+
// SVG overlay lives in .jpt-right-panel, spanning both frames and heap
|
|
571
|
+
var rightPanel = document.getElementById('rightPanel');
|
|
572
|
+
var panelRect = rightPanel.getBoundingClientRect();
|
|
573
|
+
// Find all pointer boxes in frames and matching heap objects
|
|
574
|
+
var pointers = framesSection.querySelectorAll('.jpt-var-pointer[data-heap-id]');
|
|
575
|
+
for (var i = 0; i < pointers.length; i++) {{
|
|
576
|
+
var ptr = pointers[i];
|
|
577
|
+
var heapId = ptr.getAttribute('data-heap-id');
|
|
578
|
+
if (!heapId) continue;
|
|
579
|
+
var heapObj = heapContent.querySelector('.jpt-heap-object[data-heap-id="' + heapId + '"]');
|
|
580
|
+
if (!heapObj) continue;
|
|
581
|
+
var ptrRect = ptr.getBoundingClientRect();
|
|
582
|
+
var objRect = heapObj.getBoundingClientRect();
|
|
583
|
+
// Convert to SVG coordinates (relative to rightPanel)
|
|
584
|
+
var x1 = ptrRect.right - panelRect.left;
|
|
585
|
+
var y1 = ptrRect.top + ptrRect.height / 2 - panelRect.top;
|
|
586
|
+
var x2 = objRect.left - panelRect.left;
|
|
587
|
+
var y2 = objRect.top + objRect.height / 2 - panelRect.top;
|
|
588
|
+
// Draw a curved path from pointer to heap object
|
|
589
|
+
var midX = x1 + (x2 - x1) * 0.5;
|
|
590
|
+
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
591
|
+
path.setAttribute('d', 'M ' + x1 + ' ' + y1 + ' C ' + midX + ' ' + y1 + ', ' + midX + ' ' + y2 + ', ' + x2 + ' ' + y2);
|
|
592
|
+
path.setAttribute('class', 'jpt-arrow-path');
|
|
593
|
+
svgOverlay.appendChild(path);
|
|
594
|
+
}}
|
|
595
|
+
}}
|
|
596
|
+
|
|
597
|
+
function autoResize() {{
|
|
598
|
+
// Auto-resize widget to fit content — the widget's height follows
|
|
599
|
+
// the tallest of code/frames/heap so there is no internal scroll
|
|
600
|
+
// and arrows never detach from their targets.
|
|
601
|
+
// The parent page polls the widget height and resizes the iframe.
|
|
602
|
+
var codeH = codeDisplay.scrollHeight;
|
|
603
|
+
var framesH = framesSection.scrollHeight;
|
|
604
|
+
var heapH = heapContent.scrollHeight;
|
|
605
|
+
var contentH = Math.max(codeH, framesH, heapH, {height});
|
|
606
|
+
// Add navbar + output section heights
|
|
607
|
+
var navbarH = document.querySelector('.jpt-navbar').offsetHeight;
|
|
608
|
+
var outputH = document.querySelector('.jpt-output-section').offsetHeight;
|
|
609
|
+
var totalH = contentH + navbarH + outputH + 12; // +12 for borders/padding
|
|
610
|
+
// Set widget height so parent script can read it
|
|
611
|
+
document.getElementById('jptWidget').style.height = totalH + 'px';
|
|
612
|
+
}}
|
|
613
|
+
|
|
614
|
+
function renderStep(idx) {{
|
|
615
|
+
if (idx < 0) idx = 0;
|
|
616
|
+
if (idx >= nSteps) idx = nSteps - 1;
|
|
617
|
+
curStep = idx;
|
|
618
|
+
var step = steps[idx];
|
|
619
|
+
renderCode(step.line || 0);
|
|
620
|
+
renderFrames(step);
|
|
621
|
+
renderHeap(step);
|
|
622
|
+
stdout.textContent = step.stdout || '';
|
|
623
|
+
if (!step.stdout) stdout.style.color = '#6b7280', stdout.style.fontStyle = 'italic', stdout.textContent = '(no output yet)';
|
|
624
|
+
else stdout.style.color = '#d4d4d4', stdout.style.fontStyle = 'normal';
|
|
625
|
+
stepCounter.textContent = 'Step ' + (idx + 1) + ' of ' + nSteps;
|
|
626
|
+
slider.value = idx;
|
|
627
|
+
btnFirst.disabled = (idx === 0);
|
|
628
|
+
btnPrev.disabled = (idx === 0);
|
|
629
|
+
btnNext.disabled = (idx === nSteps - 1);
|
|
630
|
+
btnLast.disabled = (idx === nSteps - 1);
|
|
631
|
+
// Draw arrows + auto-resize after DOM is updated
|
|
632
|
+
requestAnimationFrame(function() {{
|
|
633
|
+
requestAnimationFrame(function() {{
|
|
634
|
+
autoResize();
|
|
635
|
+
drawArrows();
|
|
636
|
+
}});
|
|
637
|
+
}});
|
|
638
|
+
// Safety: also redraw after a longer delay in case rAF fires too early
|
|
639
|
+
setTimeout(function() {{
|
|
640
|
+
autoResize();
|
|
641
|
+
drawArrows();
|
|
642
|
+
}}, 100);
|
|
643
|
+
}}
|
|
644
|
+
|
|
645
|
+
// Event handlers
|
|
646
|
+
slider.addEventListener('input', function() {{ renderStep(parseInt(slider.value)); }});
|
|
647
|
+
btnFirst.addEventListener('click', function() {{ renderStep(0); }});
|
|
648
|
+
btnPrev.addEventListener('click', function() {{ renderStep(curStep - 1); }});
|
|
649
|
+
btnNext.addEventListener('click', function() {{ renderStep(curStep + 1); }});
|
|
650
|
+
btnLast.addEventListener('click', function() {{ renderStep(nSteps - 1); }});
|
|
651
|
+
|
|
652
|
+
// Keyboard navigation
|
|
653
|
+
document.addEventListener('keydown', function(e) {{
|
|
654
|
+
if (e.key === 'ArrowLeft') {{ renderStep(curStep - 1); e.preventDefault(); }}
|
|
655
|
+
else if (e.key === 'ArrowRight') {{ renderStep(curStep + 1); e.preventDefault(); }}
|
|
656
|
+
}});
|
|
657
|
+
|
|
658
|
+
// Redraw arrows + auto-resize on window resize and scroll
|
|
659
|
+
window.addEventListener('resize', function() {{ autoResize(); drawArrows(); }});
|
|
660
|
+
framesSection.addEventListener('scroll', drawArrows);
|
|
661
|
+
heapContent.addEventListener('scroll', drawArrows);
|
|
662
|
+
|
|
663
|
+
// ─── Draggable dividers ──────────────────────────────────────────
|
|
664
|
+
// Divider 1: between code section and right panel
|
|
665
|
+
// Divider 2: between frames section and heap section
|
|
666
|
+
var divider1 = document.getElementById('divider1');
|
|
667
|
+
var divider2 = document.getElementById('divider2');
|
|
668
|
+
var codeSection = document.getElementById('codeSection');
|
|
669
|
+
var heapSection = document.getElementById('heapSection');
|
|
670
|
+
var mainEl = document.querySelector('.jpt-main');
|
|
671
|
+
var rightPanelEl = document.getElementById('rightPanel');
|
|
672
|
+
|
|
673
|
+
// Remember pre-collapse flex values so we can restore
|
|
674
|
+
var savedCodeFlex = '0 0 40%';
|
|
675
|
+
var savedFramesFlex = '0 0 35%';
|
|
676
|
+
|
|
677
|
+
function startDrag(divider, opts) {{
|
|
678
|
+
return function(e) {{
|
|
679
|
+
// Don't start drag if it was a double-click (collapse toggle)
|
|
680
|
+
if (divider._justCollapsed) {{
|
|
681
|
+
divider._justCollapsed = false;
|
|
682
|
+
return;
|
|
683
|
+
}}
|
|
684
|
+
e.preventDefault();
|
|
685
|
+
divider.classList.add('jpt-dragging');
|
|
686
|
+
var startX = e.clientX;
|
|
687
|
+
var startCodeW = codeSection.offsetWidth;
|
|
688
|
+
var startFramesW = framesSection.offsetWidth;
|
|
689
|
+
var startHeapW = heapSection.offsetWidth;
|
|
690
|
+
var mainW = mainEl.offsetWidth;
|
|
691
|
+
var panelW = rightPanelEl.offsetWidth;
|
|
692
|
+
|
|
693
|
+
function onMove(ev) {{
|
|
694
|
+
var dx = ev.clientX - startX;
|
|
695
|
+
if (divider === divider1) {{
|
|
696
|
+
// Resize code vs (divider + right panel)
|
|
697
|
+
var newCodeW = Math.max(30, Math.min(mainW - 60, startCodeW + dx));
|
|
698
|
+
var pct = (newCodeW / mainW) * 100;
|
|
699
|
+
codeSection.style.flex = '0 0 ' + pct + '%';
|
|
700
|
+
// Ensure right panel takes remaining space
|
|
701
|
+
rightPanelEl.style.flex = '1';
|
|
702
|
+
}} else {{
|
|
703
|
+
// Resize frames vs heap within right panel
|
|
704
|
+
var newFramesW = Math.max(30, Math.min(panelW - 36, startFramesW + dx));
|
|
705
|
+
var fPct = (newFramesW / panelW) * 100;
|
|
706
|
+
framesSection.style.flex = '0 0 ' + fPct + '%';
|
|
707
|
+
heapSection.style.flex = '1';
|
|
708
|
+
}}
|
|
709
|
+
// Live redraw: arrows must follow the new column positions
|
|
710
|
+
drawArrows();
|
|
711
|
+
autoResize();
|
|
712
|
+
}}
|
|
713
|
+
|
|
714
|
+
function onUp(ev) {{
|
|
715
|
+
divider.classList.remove('jpt-dragging');
|
|
716
|
+
document.removeEventListener('mousemove', onMove);
|
|
717
|
+
document.removeEventListener('mouseup', onUp);
|
|
718
|
+
// Save flex values for collapse restore
|
|
719
|
+
if (divider === divider1) {{
|
|
720
|
+
savedCodeFlex = codeSection.style.flex || '0 0 40%';
|
|
721
|
+
}} else {{
|
|
722
|
+
savedFramesFlex = framesSection.style.flex || '0 0 35%';
|
|
723
|
+
}}
|
|
724
|
+
drawArrows();
|
|
725
|
+
autoResize();
|
|
726
|
+
}}
|
|
727
|
+
|
|
728
|
+
document.addEventListener('mousemove', onMove);
|
|
729
|
+
document.addEventListener('mouseup', onUp);
|
|
730
|
+
}};
|
|
731
|
+
}}
|
|
732
|
+
|
|
733
|
+
// Double-click to collapse/expand
|
|
734
|
+
function toggleCollapse(divider, sectionToHide, savedFlexVar, flexVarName) {{
|
|
735
|
+
return function(e) {{
|
|
736
|
+
e.preventDefault();
|
|
737
|
+
e.stopPropagation();
|
|
738
|
+
var isCollapsed = divider.classList.contains('jpt-collapsed');
|
|
739
|
+
if (isCollapsed) {{
|
|
740
|
+
// Expand: restore saved flex
|
|
741
|
+
divider.classList.remove('jpt-collapsed');
|
|
742
|
+
sectionToHide.style.display = '';
|
|
743
|
+
if (divider === divider1) {{
|
|
744
|
+
codeSection.style.flex = savedCodeFlex;
|
|
745
|
+
rightPanelEl.style.flex = '1';
|
|
746
|
+
}} else {{
|
|
747
|
+
framesSection.style.flex = savedFramesFlex;
|
|
748
|
+
heapSection.style.flex = '1';
|
|
749
|
+
}}
|
|
750
|
+
}} else {{
|
|
751
|
+
// Collapse: save current flex then hide
|
|
752
|
+
if (divider === divider1) {{
|
|
753
|
+
savedCodeFlex = codeSection.style.flex || '0 0 40%';
|
|
754
|
+
}} else {{
|
|
755
|
+
savedFramesFlex = framesSection.style.flex || '0 0 35%';
|
|
756
|
+
}}
|
|
757
|
+
divider.classList.add('jpt-collapsed');
|
|
758
|
+
sectionToHide.style.display = 'none';
|
|
759
|
+
// Let the other side take full space
|
|
760
|
+
if (divider === divider1) {{
|
|
761
|
+
codeSection.style.flex = '0 0 0';
|
|
762
|
+
rightPanelEl.style.flex = '1';
|
|
763
|
+
}} else {{
|
|
764
|
+
framesSection.style.flex = '0 0 0';
|
|
765
|
+
heapSection.style.flex = '1';
|
|
766
|
+
}}
|
|
767
|
+
}}
|
|
768
|
+
divider._justCollapsed = true;
|
|
769
|
+
drawArrows();
|
|
770
|
+
autoResize();
|
|
771
|
+
}};
|
|
772
|
+
}}
|
|
773
|
+
|
|
774
|
+
divider1.addEventListener('mousedown', startDrag(divider1, {{}}));
|
|
775
|
+
divider2.addEventListener('mousedown', startDrag(divider2, {{}}));
|
|
776
|
+
divider1.addEventListener('dblclick', toggleCollapse(divider1, codeSection, savedCodeFlex, 'savedCodeFlex'));
|
|
777
|
+
divider2.addEventListener('dblclick', toggleCollapse(divider2, framesSection, savedFramesFlex, 'savedFramesFlex'));
|
|
778
|
+
|
|
779
|
+
// Initial render
|
|
780
|
+
renderStep(0);
|
|
781
|
+
}})();
|
|
782
|
+
</script>
|
|
783
|
+
</body>
|
|
784
|
+
</html>"""
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def render_trace(
|
|
788
|
+
steps: list[dict],
|
|
789
|
+
source_code: str | None = None,
|
|
790
|
+
height: int = 500,
|
|
791
|
+
widget_id: str | None = None,
|
|
792
|
+
) -> str:
|
|
793
|
+
"""Render trace steps as interactive HTML inside an iframe.
|
|
794
|
+
|
|
795
|
+
The entire visualization (HTML + CSS + JS) is wrapped in
|
|
796
|
+
``<iframe srcdoc="...">`` which works in trusted JupyterLab notebooks.
|
|
797
|
+
|
|
798
|
+
Features:
|
|
799
|
+
- Slider navigation (``<input type="range">``) — drag to scrub through steps
|
|
800
|
+
- SVG arrows from frame variables to heap objects (like OPT_Mentor)
|
|
801
|
+
- Single-step view — only current step shown, JS swaps content
|
|
802
|
+
- Resizable code panel (CSS ``resize: horizontal``)
|
|
803
|
+
- Functions show TRUE name (co_name), not __name__ (functools.wraps-safe)
|
|
804
|
+
- Heap only shows objects reachable from the current call stack
|
|
805
|
+
- Keyboard navigation (← → arrows)
|
|
806
|
+
"""
|
|
807
|
+
inner = _generate_inner_html(steps, source_code or "", height)
|
|
808
|
+
# Escape for srcdoc attribute
|
|
809
|
+
escaped = html.escape(inner, quote=True)
|
|
810
|
+
# The iframe auto-resizes via a polling script in the parent that checks
|
|
811
|
+
# each iframe's content height and adjusts. This works in trusted notebooks
|
|
812
|
+
# where <script> survives sanitization.
|
|
813
|
+
return (
|
|
814
|
+
f'<iframe srcdoc="{escaped}" class="jpt-iframe" '
|
|
815
|
+
f'style="width:100%;border:1px solid #d4d4d4;border-radius:6px;"></iframe>'
|
|
816
|
+
f'<script>(function(){{'
|
|
817
|
+
f'var frame=this.previousElementSibling;'
|
|
818
|
+
f'function resize(){{'
|
|
819
|
+
f'try{{'
|
|
820
|
+
f'var d=frame.contentDocument;'
|
|
821
|
+
f'if(!d)return;'
|
|
822
|
+
f'var w=d.getElementById("jptWidget");'
|
|
823
|
+
f'if(!w)return;'
|
|
824
|
+
f'var h=w.offsetHeight+20;'
|
|
825
|
+
f'if(h>0&&frame.offsetHeight!==h){{frame.style.height=h+"px";}}'
|
|
826
|
+
f'}}catch(e){{}}'
|
|
827
|
+
f'}}'
|
|
828
|
+
f'resize();'
|
|
829
|
+
f'setInterval(resize,200);'
|
|
830
|
+
f'}}).call(document.currentScript);'
|
|
831
|
+
f'</script>'
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def render_trace_widget(steps: list[dict], source_code: str | None = None) -> str:
|
|
836
|
+
"""Alias for render_trace (returns HTML string)."""
|
|
837
|
+
return render_trace(steps, source_code=source_code)
|