hyperbook 0.96.0 → 0.96.2

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.
@@ -1,153 +1,41 @@
1
- /// <reference path="../hyperbook.types.js" />
2
-
3
- /**
4
- * Python IDE with code execution.
5
- * @type {HyperbookPython}
6
- * @memberof hyperbook
7
- * @see hyperbook.store
8
- * @see hyperbook.i18n
9
- */
10
- hyperbook.python = (function () {
11
- const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
12
-
13
- const loadPyodideScript = () => {
14
- if (window.loadPyodide) {
15
- return Promise.resolve();
16
- }
17
-
18
- return new Promise((resolve, reject) => {
19
- const script = document.createElement("script");
20
- script.src = PYODIDE_CDN;
21
- script.onload = () => resolve();
22
- script.onerror = () => reject(new Error("Failed to load Pyodide"));
23
- document.head.appendChild(script);
24
- });
25
- };
26
-
27
- const pyodideReadyPromise = (async () => {
28
- await loadPyodideScript();
29
- return window.loadPyodide;
30
- })();
31
-
32
- /**
33
- * @type {Map<string, any>}
34
- */
35
- const runtimes = new Map();
36
- /**
37
- * @type {Map<string, Set<string>>}
38
- */
39
- const installedMicropipPackages = new Map();
40
- /**
41
- * @type {Map<string, any>}
42
- */
43
- const turtleModules = new Map();
44
-
45
- /**
46
- * @type {Map<string, { running: boolean, stopping: boolean, stopRequested: boolean, type: "run" | "test" | null }>}
47
- */
48
- const executionStates = new Map();
49
- /**
50
- * @type {Map<string, Int32Array>}
51
- */
52
- const interruptBuffers = new Map();
53
-
54
- const getExecutionState = (id) => {
1
+ "use strict";
2
+ (() => {
3
+ // assets/directive-pyide/src/state.js
4
+ var runtimes = /* @__PURE__ */ new Map();
5
+ var installedMicropipPackages = /* @__PURE__ */ new Map();
6
+ var turtleModules = /* @__PURE__ */ new Map();
7
+ var executionStates = /* @__PURE__ */ new Map();
8
+ var interruptBuffers = /* @__PURE__ */ new Map();
9
+ var pytamaroStdoutCarry = /* @__PURE__ */ new Map();
10
+ var getExecutionState = (id) => {
55
11
  if (!executionStates.has(id)) {
56
12
  executionStates.set(id, {
57
13
  running: false,
58
14
  stopping: false,
59
15
  stopRequested: false,
60
- type: null,
16
+ type: null
61
17
  });
62
18
  }
63
19
  return executionStates.get(id);
64
20
  };
65
21
 
66
- const getRuntime = async (id) => {
67
- if (runtimes.has(id)) {
68
- return runtimes.get(id);
69
- }
70
- const loadPyodide = await pyodideReadyPromise;
71
- const pyodide = await loadPyodide();
72
- if (typeof pyodide.registerJsModule === "function") {
73
- const turtleModule = createTurtleJsFFI(id);
74
- turtleModule.__setPyodide(pyodide);
75
- pyodide.registerJsModule("turtle", turtleModule);
76
- pyodide.registerJsModule("jturtle", turtleModule);
77
- pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
78
- turtleModules.set(id, turtleModule);
79
- }
80
- if (
81
- typeof SharedArrayBuffer !== "undefined" &&
82
- window.crossOriginIsolated &&
83
- typeof pyodide.setInterruptBuffer === "function"
84
- ) {
85
- const interruptBuffer = new Int32Array(new SharedArrayBuffer(4));
86
- pyodide.setInterruptBuffer(interruptBuffer);
87
- interruptBuffers.set(id, interruptBuffer);
88
- }
89
- runtimes.set(id, pyodide);
90
- return pyodide;
91
- };
92
-
93
- const PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
94
- const PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
95
- /**
96
- * @type {Map<string, string>}
97
- */
98
- const pytamaroStdoutCarry = new Map();
99
- const pytamaroCanvasTargets = new Set();
22
+ // assets/directive-pyide/src/constants.js
23
+ var PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
24
+ var PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
25
+ var PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
100
26
 
101
- const getOutput = (id) => {
27
+ // assets/directive-pyide/src/output.js
28
+ var getOutput = (id) => {
102
29
  return document.getElementById(id)?.getElementsByClassName("output")[0];
103
30
  };
104
-
105
- const getCanvas = (id) => {
106
- return document.getElementById(id)?.getElementsByClassName("canvas")[0];
107
- };
108
-
109
- const setPytamaroCanvasTarget = (id, enabled) => {
110
- if (enabled) {
111
- pytamaroCanvasTargets.add(id);
112
- } else {
113
- pytamaroCanvasTargets.delete(id);
114
- }
115
- };
116
-
117
- const renderPytamaroDataUri = (id, container, dataUri) => {
118
- if (id && pytamaroCanvasTargets.has(id)) {
119
- const canvas = getCanvas(id);
120
- const context = canvas?.getContext?.("2d");
121
- if (canvas && context) {
122
- const img = new Image();
123
- img.onload = () => {
124
- const width = Math.max(1, img.naturalWidth || img.width);
125
- const height = Math.max(1, img.naturalHeight || img.height);
126
- canvas.width = width;
127
- canvas.height = height;
128
- context.clearRect(0, 0, width, height);
129
- context.drawImage(img, 0, 0, width, height);
130
- };
131
- img.onerror = () => {
132
- appendOutputErrorLine(id, "Failed to render pytamaro graphic.");
133
- };
134
- img.src = dataUri;
135
- return;
136
- }
137
- }
138
-
31
+ var renderPytamaroDataUri = (id, container, dataUri) => {
139
32
  const img = document.createElement("img");
140
33
  img.src = dataUri;
141
34
  img.style.maxWidth = "100%";
142
35
  img.style.display = "block";
143
36
  container.appendChild(img);
144
37
  };
145
-
146
- /**
147
- * Renders a message that may contain pytamaro data URI image markers into
148
- * the given container, creating <img> elements for each embedded image.
149
- */
150
- const renderOutputSegments = (container, message, id = null) => {
38
+ var renderOutputSegments = (container, message, id = null) => {
151
39
  let remaining = String(message);
152
40
  while (remaining.length > 0) {
153
41
  const beginIdx = remaining.indexOf(PYTAMARO_URI_BEGIN);
@@ -156,7 +44,9 @@ hyperbook.python = (function () {
156
44
  break;
157
45
  }
158
46
  if (beginIdx > 0) {
159
- container.appendChild(document.createTextNode(remaining.slice(0, beginIdx)));
47
+ container.appendChild(
48
+ document.createTextNode(remaining.slice(0, beginIdx))
49
+ );
160
50
  }
161
51
  const afterBegin = remaining.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
162
52
  const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
@@ -169,12 +59,7 @@ hyperbook.python = (function () {
169
59
  remaining = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
170
60
  }
171
61
  };
172
-
173
- const scriptLooksLikeTurtle = (script) => {
174
- return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(String(script || ""));
175
- };
176
-
177
- const getTrailingPrefixLength = (text, marker) => {
62
+ var getTrailingPrefixLength = (text, marker) => {
178
63
  const max = Math.min(text.length, marker.length - 1);
179
64
  for (let len = max; len > 0; len -= 1) {
180
65
  if (text.endsWith(marker.slice(0, len))) {
@@ -183,61 +68,64 @@ hyperbook.python = (function () {
183
68
  }
184
69
  return 0;
185
70
  };
186
-
187
- const appendText = (output, text) => {
71
+ var appendText = (output, text) => {
188
72
  if (!text) return;
189
73
  output.appendChild(document.createTextNode(text));
190
74
  };
191
-
192
- const appendOutputLine = (id, message) => {
75
+ var appendOutputLine = (id, message) => {
193
76
  const output = getOutput(id);
194
77
  if (!output) return;
195
78
  const msg = String(message ?? "");
196
79
  const carry = pytamaroStdoutCarry.get(id) || "";
197
80
  let combined = carry + msg;
198
81
  pytamaroStdoutCarry.delete(id);
199
-
200
- // Fast path for regular stdout chunks.
201
82
  if (!combined.includes(PYTAMARO_URI_BEGIN) && carry.length === 0) {
202
- const partialBeginLength = getTrailingPrefixLength(combined, PYTAMARO_URI_BEGIN);
83
+ const partialBeginLength = getTrailingPrefixLength(
84
+ combined,
85
+ PYTAMARO_URI_BEGIN
86
+ );
203
87
  if (partialBeginLength > 0) {
204
88
  const visible = combined.slice(0, combined.length - partialBeginLength);
205
89
  appendText(output, visible);
206
- pytamaroStdoutCarry.set(id, combined.slice(combined.length - partialBeginLength));
90
+ pytamaroStdoutCarry.set(
91
+ id,
92
+ combined.slice(combined.length - partialBeginLength)
93
+ );
207
94
  } else {
208
95
  appendText(output, combined);
209
96
  }
210
97
  return;
211
98
  }
212
-
213
99
  while (combined.length > 0) {
214
100
  const beginIdx = combined.indexOf(PYTAMARO_URI_BEGIN);
215
101
  if (beginIdx === -1) {
216
- const partialBeginLength = getTrailingPrefixLength(combined, PYTAMARO_URI_BEGIN);
102
+ const partialBeginLength = getTrailingPrefixLength(
103
+ combined,
104
+ PYTAMARO_URI_BEGIN
105
+ );
217
106
  const visible = combined.slice(0, combined.length - partialBeginLength);
218
107
  appendText(output, visible);
219
108
  if (partialBeginLength > 0) {
220
- pytamaroStdoutCarry.set(id, combined.slice(combined.length - partialBeginLength));
109
+ pytamaroStdoutCarry.set(
110
+ id,
111
+ combined.slice(combined.length - partialBeginLength)
112
+ );
221
113
  }
222
114
  break;
223
115
  }
224
-
225
116
  appendText(output, combined.slice(0, beginIdx));
226
117
  const afterBegin = combined.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
227
118
  const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
228
119
  if (endIdx === -1) {
229
- // Keep incomplete marker and continue when the next stdout chunk arrives.
230
120
  pytamaroStdoutCarry.set(id, combined.slice(beginIdx));
231
121
  break;
232
122
  }
233
-
234
123
  const dataUri = afterBegin.slice(0, endIdx);
235
124
  renderPytamaroDataUri(id, output, dataUri);
236
125
  combined = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
237
126
  }
238
127
  };
239
-
240
- const appendOutputErrorLine = (id, message) => {
128
+ var appendOutputErrorLine = (id, message) => {
241
129
  const output = getOutput(id);
242
130
  if (!output) return;
243
131
  const line = document.createElement("span");
@@ -245,17 +133,13 @@ hyperbook.python = (function () {
245
133
  line.textContent = String(message);
246
134
  output.appendChild(line);
247
135
  };
248
-
249
- // ─── Python Friendly Error Messages ────────────────────────────────────────
250
- // Loaded from python-friendly-error-messages.js (built from npm package)
251
-
252
136
  if (window.PythonFriendlyErrorMessages) {
253
137
  const { loadCopydeckFor, registerAdapter, pyodideAdapter } = window.PythonFriendlyErrorMessages;
254
- loadCopydeckFor("en");
138
+ const lang = document.documentElement.lang || "en";
139
+ loadCopydeckFor(lang);
255
140
  registerAdapter("pyodide", pyodideAdapter);
256
141
  }
257
-
258
- const appendFriendlyError = (output, errorString, code) => {
142
+ var appendFriendlyError = (output, errorString, code) => {
259
143
  if (!output) return;
260
144
  let result = null;
261
145
  if (window.PythonFriendlyErrorMessages) {
@@ -263,9 +147,10 @@ hyperbook.python = (function () {
263
147
  result = window.PythonFriendlyErrorMessages.friendlyExplain({
264
148
  error: String(errorString),
265
149
  code,
266
- runtime: "pyodide",
150
+ runtime: "pyodide"
267
151
  });
268
- } catch {}
152
+ } catch {
153
+ }
269
154
  }
270
155
  if (result?.html) {
271
156
  const div = document.createElement("div");
@@ -279,11 +164,8 @@ hyperbook.python = (function () {
279
164
  output.appendChild(span);
280
165
  }
281
166
  };
282
-
283
- // ───────────────────────────────────────────────────────────────────────────
284
-
285
- const appendOutput = (output, message, isError = false, id = null) => {
286
- if (!output || message === undefined || message === null) return;
167
+ var appendOutput = (output, message, isError = false, id = null) => {
168
+ if (!output || message === void 0 || message === null) return;
287
169
  if (isError) {
288
170
  const line = document.createElement("span");
289
171
  line.classList.add("error-line");
@@ -298,75 +180,67 @@ hyperbook.python = (function () {
298
180
  }
299
181
  output.appendChild(document.createTextNode(msg));
300
182
  };
301
-
302
- const clearPytamaroStdoutCarry = (id) => {
183
+ var clearPytamaroStdoutCarry = (id) => {
303
184
  pytamaroStdoutCarry.delete(id);
304
- pytamaroCanvasTargets.delete(id);
305
- };
306
-
307
- const updateFullscreenButtonState = (elem, button) => {
308
- if (!elem || !button) return;
309
- const isFullscreen = document.fullscreenElement === elem;
310
- const label = hyperbook.i18n.get("ide-fullscreen-enter");
311
- button.textContent = "⛶";
312
- button.title = label;
313
- button.setAttribute("aria-label", label);
314
- button.classList.toggle("active", isFullscreen);
315
- };
316
-
317
- const toggleFullscreen = async (elem) => {
318
- if (!elem) return;
319
- if (document.fullscreenElement === elem) {
320
- await document.exitFullscreen();
321
- return;
322
- }
323
- await elem.requestFullscreen();
324
- };
325
-
326
- const syncFullscreenButtons = () => {
327
- const elems = document.getElementsByClassName("directive-pyide");
328
- for (const elem of elems) {
329
- const fullscreen = elem.getElementsByClassName("fullscreen")[0];
330
- updateFullscreenButtonState(elem, fullscreen);
331
- }
332
- };
333
-
334
- const releaseKeyboardCapture = (id) => {
335
- const elem = document.getElementById(id);
336
- if (!elem) return;
337
- const canvas = elem.getElementsByClassName("canvas")[0];
338
- canvas?.blur?.();
339
185
  };
340
186
 
341
- const resetCanvas = (canvas) => {
342
- if (!canvas) return;
343
- const context = canvas.getContext("2d");
344
- context?.clearRect(0, 0, canvas.width, canvas.height);
345
- };
346
-
347
- const createTurtleJsFFI = (id) => {
187
+ // assets/directive-pyide/src/turtle-ffi.js
188
+ var createTurtleJsFFI = (id) => {
348
189
  const DEFAULT_LINE_WIDTH = 1;
349
190
  const DEFAULT_FONT_SIZE = 8;
350
191
  const DEFAULT_SHAPE = "classic";
351
-
352
- // Shape polygons in canvas-local coords (+x = forward, +y = down).
353
- // Derived from CPython/RPi turtle shapes by applying a 90° CCW screen rotation:
354
- // (sx, sy) → (sy, -sx).
355
192
  const TURTLE_SHAPES = {
356
- classic: [[0, 0], [-9, 5], [-7, 0], [-9, -5]],
357
- arrow: [[0, 10], [0, -10], [10, 0]],
358
- triangle: [[-5.77, -10], [11.55, 0], [-5.77, 10]],
359
- square: [[-10, -10], [10, -10], [10, 10], [-10, 10]],
360
- circle: null, // rendered as arc, not polygon
361
- turtle: [
362
- [16, 0], [14, 2], [10, 1], [7, 4], [9, 7], [8, 9],
363
- [5, 6], [1, 7], [-3, 5], [-6, 8], [-8, 6], [-5, 4],
364
- [-7, 0], [-5, -4], [-8, -6], [-6, -8], [-3, -5],
365
- [1, -7], [5, -6], [8, -9], [9, -7], [7, -4], [10, -1], [14, -2],
193
+ classic: [
194
+ [0, 0],
195
+ [-9, 5],
196
+ [-7, 0],
197
+ [-9, -5]
198
+ ],
199
+ arrow: [
200
+ [0, 10],
201
+ [0, -10],
202
+ [10, 0]
366
203
  ],
204
+ triangle: [
205
+ [-5.77, -10],
206
+ [11.55, 0],
207
+ [-5.77, 10]
208
+ ],
209
+ square: [
210
+ [-10, -10],
211
+ [10, -10],
212
+ [10, 10],
213
+ [-10, 10]
214
+ ],
215
+ circle: null,
216
+ // rendered as arc, not polygon
217
+ turtle: [
218
+ [16, 0],
219
+ [14, 2],
220
+ [10, 1],
221
+ [7, 4],
222
+ [9, 7],
223
+ [8, 9],
224
+ [5, 6],
225
+ [1, 7],
226
+ [-3, 5],
227
+ [-6, 8],
228
+ [-8, 6],
229
+ [-5, 4],
230
+ [-7, 0],
231
+ [-5, -4],
232
+ [-8, -6],
233
+ [-6, -8],
234
+ [-3, -5],
235
+ [1, -7],
236
+ [5, -6],
237
+ [8, -9],
238
+ [9, -7],
239
+ [7, -4],
240
+ [10, -1],
241
+ [14, -2]
242
+ ]
367
243
  };
368
-
369
- // ---- Shared canvas/screen state ----
370
244
  let pyodide = null;
371
245
  let canvas = null;
372
246
  let context = null;
@@ -376,7 +250,7 @@ hyperbook.python = (function () {
376
250
  let active = false;
377
251
  let backgroundColor = "#ffffff";
378
252
  let backgroundImage = null;
379
- let colorMode = 1.0;
253
+ let colorMode = 1;
380
254
  let delayMs = 80;
381
255
  let turtleSpeed = 3;
382
256
  let screenWidth = 640;
@@ -386,22 +260,18 @@ hyperbook.python = (function () {
386
260
  let queueRunning = false;
387
261
  const textMeasureCanvas = document.createElement("canvas");
388
262
  const textMeasureContext = textMeasureCanvas.getContext("2d");
389
-
390
- // Registry of all active turtle pens (rendering state objects)
391
263
  const allPens = [];
392
-
393
- // ---- Shared helper functions ----
394
264
  const normalizeAngle = (angle) => {
395
265
  const value = Number(angle) || 0;
396
- return ((value % 360) + 360) % 360;
266
+ return (value % 360 + 360) % 360;
397
267
  };
398
-
399
- const toRadians = (angle) => (Number(angle) * Math.PI) / 180;
268
+ const toRadians = (angle) => Number(angle) * Math.PI / 180;
400
269
  const toCanvasX = (value) => cssWidth / 2 + value;
401
270
  const toCanvasY = (value) => cssHeight / 2 - value;
402
271
  const toPlainNumber = (value, fallback = Number.NaN) => {
403
- if (value === null || value === undefined) return fallback;
404
- if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
272
+ if (value === null || value === void 0) return fallback;
273
+ if (typeof value === "number")
274
+ return Number.isFinite(value) ? value : fallback;
405
275
  if (typeof value === "bigint") return Number(value);
406
276
  if (typeof value.toJs === "function") {
407
277
  return toPlainNumber(value.toJs({ pyproxies: [] }), fallback);
@@ -410,7 +280,7 @@ hyperbook.python = (function () {
410
280
  return Number.isFinite(numeric) ? numeric : fallback;
411
281
  };
412
282
  const toPlainBoolean = (value, fallback = false) => {
413
- if (value === null || value === undefined) return fallback;
283
+ if (value === null || value === void 0) return fallback;
414
284
  if (typeof value === "boolean") return value;
415
285
  if (typeof value === "number") return value !== 0;
416
286
  if (typeof value === "bigint") return value !== 0n;
@@ -429,15 +299,15 @@ hyperbook.python = (function () {
429
299
  const numeric = toPlainNumber(value, Number.NaN);
430
300
  if (!Number.isFinite(numeric) || numeric === 0) {
431
301
  const fallback = DEFAULT_FONT_SIZE;
432
- return { size: `${fallback}pt`, pxApprox: (fallback * 96) / 72 };
302
+ return { size: `${fallback}pt`, pxApprox: fallback * 96 / 72 };
433
303
  }
434
304
  if (numeric < 0) {
435
305
  return { size: `${Math.abs(numeric)}px`, pxApprox: Math.abs(numeric) };
436
306
  }
437
- return { size: `${numeric}pt`, pxApprox: (numeric * 96) / 72 };
307
+ return { size: `${numeric}pt`, pxApprox: numeric * 96 / 72 };
438
308
  };
439
309
  const toPlainString = (value, fallback = "") => {
440
- if (value === null || value === undefined) return fallback;
310
+ if (value === null || value === void 0) return fallback;
441
311
  if (typeof value === "string") return value;
442
312
  if (typeof value.toJs === "function") {
443
313
  return toPlainString(value.toJs({ pyproxies: [] }), fallback);
@@ -445,7 +315,7 @@ hyperbook.python = (function () {
445
315
  return String(value);
446
316
  };
447
317
  const toSequence = (value) => {
448
- if (value === null || value === undefined) return null;
318
+ if (value === null || value === void 0) return null;
449
319
  if (Array.isArray(value)) return value;
450
320
  if (typeof value === "string") return [value];
451
321
  if (typeof value.toJs === "function") {
@@ -454,20 +324,24 @@ hyperbook.python = (function () {
454
324
  if (typeof value[Symbol.iterator] === "function") {
455
325
  try {
456
326
  return Array.from(value);
457
- } catch {}
327
+ } catch {
328
+ }
458
329
  }
459
330
  if (typeof value === "object" && "length" in value) {
460
331
  try {
461
332
  return Array.from(value);
462
- } catch {}
333
+ } catch {
334
+ }
463
335
  }
464
336
  return null;
465
337
  };
466
338
  const toPlainObject = (value) => {
467
- if (value === null || value === undefined) return null;
339
+ if (value === null || value === void 0) return null;
468
340
  if (typeof value.toJs === "function") {
469
341
  try {
470
- return toPlainObject(value.toJs({ pyproxies: [], dict_converter: Object.fromEntries }));
342
+ return toPlainObject(
343
+ value.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
344
+ );
471
345
  } catch {
472
346
  return toPlainObject(value.toJs({ pyproxies: [] }));
473
347
  }
@@ -479,13 +353,7 @@ hyperbook.python = (function () {
479
353
  const isWriteKwargsObject = (value) => {
480
354
  const obj = toPlainObject(value);
481
355
  if (!obj) return false;
482
- return (
483
- hasOwn(obj, "arg") ||
484
- hasOwn(obj, "text") ||
485
- hasOwn(obj, "move") ||
486
- hasOwn(obj, "align") ||
487
- hasOwn(obj, "font")
488
- );
356
+ return hasOwn(obj, "arg") || hasOwn(obj, "text") || hasOwn(obj, "move") || hasOwn(obj, "align") || hasOwn(obj, "font");
489
357
  };
490
358
  const normalizeFontStyle = (value) => {
491
359
  const style = toPlainString(value, "normal").trim().toLowerCase();
@@ -516,7 +384,7 @@ hyperbook.python = (function () {
516
384
  return {
517
385
  font,
518
386
  width: String(text || "").length * pxApprox * 0.6,
519
- descent: pxApprox * 0.2,
387
+ descent: pxApprox * 0.2
520
388
  };
521
389
  }
522
390
  ctx.font = font;
@@ -527,14 +395,8 @@ hyperbook.python = (function () {
527
395
  width: metrics.width,
528
396
  left: metrics.actualBoundingBoxLeft || 0,
529
397
  right: metrics.actualBoundingBoxRight || metrics.width,
530
- ascent:
531
- metrics.fontBoundingBoxAscent ||
532
- metrics.actualBoundingBoxAscent ||
533
- pxApprox * 0.8,
534
- descent:
535
- metrics.fontBoundingBoxDescent ||
536
- metrics.actualBoundingBoxDescent ||
537
- pxApprox * 0.2,
398
+ ascent: metrics.fontBoundingBoxAscent || metrics.actualBoundingBoxAscent || pxApprox * 0.8,
399
+ descent: metrics.fontBoundingBoxDescent || metrics.actualBoundingBoxDescent || pxApprox * 0.2
538
400
  };
539
401
  };
540
402
  const toColorString = (value) => {
@@ -544,12 +406,12 @@ hyperbook.python = (function () {
544
406
  if (value && typeof value.toJs === "function") {
545
407
  return toColorString(value.toJs({ pyproxies: [] }));
546
408
  }
547
- if (Array.isArray(value) || (value && typeof value === "object" && "length" in value)) {
409
+ if (Array.isArray(value) || value && typeof value === "object" && "length" in value) {
548
410
  const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
549
411
  if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
550
412
  throw new Error(`bad color sequence: ${String(value)}`);
551
413
  }
552
- if (colorMode === 1.0) {
414
+ if (colorMode === 1) {
553
415
  if (parts.some((part) => part < 0 || part > 1)) {
554
416
  throw new Error(`bad color sequence: ${String(value)}`);
555
417
  }
@@ -567,7 +429,6 @@ hyperbook.python = (function () {
567
429
  }
568
430
  return "#000000";
569
431
  };
570
-
571
432
  const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
572
433
  const clearQueue = () => {
573
434
  queueGeneration += 1;
@@ -600,7 +461,6 @@ hyperbook.python = (function () {
600
461
  }
601
462
  })();
602
463
  };
603
-
604
464
  const ensureContext = () => {
605
465
  if (!canvas) {
606
466
  canvas = getCanvas(id);
@@ -611,22 +471,19 @@ hyperbook.python = (function () {
611
471
  }
612
472
  return !!context;
613
473
  };
614
-
615
474
  const setupCanvasResolution = () => {
616
475
  if (!ensureContext()) return false;
617
476
  dpr = window.devicePixelRatio || 1;
618
477
  const wrapperRect = canvas.parentElement?.getBoundingClientRect?.();
619
- const panelWidth =
620
- wrapperRect?.width || canvas.parentElement?.clientWidth || canvas.clientWidth || canvas.width || 800;
621
- const panelHeight =
622
- wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
478
+ const panelWidth = wrapperRect?.width || canvas.parentElement?.clientWidth || canvas.clientWidth || canvas.width || 800;
479
+ const panelHeight = wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
623
480
  const width = Math.max(
624
481
  1,
625
- Math.floor(screenWidth !== null ? screenWidth : panelWidth),
482
+ Math.floor(screenWidth !== null ? screenWidth : panelWidth)
626
483
  );
627
484
  const height = Math.max(
628
485
  1,
629
- Math.floor(screenHeight !== null ? screenHeight : panelHeight),
486
+ Math.floor(screenHeight !== null ? screenHeight : panelHeight)
630
487
  );
631
488
  cssWidth = width;
632
489
  cssHeight = height;
@@ -637,7 +494,6 @@ hyperbook.python = (function () {
637
494
  context.setTransform(dpr, 0, 0, dpr, 0, 0);
638
495
  return true;
639
496
  };
640
-
641
497
  const drawBackground = () => {
642
498
  context.save();
643
499
  context.setTransform(dpr, 0, 0, dpr, 0, 0);
@@ -648,10 +504,8 @@ hyperbook.python = (function () {
648
504
  }
649
505
  context.restore();
650
506
  };
651
-
652
507
  const drawPathSegment = (path) => {
653
508
  if (!path?.points?.length) return;
654
-
655
509
  if (path.fill) {
656
510
  context.beginPath();
657
511
  path.points.forEach((point, index) => {
@@ -668,13 +522,12 @@ hyperbook.python = (function () {
668
522
  context.fill();
669
523
  return;
670
524
  }
671
-
672
525
  let startedLine = false;
673
526
  context.beginPath();
674
527
  for (const point of path.points) {
675
528
  const px = toCanvasX(point.x);
676
529
  const py = toCanvasY(point.y);
677
- if (point.text !== undefined) {
530
+ if (point.text !== void 0) {
678
531
  const family = point.fontfamily || path.fontfamily || "Arial";
679
532
  const style = point.fontstyle || path.fontstyle || "normal";
680
533
  const size = point.fontsize ?? path.fontsize ?? DEFAULT_FONT_SIZE;
@@ -704,14 +557,12 @@ hyperbook.python = (function () {
704
557
  }
705
558
  context.lineTo(px, py);
706
559
  }
707
-
708
560
  if (path.down) {
709
561
  context.strokeStyle = path.stroke;
710
562
  context.lineWidth = path.lineWidth || DEFAULT_LINE_WIDTH;
711
563
  context.stroke();
712
564
  }
713
565
  };
714
-
715
566
  const drawTurtleShape = (pen) => {
716
567
  if (!pen.renderedTurtleVisible) return;
717
568
  context.save();
@@ -739,7 +590,6 @@ hyperbook.python = (function () {
739
590
  }
740
591
  context.restore();
741
592
  };
742
-
743
593
  const draw = () => {
744
594
  if (!active) return;
745
595
  if (!setupCanvasResolution()) return;
@@ -751,8 +601,6 @@ hyperbook.python = (function () {
751
601
  drawTurtleShape(pen);
752
602
  }
753
603
  };
754
-
755
- // ---- Per-turtle pen factory ----
756
604
  const createTurtlePen = () => {
757
605
  let x = 0;
758
606
  let y = 0;
@@ -767,8 +615,6 @@ hyperbook.python = (function () {
767
615
  let filling = false;
768
616
  let fillPath = null;
769
617
  let currentPath = null;
770
-
771
- // Mutable rendering state exposed to the shared draw() function
772
618
  const pen = {
773
619
  paths: [],
774
620
  renderedX: 0,
@@ -776,9 +622,8 @@ hyperbook.python = (function () {
776
622
  renderedHeading: 0,
777
623
  renderedTurtleVisible: true,
778
624
  shapeColor: "#000000",
779
- shapeName: DEFAULT_SHAPE,
625
+ shapeName: DEFAULT_SHAPE
780
626
  };
781
-
782
627
  const makePath = (overrides = {}) => ({
783
628
  down: penDown,
784
629
  stroke: strokeColor,
@@ -789,42 +634,36 @@ hyperbook.python = (function () {
789
634
  fill: false,
790
635
  fillstyle: fillColor,
791
636
  points: [{ x, y }],
792
- ...overrides,
637
+ ...overrides
793
638
  });
794
-
795
639
  const beginCurrentPath = () => {
796
640
  currentPath = makePath();
797
641
  pen.paths.push(currentPath);
798
642
  return currentPath;
799
643
  };
800
-
801
644
  const commitStyleToNewPath = () => {
802
645
  currentPath = makePath({
803
646
  down: penDown,
804
- lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
647
+ lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH
805
648
  });
806
649
  pen.paths.push(currentPath);
807
650
  return currentPath;
808
651
  };
809
-
810
652
  const ensurePath = () => {
811
653
  if (!currentPath) {
812
654
  beginCurrentPath();
813
655
  }
814
656
  return currentPath;
815
657
  };
816
-
817
658
  const getPenState = () => ({
818
659
  pendown: penDown,
819
660
  pencolor: strokeColor,
820
661
  fillcolor: fillColor,
821
662
  pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
822
663
  speed: turtleSpeed,
823
- shown: turtleVisible,
664
+ shown: turtleVisible
824
665
  });
825
-
826
666
  beginCurrentPath();
827
-
828
667
  const forward = (distance) => {
829
668
  if (!ensureContext()) return;
830
669
  const path = ensurePath();
@@ -845,7 +684,6 @@ hyperbook.python = (function () {
845
684
  draw();
846
685
  });
847
686
  };
848
-
849
687
  const backward = (distance) => forward(-Number(distance || 0));
850
688
  const right = (angle) => {
851
689
  heading = normalizeAngle(heading - Number(angle || 0));
@@ -877,9 +715,8 @@ hyperbook.python = (function () {
877
715
  if (!ensureContext()) return;
878
716
  let nextX = Number(a);
879
717
  let nextY = Number(b);
880
- if (b === undefined && (Array.isArray(a) || (a && typeof a === "object"))) {
881
- const source =
882
- a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
718
+ if (b === void 0 && (Array.isArray(a) || a && typeof a === "object")) {
719
+ const source = a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
883
720
  nextX = Number(source?.[0] ?? source?.x ?? 0);
884
721
  nextY = Number(source?.[1] ?? source?.y ?? 0);
885
722
  }
@@ -920,7 +757,7 @@ hyperbook.python = (function () {
920
757
  const dx = Number(tx) - x;
921
758
  const dy = Number(ty) - y;
922
759
  if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
923
- return normalizeAngle((Math.atan2(dy, dx) * 180) / Math.PI);
760
+ return normalizeAngle(Math.atan2(dy, dx) * 180 / Math.PI);
924
761
  };
925
762
  const speed = (value = 0) => {
926
763
  const numeric = Number(value);
@@ -968,7 +805,7 @@ hyperbook.python = (function () {
968
805
  fill: true,
969
806
  stroke: "transparent",
970
807
  lineWidth: 1,
971
- fillstyle: fillColor,
808
+ fillstyle: fillColor
972
809
  });
973
810
  pen.paths.push(fillPath);
974
811
  draw();
@@ -990,9 +827,9 @@ hyperbook.python = (function () {
990
827
  x,
991
828
  y,
992
829
  dotRadius: Math.max(0.5, Number(size) || 5) / 2,
993
- color: colorValue ? toColorString(colorValue) : strokeColor,
994
- },
995
- ],
830
+ color: colorValue ? toColorString(colorValue) : strokeColor
831
+ }
832
+ ]
996
833
  });
997
834
  enqueueOperation(() => {
998
835
  pen.paths.push(segment);
@@ -1004,7 +841,7 @@ hyperbook.python = (function () {
1004
841
  const stepCount = Math.max(8, Number(steps) | 0);
1005
842
  const circumference = 2 * Math.PI * Math.abs(numericRadius);
1006
843
  const stepLength = circumference / stepCount;
1007
- const stepTurn = (360 / stepCount) * (numericRadius >= 0 ? 1 : -1);
844
+ const stepTurn = 360 / stepCount * (numericRadius >= 0 ? 1 : -1);
1008
845
  for (let index = 0; index < stepCount; index += 1) {
1009
846
  forward(stepLength);
1010
847
  left(stepTurn);
@@ -1032,7 +869,6 @@ hyperbook.python = (function () {
1032
869
  if (isWriteKwargsObject(writeFont)) writeFont = null;
1033
870
  if (isWriteKwargsObject(writeMove)) writeMove = false;
1034
871
  if (isWriteKwargsObject(writeAlign)) writeAlign = "left";
1035
-
1036
872
  let family = currentFontFamily;
1037
873
  let size = fontSize;
1038
874
  let style = currentFontStyle;
@@ -1040,12 +876,13 @@ hyperbook.python = (function () {
1040
876
  const source = toSequence(writeFont);
1041
877
  if (source?.length) {
1042
878
  if (source[0]) family = toPlainString(source[0], family);
1043
- if (source[1] !== undefined && source[1] !== null) {
879
+ if (source[1] !== void 0 && source[1] !== null) {
1044
880
  size = toPlainNumber(source[1], size);
1045
881
  }
1046
882
  if (source[2]) style = toPlainString(source[2], style);
1047
883
  }
1048
- } catch {}
884
+ } catch {
885
+ }
1049
886
  const normalizedAlign = normalizeTextAlign(writeAlign);
1050
887
  const segment = makePath({
1051
888
  down: false,
@@ -1060,19 +897,23 @@ hyperbook.python = (function () {
1060
897
  fontsize: size,
1061
898
  fontfamily: family,
1062
899
  fontstyle: style,
1063
- color: strokeColor,
1064
- },
1065
- ],
900
+ color: strokeColor
901
+ }
902
+ ]
1066
903
  });
1067
904
  if (toPlainBoolean(writeMove, false)) {
1068
- const metrics = measureTurtleText(String(writeText), family, size, style);
905
+ const metrics = measureTurtleText(
906
+ String(writeText),
907
+ family,
908
+ size,
909
+ style
910
+ );
1069
911
  const textWidth = metrics.width || 0;
1070
912
  if (normalizedAlign === "left") {
1071
913
  x += textWidth;
1072
914
  } else if (normalizedAlign === "center") {
1073
915
  x += textWidth / 2;
1074
916
  }
1075
- // right alignment: turtle stays at its position (text ends there)
1076
917
  commitStyleToNewPath();
1077
918
  }
1078
919
  enqueueOperation(() => {
@@ -1082,7 +923,10 @@ hyperbook.python = (function () {
1082
923
  };
1083
924
  const setfontsize = (value) => {
1084
925
  const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
1085
- fontSize = Math.max(1, Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE);
926
+ fontSize = Math.max(
927
+ 1,
928
+ Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE
929
+ );
1086
930
  commitStyleToNewPath();
1087
931
  draw();
1088
932
  };
@@ -1101,8 +945,8 @@ hyperbook.python = (function () {
1101
945
  });
1102
946
  };
1103
947
  const isvisible = () => turtleVisible;
1104
- const shape = (name = undefined) => {
1105
- if (name === undefined || name === null) {
948
+ const shape = (name = void 0) => {
949
+ if (name === void 0 || name === null) {
1106
950
  return pen.shapeName;
1107
951
  }
1108
952
  const nameStr = toPlainString(name, DEFAULT_SHAPE).toLowerCase().trim();
@@ -1112,14 +956,11 @@ hyperbook.python = (function () {
1112
956
  draw();
1113
957
  return pen.shapeName;
1114
958
  };
1115
- const penFn = (options = undefined) => {
1116
- if (options === undefined) {
959
+ const penFn = (options = void 0) => {
960
+ if (options === void 0) {
1117
961
  return getPenState();
1118
962
  }
1119
- const source =
1120
- options && typeof options.toJs === "function"
1121
- ? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
1122
- : options;
963
+ const source = options && typeof options.toJs === "function" ? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries }) : options;
1123
964
  if (!source || typeof source !== "object") return getPenState();
1124
965
  if ("pendown" in source) {
1125
966
  penDown = !!source.pendown;
@@ -1131,7 +972,10 @@ hyperbook.python = (function () {
1131
972
  fillColor = toColorString(source.fillcolor);
1132
973
  }
1133
974
  if ("pensize" in source) {
1134
- ensurePath().lineWidth = Math.max(1, Number(source.pensize) || DEFAULT_LINE_WIDTH);
975
+ ensurePath().lineWidth = Math.max(
976
+ 1,
977
+ Number(source.pensize) || DEFAULT_LINE_WIDTH
978
+ );
1135
979
  }
1136
980
  if ("shown" in source) {
1137
981
  turtleVisible = !!source.shown;
@@ -1171,8 +1015,7 @@ hyperbook.python = (function () {
1171
1015
  filling = false;
1172
1016
  beginCurrentPath();
1173
1017
  };
1174
-
1175
- const api = {
1018
+ const api2 = {
1176
1019
  forward,
1177
1020
  fd: forward,
1178
1021
  backward,
@@ -1222,15 +1065,12 @@ hyperbook.python = (function () {
1222
1065
  isvisible,
1223
1066
  shape,
1224
1067
  clear,
1225
- reset: resetPen,
1068
+ reset: resetPen
1226
1069
  };
1227
-
1228
- return { pen, api, resetPen };
1070
+ return { pen, api: api2, resetPen };
1229
1071
  };
1230
-
1231
- // Screen-level functions
1232
- const colormode = (mode = undefined) => {
1233
- if (mode === undefined) return colorMode;
1072
+ const colormode = (mode = void 0) => {
1073
+ if (mode === void 0) return colorMode;
1234
1074
  const numeric = Number(mode);
1235
1075
  if (numeric !== 1 && numeric !== 255) {
1236
1076
  throw new Error("colormode must be 1.0 or 255");
@@ -1238,23 +1078,23 @@ hyperbook.python = (function () {
1238
1078
  colorMode = numeric;
1239
1079
  return colorMode;
1240
1080
  };
1241
- const screensize = (canvwidth = undefined, canvheight = undefined, bg = undefined) => {
1242
- if (canvwidth === undefined && canvheight === undefined && bg === undefined) {
1081
+ const screensize = (canvwidth = void 0, canvheight = void 0, bg = void 0) => {
1082
+ if (canvwidth === void 0 && canvheight === void 0 && bg === void 0) {
1243
1083
  return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
1244
1084
  }
1245
- if (canvwidth !== undefined && canvwidth !== null) {
1085
+ if (canvwidth !== void 0 && canvwidth !== null) {
1246
1086
  const width = Math.floor(Number(canvwidth));
1247
1087
  if (Number.isFinite(width) && width > 0) {
1248
1088
  screenWidth = width;
1249
1089
  }
1250
1090
  }
1251
- if (canvheight !== undefined && canvheight !== null) {
1091
+ if (canvheight !== void 0 && canvheight !== null) {
1252
1092
  const height = Math.floor(Number(canvheight));
1253
1093
  if (Number.isFinite(height) && height > 0) {
1254
1094
  screenHeight = height;
1255
1095
  }
1256
1096
  }
1257
- if (bg !== undefined && bg !== null) {
1097
+ if (bg !== void 0 && bg !== null) {
1258
1098
  backgroundColor = toColorString(bg);
1259
1099
  }
1260
1100
  draw();
@@ -1282,7 +1122,7 @@ hyperbook.python = (function () {
1282
1122
  name,
1283
1123
  `/home/pyodide/${name}`,
1284
1124
  `/home/pyodide/files/${name}`,
1285
- `/home/pyodide/uploads/${name}`,
1125
+ `/home/pyodide/uploads/${name}`
1286
1126
  ];
1287
1127
  for (const candidate of candidates) {
1288
1128
  try {
@@ -1298,14 +1138,12 @@ hyperbook.python = (function () {
1298
1138
  image.onerror = () => URL.revokeObjectURL(url);
1299
1139
  image.src = url;
1300
1140
  return;
1301
- } catch {}
1141
+ } catch {
1142
+ }
1302
1143
  }
1303
1144
  };
1304
-
1305
- // Create the default turtle pen and register it
1306
1145
  const defaultPenObj = createTurtlePen();
1307
1146
  allPens.push(defaultPenObj.pen);
1308
-
1309
1147
  const bindCanvas = (nextCanvas) => {
1310
1148
  canvas = nextCanvas || getCanvas(id);
1311
1149
  context = canvas?.getContext?.("2d") || null;
@@ -1314,7 +1152,6 @@ hyperbook.python = (function () {
1314
1152
  }
1315
1153
  draw();
1316
1154
  };
1317
-
1318
1155
  const deactivate = () => {
1319
1156
  active = false;
1320
1157
  clearQueue();
@@ -1323,27 +1160,20 @@ hyperbook.python = (function () {
1323
1160
  pen.renderedTurtleVisible = false;
1324
1161
  }
1325
1162
  };
1326
-
1327
1163
  const resetState = () => {
1328
1164
  clearQueue();
1329
- // Remove all extra turtles, keeping only the default pen
1330
1165
  allPens.length = 0;
1331
1166
  allPens.push(defaultPenObj.pen);
1332
1167
  defaultPenObj.resetPen();
1333
1168
  backgroundColor = "#ffffff";
1334
1169
  backgroundImage = null;
1335
- colorMode = 1.0;
1170
+ colorMode = 1;
1336
1171
  screenWidth = 640;
1337
1172
  screenHeight = 480;
1338
1173
  active = true;
1339
1174
  draw();
1340
1175
  };
1341
-
1342
- // Turtle constructor — creates an additional independent turtle on the same canvas.
1343
- // The pen is registered into allPens via enqueueOperation so it enters the render
1344
- // loop in queue order, preventing the turtle from appearing at center before prior
1345
- // drawing operations have completed.
1346
- const Turtle = function () {
1176
+ const Turtle = function() {
1347
1177
  const penObj = createTurtlePen();
1348
1178
  enqueueOperation(() => {
1349
1179
  allPens.push(penObj.pen);
@@ -1351,7 +1181,6 @@ hyperbook.python = (function () {
1351
1181
  });
1352
1182
  return penObj.api;
1353
1183
  };
1354
-
1355
1184
  const api = {
1356
1185
  __bindCanvas: bindCanvas,
1357
1186
  __deactivate: deactivate,
@@ -1365,16 +1194,15 @@ hyperbook.python = (function () {
1365
1194
  colormode,
1366
1195
  screensize,
1367
1196
  bgcolor,
1368
- bgpic,
1197
+ bgpic
1369
1198
  };
1370
-
1371
1199
  return api;
1372
1200
  };
1373
1201
 
1374
- const createPytamaroJsFFI = () => {
1202
+ // assets/directive-pyide/src/pytamaro-ffi.js
1203
+ var createPytamaroJsFFI = () => {
1375
1204
  const floatBuffer = new ArrayBuffer(4);
1376
1205
  const floatView = new DataView(floatBuffer);
1377
-
1378
1206
  const unProxy = (obj) => {
1379
1207
  if (typeof obj === "object" && obj !== null && typeof obj.toJs === "function") {
1380
1208
  try {
@@ -1386,31 +1214,24 @@ hyperbook.python = (function () {
1386
1214
  }
1387
1215
  return obj;
1388
1216
  };
1389
-
1390
1217
  const uint32ToFloat = (u32) => {
1391
1218
  floatView.setUint32(0, u32 >>> 0, false);
1392
1219
  return floatView.getFloat32(0, false);
1393
1220
  };
1394
-
1395
1221
  const decodePoint = (value, width, height) => {
1396
1222
  const packed = typeof value === "bigint" ? value : BigInt(value || 0);
1397
- const x = uint32ToFloat(Number((packed >> 32n) & 0xffffffffn));
1223
+ const x = uint32ToFloat(Number(packed >> 32n & 0xffffffffn));
1398
1224
  const y = uint32ToFloat(Number(packed & 0xffffffffn));
1399
1225
  return { x: x * width * 0.5, y: -y * height * 0.5 };
1400
1226
  };
1401
-
1402
1227
  const colorToCss = (value) => {
1403
- const argb =
1404
- typeof value === "bigint"
1405
- ? Number(value & 0xffffffffn)
1406
- : Number(value >>> 0);
1407
- const a = ((argb >> 24) & 0xff) / 255;
1408
- const r = (argb >> 16) & 0xff;
1409
- const g = (argb >> 8) & 0xff;
1410
- const b = argb & 0xff;
1228
+ const argb = typeof value === "bigint" ? Number(value & 0xffffffffn) : Number(value >>> 0);
1229
+ const a = (argb >> 24 & 255) / 255;
1230
+ const r = argb >> 16 & 255;
1231
+ const g = argb >> 8 & 255;
1232
+ const b = argb & 255;
1411
1233
  return `rgba(${r}, ${g}, ${b}, ${a})`;
1412
1234
  };
1413
-
1414
1235
  const rotatePoint = (point, pivot, angleRad) => {
1415
1236
  const dx = point.x - pivot.x;
1416
1237
  const dy = point.y - pivot.y;
@@ -1418,31 +1239,22 @@ hyperbook.python = (function () {
1418
1239
  const sin = Math.sin(angleRad);
1419
1240
  return {
1420
1241
  x: pivot.x + dx * cos - dy * sin,
1421
- y: pivot.y + dx * sin + dy * cos,
1242
+ y: pivot.y + dx * sin + dy * cos
1422
1243
  };
1423
1244
  };
1424
-
1425
1245
  const buildGraphic = (specs) => {
1426
1246
  const stack = [];
1427
1247
  const measureCanvas = document.createElement("canvas");
1428
1248
  const measureCtx = measureCanvas.getContext("2d");
1429
-
1430
1249
  for (const spec of specs || []) {
1431
1250
  if (!spec || typeof spec !== "object") continue;
1432
1251
  const type = spec.t;
1433
- if (
1434
- type === "Empty" ||
1435
- type === "Rectangle" ||
1436
- type === "Ellipse" ||
1437
- type === "CircularSector" ||
1438
- type === "Triangle" ||
1439
- type === "Text"
1440
- ) {
1252
+ if (type === "Empty" || type === "Rectangle" || type === "Ellipse" || type === "CircularSector" || type === "Triangle" || type === "Text") {
1441
1253
  let width = 0;
1442
1254
  let height = 0;
1443
1255
  let pin = { x: 0, y: 0 };
1444
- let draw = () => {};
1445
-
1256
+ let draw = () => {
1257
+ };
1446
1258
  if (type === "Rectangle") {
1447
1259
  width = Math.max(0, Number(spec.width) || 0);
1448
1260
  height = Math.max(0, Number(spec.height) || 0);
@@ -1470,7 +1282,7 @@ hyperbook.python = (function () {
1470
1282
  draw = (ctx) => {
1471
1283
  ctx.beginPath();
1472
1284
  ctx.moveTo(0, 0);
1473
- ctx.arc(0, 0, radius, 0, (-angle * Math.PI) / 180, true);
1285
+ ctx.arc(0, 0, radius, 0, -angle * Math.PI / 180, true);
1474
1286
  ctx.closePath();
1475
1287
  ctx.fillStyle = fill;
1476
1288
  ctx.fill();
@@ -1481,14 +1293,17 @@ hyperbook.python = (function () {
1481
1293
  const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
1482
1294
  const p1 = { x: 0, y: 0 };
1483
1295
  const p2 = { x: side1, y: 0 };
1484
- const p3 = { x: side2 * Math.cos(angle), y: -side2 * Math.sin(angle) };
1296
+ const p3 = {
1297
+ x: side2 * Math.cos(angle),
1298
+ y: -side2 * Math.sin(angle)
1299
+ };
1485
1300
  const centroid = {
1486
1301
  x: (p1.x + p2.x + p3.x) / 3,
1487
- y: (p1.y + p2.y + p3.y) / 3,
1302
+ y: (p1.y + p2.y + p3.y) / 3
1488
1303
  };
1489
1304
  const points = [p1, p2, p3].map((p) => ({
1490
1305
  x: p.x - centroid.x,
1491
- y: p.y - centroid.y,
1306
+ y: p.y - centroid.y
1492
1307
  }));
1493
1308
  const xs = points.map((p) => p.x);
1494
1309
  const ys = points.map((p) => p.y);
@@ -1525,11 +1340,9 @@ hyperbook.python = (function () {
1525
1340
  ctx.fillText(text, -width / 2, -centerY);
1526
1341
  };
1527
1342
  }
1528
-
1529
1343
  stack.push({ width, height, pin, draw });
1530
1344
  continue;
1531
1345
  }
1532
-
1533
1346
  if (type === "Pin") {
1534
1347
  const child = stack.pop();
1535
1348
  if (!child) continue;
@@ -1537,17 +1350,16 @@ hyperbook.python = (function () {
1537
1350
  stack.push({ ...child, pin });
1538
1351
  continue;
1539
1352
  }
1540
-
1541
1353
  if (type === "Rotate") {
1542
1354
  const child = stack.pop();
1543
1355
  if (!child) continue;
1544
1356
  const angleDeg = Number(spec.angle) || 0;
1545
- const angleRad = (-angleDeg * Math.PI) / 180;
1357
+ const angleRad = -angleDeg * Math.PI / 180;
1546
1358
  const corners = [
1547
1359
  { x: -child.width / 2, y: -child.height / 2 },
1548
1360
  { x: child.width / 2, y: -child.height / 2 },
1549
1361
  { x: child.width / 2, y: child.height / 2 },
1550
- { x: -child.width / 2, y: child.height / 2 },
1362
+ { x: -child.width / 2, y: child.height / 2 }
1551
1363
  ].map((p) => rotatePoint(p, child.pin, angleRad));
1552
1364
  const minX = Math.min(...corners.map((p) => p.x));
1553
1365
  const maxX = Math.max(...corners.map((p) => p.x));
@@ -1556,7 +1368,6 @@ hyperbook.python = (function () {
1556
1368
  const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1557
1369
  const offset = { x: -center.x, y: -center.y };
1558
1370
  const pin = { x: child.pin.x + offset.x, y: child.pin.y + offset.y };
1559
-
1560
1371
  stack.push({
1561
1372
  width: maxX - minX,
1562
1373
  height: maxY - minY,
@@ -1569,54 +1380,44 @@ hyperbook.python = (function () {
1569
1380
  ctx.translate(-child.pin.x, -child.pin.y);
1570
1381
  child.draw(ctx);
1571
1382
  ctx.restore();
1572
- },
1383
+ }
1573
1384
  });
1574
1385
  continue;
1575
1386
  }
1576
-
1577
1387
  if (type === "Compose") {
1578
1388
  const bg = stack.pop();
1579
1389
  const fg = stack.pop();
1580
1390
  if (!fg || !bg) continue;
1581
- const fgPin = spec.fg_pin
1582
- ? decodePoint(spec.fg_pin, fg.width, fg.height)
1583
- : fg.pin;
1584
- const bgPin = spec.bg_pin
1585
- ? decodePoint(spec.bg_pin, bg.width, bg.height)
1586
- : bg.pin;
1587
-
1391
+ const fgPin = spec.fg_pin ? decodePoint(spec.fg_pin, fg.width, fg.height) : fg.pin;
1392
+ const bgPin = spec.bg_pin ? decodePoint(spec.bg_pin, bg.width, bg.height) : bg.pin;
1588
1393
  const bgCenter = { x: 0, y: 0 };
1589
1394
  const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
1590
1395
  const minX = Math.min(
1591
1396
  fgCenter.x - fg.width / 2,
1592
- bgCenter.x - bg.width / 2,
1397
+ bgCenter.x - bg.width / 2
1593
1398
  );
1594
1399
  const maxX = Math.max(
1595
1400
  fgCenter.x + fg.width / 2,
1596
- bgCenter.x + bg.width / 2,
1401
+ bgCenter.x + bg.width / 2
1597
1402
  );
1598
1403
  const minY = Math.min(
1599
1404
  fgCenter.y - fg.height / 2,
1600
- bgCenter.y - bg.height / 2,
1405
+ bgCenter.y - bg.height / 2
1601
1406
  );
1602
1407
  const maxY = Math.max(
1603
1408
  fgCenter.y + fg.height / 2,
1604
- bgCenter.y + bg.height / 2,
1409
+ bgCenter.y + bg.height / 2
1605
1410
  );
1606
-
1607
1411
  const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1608
1412
  const fgOffset = {
1609
1413
  x: fgCenter.x - center.x,
1610
- y: fgCenter.y - center.y,
1414
+ y: fgCenter.y - center.y
1611
1415
  };
1612
1416
  const bgOffset = {
1613
1417
  x: bgCenter.x - center.x,
1614
- y: bgCenter.y - center.y,
1418
+ y: bgCenter.y - center.y
1615
1419
  };
1616
- const pin = spec.pin
1617
- ? decodePoint(spec.pin, maxX - minX, maxY - minY)
1618
- : { x: bgPin.x - center.x, y: bgPin.y - center.y };
1619
-
1420
+ const pin = spec.pin ? decodePoint(spec.pin, maxX - minX, maxY - minY) : { x: bgPin.x - center.x, y: bgPin.y - center.y };
1620
1421
  stack.push({
1621
1422
  width: maxX - minX,
1622
1423
  height: maxY - minY,
@@ -1630,44 +1431,28 @@ hyperbook.python = (function () {
1630
1431
  ctx.translate(fgOffset.x, fgOffset.y);
1631
1432
  fg.draw(ctx);
1632
1433
  ctx.restore();
1633
- },
1434
+ }
1634
1435
  });
1635
1436
  }
1636
1437
  }
1637
-
1638
- return (
1639
- stack[stack.length - 1] || {
1640
- width: 0,
1641
- height: 0,
1642
- pin: { x: 0, y: 0 },
1643
- draw: () => {},
1438
+ return stack[stack.length - 1] || {
1439
+ width: 0,
1440
+ height: 0,
1441
+ pin: { x: 0, y: 0 },
1442
+ draw: () => {
1644
1443
  }
1645
- );
1444
+ };
1646
1445
  };
1647
-
1648
- // SVG-based renderer — mirrors buildGraphic but produces SVG markup.
1649
- // Each stack node has { width, height, pin, svg } where `svg` is a string
1650
- // drawn with the coordinate origin at the graphic's centre (y-down, same as canvas).
1651
1446
  const buildSvgElements = (specs) => {
1652
1447
  const stack = [];
1653
1448
  const measureCanvas = document.createElement("canvas");
1654
1449
  const measureCtx = measureCanvas.getContext("2d");
1655
-
1656
1450
  for (const spec of specs || []) {
1657
1451
  if (!spec || typeof spec !== "object") continue;
1658
1452
  const type = spec.t;
1659
-
1660
- if (
1661
- type === "Empty" ||
1662
- type === "Rectangle" ||
1663
- type === "Ellipse" ||
1664
- type === "CircularSector" ||
1665
- type === "Triangle" ||
1666
- type === "Text"
1667
- ) {
1453
+ if (type === "Empty" || type === "Rectangle" || type === "Ellipse" || type === "CircularSector" || type === "Triangle" || type === "Text") {
1668
1454
  let width = 0, height = 0, pin = { x: 0, y: 0 };
1669
1455
  let svg = "";
1670
-
1671
1456
  if (type === "Rectangle") {
1672
1457
  width = Math.max(0, Number(spec.width) || 0);
1673
1458
  height = Math.max(0, Number(spec.height) || 0);
@@ -1684,8 +1469,6 @@ hyperbook.python = (function () {
1684
1469
  width = radius * 2;
1685
1470
  height = radius * 2;
1686
1471
  const fill = colorToCss(spec.color);
1687
- // canvas: ctx.arc(0, 0, r, 0, -angle*PI/180, counterclockwise=true)
1688
- // SVG arc from (r,0) counterclockwise to the same end-point (sweep=0)
1689
1472
  const endAngleRad = -angle * Math.PI / 180;
1690
1473
  const endX = radius * Math.cos(endAngleRad);
1691
1474
  const endY = radius * Math.sin(endAngleRad);
@@ -1697,9 +1480,18 @@ hyperbook.python = (function () {
1697
1480
  const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
1698
1481
  const p1 = { x: 0, y: 0 };
1699
1482
  const p2 = { x: side1, y: 0 };
1700
- const p3 = { x: side2 * Math.cos(angle), y: -side2 * Math.sin(angle) };
1701
- const centroid = { x: (p1.x + p2.x + p3.x) / 3, y: (p1.y + p2.y + p3.y) / 3 };
1702
- const points = [p1, p2, p3].map((p) => ({ x: p.x - centroid.x, y: p.y - centroid.y }));
1483
+ const p3 = {
1484
+ x: side2 * Math.cos(angle),
1485
+ y: -side2 * Math.sin(angle)
1486
+ };
1487
+ const centroid = {
1488
+ x: (p1.x + p2.x + p3.x) / 3,
1489
+ y: (p1.y + p2.y + p3.y) / 3
1490
+ };
1491
+ const points = [p1, p2, p3].map((p) => ({
1492
+ x: p.x - centroid.x,
1493
+ y: p.y - centroid.y
1494
+ }));
1703
1495
  const xs = points.map((p) => p.x);
1704
1496
  const ys = points.map((p) => p.y);
1705
1497
  width = Math.max(...xs) - Math.min(...xs);
@@ -1718,14 +1510,11 @@ hyperbook.python = (function () {
1718
1510
  const descent = metrics.actualBoundingBoxDescent || textSize * 0.2;
1719
1511
  height = ascent + descent;
1720
1512
  pin = { x: -width / 2, y: (ascent - descent) / 2 };
1721
- // y attribute is the text baseline; mirrors canvas fillText(-w/2, (ascent-descent)/2)
1722
1513
  svg = `<text x="${-width / 2}" y="${(ascent - descent) / 2}" font-family="${fontName}" font-size="${textSize}" fill="${fill}" text-anchor="start">${text}</text>`;
1723
1514
  }
1724
-
1725
1515
  stack.push({ width, height, pin, svg });
1726
1516
  continue;
1727
1517
  }
1728
-
1729
1518
  if (type === "Pin") {
1730
1519
  const child = stack.pop();
1731
1520
  if (!child) continue;
@@ -1733,17 +1522,16 @@ hyperbook.python = (function () {
1733
1522
  stack.push({ ...child, pin });
1734
1523
  continue;
1735
1524
  }
1736
-
1737
1525
  if (type === "Rotate") {
1738
1526
  const child = stack.pop();
1739
1527
  if (!child) continue;
1740
1528
  const angleDeg = Number(spec.angle) || 0;
1741
- const angleRad = (-angleDeg * Math.PI) / 180;
1529
+ const angleRad = -angleDeg * Math.PI / 180;
1742
1530
  const corners = [
1743
1531
  { x: -child.width / 2, y: -child.height / 2 },
1744
1532
  { x: child.width / 2, y: -child.height / 2 },
1745
1533
  { x: child.width / 2, y: child.height / 2 },
1746
- { x: -child.width / 2, y: child.height / 2 },
1534
+ { x: -child.width / 2, y: child.height / 2 }
1747
1535
  ].map((p) => rotatePoint(p, child.pin, angleRad));
1748
1536
  const minX = Math.min(...corners.map((p) => p.x));
1749
1537
  const maxX = Math.max(...corners.map((p) => p.x));
@@ -1752,18 +1540,15 @@ hyperbook.python = (function () {
1752
1540
  const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1753
1541
  const offset = { x: -center.x, y: -center.y };
1754
1542
  const pin = { x: child.pin.x + offset.x, y: child.pin.y + offset.y };
1755
- // SVG transform mirrors the canvas sequence: translate(offset) rotate(-angleDeg, pin)
1756
- // SVG and canvas share the same y-down convention so signs are identical.
1757
1543
  const transform = `translate(${offset.x},${offset.y}) rotate(${-angleDeg},${child.pin.x},${child.pin.y})`;
1758
1544
  stack.push({
1759
1545
  width: maxX - minX,
1760
1546
  height: maxY - minY,
1761
1547
  pin,
1762
- svg: `<g transform="${transform}">${child.svg}</g>`,
1548
+ svg: `<g transform="${transform}">${child.svg}</g>`
1763
1549
  });
1764
1550
  continue;
1765
1551
  }
1766
-
1767
1552
  if (type === "Compose") {
1768
1553
  const bg = stack.pop();
1769
1554
  const fg = stack.pop();
@@ -1772,30 +1557,42 @@ hyperbook.python = (function () {
1772
1557
  const bgPin = spec.bg_pin ? decodePoint(spec.bg_pin, bg.width, bg.height) : bg.pin;
1773
1558
  const bgCenter = { x: 0, y: 0 };
1774
1559
  const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
1775
- const minX = Math.min(fgCenter.x - fg.width / 2, bgCenter.x - bg.width / 2);
1776
- const maxX = Math.max(fgCenter.x + fg.width / 2, bgCenter.x + bg.width / 2);
1777
- const minY = Math.min(fgCenter.y - fg.height / 2, bgCenter.y - bg.height / 2);
1778
- const maxY = Math.max(fgCenter.y + fg.height / 2, bgCenter.y + bg.height / 2);
1560
+ const minX = Math.min(
1561
+ fgCenter.x - fg.width / 2,
1562
+ bgCenter.x - bg.width / 2
1563
+ );
1564
+ const maxX = Math.max(
1565
+ fgCenter.x + fg.width / 2,
1566
+ bgCenter.x + bg.width / 2
1567
+ );
1568
+ const minY = Math.min(
1569
+ fgCenter.y - fg.height / 2,
1570
+ bgCenter.y - bg.height / 2
1571
+ );
1572
+ const maxY = Math.max(
1573
+ fgCenter.y + fg.height / 2,
1574
+ bgCenter.y + bg.height / 2
1575
+ );
1779
1576
  const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
1780
1577
  const fgOffset = { x: fgCenter.x - center.x, y: fgCenter.y - center.y };
1781
1578
  const bgOffset = { x: bgCenter.x - center.x, y: bgCenter.y - center.y };
1782
- const pin = spec.pin
1783
- ? decodePoint(spec.pin, maxX - minX, maxY - minY)
1784
- : { x: bgPin.x - center.x, y: bgPin.y - center.y };
1785
- // bg drawn first (behind), fg on top — same draw order as canvas
1579
+ const pin = spec.pin ? decodePoint(spec.pin, maxX - minX, maxY - minY) : { x: bgPin.x - center.x, y: bgPin.y - center.y };
1786
1580
  stack.push({
1787
1581
  width: maxX - minX,
1788
1582
  height: maxY - minY,
1789
1583
  pin,
1790
- svg: `<g transform="translate(${bgOffset.x},${bgOffset.y})">${bg.svg}</g><g transform="translate(${fgOffset.x},${fgOffset.y})">${fg.svg}</g>`,
1584
+ svg: `<g transform="translate(${bgOffset.x},${bgOffset.y})">${bg.svg}</g><g transform="translate(${fgOffset.x},${fgOffset.y})">${fg.svg}</g>`
1791
1585
  });
1792
1586
  }
1793
1587
  }
1794
-
1795
- return stack[stack.length - 1] || { width: 0, height: 0, pin: { x: 0, y: 0 }, svg: "" };
1588
+ return stack[stack.length - 1] || {
1589
+ width: 0,
1590
+ height: 0,
1591
+ pin: { x: 0, y: 0 },
1592
+ svg: ""
1593
+ };
1796
1594
  };
1797
-
1798
- return {
1595
+ return {
1799
1596
  js_graphic_size: (specs) => {
1800
1597
  try {
1801
1598
  const unproxiedSpecs = unProxy(specs);
@@ -1820,7 +1617,6 @@ hyperbook.python = (function () {
1820
1617
  ctx.scale(scale, scale);
1821
1618
  ctx.translate(width / 2, height / 2);
1822
1619
  graphic.draw(ctx);
1823
-
1824
1620
  if (debug) {
1825
1621
  ctx.strokeStyle = "red";
1826
1622
  ctx.lineWidth = 1 / scale;
@@ -1833,7 +1629,6 @@ hyperbook.python = (function () {
1833
1629
  ctx.lineTo(graphic.pin.x, graphic.pin.y + 8);
1834
1630
  ctx.stroke();
1835
1631
  }
1836
-
1837
1632
  return canvas.toDataURL("image/png");
1838
1633
  } catch (e) {
1839
1634
  console.error("js_render_graphic error:", e);
@@ -1863,50 +1658,88 @@ hyperbook.python = (function () {
1863
1658
  link.href = String(content || "");
1864
1659
  link.download = String(filename || "graphic.png");
1865
1660
  link.click();
1866
- },
1661
+ }
1867
1662
  };
1868
1663
  };
1869
1664
 
1870
- // Browser/Pyodide needs periodic yielding for top-level pygame loops.
1871
- const hasExplicitMain = (code) => {
1872
- const text = String(code || "");
1873
- return (
1874
- /^(\s*)(?:async\s+def|def)\s+main\s*\(/m.test(text) ||
1875
- /__name__\s*==\s*["']__main__["']/.test(text)
1876
- );
1877
- };
1878
-
1879
- const looksLikeTopLevelGameLoop = (code) => {
1880
- const text = String(code || "");
1881
- const hasPygame =
1882
- /(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
1883
- const hasSasPygame =
1884
- /(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(text);
1885
- const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
1886
- if (!hasAnyWhile) return false;
1887
- return (hasPygame || hasSasPygame) && hasAnyWhile;
1888
- };
1889
-
1890
- const indentBlock = (source, spaces) => {
1891
- const pad = " ".repeat(spaces);
1892
- return String(source || "")
1893
- .split(/\r\n|\r|\n/)
1894
- .map((line) => (line.length ? pad + line : line))
1895
- .join("\n");
1665
+ // assets/directive-pyide/src/pyodide.js
1666
+ var loadPyodideScript = () => {
1667
+ if (window.loadPyodide) {
1668
+ return Promise.resolve();
1669
+ }
1670
+ return new Promise((resolve, reject) => {
1671
+ const script = document.createElement("script");
1672
+ script.src = PYODIDE_CDN;
1673
+ script.onload = () => resolve();
1674
+ script.onerror = () => reject(new Error("Failed to load Pyodide"));
1675
+ document.head.appendChild(script);
1676
+ });
1896
1677
  };
1897
-
1898
- const wrapTopLevelIntoAsyncMain = (userCode) => {
1899
- const code = String(userCode || "").replace(/\r\n/g, "\n");
1900
-
1901
- if (hasExplicitMain(code)) return code;
1902
-
1903
- const lines = code.split("\n");
1904
- const keep = [];
1905
- const body = [];
1906
-
1907
- let index = 0;
1908
- while (index < lines.length) {
1909
- const line = lines[index];
1678
+ var pyodideReadyPromise = (async () => {
1679
+ await loadPyodideScript();
1680
+ return window.loadPyodide;
1681
+ })();
1682
+ var getRuntime = async (id) => {
1683
+ if (runtimes.has(id)) {
1684
+ return runtimes.get(id);
1685
+ }
1686
+ const loadPyodide = await pyodideReadyPromise;
1687
+ const pyodide = await loadPyodide();
1688
+ if (typeof pyodide.registerJsModule === "function") {
1689
+ const turtleModule = createTurtleJsFFI(id);
1690
+ turtleModule.__setPyodide(pyodide);
1691
+ pyodide.registerJsModule("turtle", turtleModule);
1692
+ pyodide.registerJsModule("jturtle", turtleModule);
1693
+ pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
1694
+ turtleModules.set(id, turtleModule);
1695
+ }
1696
+ if (typeof SharedArrayBuffer !== "undefined" && window.crossOriginIsolated && typeof pyodide.setInterruptBuffer === "function") {
1697
+ const interruptBuffer = new Int32Array(new SharedArrayBuffer(4));
1698
+ pyodide.setInterruptBuffer(interruptBuffer);
1699
+ interruptBuffers.set(id, interruptBuffer);
1700
+ }
1701
+ runtimes.set(id, pyodide);
1702
+ return pyodide;
1703
+ };
1704
+
1705
+ // assets/directive-pyide/src/execution.js
1706
+ var scriptLooksLikeTurtle = (script) => {
1707
+ return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(
1708
+ String(script || "")
1709
+ );
1710
+ };
1711
+ var resetCanvas = (canvas) => {
1712
+ if (!canvas) return;
1713
+ const context = canvas.getContext("2d");
1714
+ context?.clearRect(0, 0, canvas.width, canvas.height);
1715
+ };
1716
+ var hasExplicitMain = (code) => {
1717
+ const text = String(code || "");
1718
+ return /^(\s*)(?:async\s+def|def)\s+main\s*\(/m.test(text) || /__name__\s*==\s*["']__main__["']/.test(text);
1719
+ };
1720
+ var looksLikeTopLevelGameLoop = (code) => {
1721
+ const text = String(code || "");
1722
+ const hasPygame = /(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
1723
+ const hasSasPygame = /(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(
1724
+ text
1725
+ );
1726
+ const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
1727
+ if (!hasAnyWhile) return false;
1728
+ return (hasPygame || hasSasPygame) && hasAnyWhile;
1729
+ };
1730
+ var indentBlock = (source, spaces) => {
1731
+ const pad = " ".repeat(spaces);
1732
+ return String(source || "").split(/\r\n|\r|\n/).map((line) => line.length ? pad + line : line).join("\n");
1733
+ };
1734
+ var wrapTopLevelIntoAsyncMain = (userCode) => {
1735
+ const code = String(userCode || "").replace(/\r\n/g, "\n");
1736
+ if (hasExplicitMain(code)) return code;
1737
+ const lines = code.split("\n");
1738
+ const keep = [];
1739
+ const body = [];
1740
+ let index = 0;
1741
+ while (index < lines.length) {
1742
+ const line = lines[index];
1910
1743
  if (/^\s*$/.test(line)) {
1911
1744
  keep.push(line);
1912
1745
  index += 1;
@@ -1922,7 +1755,6 @@ hyperbook.python = (function () {
1922
1755
  index += 1;
1923
1756
  continue;
1924
1757
  }
1925
-
1926
1758
  if (/^\s*(?:def|async\s+def|class)\s+/.test(line)) {
1927
1759
  keep.push(line);
1928
1760
  index += 1;
@@ -1933,12 +1765,7 @@ hyperbook.python = (function () {
1933
1765
  index += 1;
1934
1766
  continue;
1935
1767
  }
1936
- if (
1937
- /^\S/.test(nextLine) &&
1938
- !/^\s*#/.test(nextLine) &&
1939
- !/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) &&
1940
- !/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)
1941
- ) {
1768
+ if (/^\S/.test(nextLine) && !/^\s*#/.test(nextLine) && !/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) && !/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)) {
1942
1769
  break;
1943
1770
  }
1944
1771
  keep.push(nextLine);
@@ -1948,52 +1775,44 @@ hyperbook.python = (function () {
1948
1775
  }
1949
1776
  break;
1950
1777
  }
1951
-
1952
1778
  for (; index < lines.length; index += 1) {
1953
1779
  body.push(lines[index]);
1954
1780
  }
1955
-
1956
1781
  let bodyText = body.join("\n");
1957
1782
  bodyText = bodyText.replace(/^([ \t]*\n)+/, "");
1958
1783
  bodyText = bodyText.replace(/(\n[ \t]*)+$/, "");
1959
1784
  bodyText = bodyText.replace(/\t/g, " ");
1960
1785
  if (!bodyText.replace(/[\s\n]+/g, "").length) return code;
1961
-
1962
- const injected = bodyText
1963
- .replace(
1964
- /^(\s*)(\w+\s*\.\s*tick\s*\([^\)]*\)\s*)$/gm,
1965
- "$1$2\n$1await asyncio.sleep(0)",
1966
- )
1967
- .replace(
1968
- /^(\s*)(pygame\s*\.\s*display\s*\.\s*flip\s*\(\s*\)\s*)$/gm,
1969
- "$1$2\n$1await asyncio.sleep(0)",
1970
- )
1971
- .replace(
1972
- /^([\t ]*)([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_][\w]*)*\s*\.\s*step\s*\([^\)]*\)\s*)(#.*)?$/gm,
1973
- "$1$2$3\n$1await asyncio.sleep(0)",
1974
- );
1975
-
1786
+ const injected = bodyText.replace(
1787
+ /^(\s*)(\w+\s*\.\s*tick\s*\([^\)]*\)\s*)$/gm,
1788
+ "$1$2\n$1await asyncio.sleep(0)"
1789
+ ).replace(
1790
+ /^(\s*)(pygame\s*\.\s*display\s*\.\s*flip\s*\(\s*\)\s*)$/gm,
1791
+ "$1$2\n$1await asyncio.sleep(0)"
1792
+ ).replace(
1793
+ /^([\t ]*)([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_][\w]*)*\s*\.\s*step\s*\([^\)]*\)\s*)(#.*)?$/gm,
1794
+ "$1$2$3\n$1await asyncio.sleep(0)"
1795
+ );
1796
+ const hasAsyncio = /(?:^|\W)import\s+asyncio\b/m.test(code);
1797
+ const hasPygameImport = /(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(code);
1976
1798
  const prelude = [
1977
1799
  "# --- auto-wrapped by IDE for browser pygame compatibility ---",
1978
- "import asyncio",
1979
- "import pygame",
1800
+ ...hasAsyncio ? [] : ["import asyncio"],
1801
+ ...hasPygameImport ? [] : ["import pygame"],
1980
1802
  "",
1981
1803
  "async def main():",
1982
1804
  indentBlock(injected, 4),
1983
1805
  "",
1984
1806
  "await main()",
1985
1807
  "# --- end auto-wrapped ---",
1986
- "",
1808
+ ""
1987
1809
  ].join("\n");
1988
-
1989
1810
  let keepText = keep.join("\n");
1990
1811
  if (keepText && !keepText.endsWith("\n")) keepText += "\n";
1991
1812
  if (keepText && !/\n\s*\n$/.test(keepText)) keepText += "\n";
1992
-
1993
1813
  return keepText + prelude;
1994
1814
  };
1995
-
1996
- const maybeAutoWrapPygame = (code) => {
1815
+ var maybeAutoWrapPygame = (code) => {
1997
1816
  try {
1998
1817
  const text = String(code || "");
1999
1818
  if (!looksLikeTopLevelGameLoop(text)) return text;
@@ -2002,16 +1821,14 @@ hyperbook.python = (function () {
2002
1821
  return String(code || "");
2003
1822
  }
2004
1823
  };
2005
-
2006
- const ensureMicropipPackages = async (id, pyodide, packages = []) => {
1824
+ var ensureMicropipPackages = async (id, pyodide, packages = []) => {
2007
1825
  if (packages.length === 0) return;
2008
1826
  if (!installedMicropipPackages.has(id)) {
2009
- installedMicropipPackages.set(id, new Set());
1827
+ installedMicropipPackages.set(id, /* @__PURE__ */ new Set());
2010
1828
  }
2011
1829
  const installed = installedMicropipPackages.get(id);
2012
1830
  const toInstall = packages.filter((pkg) => !installed.has(pkg));
2013
1831
  if (toInstall.length === 0) return;
2014
-
2015
1832
  await pyodide.loadPackage("micropip");
2016
1833
  const micropip = pyodide.pyimport("micropip");
2017
1834
  try {
@@ -2023,15 +1840,13 @@ hyperbook.python = (function () {
2023
1840
  micropip?.destroy?.();
2024
1841
  }
2025
1842
  };
2026
-
2027
- const executeScript = async (id, script, context = {}, packages = []) => {
1843
+ var executeScript = async (id, script, context = {}, packages = []) => {
2028
1844
  const filename = "<exec>";
2029
1845
  try {
2030
1846
  const pyodide = await getRuntime(id);
2031
1847
  const { canvas, ...globalsContext } = context;
2032
1848
  const decoder = new TextDecoder("utf-8");
2033
1849
  const executableScript = maybeAutoWrapPygame(script);
2034
-
2035
1850
  if (canvas) {
2036
1851
  try {
2037
1852
  resetCanvas(canvas);
@@ -2049,7 +1864,6 @@ hyperbook.python = (function () {
2049
1864
  appendOutputErrorLine(id, `Canvas setup failed: ${error.message}`);
2050
1865
  }
2051
1866
  }
2052
-
2053
1867
  let lastStdinPrompt = "";
2054
1868
  pyodide.setStdout({
2055
1869
  write: (msg) => {
@@ -2061,28 +1875,26 @@ hyperbook.python = (function () {
2061
1875
  }
2062
1876
  appendOutputLine(id, text);
2063
1877
  return msg?.length ?? text.length;
2064
- },
1878
+ }
2065
1879
  });
2066
1880
  pyodide.setStdin({
2067
1881
  stdin: () => {
2068
- const promptText =
2069
- lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
1882
+ const promptText = lastStdinPrompt || hyperbook.i18n.get("pyide-input-prompt");
2070
1883
  lastStdinPrompt = "";
2071
1884
  const value = window.prompt(promptText);
2072
1885
  if (value === null) {
2073
1886
  return "";
2074
1887
  }
2075
1888
  return value;
2076
- },
1889
+ }
2077
1890
  });
2078
1891
  pyodide.setStderr({
2079
1892
  write: (msg) => {
2080
1893
  const text = typeof msg === "string" ? msg : decoder.decode(msg);
2081
1894
  appendOutputErrorLine(id, text);
2082
1895
  return msg?.length ?? text.length;
2083
- },
1896
+ }
2084
1897
  });
2085
-
2086
1898
  await ensureMicropipPackages(id, pyodide, packages);
2087
1899
  await pyodide.loadPackagesFromImports(executableScript);
2088
1900
  const dict = pyodide.globals.get("dict");
@@ -2094,7 +1906,7 @@ hyperbook.python = (function () {
2094
1906
  const results = await pyodide.runPythonAsync(executableScript, {
2095
1907
  globals,
2096
1908
  locals: globals,
2097
- filename,
1909
+ filename
2098
1910
  });
2099
1911
  return { results };
2100
1912
  } finally {
@@ -2110,7 +1922,7 @@ if _pg:
2110
1922
  _pg.quit()
2111
1923
  except Exception:
2112
1924
  pass`,
2113
- { filename: "<cleanup>" },
1925
+ { filename: "<cleanup>" }
2114
1926
  );
2115
1927
  } catch (e) {
2116
1928
  console.warn("pygame cleanup failed:", e);
@@ -2122,41 +1934,44 @@ if _pg:
2122
1934
  if (message.startsWith("Traceback")) {
2123
1935
  const lines = message?.split("\n") || [];
2124
1936
  const i = lines.findIndex((line) => line.includes(filename));
2125
- message = lines[0] + "\n" + lines.slice(i).join("\n");
1937
+ message = i >= 0 ? lines[0] + "\n" + lines.slice(i).join("\n") : message;
2126
1938
  }
2127
1939
  return { error: message };
2128
1940
  }
2129
1941
  };
2130
1942
 
2131
- const requestStop = (id) => {
2132
- const state = getExecutionState(id);
2133
- const hasRuntime = runtimes.has(id);
2134
- if ((!state.running && !hasRuntime) || state.stopRequested) return;
2135
- state.stopRequested = true;
2136
- state.stopping = true;
2137
- const interruptBuffer = interruptBuffers.get(id);
2138
- if (interruptBuffer) {
2139
- interruptBuffer[0] = 2;
2140
- appendOutputLine(id, "Stop requested. Interrupting execution...");
2141
- } else {
2142
- appendOutputLine(id, hyperbook.i18n.get("pyide-stop-reloading"));
1943
+ // assets/directive-pyide/src/ui.js
1944
+ var updateFullscreenButtonState = (elem, button) => {
1945
+ if (!elem || !button) return;
1946
+ const isFullscreen = document.fullscreenElement === elem;
1947
+ const label = hyperbook.i18n.get("ide-fullscreen-enter");
1948
+ button.textContent = "\u26F6";
1949
+ button.title = label;
1950
+ button.setAttribute("aria-label", label);
1951
+ button.classList.toggle("active", isFullscreen);
1952
+ };
1953
+ var toggleFullscreen = async (elem) => {
1954
+ if (!elem) return;
1955
+ if (document.fullscreenElement === elem) {
1956
+ await document.exitFullscreen();
1957
+ return;
2143
1958
  }
2144
- releaseKeyboardCapture(id);
2145
- updateRunning();
2146
- if (!interruptBuffer) {
2147
- window.setTimeout(() => {
2148
- window.location.reload();
2149
- }, 50);
1959
+ await elem.requestFullscreen();
1960
+ };
1961
+ var syncFullscreenButtons = () => {
1962
+ const elems = document.getElementsByClassName("directive-pyide");
1963
+ for (const elem of elems) {
1964
+ const fullscreen = elem.getElementsByClassName("fullscreen")[0];
1965
+ updateFullscreenButtonState(elem, fullscreen);
2150
1966
  }
2151
1967
  };
2152
-
2153
- const handleStopClick = (event) => {
2154
- const elem = event.currentTarget.closest(".directive-pyide");
2155
- if (!elem?.id) return;
2156
- requestStop(elem.id);
1968
+ var releaseKeyboardCapture = (id) => {
1969
+ const elem = document.getElementById(id);
1970
+ if (!elem) return;
1971
+ const canvas = elem.getElementsByClassName("canvas")[0];
1972
+ canvas?.blur?.();
2157
1973
  };
2158
-
2159
- const getRunningInstanceId = () => {
1974
+ var getRunningInstanceId = () => {
2160
1975
  const elems = document.getElementsByClassName("directive-pyide");
2161
1976
  for (const elem of elems) {
2162
1977
  if (getExecutionState(elem.id).running) {
@@ -2165,8 +1980,7 @@ if _pg:
2165
1980
  }
2166
1981
  return null;
2167
1982
  };
2168
-
2169
- const updateRunning = () => {
1983
+ var updateRunning = () => {
2170
1984
  const runningInstanceId = getRunningInstanceId();
2171
1985
  const elems = document.getElementsByClassName("directive-pyide");
2172
1986
  for (let elem of elems) {
@@ -2178,11 +1992,7 @@ if _pg:
2178
1992
  const state = getExecutionState(elem.id);
2179
1993
  const hasRuntime = runtimes.has(elem.id);
2180
1994
  const hasInterrupt = interruptBuffers.has(elem.id);
2181
- const lockedByOther =
2182
- runningInstanceId !== null &&
2183
- runningInstanceId !== elem.id &&
2184
- !state.running;
2185
-
1995
+ const lockedByOther = runningInstanceId !== null && runningInstanceId !== elem.id && !state.running;
2186
1996
  stop?.removeEventListener("click", handleStopClick);
2187
1997
  run.classList.remove("stopping");
2188
1998
  run.classList.remove("locked");
@@ -2190,7 +2000,6 @@ if _pg:
2190
2000
  test?.classList.remove("locked");
2191
2001
  stop?.classList.remove("stopping");
2192
2002
  elem.classList.toggle("locked-by-other", lockedByOther);
2193
-
2194
2003
  if (state.running || lockedByOther) {
2195
2004
  editor?.classList.add("running");
2196
2005
  editorCm?.setReadOnly(true);
@@ -2209,29 +2018,24 @@ if _pg:
2209
2018
  run.classList.add("running");
2210
2019
  run.disabled = true;
2211
2020
  } else {
2212
- const lockLabel = lockedByOther
2213
- ? "pyide-locked-other-instance-running"
2214
- : "pyide-run";
2021
+ const lockLabel = lockedByOther ? "pyide-locked-other-instance-running" : "pyide-run";
2215
2022
  run.textContent = hyperbook.i18n.get(lockLabel);
2216
2023
  run.classList.add("running");
2217
2024
  run.classList.toggle("locked", lockedByOther);
2218
2025
  run.disabled = true;
2219
2026
  if (test) {
2220
2027
  test.textContent = hyperbook.i18n.get(
2221
- lockedByOther ? "pyide-locked-other-instance-running" : "pyide-test",
2028
+ lockedByOther ? "pyide-locked-other-instance-running" : "pyide-test"
2222
2029
  );
2223
2030
  test.classList.toggle("locked", lockedByOther);
2224
2031
  test.classList.add("running");
2225
2032
  test.disabled = true;
2226
2033
  }
2227
2034
  }
2228
-
2229
2035
  if (stop) {
2230
2036
  const stopLabel = hasInterrupt ? "pyide-stop" : "pyide-stop-refresh";
2231
2037
  if (state.running) {
2232
- stop.textContent = state.stopping
2233
- ? hyperbook.i18n.get("pyide-stopping")
2234
- : hyperbook.i18n.get(stopLabel);
2038
+ stop.textContent = state.stopping ? hyperbook.i18n.get("pyide-stopping") : hyperbook.i18n.get(stopLabel);
2235
2039
  stop.disabled = false;
2236
2040
  stop.addEventListener("click", handleStopClick);
2237
2041
  } else {
@@ -2257,40 +2061,51 @@ if _pg:
2257
2061
  stop.classList.remove("stopping");
2258
2062
  stop.classList.remove("running");
2259
2063
  stop.textContent = hyperbook.i18n.get(
2260
- hasInterrupt ? "pyide-stop" : "pyide-stop-refresh",
2064
+ hasInterrupt ? "pyide-stop" : "pyide-stop-refresh"
2261
2065
  );
2262
2066
  stop.disabled = true;
2263
2067
  }
2264
2068
  }
2265
2069
  }
2266
2070
  };
2267
-
2268
- const setupSplitter = (
2269
- elem,
2270
- container,
2271
- editorContainer,
2272
- splitter,
2273
- onSplitChanged,
2274
- ) => {
2071
+ var requestStop = (id) => {
2072
+ const state = getExecutionState(id);
2073
+ const hasRuntime = runtimes.has(id);
2074
+ if (!state.running && !hasRuntime || state.stopRequested) return;
2075
+ state.stopRequested = true;
2076
+ state.stopping = true;
2077
+ const interruptBuffer = interruptBuffers.get(id);
2078
+ if (interruptBuffer) {
2079
+ interruptBuffer[0] = 2;
2080
+ appendOutputLine(id, "Stop requested. Interrupting execution...");
2081
+ } else {
2082
+ appendOutputLine(id, hyperbook.i18n.get("pyide-stop-reloading"));
2083
+ }
2084
+ releaseKeyboardCapture(id);
2085
+ updateRunning();
2086
+ if (!interruptBuffer) {
2087
+ window.setTimeout(() => {
2088
+ window.location.reload();
2089
+ }, 50);
2090
+ }
2091
+ };
2092
+ var handleStopClick = (event) => {
2093
+ const elem = event.currentTarget.closest(".directive-pyide");
2094
+ if (!elem?.id) return;
2095
+ requestStop(elem.id);
2096
+ };
2097
+ var setupSplitter = (elem, container, editorContainer, splitter, onSplitChanged) => {
2275
2098
  if (!container || !editorContainer || !splitter) return;
2276
-
2277
2099
  const minPanelSize = 120;
2278
-
2279
- const getIsHorizontal = () =>
2280
- getComputedStyle(elem).flexDirection.startsWith("row");
2281
-
2100
+ const getIsHorizontal = () => getComputedStyle(elem).flexDirection.startsWith("row");
2282
2101
  const applySplitSize = (rawSize, isHorizontal) => {
2283
2102
  const total = isHorizontal ? elem.clientWidth : elem.clientHeight;
2284
2103
  const splitterSize = isHorizontal ? splitter.offsetWidth : splitter.offsetHeight;
2285
- const maxSize = Math.max(
2286
- minPanelSize,
2287
- total - splitterSize - minPanelSize
2288
- );
2104
+ const maxSize = Math.max(minPanelSize, total - splitterSize - minPanelSize);
2289
2105
  const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
2290
2106
  container.style.flex = `0 0 ${clamped}px`;
2291
2107
  return clamped;
2292
2108
  };
2293
-
2294
2109
  const applyStoredSplitSize = () => {
2295
2110
  const isHorizontal = getIsHorizontal();
2296
2111
  elem.classList.toggle("split-horizontal", isHorizontal);
@@ -2303,29 +2118,21 @@ if _pg:
2303
2118
  }
2304
2119
  applySplitSize(rawStored, isHorizontal);
2305
2120
  };
2306
-
2307
2121
  applyStoredSplitSize();
2308
-
2309
2122
  splitter.addEventListener("pointerdown", (event) => {
2310
2123
  event.preventDefault();
2311
2124
  splitter.setPointerCapture(event.pointerId);
2312
-
2313
2125
  const isHorizontal = getIsHorizontal();
2314
2126
  const key = isHorizontal ? "splitHorizontal" : "splitVertical";
2315
2127
  const startPointer = isHorizontal ? event.clientX : event.clientY;
2316
- const startSize = isHorizontal
2317
- ? container.getBoundingClientRect().width
2318
- : container.getBoundingClientRect().height;
2319
-
2128
+ const startSize = isHorizontal ? container.getBoundingClientRect().width : container.getBoundingClientRect().height;
2320
2129
  elem.classList.add("resizing");
2321
-
2322
2130
  const onPointerMove = (moveEvent) => {
2323
2131
  const pointer = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
2324
2132
  const delta = pointer - startPointer;
2325
2133
  const size = applySplitSize(startSize + delta, isHorizontal);
2326
2134
  elem.dataset[key] = String(Math.round(size));
2327
2135
  };
2328
-
2329
2136
  const onPointerUp = () => {
2330
2137
  elem.classList.remove("resizing");
2331
2138
  splitter.removeEventListener("pointermove", onPointerMove);
@@ -2334,42 +2141,25 @@ if _pg:
2334
2141
  const splitHorizontal = Number(elem.dataset.splitHorizontal);
2335
2142
  const splitVertical = Number(elem.dataset.splitVertical);
2336
2143
  onSplitChanged?.({
2337
- ...(Number.isFinite(splitHorizontal) && splitHorizontal > 0
2338
- ? { splitHorizontal: Math.round(splitHorizontal) }
2339
- : {}),
2340
- ...(Number.isFinite(splitVertical) && splitVertical > 0
2341
- ? { splitVertical: Math.round(splitVertical) }
2342
- : {}),
2144
+ ...Number.isFinite(splitHorizontal) && splitHorizontal > 0 ? { splitHorizontal: Math.round(splitHorizontal) } : {},
2145
+ ...Number.isFinite(splitVertical) && splitVertical > 0 ? { splitVertical: Math.round(splitVertical) } : {}
2343
2146
  });
2344
2147
  };
2345
-
2346
2148
  splitter.addEventListener("pointermove", onPointerMove);
2347
2149
  splitter.addEventListener("pointerup", onPointerUp);
2348
2150
  splitter.addEventListener("pointercancel", onPointerUp);
2349
2151
  });
2350
-
2351
2152
  window.addEventListener("resize", applyStoredSplitSize);
2352
2153
  return applyStoredSplitSize;
2353
2154
  };
2354
-
2355
- const setupCanvasOutputSplitter = (
2356
- elem,
2357
- container,
2358
- canvasWrapper,
2359
- output,
2360
- splitter,
2361
- onSplitChanged,
2362
- ) => {
2155
+ var setupCanvasOutputSplitter = (elem, container, canvasWrapper, output, splitter, onSplitChanged) => {
2363
2156
  if (!elem || !container || !canvasWrapper || !output || !splitter) return;
2364
-
2365
2157
  const minPanelSize = 80;
2366
-
2367
2158
  const getAvailableHeight = () => {
2368
2159
  const tabs = container.querySelector(".buttons");
2369
2160
  const tabsHeight = tabs && tabs.offsetParent !== null ? tabs.offsetHeight : 0;
2370
2161
  return container.clientHeight - tabsHeight - splitter.offsetHeight;
2371
2162
  };
2372
-
2373
2163
  const applySplitSize = (rawSize) => {
2374
2164
  const total = getAvailableHeight();
2375
2165
  const maxSize = Math.max(minPanelSize, total - minPanelSize);
@@ -2378,7 +2168,6 @@ if _pg:
2378
2168
  output.style.flex = "1 1 0";
2379
2169
  return clamped;
2380
2170
  };
2381
-
2382
2171
  const applyStoredSplitSize = () => {
2383
2172
  const rawStored = Number(elem.dataset.splitCanvasOutput);
2384
2173
  if (!Number.isFinite(rawStored) || rawStored <= 0) {
@@ -2388,22 +2177,17 @@ if _pg:
2388
2177
  }
2389
2178
  applySplitSize(rawStored);
2390
2179
  };
2391
-
2392
2180
  splitter.addEventListener("pointerdown", (event) => {
2393
2181
  event.preventDefault();
2394
2182
  splitter.setPointerCapture(event.pointerId);
2395
-
2396
2183
  const startPointer = event.clientY;
2397
2184
  const startSize = canvasWrapper.getBoundingClientRect().height;
2398
-
2399
2185
  elem.classList.add("resizing");
2400
-
2401
2186
  const onPointerMove = (moveEvent) => {
2402
2187
  const delta = moveEvent.clientY - startPointer;
2403
2188
  const size = applySplitSize(startSize + delta);
2404
2189
  elem.dataset.splitCanvasOutput = String(Math.round(size));
2405
2190
  };
2406
-
2407
2191
  const onPointerUp = () => {
2408
2192
  elem.classList.remove("resizing");
2409
2193
  splitter.removeEventListener("pointermove", onPointerMove);
@@ -2414,393 +2198,345 @@ if _pg:
2414
2198
  onSplitChanged?.({ splitCanvasOutput: Math.round(splitCanvasOutput) });
2415
2199
  }
2416
2200
  };
2417
-
2418
2201
  splitter.addEventListener("pointermove", onPointerMove);
2419
2202
  splitter.addEventListener("pointerup", onPointerUp);
2420
2203
  splitter.addEventListener("pointercancel", onPointerUp);
2421
2204
  });
2422
-
2423
2205
  window.addEventListener("resize", applyStoredSplitSize);
2424
2206
  return applyStoredSplitSize;
2425
2207
  };
2426
2208
 
2427
- const init = (root) => {
2428
- const elems = root.getElementsByClassName("directive-pyide");
2429
-
2430
- for (let elem of elems) {
2431
- if (elem.getAttribute("data-pyide-initialized") === "true") continue;
2432
- elem.setAttribute("data-pyide-initialized", "true");
2433
-
2434
- const editorDiv = elem.getElementsByClassName("editor")[0];
2435
- const container = elem.getElementsByClassName("container")[0];
2436
- const editorContainer = elem.getElementsByClassName("editor-container")[0];
2437
- const splitter = elem.getElementsByClassName("splitter")[0];
2438
- const run = elem.getElementsByClassName("run")[0];
2439
- const test = elem.getElementsByClassName("test")[0];
2440
- const stop = elem.getElementsByClassName("stop")[0];
2441
- const output = elem.getElementsByClassName("output")[0];
2442
- const canvas = elem.getElementsByClassName("canvas")[0];
2443
- const canvasWrapper = elem.getElementsByClassName("canvas-wrapper")[0] || canvas;
2444
- const canvasOutputSplitter = elem.getElementsByClassName("canvas-output-splitter")[0];
2445
- const canvasHeader = elem.getElementsByClassName("canvas-header")[0];
2446
- const outputHeader = elem.getElementsByClassName("output-header")[0];
2447
- const outputBtn = elem.getElementsByClassName("output-btn")[0];
2448
- const canvasBtn = elem.getElementsByClassName("canvas-btn")[0];
2449
- const canvasTabs = outputBtn?.closest(".buttons");
2450
-
2451
- const copyEl = elem.getElementsByClassName("copy")[0];
2452
- const resetEl = elem.getElementsByClassName("reset")[0];
2453
- const downloadEl = elem.getElementsByClassName("download")[0];
2454
- const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];
2455
-
2456
- const id = elem.id;
2457
- const hasCanvas = elem.getAttribute("data-canvas") === "true";
2458
- const additionalPackages = Array.from(
2459
- new Set(
2460
- (elem.getAttribute("data-packages") || "")
2461
- .split(",")
2462
- .map((pkg) => pkg.trim())
2463
- .filter((pkg) => pkg.length > 0),
2464
- ),
2465
- );
2466
- const hasPytamaroPackage = additionalPackages.some(
2467
- (pkg) => pkg.toLowerCase() === "pytamaro",
2468
- );
2469
- const scriptLooksLikePytamaro = (script) => {
2470
- return /\bfrom\s+pytamaro\s+import\b|\bimport\s+pytamaro\b/.test(script);
2471
- };
2472
- let pyideState = { id };
2473
-
2474
- // Initialize CodeMirror
2475
- const initialSource = editorDiv ? editorDiv.textContent : "";
2476
- if (editorDiv) editorDiv.textContent = "";
2477
- const cm = editorDiv ? HyperbookCM.create(editorDiv, {
2478
- lang: editorDiv.dataset.lang || "python",
2479
- value: initialSource,
2480
- onChange: (code) => {
2481
- void persistPyideState({ script: code });
2482
- },
2483
- }) : null;
2484
- // Store CM on the element so updateRunning() can toggle readOnly
2485
- if (editorDiv && cm) editorDiv._cm = cm;
2486
-
2487
- const getEditorValue = () => cm?.getValue() ?? "";
2488
-
2489
- pyideState = { ...pyideState, script: getEditorValue() };
2490
-
2491
- const persistPyideState = (updates = {}) => {
2492
- pyideState = { ...pyideState, ...updates, id };
2493
- return hyperbook.store.db.pyide.put(pyideState);
2494
- };
2495
-
2496
- copyEl?.addEventListener("click", async () => {
2497
- try {
2498
- await navigator.clipboard.writeText(getEditorValue());
2499
- } catch (error) {
2500
- console.error(error.message);
2501
- }
2502
- });
2503
-
2504
- resetEl?.addEventListener("click", () => {
2505
- hyperbook.store.db.pyide.delete(id);
2506
- window.location.reload();
2507
- });
2508
-
2509
- downloadEl?.addEventListener("click", () => {
2510
- const a = document.createElement("a");
2511
- const blob = new Blob([getEditorValue()], { type: "text/plain" });
2512
- a.href = URL.createObjectURL(blob);
2513
- a.download = `script-${id}.py`;
2514
- a.click();
2515
- });
2516
-
2517
- fullscreenEl?.addEventListener("click", async () => {
2209
+ // assets/directive-pyide/src/index.js
2210
+ hyperbook.python = function() {
2211
+ const init = (root) => {
2212
+ const elems = root.getElementsByClassName("directive-pyide");
2213
+ for (let elem of elems) {
2214
+ let showOutput2 = function() {
2215
+ if (isWideCanvasMode()) {
2216
+ applyCanvasOutputLayout();
2217
+ return;
2218
+ }
2219
+ showOutputTab();
2220
+ }, showCanvas2 = function() {
2221
+ if (isWideCanvasMode()) {
2222
+ applyCanvasOutputLayout();
2223
+ return;
2224
+ }
2225
+ showCanvasTab();
2226
+ };
2227
+ var showOutput = showOutput2, showCanvas = showCanvas2;
2228
+ if (elem.getAttribute("data-pyide-initialized") === "true") continue;
2229
+ elem.setAttribute("data-pyide-initialized", "true");
2230
+ const editorDiv = elem.getElementsByClassName("editor")[0];
2231
+ const container = elem.getElementsByClassName("container")[0];
2232
+ const editorContainer = elem.getElementsByClassName("editor-container")[0];
2233
+ const splitter = elem.getElementsByClassName("splitter")[0];
2234
+ const run = elem.getElementsByClassName("run")[0];
2235
+ const test = elem.getElementsByClassName("test")[0];
2236
+ const stop = elem.getElementsByClassName("stop")[0];
2237
+ const output = elem.getElementsByClassName("output")[0];
2238
+ const canvas = elem.getElementsByClassName("canvas")[0];
2239
+ const canvasWrapper = elem.getElementsByClassName("canvas-wrapper")[0] || canvas;
2240
+ const canvasOutputSplitter = elem.getElementsByClassName(
2241
+ "canvas-output-splitter"
2242
+ )[0];
2243
+ const canvasHeader = elem.getElementsByClassName("canvas-header")[0];
2244
+ const outputHeader = elem.getElementsByClassName("output-header")[0];
2245
+ const outputBtn = elem.getElementsByClassName("output-btn")[0];
2246
+ const canvasBtn = elem.getElementsByClassName("canvas-btn")[0];
2247
+ const canvasTabs = outputBtn?.closest(".buttons");
2248
+ const copyEl = elem.getElementsByClassName("copy")[0];
2249
+ const resetEl = elem.getElementsByClassName("reset")[0];
2250
+ const downloadEl = elem.getElementsByClassName("download")[0];
2251
+ const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];
2252
+ const id = elem.id;
2253
+ const hasCanvas = elem.getAttribute("data-canvas") === "true";
2254
+ const additionalPackages = Array.from(
2255
+ new Set(
2256
+ (elem.getAttribute("data-packages") || "").split(",").map((pkg) => pkg.trim()).filter((pkg) => pkg.length > 0)
2257
+ )
2258
+ );
2259
+ let pyideState = { id };
2260
+ const initialSource = editorDiv ? editorDiv.textContent : "";
2261
+ if (editorDiv) editorDiv.textContent = "";
2262
+ const cm = editorDiv ? HyperbookCM.create(editorDiv, {
2263
+ lang: editorDiv.dataset.lang || "python",
2264
+ value: initialSource,
2265
+ onChange: (code) => {
2266
+ void persistPyideState({ script: code });
2267
+ }
2268
+ }) : null;
2269
+ if (editorDiv && cm) editorDiv._cm = cm;
2270
+ const getEditorValue = () => cm?.getValue() ?? "";
2271
+ pyideState = { ...pyideState, script: getEditorValue() };
2272
+ const persistPyideState = (updates = {}) => {
2273
+ pyideState = { ...pyideState, ...updates, id };
2274
+ return hyperbook.store.db.pyide.put(pyideState);
2275
+ };
2276
+ copyEl?.addEventListener("click", async () => {
2277
+ try {
2278
+ await navigator.clipboard.writeText(getEditorValue());
2279
+ } catch (error) {
2280
+ console.error(error.message);
2281
+ }
2282
+ });
2283
+ resetEl?.addEventListener("click", () => {
2284
+ hyperbook.store.db.pyide.delete(id);
2285
+ window.location.reload();
2286
+ });
2287
+ downloadEl?.addEventListener("click", () => {
2288
+ const a = document.createElement("a");
2289
+ const blob = new Blob([getEditorValue()], { type: "text/plain" });
2290
+ a.href = URL.createObjectURL(blob);
2291
+ a.download = `script-${id}.py`;
2292
+ a.click();
2293
+ });
2294
+ fullscreenEl?.addEventListener("click", async () => {
2295
+ try {
2296
+ await toggleFullscreen(elem);
2297
+ } catch (error) {
2298
+ console.error(error.message);
2299
+ }
2300
+ });
2301
+ updateFullscreenButtonState(elem, fullscreenEl);
2302
+ let tests = [];
2518
2303
  try {
2519
- await toggleFullscreen(elem);
2520
- } catch (error) {
2521
- console.error(error.message);
2304
+ tests = JSON.parse(atob(elem.getAttribute("data-tests")));
2305
+ } catch (e) {
2522
2306
  }
2523
- });
2524
- updateFullscreenButtonState(elem, fullscreenEl);
2525
- let tests = [];
2526
- try {
2527
- tests = JSON.parse(atob(elem.getAttribute("data-tests")));
2528
- } catch (e) {}
2529
-
2530
- const isWideCanvasMode = () =>
2531
- hasCanvas && window.matchMedia("(min-width: 1024px)").matches;
2532
-
2533
- let activeCanvasView = "output";
2534
-
2535
- const showOutputTab = () => {
2536
- activeCanvasView = "output";
2537
- outputBtn.classList.add("active");
2538
- if (canvasBtn) canvasBtn.classList.remove("active");
2539
- canvasHeader?.classList.add("hidden");
2540
- outputHeader?.classList.add("hidden");
2541
- output.classList.remove("hidden");
2542
- if (canvasWrapper) canvasWrapper.classList.add("hidden");
2543
- canvasOutputSplitter?.classList.add("hidden");
2544
- };
2545
- const showCanvasTab = () => {
2546
- activeCanvasView = "canvas";
2547
- outputBtn.classList.remove("active");
2548
- if (canvasBtn) canvasBtn.classList.add("active");
2549
- canvasHeader?.classList.add("hidden");
2550
- outputHeader?.classList.add("hidden");
2551
- output.classList.add("hidden");
2552
- if (canvasWrapper) canvasWrapper.classList.remove("hidden");
2553
- canvasOutputSplitter?.classList.add("hidden");
2554
- turtleModules.get(id)?.__redraw?.();
2555
- };
2556
-
2557
- const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
2558
- elem,
2559
- container,
2560
- canvasWrapper,
2561
- output,
2562
- canvasOutputSplitter,
2563
- (splitState) => {
2564
- void persistPyideState(splitState);
2565
- },
2566
- );
2567
-
2568
- const applyCanvasOutputLayout = () => {
2569
- if (!hasCanvas || !canvasWrapper || !canvasOutputSplitter) return;
2570
- if (isWideCanvasMode()) {
2571
- elem.classList.add("canvas-split-mode");
2572
- canvasTabs?.classList.add("hidden");
2573
- output.classList.remove("hidden");
2574
- canvasWrapper.classList.remove("hidden");
2575
- canvasOutputSplitter.classList.remove("hidden");
2576
- canvasHeader?.classList.remove("hidden");
2577
- outputHeader?.classList.remove("hidden");
2307
+ const isWideCanvasMode = () => hasCanvas && window.matchMedia("(min-width: 1024px)").matches;
2308
+ let activeCanvasView = "output";
2309
+ const showOutputTab = () => {
2310
+ activeCanvasView = "output";
2578
2311
  outputBtn.classList.add("active");
2579
- outputBtn.disabled = true;
2580
- if (canvasBtn) {
2581
- canvasBtn.classList.add("active");
2582
- canvasBtn.disabled = true;
2583
- }
2584
- applyStoredCanvasOutputSplit?.();
2312
+ if (canvasBtn) canvasBtn.classList.remove("active");
2313
+ canvasHeader?.classList.add("hidden");
2314
+ outputHeader?.classList.add("hidden");
2315
+ output.classList.remove("hidden");
2316
+ if (canvasWrapper) canvasWrapper.classList.add("hidden");
2317
+ canvasOutputSplitter?.classList.add("hidden");
2318
+ };
2319
+ const showCanvasTab = () => {
2320
+ activeCanvasView = "canvas";
2321
+ outputBtn.classList.remove("active");
2322
+ if (canvasBtn) canvasBtn.classList.add("active");
2323
+ canvasHeader?.classList.add("hidden");
2324
+ outputHeader?.classList.add("hidden");
2325
+ output.classList.add("hidden");
2326
+ if (canvasWrapper) canvasWrapper.classList.remove("hidden");
2327
+ canvasOutputSplitter?.classList.add("hidden");
2585
2328
  turtleModules.get(id)?.__redraw?.();
2586
- return;
2587
- }
2588
-
2589
- elem.classList.remove("canvas-split-mode");
2590
- canvasTabs?.classList.remove("hidden");
2591
- output.style.flex = "";
2592
- canvasWrapper.style.flex = "";
2593
- outputBtn.disabled = false;
2594
- if (canvasBtn) {
2595
- canvasBtn.disabled = false;
2596
- }
2597
- if (activeCanvasView === "canvas") {
2598
- showCanvasTab();
2599
- } else {
2600
- showOutputTab();
2601
- }
2602
- };
2603
-
2604
- function showOutput() {
2605
- if (isWideCanvasMode()) {
2606
- applyCanvasOutputLayout();
2607
- return;
2608
- }
2609
- showOutputTab();
2610
- }
2611
- function showCanvas() {
2612
- if (isWideCanvasMode()) {
2613
- applyCanvasOutputLayout();
2614
- return;
2615
- }
2616
- showCanvasTab();
2617
- }
2618
-
2619
- outputBtn?.addEventListener("click", showOutput);
2620
- canvasBtn?.addEventListener("click", showCanvas);
2621
- const applyStoredSplitSize = setupSplitter(
2622
- elem,
2623
- container,
2624
- editorContainer,
2625
- splitter,
2626
- (splitState) => {
2627
- void persistPyideState(splitState);
2628
- },
2629
- );
2630
-
2631
- let editorStateRestored = false;
2632
- const restoreEditorState = async () => {
2633
- if (editorStateRestored) return;
2634
- editorStateRestored = true;
2635
-
2636
- const result = await hyperbook.store.db.pyide.get(id);
2637
- if (result) {
2638
- pyideState = { ...pyideState, ...result };
2639
- if (typeof result.script === "string") {
2640
- cm?.setValue(result.script);
2329
+ };
2330
+ const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
2331
+ elem,
2332
+ container,
2333
+ canvasWrapper,
2334
+ output,
2335
+ canvasOutputSplitter,
2336
+ (splitState) => {
2337
+ void persistPyideState(splitState);
2641
2338
  }
2642
- if (
2643
- Number.isFinite(result.splitHorizontal) &&
2644
- result.splitHorizontal > 0
2645
- ) {
2646
- elem.dataset.splitHorizontal = String(Math.round(result.splitHorizontal));
2339
+ );
2340
+ const applyCanvasOutputLayout = () => {
2341
+ if (!hasCanvas || !canvasWrapper || !canvasOutputSplitter) return;
2342
+ if (isWideCanvasMode()) {
2343
+ elem.classList.add("canvas-split-mode");
2344
+ canvasTabs?.classList.add("hidden");
2345
+ output.classList.remove("hidden");
2346
+ canvasWrapper.classList.remove("hidden");
2347
+ canvasOutputSplitter.classList.remove("hidden");
2348
+ canvasHeader?.classList.remove("hidden");
2349
+ outputHeader?.classList.remove("hidden");
2350
+ outputBtn.classList.add("active");
2351
+ outputBtn.disabled = true;
2352
+ if (canvasBtn) {
2353
+ canvasBtn.classList.add("active");
2354
+ canvasBtn.disabled = true;
2355
+ }
2356
+ applyStoredCanvasOutputSplit?.();
2357
+ turtleModules.get(id)?.__redraw?.();
2358
+ return;
2647
2359
  }
2648
- if (
2649
- Number.isFinite(result.splitVertical) &&
2650
- result.splitVertical > 0
2651
- ) {
2652
- elem.dataset.splitVertical = String(Math.round(result.splitVertical));
2360
+ elem.classList.remove("canvas-split-mode");
2361
+ canvasTabs?.classList.remove("hidden");
2362
+ output.style.flex = "";
2363
+ canvasWrapper.style.flex = "";
2364
+ outputBtn.disabled = false;
2365
+ if (canvasBtn) {
2366
+ canvasBtn.disabled = false;
2653
2367
  }
2654
- if (
2655
- Number.isFinite(result.splitCanvasOutput) &&
2656
- result.splitCanvasOutput > 0
2657
- ) {
2658
- elem.dataset.splitCanvasOutput = String(
2659
- Math.round(result.splitCanvasOutput),
2660
- );
2368
+ if (activeCanvasView === "canvas") {
2369
+ showCanvasTab();
2370
+ } else {
2371
+ showOutputTab();
2661
2372
  }
2662
- applyStoredSplitSize?.();
2663
- applyCanvasOutputLayout();
2664
- }
2665
- };
2666
-
2667
- void restoreEditorState();
2668
-
2669
- window.addEventListener("resize", () => {
2670
- applyCanvasOutputLayout();
2671
- turtleModules.get(id)?.__redraw?.();
2672
- });
2673
- applyCanvasOutputLayout();
2674
-
2675
- test?.addEventListener("click", async () => {
2676
- showOutput();
2677
- const state = getExecutionState(id);
2678
- if (state.running || getRunningInstanceId() !== null) return;
2679
- state.running = true;
2680
- state.type = "test";
2681
- state.stopRequested = false;
2682
- state.stopping = false;
2683
- const interruptBuffer = interruptBuffers.get(id);
2684
- if (interruptBuffer) interruptBuffer[0] = 0;
2685
- updateRunning();
2686
-
2687
- output.innerHTML = "";
2688
- clearPytamaroStdoutCarry(id);
2689
-
2690
- const script = getEditorValue();
2691
- try {
2692
- for (let test of tests) {
2693
- if (state.stopRequested) {
2694
- appendOutputLine(id, "Stopped pending test execution.");
2695
- break;
2373
+ };
2374
+ outputBtn?.addEventListener("click", showOutput2);
2375
+ canvasBtn?.addEventListener("click", showCanvas2);
2376
+ const applyStoredSplitSize = setupSplitter(
2377
+ elem,
2378
+ container,
2379
+ editorContainer,
2380
+ splitter,
2381
+ (splitState) => {
2382
+ void persistPyideState(splitState);
2383
+ }
2384
+ );
2385
+ let editorStateRestored = false;
2386
+ const restoreEditorState = async () => {
2387
+ if (editorStateRestored) return;
2388
+ editorStateRestored = true;
2389
+ const result = await hyperbook.store.db.pyide.get(id);
2390
+ if (result) {
2391
+ pyideState = { ...pyideState, ...result };
2392
+ if (typeof result.script === "string") {
2393
+ cm?.setValue(result.script);
2696
2394
  }
2697
-
2698
- const testCode = test.code.replace("#SCRIPT#", script);
2699
-
2700
- const heading = document.createElement("div");
2701
- heading.innerHTML = `== Test ${test.name} ==`;
2702
- heading.classList.add("test-heading");
2703
- output.appendChild(heading);
2704
-
2705
- const { results, error } = await executeScript(
2706
- id,
2707
- testCode,
2708
- {},
2709
- additionalPackages,
2710
- );
2711
- if (results) {
2712
- appendOutput(output, results);
2713
- } else if (error) {
2714
- appendFriendlyError(output, error, testCode);
2395
+ if (Number.isFinite(result.splitHorizontal) && result.splitHorizontal > 0) {
2396
+ elem.dataset.splitHorizontal = String(
2397
+ Math.round(result.splitHorizontal)
2398
+ );
2399
+ }
2400
+ if (Number.isFinite(result.splitVertical) && result.splitVertical > 0) {
2401
+ elem.dataset.splitVertical = String(
2402
+ Math.round(result.splitVertical)
2403
+ );
2404
+ }
2405
+ if (Number.isFinite(result.splitCanvasOutput) && result.splitCanvasOutput > 0) {
2406
+ elem.dataset.splitCanvasOutput = String(
2407
+ Math.round(result.splitCanvasOutput)
2408
+ );
2715
2409
  }
2410
+ applyStoredSplitSize?.();
2411
+ applyCanvasOutputLayout();
2716
2412
  }
2717
- } catch (e) {
2718
- output.innerHTML = "";
2719
- appendOutput(output, `Error: ${e}`, true);
2720
- console.log(e);
2721
- } finally {
2722
- clearPytamaroStdoutCarry(id);
2723
- state.running = false;
2413
+ };
2414
+ void restoreEditorState();
2415
+ window.addEventListener("resize", () => {
2416
+ applyCanvasOutputLayout();
2417
+ turtleModules.get(id)?.__redraw?.();
2418
+ });
2419
+ applyCanvasOutputLayout();
2420
+ test?.addEventListener("click", async () => {
2421
+ showOutput2();
2422
+ const state = getExecutionState(id);
2423
+ if (state.running || getRunningInstanceId() !== null) return;
2424
+ state.running = true;
2425
+ state.type = "test";
2426
+ state.stopRequested = false;
2724
2427
  state.stopping = false;
2725
- state.type = null;
2726
- releaseKeyboardCapture(id);
2428
+ const interruptBuffer = interruptBuffers.get(id);
2429
+ if (interruptBuffer) interruptBuffer[0] = 0;
2727
2430
  updateRunning();
2728
- }
2729
- });
2730
-
2731
- run?.addEventListener("click", async () => {
2732
- const script = getEditorValue();
2733
- const usesPytamaro = hasPytamaroPackage || scriptLooksLikePytamaro(script);
2734
- const renderPytamaroToCanvas = hasCanvas && canvas && usesPytamaro;
2735
- if (hasCanvas) {
2736
- showCanvas();
2737
- } else {
2738
- showOutput();
2739
- }
2740
- const state = getExecutionState(id);
2741
- if (state.running || getRunningInstanceId() !== null) return;
2742
- state.running = true;
2743
- state.type = "run";
2744
- state.stopRequested = false;
2745
- state.stopping = false;
2746
- const interruptBuffer = interruptBuffers.get(id);
2747
- if (interruptBuffer) interruptBuffer[0] = 0;
2748
- updateRunning();
2749
-
2750
- output.innerHTML = "";
2751
- clearPytamaroStdoutCarry(id);
2752
- try {
2753
- setPytamaroCanvasTarget(id, renderPytamaroToCanvas);
2754
- const { results, error } = await executeScript(id, script, {
2755
- ...(hasCanvas && canvas ? { canvas } : {}),
2756
- }, additionalPackages);
2757
- if (!state.stopRequested) {
2758
- if (results) {
2759
- appendOutput(output, results, false, id);
2760
- } else if (error) {
2761
- showOutput();
2762
- appendFriendlyError(output, error, script);
2431
+ output.innerHTML = "";
2432
+ clearPytamaroStdoutCarry(id);
2433
+ const script = getEditorValue();
2434
+ try {
2435
+ for (let test2 of tests) {
2436
+ if (state.stopRequested) {
2437
+ appendOutputLine(id, "Stopped pending test execution.");
2438
+ break;
2439
+ }
2440
+ const testCode = test2.code.replace("#SCRIPT#", script);
2441
+ const heading = document.createElement("div");
2442
+ heading.innerHTML = `== Test ${test2.name} ==`;
2443
+ heading.classList.add("test-heading");
2444
+ output.appendChild(heading);
2445
+ const { results, error } = await executeScript(
2446
+ id,
2447
+ testCode,
2448
+ {},
2449
+ additionalPackages
2450
+ );
2451
+ if (results) {
2452
+ appendOutput(output, results);
2453
+ } else if (error) {
2454
+ appendFriendlyError(output, error, testCode);
2455
+ }
2763
2456
  }
2457
+ } catch (e) {
2458
+ output.innerHTML = "";
2459
+ appendOutput(output, `Error: ${e}`, true);
2460
+ console.log(e);
2461
+ } finally {
2462
+ clearPytamaroStdoutCarry(id);
2463
+ state.running = false;
2464
+ state.stopping = false;
2465
+ state.type = null;
2466
+ releaseKeyboardCapture(id);
2467
+ updateRunning();
2468
+ }
2469
+ });
2470
+ run?.addEventListener("click", async () => {
2471
+ const script = getEditorValue();
2472
+ if (hasCanvas) {
2473
+ showCanvas2();
2764
2474
  } else {
2765
- appendOutputLine(id, "Execution stopped.");
2475
+ showOutput2();
2766
2476
  }
2767
- } catch (e) {
2768
- showOutput();
2769
- output.innerHTML = "";
2770
- appendOutput(output, `Error: ${e}`, true, id);
2771
- console.log(e);
2772
- } finally {
2773
- clearPytamaroStdoutCarry(id);
2774
- state.running = false;
2477
+ const state = getExecutionState(id);
2478
+ if (state.running || getRunningInstanceId() !== null) return;
2479
+ state.running = true;
2480
+ state.type = "run";
2481
+ state.stopRequested = false;
2775
2482
  state.stopping = false;
2776
- state.type = null;
2777
- releaseKeyboardCapture(id);
2483
+ const interruptBuffer = interruptBuffers.get(id);
2484
+ if (interruptBuffer) interruptBuffer[0] = 0;
2778
2485
  updateRunning();
2779
- }
2780
- });
2781
-
2782
- stop?.addEventListener("click", handleStopClick);
2783
- }
2784
- };
2785
-
2786
- const observer = new MutationObserver((mutations) => {
2787
- mutations.forEach((mutation) => {
2788
- if (mutation.addedNodes.length) {
2789
- mutation.addedNodes.forEach((node) => {
2790
- if (node.type === 1 && node.classList?.contains("directive-pyide")) {
2791
- init(node);
2486
+ output.innerHTML = "";
2487
+ clearPytamaroStdoutCarry(id);
2488
+ try {
2489
+ const { results, error } = await executeScript(
2490
+ id,
2491
+ script,
2492
+ {
2493
+ ...hasCanvas && canvas ? { canvas } : {}
2494
+ },
2495
+ additionalPackages
2496
+ );
2497
+ if (!state.stopRequested) {
2498
+ if (results) {
2499
+ appendOutput(output, results, false, id);
2500
+ } else if (error) {
2501
+ showOutput2();
2502
+ appendFriendlyError(output, error, script);
2503
+ }
2504
+ } else {
2505
+ appendOutputLine(id, "Execution stopped.");
2506
+ }
2507
+ } catch (e) {
2508
+ showOutput2();
2509
+ output.innerHTML = "";
2510
+ appendOutput(output, `Error: ${e}`, true, id);
2511
+ console.log(e);
2512
+ } finally {
2513
+ clearPytamaroStdoutCarry(id);
2514
+ state.running = false;
2515
+ state.stopping = false;
2516
+ state.type = null;
2517
+ releaseKeyboardCapture(id);
2518
+ updateRunning();
2792
2519
  }
2793
2520
  });
2521
+ stop?.addEventListener("click", handleStopClick);
2794
2522
  }
2523
+ };
2524
+ const observer = new MutationObserver((mutations) => {
2525
+ mutations.forEach((mutation) => {
2526
+ if (mutation.addedNodes.length) {
2527
+ mutation.addedNodes.forEach((node) => {
2528
+ if (node.nodeType === 1 && node.classList?.contains("directive-pyide")) {
2529
+ init(node);
2530
+ }
2531
+ });
2532
+ }
2533
+ });
2795
2534
  });
2796
- });
2797
-
2798
- observer.observe(document.body, { childList: true, subtree: true });
2799
-
2800
- document.addEventListener("DOMContentLoaded", () => {
2801
- init(document);
2802
- });
2803
- document.addEventListener("fullscreenchange", syncFullscreenButtons);
2804
-
2805
- return { init };
2535
+ observer.observe(document.body, { childList: true, subtree: true });
2536
+ document.addEventListener("DOMContentLoaded", () => {
2537
+ init(document);
2538
+ });
2539
+ document.addEventListener("fullscreenchange", syncFullscreenButtons);
2540
+ return { init };
2541
+ }();
2806
2542
  })();