hyperbook 0.92.0 → 0.93.0
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.
|
@@ -45,6 +45,10 @@ hyperbook.python = (function () {
|
|
|
45
45
|
* @type {Map<string, Set<string>>}
|
|
46
46
|
*/
|
|
47
47
|
const installedMicropipPackages = new Map();
|
|
48
|
+
/**
|
|
49
|
+
* @type {Map<string, any>}
|
|
50
|
+
*/
|
|
51
|
+
const turtleModules = new Map();
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* @type {Map<string, { running: boolean, stopping: boolean, stopRequested: boolean, type: "run" | "test" | null }>}
|
|
@@ -74,7 +78,12 @@ hyperbook.python = (function () {
|
|
|
74
78
|
const loadPyodide = await pyodideReadyPromise;
|
|
75
79
|
const pyodide = await loadPyodide();
|
|
76
80
|
if (typeof pyodide.registerJsModule === "function") {
|
|
81
|
+
const turtleModule = createTurtleJsFFI(id);
|
|
82
|
+
turtleModule.__setPyodide(pyodide);
|
|
83
|
+
pyodide.registerJsModule("turtle", turtleModule);
|
|
84
|
+
pyodide.registerJsModule("jturtle", turtleModule);
|
|
77
85
|
pyodide.registerJsModule("pytamaro_js_ffi", createPytamaroJsFFI());
|
|
86
|
+
turtleModules.set(id, turtleModule);
|
|
78
87
|
}
|
|
79
88
|
if (
|
|
80
89
|
typeof SharedArrayBuffer !== "undefined" &&
|
|
@@ -95,16 +104,58 @@ hyperbook.python = (function () {
|
|
|
95
104
|
* @type {Map<string, string>}
|
|
96
105
|
*/
|
|
97
106
|
const pytamaroStdoutCarry = new Map();
|
|
107
|
+
const pytamaroCanvasTargets = new Set();
|
|
98
108
|
|
|
99
109
|
const getOutput = (id) => {
|
|
100
110
|
return document.getElementById(id)?.getElementsByClassName("output")[0];
|
|
101
111
|
};
|
|
102
112
|
|
|
113
|
+
const getCanvas = (id) => {
|
|
114
|
+
return document.getElementById(id)?.getElementsByClassName("canvas")[0];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const setPytamaroCanvasTarget = (id, enabled) => {
|
|
118
|
+
if (enabled) {
|
|
119
|
+
pytamaroCanvasTargets.add(id);
|
|
120
|
+
} else {
|
|
121
|
+
pytamaroCanvasTargets.delete(id);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const renderPytamaroDataUri = (id, container, dataUri) => {
|
|
126
|
+
if (id && pytamaroCanvasTargets.has(id)) {
|
|
127
|
+
const canvas = getCanvas(id);
|
|
128
|
+
const context = canvas?.getContext?.("2d");
|
|
129
|
+
if (canvas && context) {
|
|
130
|
+
const img = new Image();
|
|
131
|
+
img.onload = () => {
|
|
132
|
+
const width = Math.max(1, img.naturalWidth || img.width);
|
|
133
|
+
const height = Math.max(1, img.naturalHeight || img.height);
|
|
134
|
+
canvas.width = width;
|
|
135
|
+
canvas.height = height;
|
|
136
|
+
context.clearRect(0, 0, width, height);
|
|
137
|
+
context.drawImage(img, 0, 0, width, height);
|
|
138
|
+
};
|
|
139
|
+
img.onerror = () => {
|
|
140
|
+
appendOutputErrorLine(id, "Failed to render pytamaro graphic.");
|
|
141
|
+
};
|
|
142
|
+
img.src = dataUri;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const img = document.createElement("img");
|
|
148
|
+
img.src = dataUri;
|
|
149
|
+
img.style.maxWidth = "100%";
|
|
150
|
+
img.style.display = "block";
|
|
151
|
+
container.appendChild(img);
|
|
152
|
+
};
|
|
153
|
+
|
|
103
154
|
/**
|
|
104
155
|
* Renders a message that may contain pytamaro data URI image markers into
|
|
105
156
|
* the given container, creating <img> elements for each embedded image.
|
|
106
157
|
*/
|
|
107
|
-
const renderOutputSegments = (container, message) => {
|
|
158
|
+
const renderOutputSegments = (container, message, id = null) => {
|
|
108
159
|
let remaining = String(message);
|
|
109
160
|
while (remaining.length > 0) {
|
|
110
161
|
const beginIdx = remaining.indexOf(PYTAMARO_URI_BEGIN);
|
|
@@ -122,15 +173,15 @@ hyperbook.python = (function () {
|
|
|
122
173
|
break;
|
|
123
174
|
}
|
|
124
175
|
const dataUri = afterBegin.slice(0, endIdx);
|
|
125
|
-
|
|
126
|
-
img.src = dataUri;
|
|
127
|
-
img.style.maxWidth = "100%";
|
|
128
|
-
img.style.display = "block";
|
|
129
|
-
container.appendChild(img);
|
|
176
|
+
renderPytamaroDataUri(id, container, dataUri);
|
|
130
177
|
remaining = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
131
178
|
}
|
|
132
179
|
};
|
|
133
180
|
|
|
181
|
+
const scriptLooksLikeTurtle = (script) => {
|
|
182
|
+
return /\bfrom\s+turtle\s+import\b|\bimport\s+turtle\b/.test(String(script || ""));
|
|
183
|
+
};
|
|
184
|
+
|
|
134
185
|
const getTrailingPrefixLength = (text, marker) => {
|
|
135
186
|
const max = Math.min(text.length, marker.length - 1);
|
|
136
187
|
for (let len = max; len > 0; len -= 1) {
|
|
@@ -189,11 +240,7 @@ hyperbook.python = (function () {
|
|
|
189
240
|
}
|
|
190
241
|
|
|
191
242
|
const dataUri = afterBegin.slice(0, endIdx);
|
|
192
|
-
|
|
193
|
-
img.src = dataUri;
|
|
194
|
-
img.style.maxWidth = "100%";
|
|
195
|
-
img.style.display = "block";
|
|
196
|
-
output.appendChild(img);
|
|
243
|
+
renderPytamaroDataUri(id, output, dataUri);
|
|
197
244
|
combined = afterBegin.slice(endIdx + PYTAMARO_URI_END.length);
|
|
198
245
|
}
|
|
199
246
|
};
|
|
@@ -207,7 +254,7 @@ hyperbook.python = (function () {
|
|
|
207
254
|
output.appendChild(line);
|
|
208
255
|
};
|
|
209
256
|
|
|
210
|
-
const appendOutput = (output, message, isError = false) => {
|
|
257
|
+
const appendOutput = (output, message, isError = false, id = null) => {
|
|
211
258
|
if (!output || message === undefined || message === null) return;
|
|
212
259
|
if (isError) {
|
|
213
260
|
const line = document.createElement("span");
|
|
@@ -218,7 +265,7 @@ hyperbook.python = (function () {
|
|
|
218
265
|
}
|
|
219
266
|
const msg = String(message);
|
|
220
267
|
if (msg.includes(PYTAMARO_URI_BEGIN)) {
|
|
221
|
-
renderOutputSegments(output, msg);
|
|
268
|
+
renderOutputSegments(output, msg, id);
|
|
222
269
|
return;
|
|
223
270
|
}
|
|
224
271
|
output.appendChild(document.createTextNode(msg));
|
|
@@ -226,6 +273,7 @@ hyperbook.python = (function () {
|
|
|
226
273
|
|
|
227
274
|
const clearPytamaroStdoutCarry = (id) => {
|
|
228
275
|
pytamaroStdoutCarry.delete(id);
|
|
276
|
+
pytamaroCanvasTargets.delete(id);
|
|
229
277
|
};
|
|
230
278
|
|
|
231
279
|
const updateFullscreenButtonState = (elem, button) => {
|
|
@@ -268,6 +316,954 @@ hyperbook.python = (function () {
|
|
|
268
316
|
context?.clearRect(0, 0, canvas.width, canvas.height);
|
|
269
317
|
};
|
|
270
318
|
|
|
319
|
+
const createTurtleJsFFI = (id) => {
|
|
320
|
+
const DEFAULT_LINE_WIDTH = 1;
|
|
321
|
+
const DEFAULT_FONT_SIZE = 8;
|
|
322
|
+
const TK_TEXT_X_OFFSET = -1;
|
|
323
|
+
const DEFAULT_TURTLE_SCREEN_WIDTH = 400;
|
|
324
|
+
const TURTLE_SIZE = 20;
|
|
325
|
+
|
|
326
|
+
let pyodide = null;
|
|
327
|
+
let canvas = null;
|
|
328
|
+
let context = null;
|
|
329
|
+
let cssWidth = 0;
|
|
330
|
+
let cssHeight = 0;
|
|
331
|
+
let dpr = 1;
|
|
332
|
+
let active = false;
|
|
333
|
+
|
|
334
|
+
let x = 0;
|
|
335
|
+
let y = 0;
|
|
336
|
+
let heading = 0;
|
|
337
|
+
let renderedX = 0;
|
|
338
|
+
let renderedY = 0;
|
|
339
|
+
let renderedHeading = 0;
|
|
340
|
+
let penDown = true;
|
|
341
|
+
let turtleVisible = true;
|
|
342
|
+
let renderedTurtleVisible = true;
|
|
343
|
+
let strokeColor = "#000000";
|
|
344
|
+
let fillColor = "#000000";
|
|
345
|
+
let backgroundColor = "#ffffff";
|
|
346
|
+
let backgroundImage = null;
|
|
347
|
+
let fontSize = DEFAULT_FONT_SIZE;
|
|
348
|
+
let currentFontFamily = "Arial";
|
|
349
|
+
let currentFontStyle = "normal";
|
|
350
|
+
let colorMode = 1.0;
|
|
351
|
+
let delayMs = 80;
|
|
352
|
+
let turtleSpeed = 3;
|
|
353
|
+
let screenWidth = null;
|
|
354
|
+
let screenHeight = null;
|
|
355
|
+
let filling = false;
|
|
356
|
+
let fillPath = null;
|
|
357
|
+
let currentPath = null;
|
|
358
|
+
/** @type {Array<any>} */
|
|
359
|
+
let paths = [];
|
|
360
|
+
let operationQueue = [];
|
|
361
|
+
let queueGeneration = 0;
|
|
362
|
+
let queueRunning = false;
|
|
363
|
+
const textMeasureCanvas = document.createElement("canvas");
|
|
364
|
+
const textMeasureContext = textMeasureCanvas.getContext("2d");
|
|
365
|
+
|
|
366
|
+
const normalizeAngle = (angle) => {
|
|
367
|
+
const value = Number(angle) || 0;
|
|
368
|
+
return ((value % 360) + 360) % 360;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const toRadians = (angle) => (Number(angle) * Math.PI) / 180;
|
|
372
|
+
const toCanvasX = (value) => cssWidth / 2 + value;
|
|
373
|
+
const toCanvasY = (value) => cssHeight / 2 - value;
|
|
374
|
+
const toPlainNumber = (value, fallback = Number.NaN) => {
|
|
375
|
+
if (value === null || value === undefined) return fallback;
|
|
376
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
|
|
377
|
+
if (typeof value === "bigint") return Number(value);
|
|
378
|
+
if (typeof value.toJs === "function") {
|
|
379
|
+
return toPlainNumber(value.toJs({ pyproxies: [] }), fallback);
|
|
380
|
+
}
|
|
381
|
+
const numeric = Number(value);
|
|
382
|
+
return Number.isFinite(numeric) ? numeric : fallback;
|
|
383
|
+
};
|
|
384
|
+
const fontSizeToCanvasUnits = (value) => {
|
|
385
|
+
const numeric = toPlainNumber(value, Number.NaN);
|
|
386
|
+
if (!Number.isFinite(numeric) || numeric === 0) {
|
|
387
|
+
const fallback = DEFAULT_FONT_SIZE;
|
|
388
|
+
return { size: `${fallback}pt`, pxApprox: (fallback * 96) / 72 };
|
|
389
|
+
}
|
|
390
|
+
if (numeric < 0) {
|
|
391
|
+
return { size: `${Math.abs(numeric)}px`, pxApprox: Math.abs(numeric) };
|
|
392
|
+
}
|
|
393
|
+
return { size: `${numeric}pt`, pxApprox: (numeric * 96) / 72 };
|
|
394
|
+
};
|
|
395
|
+
const toPlainString = (value, fallback = "") => {
|
|
396
|
+
if (value === null || value === undefined) return fallback;
|
|
397
|
+
if (typeof value === "string") return value;
|
|
398
|
+
if (typeof value.toJs === "function") {
|
|
399
|
+
return toPlainString(value.toJs({ pyproxies: [] }), fallback);
|
|
400
|
+
}
|
|
401
|
+
return String(value);
|
|
402
|
+
};
|
|
403
|
+
const toSequence = (value) => {
|
|
404
|
+
if (value === null || value === undefined) return null;
|
|
405
|
+
if (Array.isArray(value)) return value;
|
|
406
|
+
if (typeof value === "string") return [value];
|
|
407
|
+
if (typeof value.toJs === "function") {
|
|
408
|
+
return toSequence(value.toJs({ pyproxies: [] }));
|
|
409
|
+
}
|
|
410
|
+
if (typeof value[Symbol.iterator] === "function") {
|
|
411
|
+
try {
|
|
412
|
+
return Array.from(value);
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
if (typeof value === "object" && "length" in value) {
|
|
416
|
+
try {
|
|
417
|
+
return Array.from(value);
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
};
|
|
422
|
+
const toPlainObject = (value) => {
|
|
423
|
+
if (value === null || value === undefined) return null;
|
|
424
|
+
if (typeof value.toJs === "function") {
|
|
425
|
+
try {
|
|
426
|
+
return toPlainObject(value.toJs({ pyproxies: [], dict_converter: Object.fromEntries }));
|
|
427
|
+
} catch {
|
|
428
|
+
return toPlainObject(value.toJs({ pyproxies: [] }));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
432
|
+
return value;
|
|
433
|
+
};
|
|
434
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
435
|
+
const isWriteKwargsObject = (value) => {
|
|
436
|
+
const obj = toPlainObject(value);
|
|
437
|
+
if (!obj) return false;
|
|
438
|
+
return (
|
|
439
|
+
hasOwn(obj, "arg") ||
|
|
440
|
+
hasOwn(obj, "text") ||
|
|
441
|
+
hasOwn(obj, "move") ||
|
|
442
|
+
hasOwn(obj, "align") ||
|
|
443
|
+
hasOwn(obj, "font")
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
const normalizeFontStyle = (value) => {
|
|
447
|
+
const style = toPlainString(value, "normal").trim().toLowerCase();
|
|
448
|
+
if (!style || style === "normal") return "";
|
|
449
|
+
const parts = style.split(/\s+/).filter(Boolean);
|
|
450
|
+
const hasItalic = parts.includes("italic");
|
|
451
|
+
const hasBold = parts.includes("bold");
|
|
452
|
+
if (hasItalic && hasBold) return "italic bold";
|
|
453
|
+
if (hasBold) return "bold";
|
|
454
|
+
if (hasItalic) return "italic";
|
|
455
|
+
return style;
|
|
456
|
+
};
|
|
457
|
+
const normalizeTextAlign = (value) => {
|
|
458
|
+
const align = toPlainString(value, "left").trim().toLowerCase();
|
|
459
|
+
if (align === "center" || align === "right") return align;
|
|
460
|
+
return "left";
|
|
461
|
+
};
|
|
462
|
+
const buildCanvasFont = (family, size, style) => {
|
|
463
|
+
const { size: canvasSize } = fontSizeToCanvasUnits(size);
|
|
464
|
+
const stylePart = normalizeFontStyle(style);
|
|
465
|
+
return `${stylePart ? `${stylePart} ` : ""}${canvasSize} ${toPlainString(family, "Arial")}`;
|
|
466
|
+
};
|
|
467
|
+
const measureTurtleText = (text, family, size, style) => {
|
|
468
|
+
const font = buildCanvasFont(family, size, style);
|
|
469
|
+
const { pxApprox } = fontSizeToCanvasUnits(size);
|
|
470
|
+
const ctx = textMeasureContext || context;
|
|
471
|
+
if (!ctx) {
|
|
472
|
+
return {
|
|
473
|
+
font,
|
|
474
|
+
width: String(text || "").length * pxApprox * 0.6,
|
|
475
|
+
descent: pxApprox * 0.2,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
ctx.font = font;
|
|
479
|
+
ctx.textAlign = "left";
|
|
480
|
+
const metrics = ctx.measureText(String(text || ""));
|
|
481
|
+
return {
|
|
482
|
+
font,
|
|
483
|
+
width: metrics.width,
|
|
484
|
+
left: metrics.actualBoundingBoxLeft || 0,
|
|
485
|
+
right: metrics.actualBoundingBoxRight || metrics.width,
|
|
486
|
+
ascent:
|
|
487
|
+
metrics.fontBoundingBoxAscent ||
|
|
488
|
+
metrics.actualBoundingBoxAscent ||
|
|
489
|
+
pxApprox * 0.8,
|
|
490
|
+
descent:
|
|
491
|
+
metrics.fontBoundingBoxDescent ||
|
|
492
|
+
metrics.actualBoundingBoxDescent ||
|
|
493
|
+
pxApprox * 0.2,
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
497
|
+
const clearQueue = () => {
|
|
498
|
+
queueGeneration += 1;
|
|
499
|
+
operationQueue = [];
|
|
500
|
+
queueRunning = false;
|
|
501
|
+
};
|
|
502
|
+
const enqueueOperation = (fn) => {
|
|
503
|
+
if (delayMs <= 0) {
|
|
504
|
+
fn();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const generation = queueGeneration;
|
|
508
|
+
operationQueue.push({ fn, generation });
|
|
509
|
+
if (queueRunning) return;
|
|
510
|
+
queueRunning = true;
|
|
511
|
+
void (async () => {
|
|
512
|
+
try {
|
|
513
|
+
while (operationQueue.length > 0) {
|
|
514
|
+
const operation = operationQueue.shift();
|
|
515
|
+
if (!operation || operation.generation !== queueGeneration) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
operation.fn();
|
|
519
|
+
if (delayMs > 0) {
|
|
520
|
+
await sleep(delayMs);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} finally {
|
|
524
|
+
queueRunning = false;
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const ensureContext = () => {
|
|
530
|
+
if (!canvas) {
|
|
531
|
+
canvas = getCanvas(id);
|
|
532
|
+
}
|
|
533
|
+
if (!canvas) return false;
|
|
534
|
+
if (!context) {
|
|
535
|
+
context = canvas.getContext("2d");
|
|
536
|
+
}
|
|
537
|
+
return !!context;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const setupCanvasResolution = () => {
|
|
541
|
+
if (!ensureContext()) return false;
|
|
542
|
+
dpr = window.devicePixelRatio || 1;
|
|
543
|
+
const wrapperRect = canvas.parentElement?.getBoundingClientRect?.();
|
|
544
|
+
const panelWidth =
|
|
545
|
+
wrapperRect?.width || canvas.parentElement?.clientWidth || canvas.clientWidth || canvas.width || 800;
|
|
546
|
+
const panelHeight =
|
|
547
|
+
wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
|
|
548
|
+
const width = Math.max(
|
|
549
|
+
1,
|
|
550
|
+
Math.floor(screenWidth !== null ? screenWidth : panelWidth),
|
|
551
|
+
);
|
|
552
|
+
const height = Math.max(
|
|
553
|
+
1,
|
|
554
|
+
Math.floor(screenHeight !== null ? screenHeight : panelHeight),
|
|
555
|
+
);
|
|
556
|
+
cssWidth = width;
|
|
557
|
+
cssHeight = height;
|
|
558
|
+
canvas.style.width = `${width}px`;
|
|
559
|
+
canvas.style.height = `${height}px`;
|
|
560
|
+
canvas.width = Math.max(1, Math.floor(cssWidth * dpr));
|
|
561
|
+
canvas.height = Math.max(1, Math.floor(cssHeight * dpr));
|
|
562
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
563
|
+
return true;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const makePath = (overrides = {}) => ({
|
|
567
|
+
down: penDown,
|
|
568
|
+
stroke: strokeColor,
|
|
569
|
+
lineWidth: DEFAULT_LINE_WIDTH,
|
|
570
|
+
fontsize: fontSize,
|
|
571
|
+
fontfamily: currentFontFamily,
|
|
572
|
+
fontstyle: currentFontStyle,
|
|
573
|
+
fill: false,
|
|
574
|
+
fillstyle: fillColor,
|
|
575
|
+
points: [{ x, y }],
|
|
576
|
+
...overrides,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const beginCurrentPath = () => {
|
|
580
|
+
currentPath = makePath();
|
|
581
|
+
paths.push(currentPath);
|
|
582
|
+
return currentPath;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const commitStyleToNewPath = () => {
|
|
586
|
+
currentPath = makePath({
|
|
587
|
+
down: penDown,
|
|
588
|
+
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
589
|
+
});
|
|
590
|
+
paths.push(currentPath);
|
|
591
|
+
return currentPath;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const ensurePath = () => {
|
|
595
|
+
if (!currentPath) {
|
|
596
|
+
beginCurrentPath();
|
|
597
|
+
}
|
|
598
|
+
return currentPath;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const drawBackground = () => {
|
|
602
|
+
context.save();
|
|
603
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
604
|
+
context.fillStyle = backgroundColor;
|
|
605
|
+
context.fillRect(0, 0, cssWidth, cssHeight);
|
|
606
|
+
if (backgroundImage?.complete && backgroundImage.naturalWidth > 0) {
|
|
607
|
+
context.drawImage(backgroundImage, 0, 0, cssWidth, cssHeight);
|
|
608
|
+
}
|
|
609
|
+
context.restore();
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const drawPathSegment = (path) => {
|
|
613
|
+
if (!path?.points?.length) return;
|
|
614
|
+
|
|
615
|
+
if (path.fill) {
|
|
616
|
+
context.beginPath();
|
|
617
|
+
path.points.forEach((point, index) => {
|
|
618
|
+
const px = toCanvasX(point.x);
|
|
619
|
+
const py = toCanvasY(point.y);
|
|
620
|
+
if (index === 0) {
|
|
621
|
+
context.moveTo(px, py);
|
|
622
|
+
} else {
|
|
623
|
+
context.lineTo(px, py);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
context.closePath();
|
|
627
|
+
context.fillStyle = path.fillstyle;
|
|
628
|
+
context.fill();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let startedLine = false;
|
|
633
|
+
context.beginPath();
|
|
634
|
+
for (const point of path.points) {
|
|
635
|
+
const px = toCanvasX(point.x);
|
|
636
|
+
const py = toCanvasY(point.y);
|
|
637
|
+
if (point.text !== undefined) {
|
|
638
|
+
const family = point.fontfamily || path.fontfamily || "Arial";
|
|
639
|
+
const style = point.fontstyle || path.fontstyle || "normal";
|
|
640
|
+
const size = point.fontsize ?? path.fontsize ?? DEFAULT_FONT_SIZE;
|
|
641
|
+
const text = String(point.text);
|
|
642
|
+
const metrics = measureTurtleText(text, family, size, style);
|
|
643
|
+
const align = normalizeTextAlign(point.align);
|
|
644
|
+
const left = metrics.left || 0;
|
|
645
|
+
const right = metrics.right || metrics.width || 0;
|
|
646
|
+
const descent = metrics.descent || 0;
|
|
647
|
+
const anchorX = px + TK_TEXT_X_OFFSET;
|
|
648
|
+
let drawX = anchorX + left;
|
|
649
|
+
if (align === "center") {
|
|
650
|
+
drawX = anchorX + (left - right) / 2;
|
|
651
|
+
} else if (align === "right") {
|
|
652
|
+
drawX = anchorX - right;
|
|
653
|
+
}
|
|
654
|
+
const drawY = py - descent;
|
|
655
|
+
context.font = metrics.font;
|
|
656
|
+
context.fillStyle = point.color || path.stroke;
|
|
657
|
+
context.textAlign = "left";
|
|
658
|
+
context.textBaseline = "alphabetic";
|
|
659
|
+
context.fillText(text, drawX, drawY);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (point.dotRadius) {
|
|
663
|
+
context.beginPath();
|
|
664
|
+
context.arc(px, py, point.dotRadius, 0, 2 * Math.PI);
|
|
665
|
+
context.fillStyle = point.color || path.fillstyle || path.stroke;
|
|
666
|
+
context.fill();
|
|
667
|
+
context.beginPath();
|
|
668
|
+
startedLine = false;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (!startedLine || point.move || !path.down) {
|
|
672
|
+
context.moveTo(px, py);
|
|
673
|
+
startedLine = true;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
context.lineTo(px, py);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (path.down) {
|
|
680
|
+
context.strokeStyle = path.stroke;
|
|
681
|
+
context.lineWidth = path.lineWidth || DEFAULT_LINE_WIDTH;
|
|
682
|
+
context.stroke();
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const drawTurtle = () => {
|
|
687
|
+
if (!renderedTurtleVisible) return;
|
|
688
|
+
context.save();
|
|
689
|
+
context.translate(toCanvasX(renderedX), toCanvasY(renderedY));
|
|
690
|
+
context.rotate(-toRadians(renderedHeading));
|
|
691
|
+
context.beginPath();
|
|
692
|
+
context.moveTo(TURTLE_SIZE / 2, 0);
|
|
693
|
+
context.lineTo(-TURTLE_SIZE / 2, TURTLE_SIZE / 3);
|
|
694
|
+
context.lineTo(-TURTLE_SIZE / 4, 0);
|
|
695
|
+
context.lineTo(-TURTLE_SIZE / 2, -TURTLE_SIZE / 3);
|
|
696
|
+
context.closePath();
|
|
697
|
+
context.fillStyle = "#2f9e44";
|
|
698
|
+
context.strokeStyle = "#1b5e20";
|
|
699
|
+
context.lineWidth = 1;
|
|
700
|
+
context.fill();
|
|
701
|
+
context.stroke();
|
|
702
|
+
context.restore();
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const draw = () => {
|
|
706
|
+
if (!active) return;
|
|
707
|
+
if (!setupCanvasResolution()) return;
|
|
708
|
+
drawBackground();
|
|
709
|
+
paths.forEach(drawPathSegment);
|
|
710
|
+
drawTurtle();
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const resetState = () => {
|
|
714
|
+
paths = [];
|
|
715
|
+
currentPath = null;
|
|
716
|
+
fillPath = null;
|
|
717
|
+
x = 0;
|
|
718
|
+
y = 0;
|
|
719
|
+
heading = 0;
|
|
720
|
+
renderedX = 0;
|
|
721
|
+
renderedY = 0;
|
|
722
|
+
renderedHeading = 0;
|
|
723
|
+
penDown = true;
|
|
724
|
+
turtleVisible = true;
|
|
725
|
+
renderedTurtleVisible = true;
|
|
726
|
+
strokeColor = "#000000";
|
|
727
|
+
fillColor = "#000000";
|
|
728
|
+
backgroundColor = "#ffffff";
|
|
729
|
+
backgroundImage = null;
|
|
730
|
+
fontSize = DEFAULT_FONT_SIZE;
|
|
731
|
+
currentFontFamily = "Arial";
|
|
732
|
+
currentFontStyle = "normal";
|
|
733
|
+
colorMode = 1.0;
|
|
734
|
+
screenWidth = null;
|
|
735
|
+
screenHeight = null;
|
|
736
|
+
filling = false;
|
|
737
|
+
active = true;
|
|
738
|
+
clearQueue();
|
|
739
|
+
beginCurrentPath();
|
|
740
|
+
draw();
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const deactivate = () => {
|
|
744
|
+
active = false;
|
|
745
|
+
clearQueue();
|
|
746
|
+
paths = [];
|
|
747
|
+
currentPath = null;
|
|
748
|
+
fillPath = null;
|
|
749
|
+
turtleVisible = false;
|
|
750
|
+
renderedTurtleVisible = false;
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const bindCanvas = (nextCanvas) => {
|
|
754
|
+
canvas = nextCanvas || getCanvas(id);
|
|
755
|
+
context = canvas?.getContext?.("2d") || null;
|
|
756
|
+
if (canvas) {
|
|
757
|
+
canvas.tabIndex = 0;
|
|
758
|
+
}
|
|
759
|
+
if (!currentPath) {
|
|
760
|
+
beginCurrentPath();
|
|
761
|
+
}
|
|
762
|
+
draw();
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
const toColorString = (value) => {
|
|
766
|
+
if (typeof value === "string") {
|
|
767
|
+
return value;
|
|
768
|
+
}
|
|
769
|
+
if (value && typeof value.toJs === "function") {
|
|
770
|
+
return toColorString(value.toJs({ pyproxies: [] }));
|
|
771
|
+
}
|
|
772
|
+
if (Array.isArray(value) || (value && typeof value === "object" && "length" in value)) {
|
|
773
|
+
const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
774
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
|
775
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
776
|
+
}
|
|
777
|
+
if (colorMode === 1.0) {
|
|
778
|
+
if (parts.some((part) => part < 0 || part > 1)) {
|
|
779
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
780
|
+
}
|
|
781
|
+
const [r, g, b] = parts.map((part) => Math.round(part * 255));
|
|
782
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
783
|
+
}
|
|
784
|
+
if (colorMode === 255) {
|
|
785
|
+
if (parts.some((part) => part < 0 || part > 255)) {
|
|
786
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
787
|
+
}
|
|
788
|
+
const [r, g, b] = parts.map((part) => Math.round(part));
|
|
789
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
790
|
+
}
|
|
791
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
792
|
+
}
|
|
793
|
+
return "#000000";
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
const getPenState = () => ({
|
|
797
|
+
pendown: penDown,
|
|
798
|
+
pencolor: strokeColor,
|
|
799
|
+
fillcolor: fillColor,
|
|
800
|
+
pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
801
|
+
speed: turtleSpeed,
|
|
802
|
+
shown: turtleVisible,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const forward = (distance) => {
|
|
806
|
+
if (!ensureContext()) return;
|
|
807
|
+
const path = ensurePath();
|
|
808
|
+
const fill = filling ? fillPath : null;
|
|
809
|
+
const length = Number(distance) || 0;
|
|
810
|
+
const nextX = x + length * Math.cos(toRadians(heading));
|
|
811
|
+
const nextY = y + length * Math.sin(toRadians(heading));
|
|
812
|
+
x = nextX;
|
|
813
|
+
y = nextY;
|
|
814
|
+
const point = { x, y, move: !penDown };
|
|
815
|
+
enqueueOperation(() => {
|
|
816
|
+
renderedX = point.x;
|
|
817
|
+
renderedY = point.y;
|
|
818
|
+
path.points.push(point);
|
|
819
|
+
if (fill) {
|
|
820
|
+
fill.points.push({ x, y });
|
|
821
|
+
}
|
|
822
|
+
draw();
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const backward = (distance) => forward(-Number(distance || 0));
|
|
827
|
+
const right = (angle) => {
|
|
828
|
+
heading = normalizeAngle(heading - Number(angle || 0));
|
|
829
|
+
const nextHeading = heading;
|
|
830
|
+
enqueueOperation(() => {
|
|
831
|
+
renderedHeading = nextHeading;
|
|
832
|
+
draw();
|
|
833
|
+
});
|
|
834
|
+
};
|
|
835
|
+
const left = (angle) => {
|
|
836
|
+
heading = normalizeAngle(heading + Number(angle || 0));
|
|
837
|
+
const nextHeading = heading;
|
|
838
|
+
enqueueOperation(() => {
|
|
839
|
+
renderedHeading = nextHeading;
|
|
840
|
+
draw();
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
const penup = () => {
|
|
844
|
+
penDown = false;
|
|
845
|
+
commitStyleToNewPath();
|
|
846
|
+
draw();
|
|
847
|
+
};
|
|
848
|
+
const pendownFn = () => {
|
|
849
|
+
penDown = true;
|
|
850
|
+
commitStyleToNewPath();
|
|
851
|
+
draw();
|
|
852
|
+
};
|
|
853
|
+
const goto_ = (a, b) => {
|
|
854
|
+
if (!ensureContext()) return;
|
|
855
|
+
let nextX = Number(a);
|
|
856
|
+
let nextY = Number(b);
|
|
857
|
+
if (b === undefined && (Array.isArray(a) || (a && typeof a === "object"))) {
|
|
858
|
+
const source =
|
|
859
|
+
a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
860
|
+
nextX = Number(source?.[0] ?? source?.x ?? 0);
|
|
861
|
+
nextY = Number(source?.[1] ?? source?.y ?? 0);
|
|
862
|
+
}
|
|
863
|
+
x = Number.isFinite(nextX) ? nextX : 0;
|
|
864
|
+
y = Number.isFinite(nextY) ? nextY : 0;
|
|
865
|
+
const path = ensurePath();
|
|
866
|
+
const fill = filling ? fillPath : null;
|
|
867
|
+
const point = { x, y, move: !penDown };
|
|
868
|
+
enqueueOperation(() => {
|
|
869
|
+
renderedX = point.x;
|
|
870
|
+
renderedY = point.y;
|
|
871
|
+
path.points.push(point);
|
|
872
|
+
if (fill) {
|
|
873
|
+
fill.points.push({ x, y });
|
|
874
|
+
}
|
|
875
|
+
draw();
|
|
876
|
+
});
|
|
877
|
+
};
|
|
878
|
+
const setx = (value) => goto_(Number(value), y);
|
|
879
|
+
const sety = (value) => goto_(x, Number(value));
|
|
880
|
+
const position = () => [x, y];
|
|
881
|
+
const xcor = () => x;
|
|
882
|
+
const ycor = () => y;
|
|
883
|
+
const heading_ = () => heading;
|
|
884
|
+
const setheading = (angle) => {
|
|
885
|
+
heading = normalizeAngle(angle);
|
|
886
|
+
const nextHeading = heading;
|
|
887
|
+
enqueueOperation(() => {
|
|
888
|
+
renderedHeading = nextHeading;
|
|
889
|
+
draw();
|
|
890
|
+
});
|
|
891
|
+
};
|
|
892
|
+
const home = () => {
|
|
893
|
+
goto_(0, 0);
|
|
894
|
+
setheading(0);
|
|
895
|
+
};
|
|
896
|
+
const towards = (tx, ty) => {
|
|
897
|
+
const dx = Number(tx) - x;
|
|
898
|
+
const dy = Number(ty) - y;
|
|
899
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
900
|
+
return normalizeAngle((Math.atan2(dy, dx) * 180) / Math.PI);
|
|
901
|
+
};
|
|
902
|
+
const speed = (value = 0) => {
|
|
903
|
+
const numeric = Number(value);
|
|
904
|
+
turtleSpeed = Number.isFinite(numeric) ? numeric : 0;
|
|
905
|
+
delayMs = turtleSpeed <= 0 ? 0 : Math.max(0, Math.round(300 / turtleSpeed));
|
|
906
|
+
return delayMs;
|
|
907
|
+
};
|
|
908
|
+
const colormode = (mode = undefined) => {
|
|
909
|
+
if (mode === undefined) return colorMode;
|
|
910
|
+
const numeric = Number(mode);
|
|
911
|
+
if (numeric !== 1 && numeric !== 255) {
|
|
912
|
+
throw new Error("colormode must be 1.0 or 255");
|
|
913
|
+
}
|
|
914
|
+
colorMode = numeric;
|
|
915
|
+
return colorMode;
|
|
916
|
+
};
|
|
917
|
+
const screensize = (canvwidth = undefined, canvheight = undefined, bg = undefined) => {
|
|
918
|
+
if (canvwidth === undefined && canvheight === undefined && bg === undefined) {
|
|
919
|
+
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
920
|
+
}
|
|
921
|
+
if (canvwidth !== undefined && canvwidth !== null) {
|
|
922
|
+
const width = Math.floor(Number(canvwidth));
|
|
923
|
+
if (Number.isFinite(width) && width > 0) {
|
|
924
|
+
screenWidth = width;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (canvheight !== undefined && canvheight !== null) {
|
|
928
|
+
const height = Math.floor(Number(canvheight));
|
|
929
|
+
if (Number.isFinite(height) && height > 0) {
|
|
930
|
+
screenHeight = height;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (bg !== undefined && bg !== null) {
|
|
934
|
+
backgroundColor = toColorString(bg);
|
|
935
|
+
}
|
|
936
|
+
draw();
|
|
937
|
+
if (canvas?.parentElement) {
|
|
938
|
+
const wrapper = canvas.parentElement;
|
|
939
|
+
wrapper.scrollLeft = Math.max(0, (cssWidth - wrapper.clientWidth) / 2);
|
|
940
|
+
wrapper.scrollTop = Math.max(0, (cssHeight - wrapper.clientHeight) / 2);
|
|
941
|
+
}
|
|
942
|
+
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
943
|
+
};
|
|
944
|
+
const color = (...args) => {
|
|
945
|
+
const stroke = toColorString(args[0]);
|
|
946
|
+
const fill = args.length > 1 ? toColorString(args[1]) : stroke;
|
|
947
|
+
strokeColor = stroke;
|
|
948
|
+
fillColor = fill;
|
|
949
|
+
commitStyleToNewPath();
|
|
950
|
+
if (filling && fillPath) {
|
|
951
|
+
fillPath.fillstyle = fillColor;
|
|
952
|
+
}
|
|
953
|
+
draw();
|
|
954
|
+
};
|
|
955
|
+
const pencolor = (value) => {
|
|
956
|
+
strokeColor = toColorString(value);
|
|
957
|
+
commitStyleToNewPath();
|
|
958
|
+
draw();
|
|
959
|
+
};
|
|
960
|
+
const fillcolor = (value) => {
|
|
961
|
+
fillColor = toColorString(value);
|
|
962
|
+
if (filling && fillPath) {
|
|
963
|
+
fillPath.fillstyle = fillColor;
|
|
964
|
+
}
|
|
965
|
+
commitStyleToNewPath();
|
|
966
|
+
draw();
|
|
967
|
+
};
|
|
968
|
+
const pensize = (value) => {
|
|
969
|
+
ensurePath().lineWidth = Math.max(1, Number(value) || DEFAULT_LINE_WIDTH);
|
|
970
|
+
commitStyleToNewPath();
|
|
971
|
+
draw();
|
|
972
|
+
};
|
|
973
|
+
const width = (value) => pensize(value);
|
|
974
|
+
const begin_fill = () => {
|
|
975
|
+
if (filling) return;
|
|
976
|
+
filling = true;
|
|
977
|
+
fillPath = makePath({
|
|
978
|
+
down: false,
|
|
979
|
+
fill: true,
|
|
980
|
+
stroke: "transparent",
|
|
981
|
+
lineWidth: 1,
|
|
982
|
+
fillstyle: fillColor,
|
|
983
|
+
});
|
|
984
|
+
paths.push(fillPath);
|
|
985
|
+
draw();
|
|
986
|
+
};
|
|
987
|
+
const end_fill = () => {
|
|
988
|
+
filling = false;
|
|
989
|
+
fillPath = null;
|
|
990
|
+
commitStyleToNewPath();
|
|
991
|
+
draw();
|
|
992
|
+
};
|
|
993
|
+
const dot = (size = 5, colorValue = null) => {
|
|
994
|
+
const segment = makePath({
|
|
995
|
+
down: false,
|
|
996
|
+
fill: false,
|
|
997
|
+
stroke: colorValue ? toColorString(colorValue) : strokeColor,
|
|
998
|
+
fillstyle: colorValue ? toColorString(colorValue) : strokeColor,
|
|
999
|
+
points: [
|
|
1000
|
+
{
|
|
1001
|
+
x,
|
|
1002
|
+
y,
|
|
1003
|
+
dotRadius: Math.max(0.5, Number(size) || 5) / 2,
|
|
1004
|
+
color: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
1007
|
+
});
|
|
1008
|
+
enqueueOperation(() => {
|
|
1009
|
+
paths.push(segment);
|
|
1010
|
+
draw();
|
|
1011
|
+
});
|
|
1012
|
+
};
|
|
1013
|
+
const circle = (radius, steps = 120) => {
|
|
1014
|
+
const numericRadius = Number(radius) || 0;
|
|
1015
|
+
const stepCount = Math.max(8, Number(steps) | 0);
|
|
1016
|
+
const circumference = 2 * Math.PI * Math.abs(numericRadius);
|
|
1017
|
+
const stepLength = circumference / stepCount;
|
|
1018
|
+
const stepTurn = (360 / stepCount) * (numericRadius >= 0 ? 1 : -1);
|
|
1019
|
+
for (let index = 0; index < stepCount; index += 1) {
|
|
1020
|
+
forward(stepLength);
|
|
1021
|
+
left(stepTurn);
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
const write = (text, move = false, align = "left", font = null) => {
|
|
1025
|
+
let writeText = text;
|
|
1026
|
+
let writeMove = move;
|
|
1027
|
+
let writeAlign = align;
|
|
1028
|
+
let writeFont = font;
|
|
1029
|
+
|
|
1030
|
+
if (isWriteKwargsObject(writeAlign)) {
|
|
1031
|
+
const kwargs = toPlainObject(writeAlign);
|
|
1032
|
+
if (kwargs) {
|
|
1033
|
+
if (hasOwn(kwargs, "arg")) writeText = kwargs.arg;
|
|
1034
|
+
else if (hasOwn(kwargs, "text")) writeText = kwargs.text;
|
|
1035
|
+
if (hasOwn(kwargs, "move")) writeMove = kwargs.move;
|
|
1036
|
+
if (hasOwn(kwargs, "align")) writeAlign = kwargs.align;
|
|
1037
|
+
if (hasOwn(kwargs, "font")) writeFont = kwargs.font;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (isWriteKwargsObject(writeFont)) {
|
|
1041
|
+
const kwargs = toPlainObject(writeFont);
|
|
1042
|
+
if (kwargs) {
|
|
1043
|
+
if (hasOwn(kwargs, "arg")) writeText = kwargs.arg;
|
|
1044
|
+
else if (hasOwn(kwargs, "text")) writeText = kwargs.text;
|
|
1045
|
+
if (hasOwn(kwargs, "move")) writeMove = kwargs.move;
|
|
1046
|
+
if (hasOwn(kwargs, "align")) writeAlign = kwargs.align;
|
|
1047
|
+
if (hasOwn(kwargs, "font")) writeFont = kwargs.font;
|
|
1048
|
+
}
|
|
1049
|
+
writeFont = null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let family = currentFontFamily;
|
|
1053
|
+
let size = fontSize;
|
|
1054
|
+
let style = currentFontStyle;
|
|
1055
|
+
try {
|
|
1056
|
+
const source = toSequence(writeFont);
|
|
1057
|
+
if (source?.length) {
|
|
1058
|
+
if (source[0]) family = toPlainString(source[0], family);
|
|
1059
|
+
if (source[1] !== undefined && source[1] !== null) {
|
|
1060
|
+
size = toPlainNumber(source[1], size);
|
|
1061
|
+
}
|
|
1062
|
+
if (source[2]) style = toPlainString(source[2], style);
|
|
1063
|
+
}
|
|
1064
|
+
} catch {}
|
|
1065
|
+
const normalizedAlign = normalizeTextAlign(writeAlign);
|
|
1066
|
+
const segment = makePath({
|
|
1067
|
+
down: false,
|
|
1068
|
+
fill: false,
|
|
1069
|
+
stroke: strokeColor,
|
|
1070
|
+
points: [
|
|
1071
|
+
{
|
|
1072
|
+
x,
|
|
1073
|
+
y,
|
|
1074
|
+
text: String(writeText),
|
|
1075
|
+
align: normalizedAlign,
|
|
1076
|
+
fontsize: size,
|
|
1077
|
+
fontfamily: family,
|
|
1078
|
+
fontstyle: style,
|
|
1079
|
+
color: strokeColor,
|
|
1080
|
+
},
|
|
1081
|
+
],
|
|
1082
|
+
});
|
|
1083
|
+
if (Boolean(writeMove)) {
|
|
1084
|
+
const metrics = measureTurtleText(String(writeText), family, size, style);
|
|
1085
|
+
const left = metrics.left || 0;
|
|
1086
|
+
const right = metrics.right || metrics.width || 0;
|
|
1087
|
+
const align = normalizeTextAlign(normalizedAlign);
|
|
1088
|
+
const textWidth = left + right;
|
|
1089
|
+
if (align === "left") {
|
|
1090
|
+
x += TK_TEXT_X_OFFSET + textWidth;
|
|
1091
|
+
} else if (align === "center") {
|
|
1092
|
+
x += TK_TEXT_X_OFFSET + right;
|
|
1093
|
+
} else {
|
|
1094
|
+
x += TK_TEXT_X_OFFSET;
|
|
1095
|
+
}
|
|
1096
|
+
commitStyleToNewPath();
|
|
1097
|
+
}
|
|
1098
|
+
enqueueOperation(() => {
|
|
1099
|
+
paths.push(segment);
|
|
1100
|
+
draw();
|
|
1101
|
+
});
|
|
1102
|
+
};
|
|
1103
|
+
const setfontsize = (value) => {
|
|
1104
|
+
const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
|
|
1105
|
+
fontSize = Math.max(1, Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE);
|
|
1106
|
+
commitStyleToNewPath();
|
|
1107
|
+
draw();
|
|
1108
|
+
};
|
|
1109
|
+
const bgcolor = (value) => {
|
|
1110
|
+
backgroundColor = toColorString(value);
|
|
1111
|
+
draw();
|
|
1112
|
+
};
|
|
1113
|
+
const bgpic = (filename = "") => {
|
|
1114
|
+
const name = String(filename || "").trim();
|
|
1115
|
+
if (!name) {
|
|
1116
|
+
backgroundImage = null;
|
|
1117
|
+
draw();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const fs = pyodide?.FS;
|
|
1121
|
+
if (!fs) return;
|
|
1122
|
+
const candidates = [
|
|
1123
|
+
name,
|
|
1124
|
+
`/home/pyodide/${name}`,
|
|
1125
|
+
`/home/pyodide/files/${name}`,
|
|
1126
|
+
`/home/pyodide/uploads/${name}`,
|
|
1127
|
+
];
|
|
1128
|
+
for (const candidate of candidates) {
|
|
1129
|
+
try {
|
|
1130
|
+
const data = fs.readFile(candidate);
|
|
1131
|
+
const blob = new Blob([data], { type: "image/*" });
|
|
1132
|
+
const url = URL.createObjectURL(blob);
|
|
1133
|
+
const image = new Image();
|
|
1134
|
+
image.onload = () => {
|
|
1135
|
+
backgroundImage = image;
|
|
1136
|
+
draw();
|
|
1137
|
+
URL.revokeObjectURL(url);
|
|
1138
|
+
};
|
|
1139
|
+
image.onerror = () => URL.revokeObjectURL(url);
|
|
1140
|
+
image.src = url;
|
|
1141
|
+
return;
|
|
1142
|
+
} catch {}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
const clear = () => {
|
|
1146
|
+
clearQueue();
|
|
1147
|
+
paths = [];
|
|
1148
|
+
currentPath = null;
|
|
1149
|
+
fillPath = null;
|
|
1150
|
+
beginCurrentPath();
|
|
1151
|
+
draw();
|
|
1152
|
+
};
|
|
1153
|
+
const reset = () => {
|
|
1154
|
+
resetState();
|
|
1155
|
+
};
|
|
1156
|
+
const showturtle = () => {
|
|
1157
|
+
turtleVisible = true;
|
|
1158
|
+
enqueueOperation(() => {
|
|
1159
|
+
renderedTurtleVisible = true;
|
|
1160
|
+
draw();
|
|
1161
|
+
});
|
|
1162
|
+
};
|
|
1163
|
+
const hideturtle = () => {
|
|
1164
|
+
turtleVisible = false;
|
|
1165
|
+
enqueueOperation(() => {
|
|
1166
|
+
renderedTurtleVisible = false;
|
|
1167
|
+
draw();
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
const isvisible = () => turtleVisible;
|
|
1171
|
+
const pen = (options = undefined) => {
|
|
1172
|
+
if (options === undefined) {
|
|
1173
|
+
return getPenState();
|
|
1174
|
+
}
|
|
1175
|
+
const source =
|
|
1176
|
+
options && typeof options.toJs === "function"
|
|
1177
|
+
? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
1178
|
+
: options;
|
|
1179
|
+
if (!source || typeof source !== "object") return getPenState();
|
|
1180
|
+
if ("pendown" in source) {
|
|
1181
|
+
penDown = !!source.pendown;
|
|
1182
|
+
}
|
|
1183
|
+
if ("pencolor" in source) {
|
|
1184
|
+
strokeColor = toColorString(source.pencolor);
|
|
1185
|
+
}
|
|
1186
|
+
if ("fillcolor" in source) {
|
|
1187
|
+
fillColor = toColorString(source.fillcolor);
|
|
1188
|
+
}
|
|
1189
|
+
if ("pensize" in source) {
|
|
1190
|
+
ensurePath().lineWidth = Math.max(1, Number(source.pensize) || DEFAULT_LINE_WIDTH);
|
|
1191
|
+
}
|
|
1192
|
+
if ("shown" in source) {
|
|
1193
|
+
turtleVisible = !!source.shown;
|
|
1194
|
+
renderedTurtleVisible = turtleVisible;
|
|
1195
|
+
}
|
|
1196
|
+
commitStyleToNewPath();
|
|
1197
|
+
draw();
|
|
1198
|
+
return getPenState();
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const api = {
|
|
1202
|
+
__bindCanvas: bindCanvas,
|
|
1203
|
+
__deactivate: deactivate,
|
|
1204
|
+
__redraw: draw,
|
|
1205
|
+
__resetState: resetState,
|
|
1206
|
+
__setPyodide: (runtime) => {
|
|
1207
|
+
pyodide = runtime;
|
|
1208
|
+
},
|
|
1209
|
+
forward,
|
|
1210
|
+
fd: forward,
|
|
1211
|
+
backward,
|
|
1212
|
+
bk: backward,
|
|
1213
|
+
back: backward,
|
|
1214
|
+
right,
|
|
1215
|
+
rt: right,
|
|
1216
|
+
left,
|
|
1217
|
+
lt: left,
|
|
1218
|
+
goto: (...args) => goto_(...args),
|
|
1219
|
+
setpos: (...args) => goto_(...args),
|
|
1220
|
+
setposition: (...args) => goto_(...args),
|
|
1221
|
+
setx,
|
|
1222
|
+
sety,
|
|
1223
|
+
setheading,
|
|
1224
|
+
seth: setheading,
|
|
1225
|
+
home,
|
|
1226
|
+
circle,
|
|
1227
|
+
dot,
|
|
1228
|
+
position,
|
|
1229
|
+
pos: position,
|
|
1230
|
+
xcor,
|
|
1231
|
+
ycor,
|
|
1232
|
+
heading: heading_,
|
|
1233
|
+
towards,
|
|
1234
|
+
pendown: pendownFn,
|
|
1235
|
+
pd: pendownFn,
|
|
1236
|
+
down: pendownFn,
|
|
1237
|
+
penup,
|
|
1238
|
+
pu: penup,
|
|
1239
|
+
up: penup,
|
|
1240
|
+
pensize,
|
|
1241
|
+
width,
|
|
1242
|
+
pen,
|
|
1243
|
+
write,
|
|
1244
|
+
setfontsize,
|
|
1245
|
+
color,
|
|
1246
|
+
pencolor,
|
|
1247
|
+
fillcolor,
|
|
1248
|
+
begin_fill,
|
|
1249
|
+
end_fill,
|
|
1250
|
+
bgcolor,
|
|
1251
|
+
bgpic,
|
|
1252
|
+
reset,
|
|
1253
|
+
clear,
|
|
1254
|
+
speed,
|
|
1255
|
+
colormode,
|
|
1256
|
+
screensize,
|
|
1257
|
+
showturtle,
|
|
1258
|
+
st: showturtle,
|
|
1259
|
+
hideturtle,
|
|
1260
|
+
ht: hideturtle,
|
|
1261
|
+
isvisible,
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
return api;
|
|
1265
|
+
};
|
|
1266
|
+
|
|
271
1267
|
const createPytamaroJsFFI = () => {
|
|
272
1268
|
const floatBuffer = new ArrayBuffer(4);
|
|
273
1269
|
const floatView = new DataView(floatBuffer);
|
|
@@ -596,6 +1592,142 @@ hyperbook.python = (function () {
|
|
|
596
1592
|
};
|
|
597
1593
|
};
|
|
598
1594
|
|
|
1595
|
+
// Browser/Pyodide needs periodic yielding for top-level pygame loops.
|
|
1596
|
+
const hasExplicitMain = (code) => {
|
|
1597
|
+
const text = String(code || "");
|
|
1598
|
+
return (
|
|
1599
|
+
/^(\s*)(?:async\s+def|def)\s+main\s*\(/m.test(text) ||
|
|
1600
|
+
/__name__\s*==\s*["']__main__["']/.test(text)
|
|
1601
|
+
);
|
|
1602
|
+
};
|
|
1603
|
+
|
|
1604
|
+
const looksLikeTopLevelGameLoop = (code) => {
|
|
1605
|
+
const text = String(code || "");
|
|
1606
|
+
const hasPygame =
|
|
1607
|
+
/(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
|
|
1608
|
+
const hasSasPygame =
|
|
1609
|
+
/(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(text);
|
|
1610
|
+
const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
|
|
1611
|
+
if (!hasAnyWhile) return false;
|
|
1612
|
+
return (hasPygame || hasSasPygame) && hasAnyWhile;
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
const indentBlock = (source, spaces) => {
|
|
1616
|
+
const pad = " ".repeat(spaces);
|
|
1617
|
+
return String(source || "")
|
|
1618
|
+
.split(/\r\n|\r|\n/)
|
|
1619
|
+
.map((line) => (line.length ? pad + line : line))
|
|
1620
|
+
.join("\n");
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
const wrapTopLevelIntoAsyncMain = (userCode) => {
|
|
1624
|
+
const code = String(userCode || "").replace(/\r\n/g, "\n");
|
|
1625
|
+
|
|
1626
|
+
if (hasExplicitMain(code)) return code;
|
|
1627
|
+
|
|
1628
|
+
const lines = code.split("\n");
|
|
1629
|
+
const keep = [];
|
|
1630
|
+
const body = [];
|
|
1631
|
+
|
|
1632
|
+
let index = 0;
|
|
1633
|
+
while (index < lines.length) {
|
|
1634
|
+
const line = lines[index];
|
|
1635
|
+
if (/^\s*$/.test(line)) {
|
|
1636
|
+
keep.push(line);
|
|
1637
|
+
index += 1;
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (/^\s*#/.test(line)) {
|
|
1641
|
+
keep.push(line);
|
|
1642
|
+
index += 1;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(line)) {
|
|
1646
|
+
keep.push(line);
|
|
1647
|
+
index += 1;
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (/^\s*(?:def|async\s+def|class)\s+/.test(line)) {
|
|
1652
|
+
keep.push(line);
|
|
1653
|
+
index += 1;
|
|
1654
|
+
while (index < lines.length) {
|
|
1655
|
+
const nextLine = lines[index];
|
|
1656
|
+
if (/^\s*$/.test(nextLine)) {
|
|
1657
|
+
keep.push(nextLine);
|
|
1658
|
+
index += 1;
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (
|
|
1662
|
+
/^\S/.test(nextLine) &&
|
|
1663
|
+
!/^\s*#/.test(nextLine) &&
|
|
1664
|
+
!/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) &&
|
|
1665
|
+
!/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)
|
|
1666
|
+
) {
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
keep.push(nextLine);
|
|
1670
|
+
index += 1;
|
|
1671
|
+
}
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
for (; index < lines.length; index += 1) {
|
|
1678
|
+
body.push(lines[index]);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
let bodyText = body.join("\n");
|
|
1682
|
+
bodyText = bodyText.replace(/^([ \t]*\n)+/, "");
|
|
1683
|
+
bodyText = bodyText.replace(/(\n[ \t]*)+$/, "");
|
|
1684
|
+
bodyText = bodyText.replace(/\t/g, " ");
|
|
1685
|
+
if (!bodyText.replace(/[\s\n]+/g, "").length) return code;
|
|
1686
|
+
|
|
1687
|
+
const injected = bodyText
|
|
1688
|
+
.replace(
|
|
1689
|
+
/^(\s*)(\w+\s*\.\s*tick\s*\([^\)]*\)\s*)$/gm,
|
|
1690
|
+
"$1$2\n$1await asyncio.sleep(0)",
|
|
1691
|
+
)
|
|
1692
|
+
.replace(
|
|
1693
|
+
/^(\s*)(pygame\s*\.\s*display\s*\.\s*flip\s*\(\s*\)\s*)$/gm,
|
|
1694
|
+
"$1$2\n$1await asyncio.sleep(0)",
|
|
1695
|
+
)
|
|
1696
|
+
.replace(
|
|
1697
|
+
/^([\t ]*)([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_][\w]*)*\s*\.\s*step\s*\([^\)]*\)\s*)(#.*)?$/gm,
|
|
1698
|
+
"$1$2$3\n$1await asyncio.sleep(0)",
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
const prelude = [
|
|
1702
|
+
"# --- auto-wrapped by IDE for browser pygame compatibility ---",
|
|
1703
|
+
"import asyncio",
|
|
1704
|
+
"import pygame",
|
|
1705
|
+
"",
|
|
1706
|
+
"async def main():",
|
|
1707
|
+
indentBlock(injected, 4),
|
|
1708
|
+
"",
|
|
1709
|
+
"await main()",
|
|
1710
|
+
"# --- end auto-wrapped ---",
|
|
1711
|
+
"",
|
|
1712
|
+
].join("\n");
|
|
1713
|
+
|
|
1714
|
+
let keepText = keep.join("\n");
|
|
1715
|
+
if (keepText && !keepText.endsWith("\n")) keepText += "\n";
|
|
1716
|
+
if (keepText && !/\n\s*\n$/.test(keepText)) keepText += "\n";
|
|
1717
|
+
|
|
1718
|
+
return keepText + prelude;
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
const maybeAutoWrapPygame = (code) => {
|
|
1722
|
+
try {
|
|
1723
|
+
const text = String(code || "");
|
|
1724
|
+
if (!looksLikeTopLevelGameLoop(text)) return text;
|
|
1725
|
+
return wrapTopLevelIntoAsyncMain(text);
|
|
1726
|
+
} catch {
|
|
1727
|
+
return String(code || "");
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
|
|
599
1731
|
const ensureMicropipPackages = async (id, pyodide, packages = []) => {
|
|
600
1732
|
if (packages.length === 0) return;
|
|
601
1733
|
if (!installedMicropipPackages.has(id)) {
|
|
@@ -623,10 +1755,20 @@ hyperbook.python = (function () {
|
|
|
623
1755
|
const pyodide = await getRuntime(id);
|
|
624
1756
|
const { canvas, ...globalsContext } = context;
|
|
625
1757
|
const decoder = new TextDecoder("utf-8");
|
|
1758
|
+
const executableScript = maybeAutoWrapPygame(script);
|
|
626
1759
|
|
|
627
1760
|
if (canvas) {
|
|
628
1761
|
try {
|
|
629
1762
|
resetCanvas(canvas);
|
|
1763
|
+
const usesTurtle = scriptLooksLikeTurtle(executableScript);
|
|
1764
|
+
if (usesTurtle) {
|
|
1765
|
+
turtleModules.get(id)?.__bindCanvas(canvas);
|
|
1766
|
+
turtleModules.get(id)?.__resetState();
|
|
1767
|
+
} else {
|
|
1768
|
+
turtleModules.get(id)?.__deactivate?.();
|
|
1769
|
+
canvas.style.width = "";
|
|
1770
|
+
canvas.style.height = "";
|
|
1771
|
+
}
|
|
630
1772
|
pyodide.canvas.setCanvas2D(canvas);
|
|
631
1773
|
} catch (error) {
|
|
632
1774
|
appendOutputErrorLine(id, `Canvas setup failed: ${error.message}`);
|
|
@@ -658,14 +1800,14 @@ hyperbook.python = (function () {
|
|
|
658
1800
|
});
|
|
659
1801
|
|
|
660
1802
|
await ensureMicropipPackages(id, pyodide, packages);
|
|
661
|
-
await pyodide.loadPackagesFromImports(
|
|
1803
|
+
await pyodide.loadPackagesFromImports(executableScript);
|
|
662
1804
|
const dict = pyodide.globals.get("dict");
|
|
663
1805
|
const globals = dict();
|
|
664
1806
|
try {
|
|
665
1807
|
for (const [key, value] of Object.entries(globalsContext)) {
|
|
666
1808
|
globals.set(key, value);
|
|
667
1809
|
}
|
|
668
|
-
const results = await pyodide.runPythonAsync(
|
|
1810
|
+
const results = await pyodide.runPythonAsync(executableScript, {
|
|
669
1811
|
globals,
|
|
670
1812
|
locals: globals,
|
|
671
1813
|
filename,
|
|
@@ -1122,6 +2264,7 @@ if _pg:
|
|
|
1122
2264
|
output.classList.add("hidden");
|
|
1123
2265
|
if (canvasWrapper) canvasWrapper.classList.remove("hidden");
|
|
1124
2266
|
canvasOutputSplitter?.classList.add("hidden");
|
|
2267
|
+
turtleModules.get(id)?.__redraw?.();
|
|
1125
2268
|
};
|
|
1126
2269
|
|
|
1127
2270
|
const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
|
|
@@ -1152,6 +2295,7 @@ if _pg:
|
|
|
1152
2295
|
canvasBtn.disabled = true;
|
|
1153
2296
|
}
|
|
1154
2297
|
applyStoredCanvasOutputSplit?.();
|
|
2298
|
+
turtleModules.get(id)?.__redraw?.();
|
|
1155
2299
|
return;
|
|
1156
2300
|
}
|
|
1157
2301
|
|
|
@@ -1238,7 +2382,10 @@ if _pg:
|
|
|
1238
2382
|
void restoreEditorState();
|
|
1239
2383
|
}
|
|
1240
2384
|
|
|
1241
|
-
window.addEventListener("resize",
|
|
2385
|
+
window.addEventListener("resize", () => {
|
|
2386
|
+
applyCanvasOutputLayout();
|
|
2387
|
+
turtleModules.get(id)?.__redraw?.();
|
|
2388
|
+
});
|
|
1242
2389
|
applyCanvasOutputLayout();
|
|
1243
2390
|
|
|
1244
2391
|
editor.addEventListener("input", () => {
|
|
@@ -1303,8 +2450,9 @@ if _pg:
|
|
|
1303
2450
|
|
|
1304
2451
|
run?.addEventListener("click", async () => {
|
|
1305
2452
|
const script = getEditorValue();
|
|
1306
|
-
const
|
|
1307
|
-
|
|
2453
|
+
const usesPytamaro = hasPytamaroPackage || scriptLooksLikePytamaro(script);
|
|
2454
|
+
const renderPytamaroToCanvas = hasCanvas && canvas && usesPytamaro;
|
|
2455
|
+
if (hasCanvas) {
|
|
1308
2456
|
showCanvas();
|
|
1309
2457
|
} else {
|
|
1310
2458
|
showOutput();
|
|
@@ -1322,15 +2470,16 @@ if _pg:
|
|
|
1322
2470
|
output.innerHTML = "";
|
|
1323
2471
|
clearPytamaroStdoutCarry(id);
|
|
1324
2472
|
try {
|
|
2473
|
+
setPytamaroCanvasTarget(id, renderPytamaroToCanvas);
|
|
1325
2474
|
const { results, error } = await executeScript(id, script, {
|
|
1326
|
-
...(hasCanvas && canvas
|
|
2475
|
+
...(hasCanvas && canvas ? { canvas } : {}),
|
|
1327
2476
|
}, additionalPackages);
|
|
1328
2477
|
if (!state.stopRequested) {
|
|
1329
2478
|
if (results) {
|
|
1330
|
-
appendOutput(output, results);
|
|
2479
|
+
appendOutput(output, results, false, id);
|
|
1331
2480
|
} else if (error) {
|
|
1332
2481
|
showOutput();
|
|
1333
|
-
appendOutput(output, error, true);
|
|
2482
|
+
appendOutput(output, error, true, id);
|
|
1334
2483
|
}
|
|
1335
2484
|
} else {
|
|
1336
2485
|
appendOutputLine(id, "Execution stopped.");
|
|
@@ -1338,7 +2487,7 @@ if _pg:
|
|
|
1338
2487
|
} catch (e) {
|
|
1339
2488
|
showOutput();
|
|
1340
2489
|
output.innerHTML = "";
|
|
1341
|
-
appendOutput(output, `Error: ${e}`, true);
|
|
2490
|
+
appendOutput(output, `Error: ${e}`, true, id);
|
|
1342
2491
|
console.log(e);
|
|
1343
2492
|
} finally {
|
|
1344
2493
|
clearPytamaroStdoutCarry(id);
|
package/dist/index.js
CHANGED
|
@@ -202149,7 +202149,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"application/1d-interleaved-parityfec
|
|
|
202149
202149
|
/***/ ((module) => {
|
|
202150
202150
|
|
|
202151
202151
|
"use strict";
|
|
202152
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"hyperbook","version":"0.
|
|
202152
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"hyperbook","version":"0.93.0","author":"Mike Barkmin","homepage":"https://github.com/openpatch/hyperbook#readme","license":"MIT","bin":{"hyperbook":"./dist/index.js"},"files":["dist"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/openpatch/hyperbook.git","directory":"packages/hyperbook"},"bugs":{"url":"https://github.com/openpatch/hyperbook/issues"},"engines":{"node":">=18"},"scripts":{"version":"pnpm build","lint":"tsc --noEmit","dev":"ncc build ./index.ts -w -o dist/","build":"rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register --external favicons --external sharp && node postbuild.mjs"},"dependencies":{"favicons":"^7.2.0"},"devDependencies":{"create-hyperbook":"workspace:*","@hyperbook/fs":"workspace:*","@hyperbook/markdown":"workspace:*","@hyperbook/types":"workspace:*","@pnpm/exportable-manifest":"1000.0.6","@types/archiver":"6.0.3","@types/async-retry":"1.4.9","@types/cross-spawn":"6.0.6","@types/lunr":"^2.3.7","@types/prompts":"2.4.9","@types/tar":"6.1.13","@types/ws":"^8.5.14","@vercel/ncc":"0.38.3","archiver":"7.0.1","async-retry":"1.3.3","chalk":"5.4.1","chokidar":"4.0.3","commander":"12.1.0","cpy":"11.1.0","cross-spawn":"7.0.6","domutils":"^3.2.2","extract-zip":"^2.0.1","got":"12.6.0","htmlparser2":"^10.0.0","lunr":"^2.3.9","lunr-languages":"^1.14.0","mime":"^4.0.6","prompts":"2.4.2","rimraf":"6.0.1","tar":"7.4.3","update-check":"1.5.4","ws":"^8.18.0"}}');
|
|
202153
202153
|
|
|
202154
202154
|
/***/ })
|
|
202155
202155
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hyperbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.0",
|
|
4
4
|
"author": "Mike Barkmin",
|
|
5
5
|
"homepage": "https://github.com/openpatch/hyperbook#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"ws": "^8.18.0",
|
|
59
59
|
"create-hyperbook": "0.3.6",
|
|
60
60
|
"@hyperbook/fs": "0.25.0",
|
|
61
|
-
"@hyperbook/
|
|
62
|
-
"@hyperbook/
|
|
61
|
+
"@hyperbook/markdown": "0.64.0",
|
|
62
|
+
"@hyperbook/types": "0.23.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"version": "pnpm build",
|