hyperbook 0.90.0 → 0.91.1
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.
- package/dist/assets/code-input/auto-close-brackets.min.js +1 -1
- package/dist/assets/code-input/code-input.min.css +1 -1
- package/dist/assets/code-input/code-input.min.js +12 -1
- package/dist/assets/code-input/indent.min.js +1 -1
- package/dist/assets/directive-excalidraw/hyperbook-excalidraw.umd.js +1 -1
- package/dist/assets/directive-onlineide/style.css +8 -0
- package/dist/assets/directive-p5/client.js +113 -0
- package/dist/assets/directive-p5/style.css +67 -3
- package/dist/assets/directive-pyide/client.js +1226 -146
- package/dist/assets/directive-pyide/style.css +183 -6
- package/dist/assets/directive-sqlide/style.css +8 -0
- package/dist/assets/directive-typst/client.js +154 -14
- package/dist/assets/directive-typst/style.css +65 -4
- package/dist/assets/directive-webide/client.js +114 -0
- package/dist/assets/directive-webide/style.css +61 -3
- package/dist/index.js +252 -54
- package/dist/locales/de.json +12 -0
- package/dist/locales/en.json +12 -0
- package/package.json +3 -3
- package/dist/assets/directive-pyide/webworker.js +0 -86
|
@@ -16,154 +16,1057 @@ hyperbook.python = (function () {
|
|
|
16
16
|
])
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
)
|
|
19
|
+
const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js";
|
|
20
|
+
|
|
21
|
+
const loadPyodideScript = () => {
|
|
22
|
+
if (window.loadPyodide) {
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const script = document.createElement("script");
|
|
28
|
+
script.src = PYODIDE_CDN;
|
|
29
|
+
script.onload = () => resolve();
|
|
30
|
+
script.onerror = () => reject(new Error("Failed to load Pyodide"));
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const pyodideReadyPromise = (async () => {
|
|
36
|
+
await loadPyodideScript();
|
|
37
|
+
return window.loadPyodide;
|
|
38
|
+
})();
|
|
22
39
|
|
|
23
|
-
let callback = null;
|
|
24
40
|
/**
|
|
25
|
-
* @type
|
|
41
|
+
* @type {Map<string, any>}
|
|
26
42
|
*/
|
|
27
|
-
|
|
43
|
+
const runtimes = new Map();
|
|
28
44
|
/**
|
|
29
|
-
* @type
|
|
45
|
+
* @type {Map<string, Set<string>>}
|
|
30
46
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
interruptBuffer[0] = 0;
|
|
50
|
-
return (script, context) => {
|
|
51
|
-
return new Promise((onSuccess) => {
|
|
52
|
-
callback = onSuccess;
|
|
53
|
-
updateRunning(id, type);
|
|
54
|
-
pyodideWorker.postMessage({
|
|
55
|
-
type: "run",
|
|
56
|
-
payload: {
|
|
57
|
-
...context,
|
|
58
|
-
python: script,
|
|
59
|
-
},
|
|
60
|
-
id,
|
|
61
|
-
});
|
|
47
|
+
const installedMicropipPackages = new Map();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @type {Map<string, { running: boolean, stopping: boolean, stopRequested: boolean, type: "run" | "test" | null }>}
|
|
51
|
+
*/
|
|
52
|
+
const executionStates = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* @type {Map<string, Int32Array>}
|
|
55
|
+
*/
|
|
56
|
+
const interruptBuffers = new Map();
|
|
57
|
+
|
|
58
|
+
const getExecutionState = (id) => {
|
|
59
|
+
if (!executionStates.has(id)) {
|
|
60
|
+
executionStates.set(id, {
|
|
61
|
+
running: false,
|
|
62
|
+
stopping: false,
|
|
63
|
+
stopRequested: false,
|
|
64
|
+
type: null,
|
|
62
65
|
});
|
|
66
|
+
}
|
|
67
|
+
return executionStates.get(id);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getRuntime = async (id) => {
|
|
71
|
+
if (runtimes.has(id)) {
|
|
72
|
+
return runtimes.get(id);
|
|
73
|
+
}
|
|
74
|
+
const loadPyodide = await pyodideReadyPromise;
|
|
75
|
+
const pyodide = await loadPyodide();
|
|
76
|
+
if (typeof pyodide.registerJsModule === "function") {
|
|
77
|
+
pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
typeof SharedArrayBuffer !== "undefined" &&
|
|
81
|
+
window.crossOriginIsolated &&
|
|
82
|
+
typeof pyodide.setInterruptBuffer === "function"
|
|
83
|
+
) {
|
|
84
|
+
const interruptBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
85
|
+
pyodide.setInterruptBuffer(interruptBuffer);
|
|
86
|
+
interruptBuffers.set(id, interruptBuffer);
|
|
87
|
+
}
|
|
88
|
+
runtimes.set(id, pyodide);
|
|
89
|
+
return pyodide;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const PYTAMARO_URI_BEGIN = "@@@PYTAMARO_DATA_URI_BEGIN@@@";
|
|
93
|
+
const PYTAMARO_URI_END = "@@@PYTAMARO_DATA_URI_END@@@";
|
|
94
|
+
/**
|
|
95
|
+
* @type {Map<string, string>}
|
|
96
|
+
*/
|
|
97
|
+
const pytamaroStdoutCarry = new Map();
|
|
98
|
+
|
|
99
|
+
const getOutput = (id) => {
|
|
100
|
+
return document.getElementById(id)?.getElementsByClassName("output")[0];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders a message that may contain pytamaro data URI image markers into
|
|
105
|
+
* the given container, creating <img> elements for each embedded image.
|
|
106
|
+
*/
|
|
107
|
+
const renderOutputSegments = (container, message) => {
|
|
108
|
+
let remaining = String(message);
|
|
109
|
+
while (remaining.length > 0) {
|
|
110
|
+
const beginIdx = remaining.indexOf(PYTAMARO_URI_BEGIN);
|
|
111
|
+
if (beginIdx === -1) {
|
|
112
|
+
container.appendChild(document.createTextNode(remaining));
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (beginIdx > 0) {
|
|
116
|
+
container.appendChild(document.createTextNode(remaining.slice(0, beginIdx)));
|
|
117
|
+
}
|
|
118
|
+
const afterBegin = remaining.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
|
|
119
|
+
const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
|
|
120
|
+
if (endIdx === -1) {
|
|
121
|
+
container.appendChild(document.createTextNode(remaining.slice(beginIdx)));
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
const dataUri = afterBegin.slice(0, endIdx);
|
|
125
|
+
const img = document.createElement("img");
|
|
126
|
+
img.src = dataUri;
|
|
127
|
+
img.style.maxWidth = "100%";
|
|
128
|
+
img.style.display = "block";
|
|
129
|
+
container.appendChild(img);
|
|
130
|
+
remaining = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const getTrailingPrefixLength = (text, marker) => {
|
|
135
|
+
const max = Math.min(text.length, marker.length - 1);
|
|
136
|
+
for (let len = max; len > 0; len -= 1) {
|
|
137
|
+
if (text.endsWith(marker.slice(0, len))) {
|
|
138
|
+
return len;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const appendText = (output, text) => {
|
|
145
|
+
if (!text) return;
|
|
146
|
+
output.appendChild(document.createTextNode(text));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const appendOutputLine = (id, message) => {
|
|
150
|
+
const output = getOutput(id);
|
|
151
|
+
if (!output) return;
|
|
152
|
+
const msg = String(message ?? "");
|
|
153
|
+
const carry = pytamaroStdoutCarry.get(id) || "";
|
|
154
|
+
let combined = carry + msg;
|
|
155
|
+
pytamaroStdoutCarry.delete(id);
|
|
156
|
+
|
|
157
|
+
// Fast path for regular stdout chunks.
|
|
158
|
+
if (!combined.includes(PYTAMARO_URI_BEGIN) && carry.length === 0) {
|
|
159
|
+
const partialBeginLength = getTrailingPrefixLength(combined, PYTAMARO_URI_BEGIN);
|
|
160
|
+
if (partialBeginLength > 0) {
|
|
161
|
+
const visible = combined.slice(0, combined.length - partialBeginLength);
|
|
162
|
+
appendText(output, visible);
|
|
163
|
+
pytamaroStdoutCarry.set(id, combined.slice(combined.length - partialBeginLength));
|
|
164
|
+
} else {
|
|
165
|
+
appendText(output, combined);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
while (combined.length > 0) {
|
|
171
|
+
const beginIdx = combined.indexOf(PYTAMARO_URI_BEGIN);
|
|
172
|
+
if (beginIdx === -1) {
|
|
173
|
+
const partialBeginLength = getTrailingPrefixLength(combined, PYTAMARO_URI_BEGIN);
|
|
174
|
+
const visible = combined.slice(0, combined.length - partialBeginLength);
|
|
175
|
+
appendText(output, visible);
|
|
176
|
+
if (partialBeginLength > 0) {
|
|
177
|
+
pytamaroStdoutCarry.set(id, combined.slice(combined.length - partialBeginLength));
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
appendText(output, combined.slice(0, beginIdx));
|
|
183
|
+
const afterBegin = combined.slice(beginIdx + PYTAMARO_URI_BEGIN.length);
|
|
184
|
+
const endIdx = afterBegin.indexOf(PYTAMARO_URI_END);
|
|
185
|
+
if (endIdx === -1) {
|
|
186
|
+
// Keep incomplete marker and continue when the next stdout chunk arrives.
|
|
187
|
+
pytamaroStdoutCarry.set(id, combined.slice(beginIdx));
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dataUri = afterBegin.slice(0, endIdx);
|
|
192
|
+
const img = document.createElement("img");
|
|
193
|
+
img.src = dataUri;
|
|
194
|
+
img.style.maxWidth = "100%";
|
|
195
|
+
img.style.display = "block";
|
|
196
|
+
output.appendChild(img);
|
|
197
|
+
combined = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const appendOutputErrorLine = (id, message) => {
|
|
202
|
+
const output = getOutput(id);
|
|
203
|
+
if (!output) return;
|
|
204
|
+
const line = document.createElement("span");
|
|
205
|
+
line.classList.add("error-line");
|
|
206
|
+
line.textContent = String(message);
|
|
207
|
+
output.appendChild(line);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const appendOutput = (output, message, isError = false) => {
|
|
211
|
+
if (!output || message === undefined || message === null) return;
|
|
212
|
+
if (isError) {
|
|
213
|
+
const line = document.createElement("span");
|
|
214
|
+
line.classList.add("error-line");
|
|
215
|
+
line.textContent = String(message);
|
|
216
|
+
output.appendChild(line);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const msg = String(message);
|
|
220
|
+
if (msg.includes(PYTAMARO_URI_BEGIN)) {
|
|
221
|
+
renderOutputSegments(output, msg);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
output.appendChild(document.createTextNode(msg));
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const clearPytamaroStdoutCarry = (id) => {
|
|
228
|
+
pytamaroStdoutCarry.delete(id);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const updateFullscreenButtonState = (elem, button) => {
|
|
232
|
+
if (!elem || !button) return;
|
|
233
|
+
const isFullscreen = document.fullscreenElement === elem;
|
|
234
|
+
const label = hyperbook.i18n.get("ide-fullscreen-enter");
|
|
235
|
+
button.textContent = "⛶";
|
|
236
|
+
button.title = label;
|
|
237
|
+
button.setAttribute("aria-label", label);
|
|
238
|
+
button.classList.toggle("active", isFullscreen);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const toggleFullscreen = async (elem) => {
|
|
242
|
+
if (!elem) return;
|
|
243
|
+
if (document.fullscreenElement === elem) {
|
|
244
|
+
await document.exitFullscreen();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
await elem.requestFullscreen();
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const syncFullscreenButtons = () => {
|
|
251
|
+
const elems = document.getElementsByClassName("directive-pyide");
|
|
252
|
+
for (const elem of elems) {
|
|
253
|
+
const fullscreen = elem.getElementsByClassName("fullscreen")[0];
|
|
254
|
+
updateFullscreenButtonState(elem, fullscreen);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const releaseKeyboardCapture = (id) => {
|
|
259
|
+
const elem = document.getElementById(id);
|
|
260
|
+
if (!elem) return;
|
|
261
|
+
const canvas = elem.getElementsByClassName("canvas")[0];
|
|
262
|
+
canvas?.blur?.();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const resetCanvas = (canvas) => {
|
|
266
|
+
if (!canvas) return;
|
|
267
|
+
const context = canvas.getContext("2d");
|
|
268
|
+
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const createPytamaroJsFFI = () => {
|
|
272
|
+
const floatBuffer = new ArrayBuffer(4);
|
|
273
|
+
const floatView = new DataView(floatBuffer);
|
|
274
|
+
|
|
275
|
+
const unProxy = (obj) => {
|
|
276
|
+
if (typeof obj === "object" && obj !== null && typeof obj.toJs === "function") {
|
|
277
|
+
try {
|
|
278
|
+
return obj.toJs({ pyproxies: [], dict_converter: Object.fromEntries });
|
|
279
|
+
} catch (e) {
|
|
280
|
+
console.error("Error converting PyProxy:", e);
|
|
281
|
+
return obj;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return obj;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const uint32ToFloat = (u32) => {
|
|
288
|
+
floatView.setUint32(0, u32 >>> 0, false);
|
|
289
|
+
return floatView.getFloat32(0, false);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const decodePoint = (value, width, height) => {
|
|
293
|
+
const packed = typeof value === "bigint" ? value : BigInt(value || 0);
|
|
294
|
+
const x = uint32ToFloat(Number((packed >> 32n) & 0xffffffffn));
|
|
295
|
+
const y = uint32ToFloat(Number(packed & 0xffffffffn));
|
|
296
|
+
return { x: x * width * 0.5, y: -y * height * 0.5 };
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const colorToCss = (value) => {
|
|
300
|
+
const argb =
|
|
301
|
+
typeof value === "bigint"
|
|
302
|
+
? Number(value & 0xffffffffn)
|
|
303
|
+
: Number(value >>> 0);
|
|
304
|
+
const a = ((argb >> 24) & 0xff) / 255;
|
|
305
|
+
const r = (argb >> 16) & 0xff;
|
|
306
|
+
const g = (argb >> 8) & 0xff;
|
|
307
|
+
const b = argb & 0xff;
|
|
308
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const rotatePoint = (point, pivot, angleRad) => {
|
|
312
|
+
const dx = point.x - pivot.x;
|
|
313
|
+
const dy = point.y - pivot.y;
|
|
314
|
+
const cos = Math.cos(angleRad);
|
|
315
|
+
const sin = Math.sin(angleRad);
|
|
316
|
+
return {
|
|
317
|
+
x: pivot.x + dx * cos - dy * sin,
|
|
318
|
+
y: pivot.y + dx * sin + dy * cos,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const buildGraphic = (specs) => {
|
|
323
|
+
const stack = [];
|
|
324
|
+
const measureCanvas = document.createElement("canvas");
|
|
325
|
+
const measureCtx = measureCanvas.getContext("2d");
|
|
326
|
+
|
|
327
|
+
for (const spec of specs || []) {
|
|
328
|
+
if (!spec || typeof spec !== "object") continue;
|
|
329
|
+
const type = spec.t;
|
|
330
|
+
if (
|
|
331
|
+
type === "Empty" ||
|
|
332
|
+
type === "Rectangle" ||
|
|
333
|
+
type === "Ellipse" ||
|
|
334
|
+
type === "CircularSector" ||
|
|
335
|
+
type === "Triangle" ||
|
|
336
|
+
type === "Text"
|
|
337
|
+
) {
|
|
338
|
+
let width = 0;
|
|
339
|
+
let height = 0;
|
|
340
|
+
let pin = { x: 0, y: 0 };
|
|
341
|
+
let draw = () => {};
|
|
342
|
+
|
|
343
|
+
if (type === "Rectangle") {
|
|
344
|
+
width = Math.max(0, Number(spec.width) || 0);
|
|
345
|
+
height = Math.max(0, Number(spec.height) || 0);
|
|
346
|
+
const fill = colorToCss(spec.color);
|
|
347
|
+
draw = (ctx) => {
|
|
348
|
+
ctx.fillStyle = fill;
|
|
349
|
+
ctx.fillRect(-width / 2, -height / 2, width, height);
|
|
350
|
+
};
|
|
351
|
+
} else if (type === "Ellipse") {
|
|
352
|
+
width = Math.max(0, Number(spec.width) || 0);
|
|
353
|
+
height = Math.max(0, Number(spec.height) || 0);
|
|
354
|
+
const fill = colorToCss(spec.color);
|
|
355
|
+
draw = (ctx) => {
|
|
356
|
+
ctx.beginPath();
|
|
357
|
+
ctx.ellipse(0, 0, width / 2, height / 2, 0, 0, 2 * Math.PI);
|
|
358
|
+
ctx.fillStyle = fill;
|
|
359
|
+
ctx.fill();
|
|
360
|
+
};
|
|
361
|
+
} else if (type === "CircularSector") {
|
|
362
|
+
const radius = Math.max(0, Number(spec.radius) || 0);
|
|
363
|
+
const angle = Number(spec.angle) || 0;
|
|
364
|
+
width = radius * 2;
|
|
365
|
+
height = radius * 2;
|
|
366
|
+
const fill = colorToCss(spec.color);
|
|
367
|
+
draw = (ctx) => {
|
|
368
|
+
ctx.beginPath();
|
|
369
|
+
ctx.moveTo(0, 0);
|
|
370
|
+
ctx.arc(0, 0, radius, 0, (-angle * Math.PI) / 180, true);
|
|
371
|
+
ctx.closePath();
|
|
372
|
+
ctx.fillStyle = fill;
|
|
373
|
+
ctx.fill();
|
|
374
|
+
};
|
|
375
|
+
} else if (type === "Triangle") {
|
|
376
|
+
const side1 = Math.max(0, Number(spec.side1) || 0);
|
|
377
|
+
const side2 = Math.max(0, Number(spec.side2) || 0);
|
|
378
|
+
const angle = (Number(spec.angle) || 0) * (Math.PI / 180);
|
|
379
|
+
const p1 = { x: 0, y: 0 };
|
|
380
|
+
const p2 = { x: side1, y: 0 };
|
|
381
|
+
const p3 = { x: side2 * Math.cos(angle), y: -side2 * Math.sin(angle) };
|
|
382
|
+
const centroid = {
|
|
383
|
+
x: (p1.x + p2.x + p3.x) / 3,
|
|
384
|
+
y: (p1.y + p2.y + p3.y) / 3,
|
|
385
|
+
};
|
|
386
|
+
const points = [p1, p2, p3].map((p) => ({
|
|
387
|
+
x: p.x - centroid.x,
|
|
388
|
+
y: p.y - centroid.y,
|
|
389
|
+
}));
|
|
390
|
+
const xs = points.map((p) => p.x);
|
|
391
|
+
const ys = points.map((p) => p.y);
|
|
392
|
+
width = Math.max(...xs) - Math.min(...xs);
|
|
393
|
+
height = Math.max(...ys) - Math.min(...ys);
|
|
394
|
+
const fill = colorToCss(spec.color);
|
|
395
|
+
draw = (ctx) => {
|
|
396
|
+
ctx.beginPath();
|
|
397
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
398
|
+
ctx.lineTo(points[1].x, points[1].y);
|
|
399
|
+
ctx.lineTo(points[2].x, points[2].y);
|
|
400
|
+
ctx.closePath();
|
|
401
|
+
ctx.fillStyle = fill;
|
|
402
|
+
ctx.fill();
|
|
403
|
+
};
|
|
404
|
+
} else if (type === "Text") {
|
|
405
|
+
const text = String(spec.text || "");
|
|
406
|
+
const fontName = String(spec.font_name || "sans-serif");
|
|
407
|
+
const textSize = Math.max(1, Number(spec.text_size) || 1);
|
|
408
|
+
const fill = colorToCss(spec.color);
|
|
409
|
+
measureCtx.font = `${textSize}px ${fontName}`;
|
|
410
|
+
const metrics = measureCtx.measureText(text);
|
|
411
|
+
width = Math.max(0, metrics.width || 0);
|
|
412
|
+
const ascent = metrics.actualBoundingBoxAscent || textSize * 0.8;
|
|
413
|
+
const descent = metrics.actualBoundingBoxDescent || textSize * 0.2;
|
|
414
|
+
height = ascent + descent;
|
|
415
|
+
pin = { x: -width / 2, y: (ascent - descent) / 2 };
|
|
416
|
+
draw = (ctx) => {
|
|
417
|
+
ctx.fillStyle = fill;
|
|
418
|
+
ctx.font = `${textSize}px ${fontName}`;
|
|
419
|
+
ctx.textAlign = "left";
|
|
420
|
+
ctx.textBaseline = "alphabetic";
|
|
421
|
+
const centerY = (descent - ascent) / 2;
|
|
422
|
+
ctx.fillText(text, -width / 2, -centerY);
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
stack.push({ width, height, pin, draw });
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (type === "Pin") {
|
|
431
|
+
const child = stack.pop();
|
|
432
|
+
if (!child) continue;
|
|
433
|
+
const pin = decodePoint(spec.pin, child.width, child.height);
|
|
434
|
+
stack.push({ ...child, pin });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (type === "Rotate") {
|
|
439
|
+
const child = stack.pop();
|
|
440
|
+
if (!child) continue;
|
|
441
|
+
const angleDeg = Number(spec.angle) || 0;
|
|
442
|
+
const angleRad = (-angleDeg * Math.PI) / 180;
|
|
443
|
+
const corners = [
|
|
444
|
+
{ x: -child.width / 2, y: -child.height / 2 },
|
|
445
|
+
{ x: child.width / 2, y: -child.height / 2 },
|
|
446
|
+
{ x: child.width / 2, y: child.height / 2 },
|
|
447
|
+
{ x: -child.width / 2, y: child.height / 2 },
|
|
448
|
+
].map((p) => rotatePoint(p, child.pin, angleRad));
|
|
449
|
+
const minX = Math.min(...corners.map((p) => p.x));
|
|
450
|
+
const maxX = Math.max(...corners.map((p) => p.x));
|
|
451
|
+
const minY = Math.min(...corners.map((p) => p.y));
|
|
452
|
+
const maxY = Math.max(...corners.map((p) => p.y));
|
|
453
|
+
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
454
|
+
const offset = { x: -center.x, y: -center.y };
|
|
455
|
+
const pin = { x: child.pin.x + offset.x, y: child.pin.y + offset.y };
|
|
456
|
+
|
|
457
|
+
stack.push({
|
|
458
|
+
width: maxX - minX,
|
|
459
|
+
height: maxY - minY,
|
|
460
|
+
pin,
|
|
461
|
+
draw: (ctx) => {
|
|
462
|
+
ctx.save();
|
|
463
|
+
ctx.translate(offset.x, offset.y);
|
|
464
|
+
ctx.translate(child.pin.x, child.pin.y);
|
|
465
|
+
ctx.rotate(angleRad);
|
|
466
|
+
ctx.translate(-child.pin.x, -child.pin.y);
|
|
467
|
+
child.draw(ctx);
|
|
468
|
+
ctx.restore();
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (type === "Compose") {
|
|
475
|
+
const bg = stack.pop();
|
|
476
|
+
const fg = stack.pop();
|
|
477
|
+
if (!fg || !bg) continue;
|
|
478
|
+
const fgPin = spec.fg_pin
|
|
479
|
+
? decodePoint(spec.fg_pin, fg.width, fg.height)
|
|
480
|
+
: fg.pin;
|
|
481
|
+
const bgPin = spec.bg_pin
|
|
482
|
+
? decodePoint(spec.bg_pin, bg.width, bg.height)
|
|
483
|
+
: bg.pin;
|
|
484
|
+
|
|
485
|
+
const bgCenter = { x: 0, y: 0 };
|
|
486
|
+
const fgCenter = { x: bgPin.x - fgPin.x, y: bgPin.y - fgPin.y };
|
|
487
|
+
const minX = Math.min(
|
|
488
|
+
fgCenter.x - fg.width / 2,
|
|
489
|
+
bgCenter.x - bg.width / 2,
|
|
490
|
+
);
|
|
491
|
+
const maxX = Math.max(
|
|
492
|
+
fgCenter.x + fg.width / 2,
|
|
493
|
+
bgCenter.x + bg.width / 2,
|
|
494
|
+
);
|
|
495
|
+
const minY = Math.min(
|
|
496
|
+
fgCenter.y - fg.height / 2,
|
|
497
|
+
bgCenter.y - bg.height / 2,
|
|
498
|
+
);
|
|
499
|
+
const maxY = Math.max(
|
|
500
|
+
fgCenter.y + fg.height / 2,
|
|
501
|
+
bgCenter.y + bg.height / 2,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const center = { x: (minX + maxX) / 2, y: (minY + maxY) / 2 };
|
|
505
|
+
const fgOffset = {
|
|
506
|
+
x: fgCenter.x - center.x,
|
|
507
|
+
y: fgCenter.y - center.y,
|
|
508
|
+
};
|
|
509
|
+
const bgOffset = {
|
|
510
|
+
x: bgCenter.x - center.x,
|
|
511
|
+
y: bgCenter.y - center.y,
|
|
512
|
+
};
|
|
513
|
+
const pin = spec.pin
|
|
514
|
+
? decodePoint(spec.pin, maxX - minX, maxY - minY)
|
|
515
|
+
: { x: bgPin.x - center.x, y: bgPin.y - center.y };
|
|
516
|
+
|
|
517
|
+
stack.push({
|
|
518
|
+
width: maxX - minX,
|
|
519
|
+
height: maxY - minY,
|
|
520
|
+
pin,
|
|
521
|
+
draw: (ctx) => {
|
|
522
|
+
ctx.save();
|
|
523
|
+
ctx.translate(bgOffset.x, bgOffset.y);
|
|
524
|
+
bg.draw(ctx);
|
|
525
|
+
ctx.restore();
|
|
526
|
+
ctx.save();
|
|
527
|
+
ctx.translate(fgOffset.x, fgOffset.y);
|
|
528
|
+
fg.draw(ctx);
|
|
529
|
+
ctx.restore();
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return (
|
|
536
|
+
stack[stack.length - 1] || {
|
|
537
|
+
width: 0,
|
|
538
|
+
height: 0,
|
|
539
|
+
pin: { x: 0, y: 0 },
|
|
540
|
+
draw: () => {},
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
js_graphic_size: (specs) => {
|
|
547
|
+
try {
|
|
548
|
+
const unproxiedSpecs = unProxy(specs);
|
|
549
|
+
const graphic = buildGraphic(unproxiedSpecs);
|
|
550
|
+
return { width: graphic.width, height: graphic.height };
|
|
551
|
+
} catch (e) {
|
|
552
|
+
console.error("js_graphic_size error:", e);
|
|
553
|
+
throw e;
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
js_render_graphic: (specs, scalingFactor, debug) => {
|
|
557
|
+
try {
|
|
558
|
+
const unproxiedSpecs = unProxy(specs);
|
|
559
|
+
const graphic = buildGraphic(unproxiedSpecs);
|
|
560
|
+
const width = Math.max(1, Math.ceil(graphic.width));
|
|
561
|
+
const height = Math.max(1, Math.ceil(graphic.height));
|
|
562
|
+
const scale = Math.max(1, Number(scalingFactor) || 1);
|
|
563
|
+
const canvas = document.createElement("canvas");
|
|
564
|
+
canvas.width = Math.max(1, Math.ceil(width * scale));
|
|
565
|
+
canvas.height = Math.max(1, Math.ceil(height * scale));
|
|
566
|
+
const ctx = canvas.getContext("2d");
|
|
567
|
+
ctx.scale(scale, scale);
|
|
568
|
+
ctx.translate(width / 2, height / 2);
|
|
569
|
+
graphic.draw(ctx);
|
|
570
|
+
|
|
571
|
+
if (debug) {
|
|
572
|
+
ctx.strokeStyle = "red";
|
|
573
|
+
ctx.lineWidth = 1 / scale;
|
|
574
|
+
ctx.strokeRect(-width / 2, -height / 2, width, height);
|
|
575
|
+
ctx.strokeStyle = "rgba(255, 255, 0, 0.8)";
|
|
576
|
+
ctx.beginPath();
|
|
577
|
+
ctx.moveTo(graphic.pin.x - 8, graphic.pin.y);
|
|
578
|
+
ctx.lineTo(graphic.pin.x + 8, graphic.pin.y);
|
|
579
|
+
ctx.moveTo(graphic.pin.x, graphic.pin.y - 8);
|
|
580
|
+
ctx.lineTo(graphic.pin.x, graphic.pin.y + 8);
|
|
581
|
+
ctx.stroke();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return canvas.toDataURL("image/png");
|
|
585
|
+
} catch (e) {
|
|
586
|
+
console.error("js_render_graphic error:", e);
|
|
587
|
+
throw e;
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
js_save: (filename, content) => {
|
|
591
|
+
const link = document.createElement("a");
|
|
592
|
+
link.href = String(content || "");
|
|
593
|
+
link.download = String(filename || "graphic.png");
|
|
594
|
+
link.click();
|
|
595
|
+
},
|
|
63
596
|
};
|
|
64
597
|
};
|
|
65
598
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
599
|
+
const ensureMicropipPackages = async (id, pyodide, packages = []) => {
|
|
600
|
+
if (packages.length === 0) return;
|
|
601
|
+
if (!installedMicropipPackages.has(id)) {
|
|
602
|
+
installedMicropipPackages.set(id, new Set());
|
|
603
|
+
}
|
|
604
|
+
const installed = installedMicropipPackages.get(id);
|
|
605
|
+
const toInstall = packages.filter((pkg) => !installed.has(pkg));
|
|
606
|
+
if (toInstall.length === 0) return;
|
|
70
607
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
608
|
+
await pyodide.loadPackage("micropip");
|
|
609
|
+
const micropip = pyodide.pyimport("micropip");
|
|
610
|
+
try {
|
|
611
|
+
for (const pkg of toInstall) {
|
|
612
|
+
await micropip.install(pkg);
|
|
613
|
+
installed.add(pkg);
|
|
614
|
+
}
|
|
615
|
+
} finally {
|
|
616
|
+
micropip?.destroy?.();
|
|
617
|
+
}
|
|
618
|
+
};
|
|
74
619
|
|
|
75
|
-
const
|
|
620
|
+
const executeScript = async (id, script, context = {}, packages = []) => {
|
|
621
|
+
const filename = "<exec>";
|
|
622
|
+
try {
|
|
623
|
+
const pyodide = await getRuntime(id);
|
|
624
|
+
const { canvas, ...globalsContext } = context;
|
|
625
|
+
const decoder = new TextDecoder("utf-8");
|
|
626
|
+
|
|
627
|
+
if (canvas) {
|
|
628
|
+
try {
|
|
629
|
+
resetCanvas(canvas);
|
|
630
|
+
pyodide.canvas.setCanvas2D(canvas);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
appendOutputErrorLine(id, `Canvas setup failed: ${error.message}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
pyodide.setStdin({
|
|
637
|
+
stdin: () => {
|
|
638
|
+
const value = window.prompt(hyperbook.i18n.get("pyide-input-prompt"));
|
|
639
|
+
if (value === null) {
|
|
640
|
+
return "";
|
|
641
|
+
}
|
|
642
|
+
return value;
|
|
643
|
+
},
|
|
644
|
+
});
|
|
645
|
+
pyodide.setStdout({
|
|
646
|
+
write: (msg) => {
|
|
647
|
+
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
648
|
+
appendOutputLine(id, text);
|
|
649
|
+
return msg?.length ?? text.length;
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
pyodide.setStderr({
|
|
653
|
+
write: (msg) => {
|
|
654
|
+
const text = typeof msg === "string" ? msg : decoder.decode(msg);
|
|
655
|
+
appendOutputErrorLine(id, text);
|
|
656
|
+
return msg?.length ?? text.length;
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
await ensureMicropipPackages(id, pyodide, packages);
|
|
661
|
+
await pyodide.loadPackagesFromImports(script);
|
|
662
|
+
const dict = pyodide.globals.get("dict");
|
|
663
|
+
const globals = dict();
|
|
664
|
+
try {
|
|
665
|
+
for (const [key, value] of Object.entries(globalsContext)) {
|
|
666
|
+
globals.set(key, value);
|
|
667
|
+
}
|
|
668
|
+
const results = await pyodide.runPythonAsync(script, {
|
|
669
|
+
globals,
|
|
670
|
+
locals: globals,
|
|
671
|
+
filename,
|
|
672
|
+
});
|
|
673
|
+
return { results };
|
|
674
|
+
} finally {
|
|
675
|
+
globals.destroy();
|
|
676
|
+
dict.destroy();
|
|
677
|
+
if (canvas) {
|
|
678
|
+
try {
|
|
679
|
+
await pyodide.runPythonAsync(
|
|
680
|
+
`import sys as _sys
|
|
681
|
+
_pg = _sys.modules.get('pygame')
|
|
682
|
+
if _pg:
|
|
683
|
+
try:
|
|
684
|
+
_pg.quit()
|
|
685
|
+
except Exception:
|
|
686
|
+
pass`,
|
|
687
|
+
{ filename: "<cleanup>" },
|
|
688
|
+
);
|
|
689
|
+
} catch (e) {
|
|
690
|
+
console.warn("pygame cleanup failed:", e);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (error) {
|
|
695
|
+
let message = error.message;
|
|
696
|
+
if (message.startsWith("Traceback")) {
|
|
697
|
+
const lines = message?.split("\n") || [];
|
|
698
|
+
const i = lines.findIndex((line) => line.includes(filename));
|
|
699
|
+
message = lines[0] + "\n" + lines.slice(i).join("\n");
|
|
700
|
+
}
|
|
701
|
+
return { error: message };
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const requestStop = (id) => {
|
|
706
|
+
const state = getExecutionState(id);
|
|
707
|
+
const hasRuntime = runtimes.has(id);
|
|
708
|
+
if ((!state.running && !hasRuntime) || state.stopRequested) return;
|
|
709
|
+
state.stopRequested = true;
|
|
710
|
+
state.stopping = true;
|
|
711
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
712
|
+
if (interruptBuffer) {
|
|
713
|
+
interruptBuffer[0] = 2;
|
|
714
|
+
appendOutputLine(id, "Stop requested. Interrupting execution...");
|
|
715
|
+
} else {
|
|
716
|
+
appendOutputLine(id, hyperbook.i18n.get("pyide-stop-reloading"));
|
|
717
|
+
}
|
|
718
|
+
releaseKeyboardCapture(id);
|
|
719
|
+
updateRunning();
|
|
720
|
+
if (!interruptBuffer) {
|
|
721
|
+
window.setTimeout(() => {
|
|
722
|
+
window.location.reload();
|
|
723
|
+
}, 50);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const handleStopClick = (event) => {
|
|
728
|
+
const elem = event.currentTarget.closest(".directive-pyide");
|
|
729
|
+
if (!elem?.id) return;
|
|
730
|
+
requestStop(elem.id);
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const getRunningInstanceId = () => {
|
|
734
|
+
const elems = document.getElementsByClassName("directive-pyide");
|
|
735
|
+
for (const elem of elems) {
|
|
736
|
+
if (getExecutionState(elem.id).running) {
|
|
737
|
+
return elem.id;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const updateRunning = () => {
|
|
744
|
+
const runningInstanceId = getRunningInstanceId();
|
|
76
745
|
const elems = document.getElementsByClassName("directive-pyide");
|
|
77
746
|
for (let elem of elems) {
|
|
78
747
|
const run = elem.getElementsByClassName("run")[0];
|
|
79
748
|
const test = elem.getElementsByClassName("test")[0];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
749
|
+
const stop = elem.getElementsByClassName("stop")[0];
|
|
750
|
+
const editor = elem.getElementsByClassName("editor")[0];
|
|
751
|
+
const editorTextarea = editor?.querySelector("textarea");
|
|
752
|
+
const state = getExecutionState(elem.id);
|
|
753
|
+
const hasRuntime = runtimes.has(elem.id);
|
|
754
|
+
const hasInterrupt = interruptBuffers.has(elem.id);
|
|
755
|
+
const lockedByOther =
|
|
756
|
+
runningInstanceId !== null &&
|
|
757
|
+
runningInstanceId !== elem.id &&
|
|
758
|
+
!state.running;
|
|
87
759
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
760
|
+
stop?.removeEventListener("click", handleStopClick);
|
|
761
|
+
run.classList.remove("stopping");
|
|
762
|
+
run.classList.remove("locked");
|
|
763
|
+
test?.classList.remove("stopping");
|
|
764
|
+
test?.classList.remove("locked");
|
|
765
|
+
stop?.classList.remove("stopping");
|
|
766
|
+
elem.classList.toggle("locked-by-other", lockedByOther);
|
|
767
|
+
|
|
768
|
+
if (state.running || lockedByOther) {
|
|
769
|
+
editor?.setAttribute("disabled", "");
|
|
770
|
+
editor?.classList.add("running");
|
|
771
|
+
if (editorTextarea) {
|
|
772
|
+
editorTextarea.readOnly = true;
|
|
773
|
+
}
|
|
774
|
+
if (state.running && state.type === "run") {
|
|
775
|
+
run.textContent = hyperbook.i18n.get("pyide-running");
|
|
776
|
+
run.disabled = true;
|
|
777
|
+
run.classList.add("running");
|
|
778
|
+
if (test) {
|
|
779
|
+
test.classList.add("running");
|
|
780
|
+
test.disabled = true;
|
|
97
781
|
}
|
|
782
|
+
} else if (state.running && state.type === "test" && test) {
|
|
783
|
+
test.textContent = hyperbook.i18n.get("pyide-testing");
|
|
784
|
+
test.disabled = true;
|
|
785
|
+
test.classList.add("running");
|
|
786
|
+
run.classList.add("running");
|
|
787
|
+
run.disabled = true;
|
|
98
788
|
} else {
|
|
789
|
+
const lockLabel = lockedByOther
|
|
790
|
+
? "pyide-locked-other-instance-running"
|
|
791
|
+
: "pyide-run";
|
|
792
|
+
run.textContent = hyperbook.i18n.get(lockLabel);
|
|
99
793
|
run.classList.add("running");
|
|
794
|
+
run.classList.toggle("locked", lockedByOther);
|
|
100
795
|
run.disabled = true;
|
|
101
796
|
if (test) {
|
|
797
|
+
test.textContent = hyperbook.i18n.get(
|
|
798
|
+
lockedByOther ? "pyide-locked-other-instance-running" : "pyide-test",
|
|
799
|
+
);
|
|
800
|
+
test.classList.toggle("locked", lockedByOther);
|
|
102
801
|
test.classList.add("running");
|
|
103
802
|
test.disabled = true;
|
|
104
803
|
}
|
|
105
804
|
}
|
|
805
|
+
|
|
806
|
+
if (stop) {
|
|
807
|
+
const stopLabel = hasInterrupt ? "pyide-stop" : "pyide-stop-refresh";
|
|
808
|
+
if (state.running) {
|
|
809
|
+
stop.textContent = state.stopping
|
|
810
|
+
? hyperbook.i18n.get("pyide-stopping")
|
|
811
|
+
: hyperbook.i18n.get(stopLabel);
|
|
812
|
+
stop.disabled = false;
|
|
813
|
+
stop.addEventListener("click", handleStopClick);
|
|
814
|
+
} else {
|
|
815
|
+
stop.textContent = hyperbook.i18n.get(stopLabel);
|
|
816
|
+
stop.disabled = true;
|
|
817
|
+
}
|
|
818
|
+
stop.classList.toggle("stopping", state.stopping);
|
|
819
|
+
}
|
|
106
820
|
} else {
|
|
821
|
+
editor?.removeAttribute("disabled");
|
|
822
|
+
editor?.classList.remove("running");
|
|
823
|
+
if (editorTextarea) {
|
|
824
|
+
editorTextarea.readOnly = false;
|
|
825
|
+
}
|
|
826
|
+
run.classList.remove("stopping");
|
|
107
827
|
run.classList.remove("running");
|
|
108
828
|
run.textContent = hyperbook.i18n.get("pyide-run");
|
|
109
829
|
run.disabled = false;
|
|
110
|
-
run.removeEventListener("click", interruptExecution);
|
|
111
|
-
run.removeEventListener("click", reload);
|
|
112
830
|
if (test) {
|
|
831
|
+
test.classList.remove("stopping");
|
|
113
832
|
test.classList.remove("running");
|
|
114
833
|
test.textContent = hyperbook.i18n.get("pyide-test");
|
|
115
834
|
test.disabled = false;
|
|
116
|
-
|
|
117
|
-
|
|
835
|
+
}
|
|
836
|
+
if (stop) {
|
|
837
|
+
stop.classList.remove("stopping");
|
|
838
|
+
stop.classList.remove("running");
|
|
839
|
+
stop.textContent = hyperbook.i18n.get(
|
|
840
|
+
hasInterrupt ? "pyide-stop" : "pyide-stop-refresh",
|
|
841
|
+
);
|
|
842
|
+
stop.disabled = true;
|
|
118
843
|
}
|
|
119
844
|
}
|
|
120
845
|
}
|
|
121
846
|
};
|
|
122
847
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
848
|
+
const setupSplitter = (
|
|
849
|
+
elem,
|
|
850
|
+
container,
|
|
851
|
+
editorContainer,
|
|
852
|
+
splitter,
|
|
853
|
+
onSplitChanged,
|
|
854
|
+
) => {
|
|
855
|
+
if (!container || !editorContainer || !splitter) return;
|
|
856
|
+
|
|
857
|
+
const minPanelSize = 120;
|
|
858
|
+
|
|
859
|
+
const getIsHorizontal = () =>
|
|
860
|
+
getComputedStyle(elem).flexDirection.startsWith("row");
|
|
861
|
+
|
|
862
|
+
const applySplitSize = (rawSize, isHorizontal) => {
|
|
863
|
+
const total = isHorizontal ? elem.clientWidth : elem.clientHeight;
|
|
864
|
+
const splitterSize = isHorizontal ? splitter.offsetWidth : splitter.offsetHeight;
|
|
865
|
+
const maxSize = Math.max(
|
|
866
|
+
minPanelSize,
|
|
867
|
+
total - splitterSize - minPanelSize
|
|
868
|
+
);
|
|
869
|
+
const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
|
|
870
|
+
container.style.flex = `0 0 ${clamped}px`;
|
|
871
|
+
return clamped;
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const applyStoredSplitSize = () => {
|
|
875
|
+
const isHorizontal = getIsHorizontal();
|
|
876
|
+
elem.classList.toggle("split-horizontal", isHorizontal);
|
|
877
|
+
elem.classList.toggle("split-vertical", !isHorizontal);
|
|
878
|
+
const key = isHorizontal ? "splitHorizontal" : "splitVertical";
|
|
879
|
+
const rawStored = Number(elem.dataset[key]);
|
|
880
|
+
if (!Number.isFinite(rawStored) || rawStored <= 0) {
|
|
881
|
+
container.style.flex = "";
|
|
882
|
+
return;
|
|
137
883
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
884
|
+
applySplitSize(rawStored, isHorizontal);
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
applyStoredSplitSize();
|
|
888
|
+
|
|
889
|
+
splitter.addEventListener("pointerdown", (event) => {
|
|
890
|
+
event.preventDefault();
|
|
891
|
+
splitter.setPointerCapture(event.pointerId);
|
|
892
|
+
|
|
893
|
+
const isHorizontal = getIsHorizontal();
|
|
894
|
+
const key = isHorizontal ? "splitHorizontal" : "splitVertical";
|
|
895
|
+
const startPointer = isHorizontal ? event.clientX : event.clientY;
|
|
896
|
+
const startSize = isHorizontal
|
|
897
|
+
? container.getBoundingClientRect().width
|
|
898
|
+
: container.getBoundingClientRect().height;
|
|
899
|
+
|
|
900
|
+
elem.classList.add("resizing");
|
|
901
|
+
|
|
902
|
+
const onPointerMove = (moveEvent) => {
|
|
903
|
+
const pointer = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
|
|
904
|
+
const delta = pointer - startPointer;
|
|
905
|
+
const size = applySplitSize(startSize + delta, isHorizontal);
|
|
906
|
+
elem.dataset[key] = String(Math.round(size));
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const onPointerUp = () => {
|
|
910
|
+
elem.classList.remove("resizing");
|
|
911
|
+
splitter.removeEventListener("pointermove", onPointerMove);
|
|
912
|
+
splitter.removeEventListener("pointerup", onPointerUp);
|
|
913
|
+
splitter.removeEventListener("pointercancel", onPointerUp);
|
|
914
|
+
const splitHorizontal = Number(elem.dataset.splitHorizontal);
|
|
915
|
+
const splitVertical = Number(elem.dataset.splitVertical);
|
|
916
|
+
onSplitChanged?.({
|
|
917
|
+
...(Number.isFinite(splitHorizontal) && splitHorizontal > 0
|
|
918
|
+
? { splitHorizontal: Math.round(splitHorizontal) }
|
|
919
|
+
: {}),
|
|
920
|
+
...(Number.isFinite(splitVertical) && splitVertical > 0
|
|
921
|
+
? { splitVertical: Math.round(splitVertical) }
|
|
922
|
+
: {}),
|
|
923
|
+
});
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
splitter.addEventListener("pointermove", onPointerMove);
|
|
927
|
+
splitter.addEventListener("pointerup", onPointerUp);
|
|
928
|
+
splitter.addEventListener("pointercancel", onPointerUp);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
window.addEventListener("resize", applyStoredSplitSize);
|
|
932
|
+
return applyStoredSplitSize;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const setupCanvasOutputSplitter = (
|
|
936
|
+
elem,
|
|
937
|
+
container,
|
|
938
|
+
canvasWrapper,
|
|
939
|
+
output,
|
|
940
|
+
splitter,
|
|
941
|
+
onSplitChanged,
|
|
942
|
+
) => {
|
|
943
|
+
if (!elem || !container || !canvasWrapper || !output || !splitter) return;
|
|
944
|
+
|
|
945
|
+
const minPanelSize = 80;
|
|
946
|
+
|
|
947
|
+
const getAvailableHeight = () => {
|
|
948
|
+
const tabs = container.querySelector(".buttons");
|
|
949
|
+
const tabsHeight = tabs && tabs.offsetParent !== null ? tabs.offsetHeight : 0;
|
|
950
|
+
return container.clientHeight - tabsHeight - splitter.offsetHeight;
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const applySplitSize = (rawSize) => {
|
|
954
|
+
const total = getAvailableHeight();
|
|
955
|
+
const maxSize = Math.max(minPanelSize, total - minPanelSize);
|
|
956
|
+
const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
|
|
957
|
+
canvasWrapper.style.flex = `0 0 ${clamped}px`;
|
|
958
|
+
output.style.flex = "1 1 0";
|
|
959
|
+
return clamped;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
const applyStoredSplitSize = () => {
|
|
963
|
+
const rawStored = Number(elem.dataset.splitCanvasOutput);
|
|
964
|
+
if (!Number.isFinite(rawStored) || rawStored <= 0) {
|
|
965
|
+
canvasWrapper.style.flex = "1 1 0";
|
|
966
|
+
output.style.flex = "1 1 0";
|
|
967
|
+
return;
|
|
142
968
|
}
|
|
143
|
-
|
|
969
|
+
applySplitSize(rawStored);
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
splitter.addEventListener("pointerdown", (event) => {
|
|
973
|
+
event.preventDefault();
|
|
974
|
+
splitter.setPointerCapture(event.pointerId);
|
|
975
|
+
|
|
976
|
+
const startPointer = event.clientY;
|
|
977
|
+
const startSize = canvasWrapper.getBoundingClientRect().height;
|
|
978
|
+
|
|
979
|
+
elem.classList.add("resizing");
|
|
980
|
+
|
|
981
|
+
const onPointerMove = (moveEvent) => {
|
|
982
|
+
const delta = moveEvent.clientY - startPointer;
|
|
983
|
+
const size = applySplitSize(startSize + delta);
|
|
984
|
+
elem.dataset.splitCanvasOutput = String(Math.round(size));
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const onPointerUp = () => {
|
|
988
|
+
elem.classList.remove("resizing");
|
|
989
|
+
splitter.removeEventListener("pointermove", onPointerMove);
|
|
990
|
+
splitter.removeEventListener("pointerup", onPointerUp);
|
|
991
|
+
splitter.removeEventListener("pointercancel", onPointerUp);
|
|
992
|
+
const splitCanvasOutput = Number(elem.dataset.splitCanvasOutput);
|
|
993
|
+
if (Number.isFinite(splitCanvasOutput) && splitCanvasOutput > 0) {
|
|
994
|
+
onSplitChanged?.({ splitCanvasOutput: Math.round(splitCanvasOutput) });
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
splitter.addEventListener("pointermove", onPointerMove);
|
|
999
|
+
splitter.addEventListener("pointerup", onPointerUp);
|
|
1000
|
+
splitter.addEventListener("pointercancel", onPointerUp);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
window.addEventListener("resize", applyStoredSplitSize);
|
|
1004
|
+
return applyStoredSplitSize;
|
|
144
1005
|
};
|
|
145
1006
|
|
|
146
1007
|
const init = (root) => {
|
|
147
1008
|
const elems = root.getElementsByClassName("directive-pyide");
|
|
148
1009
|
|
|
149
1010
|
for (let elem of elems) {
|
|
1011
|
+
if (elem.getAttribute("data-pyide-initialized") === "true") continue;
|
|
1012
|
+
elem.setAttribute("data-pyide-initialized", "true");
|
|
1013
|
+
|
|
150
1014
|
const editor = elem.getElementsByClassName("editor")[0];
|
|
1015
|
+
const container = elem.getElementsByClassName("container")[0];
|
|
1016
|
+
const editorContainer = elem.getElementsByClassName("editor-container")[0];
|
|
1017
|
+
const splitter = elem.getElementsByClassName("splitter")[0];
|
|
151
1018
|
const run = elem.getElementsByClassName("run")[0];
|
|
152
1019
|
const test = elem.getElementsByClassName("test")[0];
|
|
1020
|
+
const stop = elem.getElementsByClassName("stop")[0];
|
|
153
1021
|
const output = elem.getElementsByClassName("output")[0];
|
|
154
|
-
const
|
|
1022
|
+
const canvas = elem.getElementsByClassName("canvas")[0];
|
|
1023
|
+
const canvasWrapper = elem.getElementsByClassName("canvas-wrapper")[0] || canvas;
|
|
1024
|
+
const canvasOutputSplitter = elem.getElementsByClassName("canvas-output-splitter")[0];
|
|
1025
|
+
const canvasHeader = elem.getElementsByClassName("canvas-header")[0];
|
|
1026
|
+
const outputHeader = elem.getElementsByClassName("output-header")[0];
|
|
155
1027
|
const outputBtn = elem.getElementsByClassName("output-btn")[0];
|
|
156
|
-
const
|
|
1028
|
+
const canvasBtn = elem.getElementsByClassName("canvas-btn")[0];
|
|
1029
|
+
const canvasTabs = outputBtn?.closest(".buttons");
|
|
157
1030
|
|
|
158
1031
|
const copyEl = elem.getElementsByClassName("copy")[0];
|
|
159
1032
|
const resetEl = elem.getElementsByClassName("reset")[0];
|
|
160
1033
|
const downloadEl = elem.getElementsByClassName("download")[0];
|
|
1034
|
+
const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];
|
|
161
1035
|
|
|
162
1036
|
const id = elem.id;
|
|
1037
|
+
const hasCanvas = elem.getAttribute("data-canvas") === "true";
|
|
1038
|
+
const additionalPackages = Array.from(
|
|
1039
|
+
new Set(
|
|
1040
|
+
(elem.getAttribute("data-packages") || "")
|
|
1041
|
+
.split(",")
|
|
1042
|
+
.map((pkg) => pkg.trim())
|
|
1043
|
+
.filter((pkg) => pkg.length > 0),
|
|
1044
|
+
),
|
|
1045
|
+
);
|
|
1046
|
+
const hasPytamaroPackage = additionalPackages.some(
|
|
1047
|
+
(pkg) => pkg.toLowerCase() === "pytamaro",
|
|
1048
|
+
);
|
|
1049
|
+
const scriptLooksLikePytamaro = (script) => {
|
|
1050
|
+
return /\bfrom\s+pytamaro\s+import\b|\bimport\s+pytamaro\b/.test(script);
|
|
1051
|
+
};
|
|
1052
|
+
let pyideState = { id };
|
|
1053
|
+
|
|
1054
|
+
const getEditorValue = () => {
|
|
1055
|
+
const textarea = editor?.querySelector("textarea");
|
|
1056
|
+
if (textarea) return textarea.value;
|
|
1057
|
+
return typeof editor?.textContent === "string" ? editor.textContent : "";
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
pyideState = { ...pyideState, script: getEditorValue() };
|
|
1061
|
+
|
|
1062
|
+
const persistPyideState = (updates = {}) => {
|
|
1063
|
+
pyideState = { ...pyideState, ...updates, id };
|
|
1064
|
+
return hyperbook.store.db.pyide.put(pyideState);
|
|
1065
|
+
};
|
|
163
1066
|
|
|
164
1067
|
copyEl?.addEventListener("click", async () => {
|
|
165
1068
|
try {
|
|
166
|
-
await navigator.clipboard.writeText(
|
|
1069
|
+
await navigator.clipboard.writeText(getEditorValue());
|
|
167
1070
|
} catch (error) {
|
|
168
1071
|
console.error(error.message);
|
|
169
1072
|
}
|
|
@@ -176,102 +1079,278 @@ hyperbook.python = (function () {
|
|
|
176
1079
|
|
|
177
1080
|
downloadEl?.addEventListener("click", () => {
|
|
178
1081
|
const a = document.createElement("a");
|
|
179
|
-
const blob = new Blob([
|
|
1082
|
+
const blob = new Blob([getEditorValue()], { type: "text/plain" });
|
|
180
1083
|
a.href = URL.createObjectURL(blob);
|
|
181
1084
|
a.download = `script-${id}.py`;
|
|
182
1085
|
a.click();
|
|
183
1086
|
});
|
|
1087
|
+
|
|
1088
|
+
fullscreenEl?.addEventListener("click", async () => {
|
|
1089
|
+
try {
|
|
1090
|
+
await toggleFullscreen(elem);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
console.error(error.message);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
updateFullscreenButtonState(elem, fullscreenEl);
|
|
184
1096
|
let tests = [];
|
|
185
1097
|
try {
|
|
186
1098
|
tests = JSON.parse(atob(elem.getAttribute("data-tests")));
|
|
187
1099
|
} catch (e) {}
|
|
188
1100
|
|
|
189
|
-
|
|
1101
|
+
const isWideCanvasMode = () =>
|
|
1102
|
+
hasCanvas && window.matchMedia("(min-width: 1024px)").matches;
|
|
1103
|
+
|
|
1104
|
+
let activeCanvasView = "output";
|
|
1105
|
+
|
|
1106
|
+
const showOutputTab = () => {
|
|
1107
|
+
activeCanvasView = "output";
|
|
1108
|
+
outputBtn.classList.add("active");
|
|
1109
|
+
if (canvasBtn) canvasBtn.classList.remove("active");
|
|
1110
|
+
canvasHeader?.classList.add("hidden");
|
|
1111
|
+
outputHeader?.classList.add("hidden");
|
|
1112
|
+
output.classList.remove("hidden");
|
|
1113
|
+
if (canvasWrapper) canvasWrapper.classList.add("hidden");
|
|
1114
|
+
canvasOutputSplitter?.classList.add("hidden");
|
|
1115
|
+
};
|
|
1116
|
+
const showCanvasTab = () => {
|
|
1117
|
+
activeCanvasView = "canvas";
|
|
190
1118
|
outputBtn.classList.remove("active");
|
|
191
|
-
|
|
1119
|
+
if (canvasBtn) canvasBtn.classList.add("active");
|
|
1120
|
+
canvasHeader?.classList.add("hidden");
|
|
1121
|
+
outputHeader?.classList.add("hidden");
|
|
192
1122
|
output.classList.add("hidden");
|
|
193
|
-
|
|
194
|
-
|
|
1123
|
+
if (canvasWrapper) canvasWrapper.classList.remove("hidden");
|
|
1124
|
+
canvasOutputSplitter?.classList.add("hidden");
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
|
|
1128
|
+
elem,
|
|
1129
|
+
container,
|
|
1130
|
+
canvasWrapper,
|
|
1131
|
+
output,
|
|
1132
|
+
canvasOutputSplitter,
|
|
1133
|
+
(splitState) => {
|
|
1134
|
+
void persistPyideState(splitState);
|
|
1135
|
+
},
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
const applyCanvasOutputLayout = () => {
|
|
1139
|
+
if (!hasCanvas || !canvasWrapper || !canvasOutputSplitter) return;
|
|
1140
|
+
if (isWideCanvasMode()) {
|
|
1141
|
+
elem.classList.add("canvas-split-mode");
|
|
1142
|
+
canvasTabs?.classList.add("hidden");
|
|
1143
|
+
output.classList.remove("hidden");
|
|
1144
|
+
canvasWrapper.classList.remove("hidden");
|
|
1145
|
+
canvasOutputSplitter.classList.remove("hidden");
|
|
1146
|
+
canvasHeader?.classList.remove("hidden");
|
|
1147
|
+
outputHeader?.classList.remove("hidden");
|
|
1148
|
+
outputBtn.classList.add("active");
|
|
1149
|
+
outputBtn.disabled = true;
|
|
1150
|
+
if (canvasBtn) {
|
|
1151
|
+
canvasBtn.classList.add("active");
|
|
1152
|
+
canvasBtn.disabled = true;
|
|
1153
|
+
}
|
|
1154
|
+
applyStoredCanvasOutputSplit?.();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
elem.classList.remove("canvas-split-mode");
|
|
1159
|
+
canvasTabs?.classList.remove("hidden");
|
|
1160
|
+
output.style.flex = "";
|
|
1161
|
+
canvasWrapper.style.flex = "";
|
|
1162
|
+
outputBtn.disabled = false;
|
|
1163
|
+
if (canvasBtn) {
|
|
1164
|
+
canvasBtn.disabled = false;
|
|
1165
|
+
}
|
|
1166
|
+
if (activeCanvasView === "canvas") {
|
|
1167
|
+
showCanvasTab();
|
|
1168
|
+
} else {
|
|
1169
|
+
showOutputTab();
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
|
|
195
1173
|
function showOutput() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
1174
|
+
if (isWideCanvasMode()) {
|
|
1175
|
+
applyCanvasOutputLayout();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
showOutputTab();
|
|
1179
|
+
}
|
|
1180
|
+
function showCanvas() {
|
|
1181
|
+
if (isWideCanvasMode()) {
|
|
1182
|
+
applyCanvasOutputLayout();
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
showCanvasTab();
|
|
200
1186
|
}
|
|
201
1187
|
|
|
202
1188
|
outputBtn?.addEventListener("click", showOutput);
|
|
203
|
-
|
|
1189
|
+
canvasBtn?.addEventListener("click", showCanvas);
|
|
1190
|
+
const applyStoredSplitSize = setupSplitter(
|
|
1191
|
+
elem,
|
|
1192
|
+
container,
|
|
1193
|
+
editorContainer,
|
|
1194
|
+
splitter,
|
|
1195
|
+
(splitState) => {
|
|
1196
|
+
void persistPyideState(splitState);
|
|
1197
|
+
},
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
let editorStateRestored = false;
|
|
1201
|
+
const restoreEditorState = async () => {
|
|
1202
|
+
if (editorStateRestored) return;
|
|
1203
|
+
editorStateRestored = true;
|
|
204
1204
|
|
|
205
|
-
editor.addEventListener("code-input_load", async () => {
|
|
206
1205
|
const result = await hyperbook.store.db.pyide.get(id);
|
|
207
1206
|
if (result) {
|
|
208
|
-
|
|
1207
|
+
pyideState = { ...pyideState, ...result };
|
|
1208
|
+
if (typeof result.script === "string") {
|
|
1209
|
+
editor.value = result.script;
|
|
1210
|
+
}
|
|
1211
|
+
if (
|
|
1212
|
+
Number.isFinite(result.splitHorizontal) &&
|
|
1213
|
+
result.splitHorizontal > 0
|
|
1214
|
+
) {
|
|
1215
|
+
elem.dataset.splitHorizontal = String(Math.round(result.splitHorizontal));
|
|
1216
|
+
}
|
|
1217
|
+
if (
|
|
1218
|
+
Number.isFinite(result.splitVertical) &&
|
|
1219
|
+
result.splitVertical > 0
|
|
1220
|
+
) {
|
|
1221
|
+
elem.dataset.splitVertical = String(Math.round(result.splitVertical));
|
|
1222
|
+
}
|
|
1223
|
+
if (
|
|
1224
|
+
Number.isFinite(result.splitCanvasOutput) &&
|
|
1225
|
+
result.splitCanvasOutput > 0
|
|
1226
|
+
) {
|
|
1227
|
+
elem.dataset.splitCanvasOutput = String(
|
|
1228
|
+
Math.round(result.splitCanvasOutput),
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
applyStoredSplitSize?.();
|
|
1232
|
+
applyCanvasOutputLayout();
|
|
209
1233
|
}
|
|
210
|
-
}
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
editor.addEventListener("code-input_load", restoreEditorState);
|
|
1237
|
+
if (editor.querySelector("textarea")) {
|
|
1238
|
+
void restoreEditorState();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
window.addEventListener("resize", applyCanvasOutputLayout);
|
|
1242
|
+
applyCanvasOutputLayout();
|
|
211
1243
|
|
|
212
1244
|
editor.addEventListener("input", () => {
|
|
213
|
-
|
|
1245
|
+
void persistPyideState({ script: getEditorValue() });
|
|
214
1246
|
});
|
|
215
1247
|
|
|
216
1248
|
test?.addEventListener("click", async () => {
|
|
217
1249
|
showOutput();
|
|
218
|
-
|
|
1250
|
+
const state = getExecutionState(id);
|
|
1251
|
+
if (state.running || getRunningInstanceId() !== null) return;
|
|
1252
|
+
state.running = true;
|
|
1253
|
+
state.type = "test";
|
|
1254
|
+
state.stopRequested = false;
|
|
1255
|
+
state.stopping = false;
|
|
1256
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
1257
|
+
if (interruptBuffer) interruptBuffer[0] = 0;
|
|
1258
|
+
updateRunning();
|
|
219
1259
|
|
|
220
1260
|
output.innerHTML = "";
|
|
1261
|
+
clearPytamaroStdoutCarry(id);
|
|
1262
|
+
|
|
1263
|
+
const script = getEditorValue();
|
|
1264
|
+
try {
|
|
1265
|
+
for (let test of tests) {
|
|
1266
|
+
if (state.stopRequested) {
|
|
1267
|
+
appendOutputLine(id, "Stopped pending test execution.");
|
|
1268
|
+
break;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const testCode = test.code.replace("#SCRIPT#", script);
|
|
221
1272
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
1273
|
+
const heading = document.createElement("div");
|
|
1274
|
+
heading.innerHTML = `== Test ${test.name} ==`;
|
|
1275
|
+
heading.classList.add("test-heading");
|
|
1276
|
+
output.appendChild(heading);
|
|
1277
|
+
|
|
1278
|
+
const { results, error } = await executeScript(
|
|
1279
|
+
id,
|
|
1280
|
+
testCode,
|
|
1281
|
+
{},
|
|
1282
|
+
additionalPackages,
|
|
1283
|
+
);
|
|
1284
|
+
if (results) {
|
|
1285
|
+
appendOutput(output, results);
|
|
1286
|
+
} else if (error) {
|
|
1287
|
+
appendOutput(output, error, true);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
output.innerHTML = "";
|
|
1292
|
+
appendOutput(output, `Error: ${e}`, true);
|
|
1293
|
+
console.log(e);
|
|
1294
|
+
} finally {
|
|
1295
|
+
clearPytamaroStdoutCarry(id);
|
|
1296
|
+
state.running = false;
|
|
1297
|
+
state.stopping = false;
|
|
1298
|
+
state.type = null;
|
|
1299
|
+
releaseKeyboardCapture(id);
|
|
1300
|
+
updateRunning();
|
|
247
1301
|
}
|
|
248
1302
|
});
|
|
249
1303
|
|
|
250
1304
|
run?.addEventListener("click", async () => {
|
|
251
|
-
|
|
252
|
-
|
|
1305
|
+
const script = getEditorValue();
|
|
1306
|
+
const useOutputForPytamaro = hasPytamaroPackage || scriptLooksLikePytamaro(script);
|
|
1307
|
+
if (hasCanvas && !useOutputForPytamaro) {
|
|
1308
|
+
showCanvas();
|
|
1309
|
+
} else {
|
|
1310
|
+
showOutput();
|
|
1311
|
+
}
|
|
1312
|
+
const state = getExecutionState(id);
|
|
1313
|
+
if (state.running || getRunningInstanceId() !== null) return;
|
|
1314
|
+
state.running = true;
|
|
1315
|
+
state.type = "run";
|
|
1316
|
+
state.stopRequested = false;
|
|
1317
|
+
state.stopping = false;
|
|
1318
|
+
const interruptBuffer = interruptBuffers.get(id);
|
|
1319
|
+
if (interruptBuffer) interruptBuffer[0] = 0;
|
|
1320
|
+
updateRunning();
|
|
253
1321
|
|
|
254
|
-
const script = editor.value;
|
|
255
1322
|
output.innerHTML = "";
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
1323
|
+
clearPytamaroStdoutCarry(id);
|
|
1324
|
+
try {
|
|
1325
|
+
const { results, error } = await executeScript(id, script, {
|
|
1326
|
+
...(hasCanvas && canvas && !useOutputForPytamaro ? { canvas } : {}),
|
|
1327
|
+
}, additionalPackages);
|
|
1328
|
+
if (!state.stopRequested) {
|
|
260
1329
|
if (results) {
|
|
261
|
-
output
|
|
1330
|
+
appendOutput(output, results);
|
|
262
1331
|
} else if (error) {
|
|
263
|
-
|
|
1332
|
+
showOutput();
|
|
1333
|
+
appendOutput(output, error, true);
|
|
264
1334
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1335
|
+
} else {
|
|
1336
|
+
appendOutputLine(id, "Execution stopped.");
|
|
1337
|
+
}
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
showOutput();
|
|
1340
|
+
output.innerHTML = "";
|
|
1341
|
+
appendOutput(output, `Error: ${e}`, true);
|
|
1342
|
+
console.log(e);
|
|
1343
|
+
} finally {
|
|
1344
|
+
clearPytamaroStdoutCarry(id);
|
|
1345
|
+
state.running = false;
|
|
1346
|
+
state.stopping = false;
|
|
1347
|
+
state.type = null;
|
|
1348
|
+
releaseKeyboardCapture(id);
|
|
1349
|
+
updateRunning();
|
|
1350
|
+
}
|
|
274
1351
|
});
|
|
1352
|
+
|
|
1353
|
+
stop?.addEventListener("click", handleStopClick);
|
|
275
1354
|
}
|
|
276
1355
|
};
|
|
277
1356
|
|
|
@@ -292,6 +1371,7 @@ hyperbook.python = (function () {
|
|
|
292
1371
|
document.addEventListener("DOMContentLoaded", () => {
|
|
293
1372
|
init(document);
|
|
294
1373
|
});
|
|
1374
|
+
document.addEventListener("fullscreenchange", syncFullscreenButtons);
|
|
295
1375
|
|
|
296
1376
|
return { init };
|
|
297
1377
|
})();
|