hyperbook 0.92.0 → 0.93.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.
|
@@ -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,963 @@ 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 toPlainBoolean = (value, fallback = false) => {
|
|
385
|
+
if (value === null || value === undefined) return fallback;
|
|
386
|
+
if (typeof value === "boolean") return value;
|
|
387
|
+
if (typeof value === "number") return value !== 0;
|
|
388
|
+
if (typeof value === "bigint") return value !== 0n;
|
|
389
|
+
if (typeof value === "string") {
|
|
390
|
+
const normalized = value.trim().toLowerCase();
|
|
391
|
+
if (!normalized) return fallback;
|
|
392
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
393
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
394
|
+
}
|
|
395
|
+
if (typeof value.toJs === "function") {
|
|
396
|
+
return toPlainBoolean(value.toJs({ pyproxies: [] }), fallback);
|
|
397
|
+
}
|
|
398
|
+
return Boolean(value);
|
|
399
|
+
};
|
|
400
|
+
const fontSizeToCanvasUnits = (value) => {
|
|
401
|
+
const numeric = toPlainNumber(value, Number.NaN);
|
|
402
|
+
if (!Number.isFinite(numeric) || numeric === 0) {
|
|
403
|
+
const fallback = DEFAULT_FONT_SIZE;
|
|
404
|
+
return { size: `${fallback}pt`, pxApprox: (fallback * 96) / 72 };
|
|
405
|
+
}
|
|
406
|
+
if (numeric < 0) {
|
|
407
|
+
return { size: `${Math.abs(numeric)}px`, pxApprox: Math.abs(numeric) };
|
|
408
|
+
}
|
|
409
|
+
return { size: `${numeric}pt`, pxApprox: (numeric * 96) / 72 };
|
|
410
|
+
};
|
|
411
|
+
const toPlainString = (value, fallback = "") => {
|
|
412
|
+
if (value === null || value === undefined) return fallback;
|
|
413
|
+
if (typeof value === "string") return value;
|
|
414
|
+
if (typeof value.toJs === "function") {
|
|
415
|
+
return toPlainString(value.toJs({ pyproxies: [] }), fallback);
|
|
416
|
+
}
|
|
417
|
+
return String(value);
|
|
418
|
+
};
|
|
419
|
+
const toSequence = (value) => {
|
|
420
|
+
if (value === null || value === undefined) return null;
|
|
421
|
+
if (Array.isArray(value)) return value;
|
|
422
|
+
if (typeof value === "string") return [value];
|
|
423
|
+
if (typeof value.toJs === "function") {
|
|
424
|
+
return toSequence(value.toJs({ pyproxies: [] }));
|
|
425
|
+
}
|
|
426
|
+
if (typeof value[Symbol.iterator] === "function") {
|
|
427
|
+
try {
|
|
428
|
+
return Array.from(value);
|
|
429
|
+
} catch {}
|
|
430
|
+
}
|
|
431
|
+
if (typeof value === "object" && "length" in value) {
|
|
432
|
+
try {
|
|
433
|
+
return Array.from(value);
|
|
434
|
+
} catch {}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
};
|
|
438
|
+
const toPlainObject = (value) => {
|
|
439
|
+
if (value === null || value === undefined) return null;
|
|
440
|
+
if (typeof value.toJs === "function") {
|
|
441
|
+
try {
|
|
442
|
+
return toPlainObject(value.toJs({ pyproxies: [], dict_converter: Object.fromEntries }));
|
|
443
|
+
} catch {
|
|
444
|
+
return toPlainObject(value.toJs({ pyproxies: [] }));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
448
|
+
return value;
|
|
449
|
+
};
|
|
450
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
451
|
+
const isWriteKwargsObject = (value) => {
|
|
452
|
+
const obj = toPlainObject(value);
|
|
453
|
+
if (!obj) return false;
|
|
454
|
+
return (
|
|
455
|
+
hasOwn(obj, "arg") ||
|
|
456
|
+
hasOwn(obj, "text") ||
|
|
457
|
+
hasOwn(obj, "move") ||
|
|
458
|
+
hasOwn(obj, "align") ||
|
|
459
|
+
hasOwn(obj, "font")
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
const normalizeFontStyle = (value) => {
|
|
463
|
+
const style = toPlainString(value, "normal").trim().toLowerCase();
|
|
464
|
+
if (!style || style === "normal") return "";
|
|
465
|
+
const parts = style.split(/\s+/).filter(Boolean);
|
|
466
|
+
const hasItalic = parts.includes("italic");
|
|
467
|
+
const hasBold = parts.includes("bold");
|
|
468
|
+
if (hasItalic && hasBold) return "italic bold";
|
|
469
|
+
if (hasBold) return "bold";
|
|
470
|
+
if (hasItalic) return "italic";
|
|
471
|
+
return style;
|
|
472
|
+
};
|
|
473
|
+
const normalizeTextAlign = (value) => {
|
|
474
|
+
const align = toPlainString(value, "left").trim().toLowerCase();
|
|
475
|
+
if (align === "center" || align === "right") return align;
|
|
476
|
+
return "left";
|
|
477
|
+
};
|
|
478
|
+
const buildCanvasFont = (family, size, style) => {
|
|
479
|
+
const { size: canvasSize } = fontSizeToCanvasUnits(size);
|
|
480
|
+
const stylePart = normalizeFontStyle(style);
|
|
481
|
+
return `${stylePart ? `${stylePart} ` : ""}${canvasSize} ${toPlainString(family, "Arial")}`;
|
|
482
|
+
};
|
|
483
|
+
const measureTurtleText = (text, family, size, style) => {
|
|
484
|
+
const font = buildCanvasFont(family, size, style);
|
|
485
|
+
const { pxApprox } = fontSizeToCanvasUnits(size);
|
|
486
|
+
const ctx = textMeasureContext || context;
|
|
487
|
+
if (!ctx) {
|
|
488
|
+
return {
|
|
489
|
+
font,
|
|
490
|
+
width: String(text || "").length * pxApprox * 0.6,
|
|
491
|
+
descent: pxApprox * 0.2,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
ctx.font = font;
|
|
495
|
+
ctx.textAlign = "left";
|
|
496
|
+
const metrics = ctx.measureText(String(text || ""));
|
|
497
|
+
return {
|
|
498
|
+
font,
|
|
499
|
+
width: metrics.width,
|
|
500
|
+
left: metrics.actualBoundingBoxLeft || 0,
|
|
501
|
+
right: metrics.actualBoundingBoxRight || metrics.width,
|
|
502
|
+
ascent:
|
|
503
|
+
metrics.fontBoundingBoxAscent ||
|
|
504
|
+
metrics.actualBoundingBoxAscent ||
|
|
505
|
+
pxApprox * 0.8,
|
|
506
|
+
descent:
|
|
507
|
+
metrics.fontBoundingBoxDescent ||
|
|
508
|
+
metrics.actualBoundingBoxDescent ||
|
|
509
|
+
pxApprox * 0.2,
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
513
|
+
const clearQueue = () => {
|
|
514
|
+
queueGeneration += 1;
|
|
515
|
+
operationQueue = [];
|
|
516
|
+
queueRunning = false;
|
|
517
|
+
};
|
|
518
|
+
const enqueueOperation = (fn) => {
|
|
519
|
+
if (delayMs <= 0) {
|
|
520
|
+
fn();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const generation = queueGeneration;
|
|
524
|
+
operationQueue.push({ fn, generation });
|
|
525
|
+
if (queueRunning) return;
|
|
526
|
+
queueRunning = true;
|
|
527
|
+
void (async () => {
|
|
528
|
+
try {
|
|
529
|
+
while (operationQueue.length > 0) {
|
|
530
|
+
const operation = operationQueue.shift();
|
|
531
|
+
if (!operation || operation.generation !== queueGeneration) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
operation.fn();
|
|
535
|
+
if (delayMs > 0) {
|
|
536
|
+
await sleep(delayMs);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} finally {
|
|
540
|
+
queueRunning = false;
|
|
541
|
+
}
|
|
542
|
+
})();
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const ensureContext = () => {
|
|
546
|
+
if (!canvas) {
|
|
547
|
+
canvas = getCanvas(id);
|
|
548
|
+
}
|
|
549
|
+
if (!canvas) return false;
|
|
550
|
+
if (!context) {
|
|
551
|
+
context = canvas.getContext("2d");
|
|
552
|
+
}
|
|
553
|
+
return !!context;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const setupCanvasResolution = () => {
|
|
557
|
+
if (!ensureContext()) return false;
|
|
558
|
+
dpr = window.devicePixelRatio || 1;
|
|
559
|
+
const wrapperRect = canvas.parentElement?.getBoundingClientRect?.();
|
|
560
|
+
const panelWidth =
|
|
561
|
+
wrapperRect?.width || canvas.parentElement?.clientWidth || canvas.clientWidth || canvas.width || 800;
|
|
562
|
+
const panelHeight =
|
|
563
|
+
wrapperRect?.height || canvas.parentElement?.clientHeight || canvas.clientHeight || canvas.height || 400;
|
|
564
|
+
const width = Math.max(
|
|
565
|
+
1,
|
|
566
|
+
Math.floor(screenWidth !== null ? screenWidth : panelWidth),
|
|
567
|
+
);
|
|
568
|
+
const height = Math.max(
|
|
569
|
+
1,
|
|
570
|
+
Math.floor(screenHeight !== null ? screenHeight : panelHeight),
|
|
571
|
+
);
|
|
572
|
+
cssWidth = width;
|
|
573
|
+
cssHeight = height;
|
|
574
|
+
canvas.style.width = `${width}px`;
|
|
575
|
+
canvas.style.height = `${height}px`;
|
|
576
|
+
canvas.width = Math.max(1, Math.floor(cssWidth * dpr));
|
|
577
|
+
canvas.height = Math.max(1, Math.floor(cssHeight * dpr));
|
|
578
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
579
|
+
return true;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const makePath = (overrides = {}) => ({
|
|
583
|
+
down: penDown,
|
|
584
|
+
stroke: strokeColor,
|
|
585
|
+
lineWidth: DEFAULT_LINE_WIDTH,
|
|
586
|
+
fontsize: fontSize,
|
|
587
|
+
fontfamily: currentFontFamily,
|
|
588
|
+
fontstyle: currentFontStyle,
|
|
589
|
+
fill: false,
|
|
590
|
+
fillstyle: fillColor,
|
|
591
|
+
points: [{ x, y }],
|
|
592
|
+
...overrides,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const beginCurrentPath = () => {
|
|
596
|
+
currentPath = makePath();
|
|
597
|
+
paths.push(currentPath);
|
|
598
|
+
return currentPath;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const commitStyleToNewPath = () => {
|
|
602
|
+
currentPath = makePath({
|
|
603
|
+
down: penDown,
|
|
604
|
+
lineWidth: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
605
|
+
});
|
|
606
|
+
paths.push(currentPath);
|
|
607
|
+
return currentPath;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const ensurePath = () => {
|
|
611
|
+
if (!currentPath) {
|
|
612
|
+
beginCurrentPath();
|
|
613
|
+
}
|
|
614
|
+
return currentPath;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const drawBackground = () => {
|
|
618
|
+
context.save();
|
|
619
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
620
|
+
context.fillStyle = backgroundColor;
|
|
621
|
+
context.fillRect(0, 0, cssWidth, cssHeight);
|
|
622
|
+
if (backgroundImage?.complete && backgroundImage.naturalWidth > 0) {
|
|
623
|
+
context.drawImage(backgroundImage, 0, 0, cssWidth, cssHeight);
|
|
624
|
+
}
|
|
625
|
+
context.restore();
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const drawPathSegment = (path) => {
|
|
629
|
+
if (!path?.points?.length) return;
|
|
630
|
+
|
|
631
|
+
if (path.fill) {
|
|
632
|
+
context.beginPath();
|
|
633
|
+
path.points.forEach((point, index) => {
|
|
634
|
+
const px = toCanvasX(point.x);
|
|
635
|
+
const py = toCanvasY(point.y);
|
|
636
|
+
if (index === 0) {
|
|
637
|
+
context.moveTo(px, py);
|
|
638
|
+
} else {
|
|
639
|
+
context.lineTo(px, py);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
context.closePath();
|
|
643
|
+
context.fillStyle = path.fillstyle;
|
|
644
|
+
context.fill();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let startedLine = false;
|
|
649
|
+
context.beginPath();
|
|
650
|
+
for (const point of path.points) {
|
|
651
|
+
const px = toCanvasX(point.x);
|
|
652
|
+
const py = toCanvasY(point.y);
|
|
653
|
+
if (point.text !== undefined) {
|
|
654
|
+
const family = point.fontfamily || path.fontfamily || "Arial";
|
|
655
|
+
const style = point.fontstyle || path.fontstyle || "normal";
|
|
656
|
+
const size = point.fontsize ?? path.fontsize ?? DEFAULT_FONT_SIZE;
|
|
657
|
+
const text = String(point.text);
|
|
658
|
+
const metrics = measureTurtleText(text, family, size, style);
|
|
659
|
+
const align = normalizeTextAlign(point.align);
|
|
660
|
+
const left = metrics.left || 0;
|
|
661
|
+
const right = metrics.right || metrics.width || 0;
|
|
662
|
+
const descent = metrics.descent || 0;
|
|
663
|
+
const anchorX = px + TK_TEXT_X_OFFSET;
|
|
664
|
+
let drawX = anchorX + left;
|
|
665
|
+
if (align === "center") {
|
|
666
|
+
drawX = anchorX + (left - right) / 2;
|
|
667
|
+
} else if (align === "right") {
|
|
668
|
+
drawX = anchorX - right;
|
|
669
|
+
}
|
|
670
|
+
const drawY = py - descent;
|
|
671
|
+
context.font = metrics.font;
|
|
672
|
+
context.fillStyle = point.color || path.stroke;
|
|
673
|
+
context.textAlign = "left";
|
|
674
|
+
context.textBaseline = "alphabetic";
|
|
675
|
+
context.fillText(text, drawX, drawY);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (point.dotRadius) {
|
|
679
|
+
context.beginPath();
|
|
680
|
+
context.arc(px, py, point.dotRadius, 0, 2 * Math.PI);
|
|
681
|
+
context.fillStyle = point.color || path.fillstyle || path.stroke;
|
|
682
|
+
context.fill();
|
|
683
|
+
context.beginPath();
|
|
684
|
+
startedLine = false;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (!startedLine || point.move || !path.down) {
|
|
688
|
+
context.moveTo(px, py);
|
|
689
|
+
startedLine = true;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
context.lineTo(px, py);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (path.down) {
|
|
696
|
+
context.strokeStyle = path.stroke;
|
|
697
|
+
context.lineWidth = path.lineWidth || DEFAULT_LINE_WIDTH;
|
|
698
|
+
context.stroke();
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const drawTurtle = () => {
|
|
703
|
+
if (!renderedTurtleVisible) return;
|
|
704
|
+
context.save();
|
|
705
|
+
context.translate(toCanvasX(renderedX), toCanvasY(renderedY));
|
|
706
|
+
context.rotate(-toRadians(renderedHeading));
|
|
707
|
+
context.beginPath();
|
|
708
|
+
context.moveTo(TURTLE_SIZE / 2, 0);
|
|
709
|
+
context.lineTo(-TURTLE_SIZE / 2, TURTLE_SIZE / 3);
|
|
710
|
+
context.lineTo(-TURTLE_SIZE / 4, 0);
|
|
711
|
+
context.lineTo(-TURTLE_SIZE / 2, -TURTLE_SIZE / 3);
|
|
712
|
+
context.closePath();
|
|
713
|
+
context.fillStyle = "#2f9e44";
|
|
714
|
+
context.strokeStyle = "#1b5e20";
|
|
715
|
+
context.lineWidth = 1;
|
|
716
|
+
context.fill();
|
|
717
|
+
context.stroke();
|
|
718
|
+
context.restore();
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const draw = () => {
|
|
722
|
+
if (!active) return;
|
|
723
|
+
if (!setupCanvasResolution()) return;
|
|
724
|
+
drawBackground();
|
|
725
|
+
paths.forEach(drawPathSegment);
|
|
726
|
+
drawTurtle();
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const resetState = () => {
|
|
730
|
+
paths = [];
|
|
731
|
+
currentPath = null;
|
|
732
|
+
fillPath = null;
|
|
733
|
+
x = 0;
|
|
734
|
+
y = 0;
|
|
735
|
+
heading = 0;
|
|
736
|
+
renderedX = 0;
|
|
737
|
+
renderedY = 0;
|
|
738
|
+
renderedHeading = 0;
|
|
739
|
+
penDown = true;
|
|
740
|
+
turtleVisible = true;
|
|
741
|
+
renderedTurtleVisible = true;
|
|
742
|
+
strokeColor = "#000000";
|
|
743
|
+
fillColor = "#000000";
|
|
744
|
+
backgroundColor = "#ffffff";
|
|
745
|
+
backgroundImage = null;
|
|
746
|
+
fontSize = DEFAULT_FONT_SIZE;
|
|
747
|
+
currentFontFamily = "Arial";
|
|
748
|
+
currentFontStyle = "normal";
|
|
749
|
+
colorMode = 1.0;
|
|
750
|
+
screenWidth = null;
|
|
751
|
+
screenHeight = null;
|
|
752
|
+
filling = false;
|
|
753
|
+
active = true;
|
|
754
|
+
clearQueue();
|
|
755
|
+
beginCurrentPath();
|
|
756
|
+
draw();
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
const deactivate = () => {
|
|
760
|
+
active = false;
|
|
761
|
+
clearQueue();
|
|
762
|
+
paths = [];
|
|
763
|
+
currentPath = null;
|
|
764
|
+
fillPath = null;
|
|
765
|
+
turtleVisible = false;
|
|
766
|
+
renderedTurtleVisible = false;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const bindCanvas = (nextCanvas) => {
|
|
770
|
+
canvas = nextCanvas || getCanvas(id);
|
|
771
|
+
context = canvas?.getContext?.("2d") || null;
|
|
772
|
+
if (canvas) {
|
|
773
|
+
canvas.tabIndex = 0;
|
|
774
|
+
}
|
|
775
|
+
if (!currentPath) {
|
|
776
|
+
beginCurrentPath();
|
|
777
|
+
}
|
|
778
|
+
draw();
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const toColorString = (value) => {
|
|
782
|
+
if (typeof value === "string") {
|
|
783
|
+
return value;
|
|
784
|
+
}
|
|
785
|
+
if (value && typeof value.toJs === "function") {
|
|
786
|
+
return toColorString(value.toJs({ pyproxies: [] }));
|
|
787
|
+
}
|
|
788
|
+
if (Array.isArray(value) || (value && typeof value === "object" && "length" in value)) {
|
|
789
|
+
const parts = Array.from(value).slice(0, 3).map((part) => Number(part));
|
|
790
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isFinite(part))) {
|
|
791
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
792
|
+
}
|
|
793
|
+
if (colorMode === 1.0) {
|
|
794
|
+
if (parts.some((part) => part < 0 || part > 1)) {
|
|
795
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
796
|
+
}
|
|
797
|
+
const [r, g, b] = parts.map((part) => Math.round(part * 255));
|
|
798
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
799
|
+
}
|
|
800
|
+
if (colorMode === 255) {
|
|
801
|
+
if (parts.some((part) => part < 0 || part > 255)) {
|
|
802
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
803
|
+
}
|
|
804
|
+
const [r, g, b] = parts.map((part) => Math.round(part));
|
|
805
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
806
|
+
}
|
|
807
|
+
throw new Error(`bad color sequence: ${String(value)}`);
|
|
808
|
+
}
|
|
809
|
+
return "#000000";
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const getPenState = () => ({
|
|
813
|
+
pendown: penDown,
|
|
814
|
+
pencolor: strokeColor,
|
|
815
|
+
fillcolor: fillColor,
|
|
816
|
+
pensize: currentPath?.lineWidth ?? DEFAULT_LINE_WIDTH,
|
|
817
|
+
speed: turtleSpeed,
|
|
818
|
+
shown: turtleVisible,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const forward = (distance) => {
|
|
822
|
+
if (!ensureContext()) return;
|
|
823
|
+
const path = ensurePath();
|
|
824
|
+
const fill = filling ? fillPath : null;
|
|
825
|
+
const length = Number(distance) || 0;
|
|
826
|
+
const nextX = x + length * Math.cos(toRadians(heading));
|
|
827
|
+
const nextY = y + length * Math.sin(toRadians(heading));
|
|
828
|
+
x = nextX;
|
|
829
|
+
y = nextY;
|
|
830
|
+
const point = { x, y, move: !penDown };
|
|
831
|
+
enqueueOperation(() => {
|
|
832
|
+
renderedX = point.x;
|
|
833
|
+
renderedY = point.y;
|
|
834
|
+
path.points.push(point);
|
|
835
|
+
if (fill) {
|
|
836
|
+
fill.points.push({ x, y });
|
|
837
|
+
}
|
|
838
|
+
draw();
|
|
839
|
+
});
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const backward = (distance) => forward(-Number(distance || 0));
|
|
843
|
+
const right = (angle) => {
|
|
844
|
+
heading = normalizeAngle(heading - Number(angle || 0));
|
|
845
|
+
const nextHeading = heading;
|
|
846
|
+
enqueueOperation(() => {
|
|
847
|
+
renderedHeading = nextHeading;
|
|
848
|
+
draw();
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
const left = (angle) => {
|
|
852
|
+
heading = normalizeAngle(heading + Number(angle || 0));
|
|
853
|
+
const nextHeading = heading;
|
|
854
|
+
enqueueOperation(() => {
|
|
855
|
+
renderedHeading = nextHeading;
|
|
856
|
+
draw();
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
const penup = () => {
|
|
860
|
+
penDown = false;
|
|
861
|
+
commitStyleToNewPath();
|
|
862
|
+
draw();
|
|
863
|
+
};
|
|
864
|
+
const pendownFn = () => {
|
|
865
|
+
penDown = true;
|
|
866
|
+
commitStyleToNewPath();
|
|
867
|
+
draw();
|
|
868
|
+
};
|
|
869
|
+
const goto_ = (a, b) => {
|
|
870
|
+
if (!ensureContext()) return;
|
|
871
|
+
let nextX = Number(a);
|
|
872
|
+
let nextY = Number(b);
|
|
873
|
+
if (b === undefined && (Array.isArray(a) || (a && typeof a === "object"))) {
|
|
874
|
+
const source =
|
|
875
|
+
a && typeof a.toJs === "function" ? a.toJs({ pyproxies: [] }) : a;
|
|
876
|
+
nextX = Number(source?.[0] ?? source?.x ?? 0);
|
|
877
|
+
nextY = Number(source?.[1] ?? source?.y ?? 0);
|
|
878
|
+
}
|
|
879
|
+
x = Number.isFinite(nextX) ? nextX : 0;
|
|
880
|
+
y = Number.isFinite(nextY) ? nextY : 0;
|
|
881
|
+
const path = ensurePath();
|
|
882
|
+
const fill = filling ? fillPath : null;
|
|
883
|
+
const point = { x, y, move: !penDown };
|
|
884
|
+
enqueueOperation(() => {
|
|
885
|
+
renderedX = point.x;
|
|
886
|
+
renderedY = point.y;
|
|
887
|
+
path.points.push(point);
|
|
888
|
+
if (fill) {
|
|
889
|
+
fill.points.push({ x, y });
|
|
890
|
+
}
|
|
891
|
+
draw();
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
const setx = (value) => goto_(Number(value), y);
|
|
895
|
+
const sety = (value) => goto_(x, Number(value));
|
|
896
|
+
const position = () => [x, y];
|
|
897
|
+
const xcor = () => x;
|
|
898
|
+
const ycor = () => y;
|
|
899
|
+
const heading_ = () => heading;
|
|
900
|
+
const setheading = (angle) => {
|
|
901
|
+
heading = normalizeAngle(angle);
|
|
902
|
+
const nextHeading = heading;
|
|
903
|
+
enqueueOperation(() => {
|
|
904
|
+
renderedHeading = nextHeading;
|
|
905
|
+
draw();
|
|
906
|
+
});
|
|
907
|
+
};
|
|
908
|
+
const home = () => {
|
|
909
|
+
goto_(0, 0);
|
|
910
|
+
setheading(0);
|
|
911
|
+
};
|
|
912
|
+
const towards = (tx, ty) => {
|
|
913
|
+
const dx = Number(tx) - x;
|
|
914
|
+
const dy = Number(ty) - y;
|
|
915
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy)) return 0;
|
|
916
|
+
return normalizeAngle((Math.atan2(dy, dx) * 180) / Math.PI);
|
|
917
|
+
};
|
|
918
|
+
const speed = (value = 0) => {
|
|
919
|
+
const numeric = Number(value);
|
|
920
|
+
turtleSpeed = Number.isFinite(numeric) ? numeric : 0;
|
|
921
|
+
delayMs = turtleSpeed <= 0 ? 0 : Math.max(0, Math.round(300 / turtleSpeed));
|
|
922
|
+
return delayMs;
|
|
923
|
+
};
|
|
924
|
+
const colormode = (mode = undefined) => {
|
|
925
|
+
if (mode === undefined) return colorMode;
|
|
926
|
+
const numeric = Number(mode);
|
|
927
|
+
if (numeric !== 1 && numeric !== 255) {
|
|
928
|
+
throw new Error("colormode must be 1.0 or 255");
|
|
929
|
+
}
|
|
930
|
+
colorMode = numeric;
|
|
931
|
+
return colorMode;
|
|
932
|
+
};
|
|
933
|
+
const screensize = (canvwidth = undefined, canvheight = undefined, bg = undefined) => {
|
|
934
|
+
if (canvwidth === undefined && canvheight === undefined && bg === undefined) {
|
|
935
|
+
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
936
|
+
}
|
|
937
|
+
if (canvwidth !== undefined && canvwidth !== null) {
|
|
938
|
+
const width = Math.floor(Number(canvwidth));
|
|
939
|
+
if (Number.isFinite(width) && width > 0) {
|
|
940
|
+
screenWidth = width;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (canvheight !== undefined && canvheight !== null) {
|
|
944
|
+
const height = Math.floor(Number(canvheight));
|
|
945
|
+
if (Number.isFinite(height) && height > 0) {
|
|
946
|
+
screenHeight = height;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (bg !== undefined && bg !== null) {
|
|
950
|
+
backgroundColor = toColorString(bg);
|
|
951
|
+
}
|
|
952
|
+
draw();
|
|
953
|
+
if (canvas?.parentElement) {
|
|
954
|
+
const wrapper = canvas.parentElement;
|
|
955
|
+
wrapper.scrollLeft = Math.max(0, (cssWidth - wrapper.clientWidth) / 2);
|
|
956
|
+
wrapper.scrollTop = Math.max(0, (cssHeight - wrapper.clientHeight) / 2);
|
|
957
|
+
}
|
|
958
|
+
return [screenWidth || cssWidth || 0, screenHeight || cssHeight || 0];
|
|
959
|
+
};
|
|
960
|
+
const color = (...args) => {
|
|
961
|
+
const stroke = toColorString(args[0]);
|
|
962
|
+
const fill = args.length > 1 ? toColorString(args[1]) : stroke;
|
|
963
|
+
strokeColor = stroke;
|
|
964
|
+
fillColor = fill;
|
|
965
|
+
commitStyleToNewPath();
|
|
966
|
+
if (filling && fillPath) {
|
|
967
|
+
fillPath.fillstyle = fillColor;
|
|
968
|
+
}
|
|
969
|
+
draw();
|
|
970
|
+
};
|
|
971
|
+
const pencolor = (value) => {
|
|
972
|
+
strokeColor = toColorString(value);
|
|
973
|
+
commitStyleToNewPath();
|
|
974
|
+
draw();
|
|
975
|
+
};
|
|
976
|
+
const fillcolor = (value) => {
|
|
977
|
+
fillColor = toColorString(value);
|
|
978
|
+
if (filling && fillPath) {
|
|
979
|
+
fillPath.fillstyle = fillColor;
|
|
980
|
+
}
|
|
981
|
+
commitStyleToNewPath();
|
|
982
|
+
draw();
|
|
983
|
+
};
|
|
984
|
+
const pensize = (value) => {
|
|
985
|
+
ensurePath().lineWidth = Math.max(1, Number(value) || DEFAULT_LINE_WIDTH);
|
|
986
|
+
commitStyleToNewPath();
|
|
987
|
+
draw();
|
|
988
|
+
};
|
|
989
|
+
const width = (value) => pensize(value);
|
|
990
|
+
const begin_fill = () => {
|
|
991
|
+
if (filling) return;
|
|
992
|
+
filling = true;
|
|
993
|
+
fillPath = makePath({
|
|
994
|
+
down: false,
|
|
995
|
+
fill: true,
|
|
996
|
+
stroke: "transparent",
|
|
997
|
+
lineWidth: 1,
|
|
998
|
+
fillstyle: fillColor,
|
|
999
|
+
});
|
|
1000
|
+
paths.push(fillPath);
|
|
1001
|
+
draw();
|
|
1002
|
+
};
|
|
1003
|
+
const end_fill = () => {
|
|
1004
|
+
filling = false;
|
|
1005
|
+
fillPath = null;
|
|
1006
|
+
commitStyleToNewPath();
|
|
1007
|
+
draw();
|
|
1008
|
+
};
|
|
1009
|
+
const dot = (size = 5, colorValue = null) => {
|
|
1010
|
+
const segment = makePath({
|
|
1011
|
+
down: false,
|
|
1012
|
+
fill: false,
|
|
1013
|
+
stroke: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1014
|
+
fillstyle: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1015
|
+
points: [
|
|
1016
|
+
{
|
|
1017
|
+
x,
|
|
1018
|
+
y,
|
|
1019
|
+
dotRadius: Math.max(0.5, Number(size) || 5) / 2,
|
|
1020
|
+
color: colorValue ? toColorString(colorValue) : strokeColor,
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
});
|
|
1024
|
+
enqueueOperation(() => {
|
|
1025
|
+
paths.push(segment);
|
|
1026
|
+
draw();
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
const circle = (radius, steps = 120) => {
|
|
1030
|
+
const numericRadius = Number(radius) || 0;
|
|
1031
|
+
const stepCount = Math.max(8, Number(steps) | 0);
|
|
1032
|
+
const circumference = 2 * Math.PI * Math.abs(numericRadius);
|
|
1033
|
+
const stepLength = circumference / stepCount;
|
|
1034
|
+
const stepTurn = (360 / stepCount) * (numericRadius >= 0 ? 1 : -1);
|
|
1035
|
+
for (let index = 0; index < stepCount; index += 1) {
|
|
1036
|
+
forward(stepLength);
|
|
1037
|
+
left(stepTurn);
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
const write = (text, move = false, align = "left", font = null) => {
|
|
1041
|
+
let writeText = text;
|
|
1042
|
+
let writeMove = move;
|
|
1043
|
+
let writeAlign = align;
|
|
1044
|
+
let writeFont = font;
|
|
1045
|
+
const applyWriteKwargs = (candidate) => {
|
|
1046
|
+
if (!isWriteKwargsObject(candidate)) return;
|
|
1047
|
+
const kwargs = toPlainObject(candidate);
|
|
1048
|
+
if (!kwargs) return;
|
|
1049
|
+
if (hasOwn(kwargs, "arg")) writeText = kwargs.arg;
|
|
1050
|
+
else if (hasOwn(kwargs, "text")) writeText = kwargs.text;
|
|
1051
|
+
if (hasOwn(kwargs, "move")) writeMove = kwargs.move;
|
|
1052
|
+
if (hasOwn(kwargs, "align")) writeAlign = kwargs.align;
|
|
1053
|
+
if (hasOwn(kwargs, "font")) writeFont = kwargs.font;
|
|
1054
|
+
};
|
|
1055
|
+
applyWriteKwargs(writeText);
|
|
1056
|
+
applyWriteKwargs(writeMove);
|
|
1057
|
+
applyWriteKwargs(writeAlign);
|
|
1058
|
+
applyWriteKwargs(writeFont);
|
|
1059
|
+
if (isWriteKwargsObject(writeFont)) writeFont = null;
|
|
1060
|
+
|
|
1061
|
+
let family = currentFontFamily;
|
|
1062
|
+
let size = fontSize;
|
|
1063
|
+
let style = currentFontStyle;
|
|
1064
|
+
try {
|
|
1065
|
+
const source = toSequence(writeFont);
|
|
1066
|
+
if (source?.length) {
|
|
1067
|
+
if (source[0]) family = toPlainString(source[0], family);
|
|
1068
|
+
if (source[1] !== undefined && source[1] !== null) {
|
|
1069
|
+
size = toPlainNumber(source[1], size);
|
|
1070
|
+
}
|
|
1071
|
+
if (source[2]) style = toPlainString(source[2], style);
|
|
1072
|
+
}
|
|
1073
|
+
} catch {}
|
|
1074
|
+
const normalizedAlign = normalizeTextAlign(writeAlign);
|
|
1075
|
+
const segment = makePath({
|
|
1076
|
+
down: false,
|
|
1077
|
+
fill: false,
|
|
1078
|
+
stroke: strokeColor,
|
|
1079
|
+
points: [
|
|
1080
|
+
{
|
|
1081
|
+
x,
|
|
1082
|
+
y,
|
|
1083
|
+
text: String(writeText),
|
|
1084
|
+
align: normalizedAlign,
|
|
1085
|
+
fontsize: size,
|
|
1086
|
+
fontfamily: family,
|
|
1087
|
+
fontstyle: style,
|
|
1088
|
+
color: strokeColor,
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
});
|
|
1092
|
+
if (toPlainBoolean(writeMove, false)) {
|
|
1093
|
+
const metrics = measureTurtleText(String(writeText), family, size, style);
|
|
1094
|
+
const left = metrics.left || 0;
|
|
1095
|
+
const right = metrics.right || metrics.width || 0;
|
|
1096
|
+
const align = normalizeTextAlign(normalizedAlign);
|
|
1097
|
+
const textWidth = left + right;
|
|
1098
|
+
if (align === "left") {
|
|
1099
|
+
x += TK_TEXT_X_OFFSET + textWidth;
|
|
1100
|
+
} else if (align === "center") {
|
|
1101
|
+
x += TK_TEXT_X_OFFSET + right;
|
|
1102
|
+
} else {
|
|
1103
|
+
x += TK_TEXT_X_OFFSET;
|
|
1104
|
+
}
|
|
1105
|
+
commitStyleToNewPath();
|
|
1106
|
+
}
|
|
1107
|
+
enqueueOperation(() => {
|
|
1108
|
+
paths.push(segment);
|
|
1109
|
+
draw();
|
|
1110
|
+
});
|
|
1111
|
+
};
|
|
1112
|
+
const setfontsize = (value) => {
|
|
1113
|
+
const nextSize = toPlainNumber(value, DEFAULT_FONT_SIZE);
|
|
1114
|
+
fontSize = Math.max(1, Number.isFinite(nextSize) ? nextSize : DEFAULT_FONT_SIZE);
|
|
1115
|
+
commitStyleToNewPath();
|
|
1116
|
+
draw();
|
|
1117
|
+
};
|
|
1118
|
+
const bgcolor = (value) => {
|
|
1119
|
+
backgroundColor = toColorString(value);
|
|
1120
|
+
draw();
|
|
1121
|
+
};
|
|
1122
|
+
const bgpic = (filename = "") => {
|
|
1123
|
+
const name = String(filename || "").trim();
|
|
1124
|
+
if (!name) {
|
|
1125
|
+
backgroundImage = null;
|
|
1126
|
+
draw();
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const fs = pyodide?.FS;
|
|
1130
|
+
if (!fs) return;
|
|
1131
|
+
const candidates = [
|
|
1132
|
+
name,
|
|
1133
|
+
`/home/pyodide/${name}`,
|
|
1134
|
+
`/home/pyodide/files/${name}`,
|
|
1135
|
+
`/home/pyodide/uploads/${name}`,
|
|
1136
|
+
];
|
|
1137
|
+
for (const candidate of candidates) {
|
|
1138
|
+
try {
|
|
1139
|
+
const data = fs.readFile(candidate);
|
|
1140
|
+
const blob = new Blob([data], { type: "image/*" });
|
|
1141
|
+
const url = URL.createObjectURL(blob);
|
|
1142
|
+
const image = new Image();
|
|
1143
|
+
image.onload = () => {
|
|
1144
|
+
backgroundImage = image;
|
|
1145
|
+
draw();
|
|
1146
|
+
URL.revokeObjectURL(url);
|
|
1147
|
+
};
|
|
1148
|
+
image.onerror = () => URL.revokeObjectURL(url);
|
|
1149
|
+
image.src = url;
|
|
1150
|
+
return;
|
|
1151
|
+
} catch {}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
const clear = () => {
|
|
1155
|
+
clearQueue();
|
|
1156
|
+
paths = [];
|
|
1157
|
+
currentPath = null;
|
|
1158
|
+
fillPath = null;
|
|
1159
|
+
beginCurrentPath();
|
|
1160
|
+
draw();
|
|
1161
|
+
};
|
|
1162
|
+
const reset = () => {
|
|
1163
|
+
resetState();
|
|
1164
|
+
};
|
|
1165
|
+
const showturtle = () => {
|
|
1166
|
+
turtleVisible = true;
|
|
1167
|
+
enqueueOperation(() => {
|
|
1168
|
+
renderedTurtleVisible = true;
|
|
1169
|
+
draw();
|
|
1170
|
+
});
|
|
1171
|
+
};
|
|
1172
|
+
const hideturtle = () => {
|
|
1173
|
+
turtleVisible = false;
|
|
1174
|
+
enqueueOperation(() => {
|
|
1175
|
+
renderedTurtleVisible = false;
|
|
1176
|
+
draw();
|
|
1177
|
+
});
|
|
1178
|
+
};
|
|
1179
|
+
const isvisible = () => turtleVisible;
|
|
1180
|
+
const pen = (options = undefined) => {
|
|
1181
|
+
if (options === undefined) {
|
|
1182
|
+
return getPenState();
|
|
1183
|
+
}
|
|
1184
|
+
const source =
|
|
1185
|
+
options && typeof options.toJs === "function"
|
|
1186
|
+
? options.toJs({ pyproxies: [], dict_converter: Object.fromEntries })
|
|
1187
|
+
: options;
|
|
1188
|
+
if (!source || typeof source !== "object") return getPenState();
|
|
1189
|
+
if ("pendown" in source) {
|
|
1190
|
+
penDown = !!source.pendown;
|
|
1191
|
+
}
|
|
1192
|
+
if ("pencolor" in source) {
|
|
1193
|
+
strokeColor = toColorString(source.pencolor);
|
|
1194
|
+
}
|
|
1195
|
+
if ("fillcolor" in source) {
|
|
1196
|
+
fillColor = toColorString(source.fillcolor);
|
|
1197
|
+
}
|
|
1198
|
+
if ("pensize" in source) {
|
|
1199
|
+
ensurePath().lineWidth = Math.max(1, Number(source.pensize) || DEFAULT_LINE_WIDTH);
|
|
1200
|
+
}
|
|
1201
|
+
if ("shown" in source) {
|
|
1202
|
+
turtleVisible = !!source.shown;
|
|
1203
|
+
renderedTurtleVisible = turtleVisible;
|
|
1204
|
+
}
|
|
1205
|
+
commitStyleToNewPath();
|
|
1206
|
+
draw();
|
|
1207
|
+
return getPenState();
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
const api = {
|
|
1211
|
+
__bindCanvas: bindCanvas,
|
|
1212
|
+
__deactivate: deactivate,
|
|
1213
|
+
__redraw: draw,
|
|
1214
|
+
__resetState: resetState,
|
|
1215
|
+
__setPyodide: (runtime) => {
|
|
1216
|
+
pyodide = runtime;
|
|
1217
|
+
},
|
|
1218
|
+
forward,
|
|
1219
|
+
fd: forward,
|
|
1220
|
+
backward,
|
|
1221
|
+
bk: backward,
|
|
1222
|
+
back: backward,
|
|
1223
|
+
right,
|
|
1224
|
+
rt: right,
|
|
1225
|
+
left,
|
|
1226
|
+
lt: left,
|
|
1227
|
+
goto: (...args) => goto_(...args),
|
|
1228
|
+
setpos: (...args) => goto_(...args),
|
|
1229
|
+
setposition: (...args) => goto_(...args),
|
|
1230
|
+
setx,
|
|
1231
|
+
sety,
|
|
1232
|
+
setheading,
|
|
1233
|
+
seth: setheading,
|
|
1234
|
+
home,
|
|
1235
|
+
circle,
|
|
1236
|
+
dot,
|
|
1237
|
+
position,
|
|
1238
|
+
pos: position,
|
|
1239
|
+
xcor,
|
|
1240
|
+
ycor,
|
|
1241
|
+
heading: heading_,
|
|
1242
|
+
towards,
|
|
1243
|
+
pendown: pendownFn,
|
|
1244
|
+
pd: pendownFn,
|
|
1245
|
+
down: pendownFn,
|
|
1246
|
+
penup,
|
|
1247
|
+
pu: penup,
|
|
1248
|
+
up: penup,
|
|
1249
|
+
pensize,
|
|
1250
|
+
width,
|
|
1251
|
+
pen,
|
|
1252
|
+
write,
|
|
1253
|
+
setfontsize,
|
|
1254
|
+
color,
|
|
1255
|
+
pencolor,
|
|
1256
|
+
fillcolor,
|
|
1257
|
+
begin_fill,
|
|
1258
|
+
end_fill,
|
|
1259
|
+
bgcolor,
|
|
1260
|
+
bgpic,
|
|
1261
|
+
reset,
|
|
1262
|
+
clear,
|
|
1263
|
+
speed,
|
|
1264
|
+
colormode,
|
|
1265
|
+
screensize,
|
|
1266
|
+
showturtle,
|
|
1267
|
+
st: showturtle,
|
|
1268
|
+
hideturtle,
|
|
1269
|
+
ht: hideturtle,
|
|
1270
|
+
isvisible,
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
return api;
|
|
1274
|
+
};
|
|
1275
|
+
|
|
271
1276
|
const createPytamaroJsFFI = () => {
|
|
272
1277
|
const floatBuffer = new ArrayBuffer(4);
|
|
273
1278
|
const floatView = new DataView(floatBuffer);
|
|
@@ -596,6 +1601,142 @@ hyperbook.python = (function () {
|
|
|
596
1601
|
};
|
|
597
1602
|
};
|
|
598
1603
|
|
|
1604
|
+
// Browser/Pyodide needs periodic yielding for top-level pygame loops.
|
|
1605
|
+
const hasExplicitMain = (code) => {
|
|
1606
|
+
const text = String(code || "");
|
|
1607
|
+
return (
|
|
1608
|
+
/^(\s*)(?:async\s+def|def)\s+main\s*\(/m.test(text) ||
|
|
1609
|
+
/__name__\s*==\s*["']__main__["']/.test(text)
|
|
1610
|
+
);
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1613
|
+
const looksLikeTopLevelGameLoop = (code) => {
|
|
1614
|
+
const text = String(code || "");
|
|
1615
|
+
const hasPygame =
|
|
1616
|
+
/(?:^|\W)(?:import\s+pygame\b|from\s+pygame\b\s+import\b)/m.test(text);
|
|
1617
|
+
const hasSasPygame =
|
|
1618
|
+
/(?:^|\W)(?:import\s+sas_pygame\b|from\s+sas_pygame\b\s+import\b)/m.test(text);
|
|
1619
|
+
const hasAnyWhile = /^\s*while\s+.+:\s*$/m.test(text);
|
|
1620
|
+
if (!hasAnyWhile) return false;
|
|
1621
|
+
return (hasPygame || hasSasPygame) && hasAnyWhile;
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const indentBlock = (source, spaces) => {
|
|
1625
|
+
const pad = " ".repeat(spaces);
|
|
1626
|
+
return String(source || "")
|
|
1627
|
+
.split(/\r\n|\r|\n/)
|
|
1628
|
+
.map((line) => (line.length ? pad + line : line))
|
|
1629
|
+
.join("\n");
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
const wrapTopLevelIntoAsyncMain = (userCode) => {
|
|
1633
|
+
const code = String(userCode || "").replace(/\r\n/g, "\n");
|
|
1634
|
+
|
|
1635
|
+
if (hasExplicitMain(code)) return code;
|
|
1636
|
+
|
|
1637
|
+
const lines = code.split("\n");
|
|
1638
|
+
const keep = [];
|
|
1639
|
+
const body = [];
|
|
1640
|
+
|
|
1641
|
+
let index = 0;
|
|
1642
|
+
while (index < lines.length) {
|
|
1643
|
+
const line = lines[index];
|
|
1644
|
+
if (/^\s*$/.test(line)) {
|
|
1645
|
+
keep.push(line);
|
|
1646
|
+
index += 1;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (/^\s*#/.test(line)) {
|
|
1650
|
+
keep.push(line);
|
|
1651
|
+
index += 1;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(line)) {
|
|
1655
|
+
keep.push(line);
|
|
1656
|
+
index += 1;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (/^\s*(?:def|async\s+def|class)\s+/.test(line)) {
|
|
1661
|
+
keep.push(line);
|
|
1662
|
+
index += 1;
|
|
1663
|
+
while (index < lines.length) {
|
|
1664
|
+
const nextLine = lines[index];
|
|
1665
|
+
if (/^\s*$/.test(nextLine)) {
|
|
1666
|
+
keep.push(nextLine);
|
|
1667
|
+
index += 1;
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
if (
|
|
1671
|
+
/^\S/.test(nextLine) &&
|
|
1672
|
+
!/^\s*#/.test(nextLine) &&
|
|
1673
|
+
!/^\s*(?:from\s+\S+\s+import\b|import\s+\S+)/.test(nextLine) &&
|
|
1674
|
+
!/^\s*(?:def|async\s+def|class)\s+/.test(nextLine)
|
|
1675
|
+
) {
|
|
1676
|
+
break;
|
|
1677
|
+
}
|
|
1678
|
+
keep.push(nextLine);
|
|
1679
|
+
index += 1;
|
|
1680
|
+
}
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
for (; index < lines.length; index += 1) {
|
|
1687
|
+
body.push(lines[index]);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
let bodyText = body.join("\n");
|
|
1691
|
+
bodyText = bodyText.replace(/^([ \t]*\n)+/, "");
|
|
1692
|
+
bodyText = bodyText.replace(/(\n[ \t]*)+$/, "");
|
|
1693
|
+
bodyText = bodyText.replace(/\t/g, " ");
|
|
1694
|
+
if (!bodyText.replace(/[\s\n]+/g, "").length) return code;
|
|
1695
|
+
|
|
1696
|
+
const injected = bodyText
|
|
1697
|
+
.replace(
|
|
1698
|
+
/^(\s*)(\w+\s*\.\s*tick\s*\([^\)]*\)\s*)$/gm,
|
|
1699
|
+
"$1$2\n$1await asyncio.sleep(0)",
|
|
1700
|
+
)
|
|
1701
|
+
.replace(
|
|
1702
|
+
/^(\s*)(pygame\s*\.\s*display\s*\.\s*flip\s*\(\s*\)\s*)$/gm,
|
|
1703
|
+
"$1$2\n$1await asyncio.sleep(0)",
|
|
1704
|
+
)
|
|
1705
|
+
.replace(
|
|
1706
|
+
/^([\t ]*)([A-Za-z_][\w]*(?:\s*\.\s*[A-Za-z_][\w]*)*\s*\.\s*step\s*\([^\)]*\)\s*)(#.*)?$/gm,
|
|
1707
|
+
"$1$2$3\n$1await asyncio.sleep(0)",
|
|
1708
|
+
);
|
|
1709
|
+
|
|
1710
|
+
const prelude = [
|
|
1711
|
+
"# --- auto-wrapped by IDE for browser pygame compatibility ---",
|
|
1712
|
+
"import asyncio",
|
|
1713
|
+
"import pygame",
|
|
1714
|
+
"",
|
|
1715
|
+
"async def main():",
|
|
1716
|
+
indentBlock(injected, 4),
|
|
1717
|
+
"",
|
|
1718
|
+
"await main()",
|
|
1719
|
+
"# --- end auto-wrapped ---",
|
|
1720
|
+
"",
|
|
1721
|
+
].join("\n");
|
|
1722
|
+
|
|
1723
|
+
let keepText = keep.join("\n");
|
|
1724
|
+
if (keepText && !keepText.endsWith("\n")) keepText += "\n";
|
|
1725
|
+
if (keepText && !/\n\s*\n$/.test(keepText)) keepText += "\n";
|
|
1726
|
+
|
|
1727
|
+
return keepText + prelude;
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
const maybeAutoWrapPygame = (code) => {
|
|
1731
|
+
try {
|
|
1732
|
+
const text = String(code || "");
|
|
1733
|
+
if (!looksLikeTopLevelGameLoop(text)) return text;
|
|
1734
|
+
return wrapTopLevelIntoAsyncMain(text);
|
|
1735
|
+
} catch {
|
|
1736
|
+
return String(code || "");
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
|
|
599
1740
|
const ensureMicropipPackages = async (id, pyodide, packages = []) => {
|
|
600
1741
|
if (packages.length === 0) return;
|
|
601
1742
|
if (!installedMicropipPackages.has(id)) {
|
|
@@ -623,10 +1764,20 @@ hyperbook.python = (function () {
|
|
|
623
1764
|
const pyodide = await getRuntime(id);
|
|
624
1765
|
const { canvas, ...globalsContext } = context;
|
|
625
1766
|
const decoder = new TextDecoder("utf-8");
|
|
1767
|
+
const executableScript = maybeAutoWrapPygame(script);
|
|
626
1768
|
|
|
627
1769
|
if (canvas) {
|
|
628
1770
|
try {
|
|
629
1771
|
resetCanvas(canvas);
|
|
1772
|
+
const usesTurtle = scriptLooksLikeTurtle(executableScript);
|
|
1773
|
+
if (usesTurtle) {
|
|
1774
|
+
turtleModules.get(id)?.__bindCanvas(canvas);
|
|
1775
|
+
turtleModules.get(id)?.__resetState();
|
|
1776
|
+
} else {
|
|
1777
|
+
turtleModules.get(id)?.__deactivate?.();
|
|
1778
|
+
canvas.style.width = "";
|
|
1779
|
+
canvas.style.height = "";
|
|
1780
|
+
}
|
|
630
1781
|
pyodide.canvas.setCanvas2D(canvas);
|
|
631
1782
|
} catch (error) {
|
|
632
1783
|
appendOutputErrorLine(id, `Canvas setup failed: ${error.message}`);
|
|
@@ -658,14 +1809,14 @@ hyperbook.python = (function () {
|
|
|
658
1809
|
});
|
|
659
1810
|
|
|
660
1811
|
await ensureMicropipPackages(id, pyodide, packages);
|
|
661
|
-
await pyodide.loadPackagesFromImports(
|
|
1812
|
+
await pyodide.loadPackagesFromImports(executableScript);
|
|
662
1813
|
const dict = pyodide.globals.get("dict");
|
|
663
1814
|
const globals = dict();
|
|
664
1815
|
try {
|
|
665
1816
|
for (const [key, value] of Object.entries(globalsContext)) {
|
|
666
1817
|
globals.set(key, value);
|
|
667
1818
|
}
|
|
668
|
-
const results = await pyodide.runPythonAsync(
|
|
1819
|
+
const results = await pyodide.runPythonAsync(executableScript, {
|
|
669
1820
|
globals,
|
|
670
1821
|
locals: globals,
|
|
671
1822
|
filename,
|
|
@@ -1122,6 +2273,7 @@ if _pg:
|
|
|
1122
2273
|
output.classList.add("hidden");
|
|
1123
2274
|
if (canvasWrapper) canvasWrapper.classList.remove("hidden");
|
|
1124
2275
|
canvasOutputSplitter?.classList.add("hidden");
|
|
2276
|
+
turtleModules.get(id)?.__redraw?.();
|
|
1125
2277
|
};
|
|
1126
2278
|
|
|
1127
2279
|
const applyStoredCanvasOutputSplit = setupCanvasOutputSplitter(
|
|
@@ -1152,6 +2304,7 @@ if _pg:
|
|
|
1152
2304
|
canvasBtn.disabled = true;
|
|
1153
2305
|
}
|
|
1154
2306
|
applyStoredCanvasOutputSplit?.();
|
|
2307
|
+
turtleModules.get(id)?.__redraw?.();
|
|
1155
2308
|
return;
|
|
1156
2309
|
}
|
|
1157
2310
|
|
|
@@ -1238,7 +2391,10 @@ if _pg:
|
|
|
1238
2391
|
void restoreEditorState();
|
|
1239
2392
|
}
|
|
1240
2393
|
|
|
1241
|
-
window.addEventListener("resize",
|
|
2394
|
+
window.addEventListener("resize", () => {
|
|
2395
|
+
applyCanvasOutputLayout();
|
|
2396
|
+
turtleModules.get(id)?.__redraw?.();
|
|
2397
|
+
});
|
|
1242
2398
|
applyCanvasOutputLayout();
|
|
1243
2399
|
|
|
1244
2400
|
editor.addEventListener("input", () => {
|
|
@@ -1303,8 +2459,9 @@ if _pg:
|
|
|
1303
2459
|
|
|
1304
2460
|
run?.addEventListener("click", async () => {
|
|
1305
2461
|
const script = getEditorValue();
|
|
1306
|
-
const
|
|
1307
|
-
|
|
2462
|
+
const usesPytamaro = hasPytamaroPackage || scriptLooksLikePytamaro(script);
|
|
2463
|
+
const renderPytamaroToCanvas = hasCanvas && canvas && usesPytamaro;
|
|
2464
|
+
if (hasCanvas) {
|
|
1308
2465
|
showCanvas();
|
|
1309
2466
|
} else {
|
|
1310
2467
|
showOutput();
|
|
@@ -1322,15 +2479,16 @@ if _pg:
|
|
|
1322
2479
|
output.innerHTML = "";
|
|
1323
2480
|
clearPytamaroStdoutCarry(id);
|
|
1324
2481
|
try {
|
|
2482
|
+
setPytamaroCanvasTarget(id, renderPytamaroToCanvas);
|
|
1325
2483
|
const { results, error } = await executeScript(id, script, {
|
|
1326
|
-
...(hasCanvas && canvas
|
|
2484
|
+
...(hasCanvas && canvas ? { canvas } : {}),
|
|
1327
2485
|
}, additionalPackages);
|
|
1328
2486
|
if (!state.stopRequested) {
|
|
1329
2487
|
if (results) {
|
|
1330
|
-
appendOutput(output, results);
|
|
2488
|
+
appendOutput(output, results, false, id);
|
|
1331
2489
|
} else if (error) {
|
|
1332
2490
|
showOutput();
|
|
1333
|
-
appendOutput(output, error, true);
|
|
2491
|
+
appendOutput(output, error, true, id);
|
|
1334
2492
|
}
|
|
1335
2493
|
} else {
|
|
1336
2494
|
appendOutputLine(id, "Execution stopped.");
|
|
@@ -1338,7 +2496,7 @@ if _pg:
|
|
|
1338
2496
|
} catch (e) {
|
|
1339
2497
|
showOutput();
|
|
1340
2498
|
output.innerHTML = "";
|
|
1341
|
-
appendOutput(output, `Error: ${e}`, true);
|
|
2499
|
+
appendOutput(output, `Error: ${e}`, true, id);
|
|
1342
2500
|
console.log(e);
|
|
1343
2501
|
} finally {
|
|
1344
2502
|
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.1","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.1",
|
|
4
4
|
"author": "Mike Barkmin",
|
|
5
5
|
"homepage": "https://github.com/openpatch/hyperbook#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"create-hyperbook": "0.3.6",
|
|
60
60
|
"@hyperbook/fs": "0.25.0",
|
|
61
61
|
"@hyperbook/types": "0.23.0",
|
|
62
|
-
"@hyperbook/markdown": "0.
|
|
62
|
+
"@hyperbook/markdown": "0.64.1"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"version": "pnpm build",
|