@zikojs/notebook 0.8.0 → 0.10.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.
package/src/notebook.js CHANGED
@@ -1,371 +1,791 @@
1
+
1
2
  import { NotebookStore } from "./store.js";
2
- import {
3
- Header,
4
- CellItem,
3
+ import {
4
+ Header,
5
+ CellItem,
5
6
  MarkdownView,
6
- ErrorOutput
7
+ ErrorOutput,
7
8
  } from "./components/index.js";
8
9
  import { evaluateCodeAsync } from "./compiler/index.js";
9
10
  import { tags, UIElement } from "ziko/dom";
10
- import { call_with_optional_props } from 'ziko/dom/internal-utils'
11
- import { useState, useDerived } from "ziko/hooks";
12
- const { div } = tags
13
- // const [st, setSt] = useState('hh')
14
- // globalThis.mm = useDerived((n)=>n, [st])
15
- // globalThis.pp = tags.p({}, mm).mount(document.body)
16
- // globalThis.st = st
17
- // globalThis.setSt = setSt
18
-
19
- export class UINotebook extends UIElement{
11
+ import { call_with_optional_props } from "ziko/dom/internal-utils";
12
+
13
+ const { div } = tags;
14
+
15
+ const isPromiseLike = (value) =>
16
+ value != null &&
17
+ (typeof value === "object" || typeof value === "function") &&
18
+ typeof value.then === "function";
19
+
20
+ const resolveCode = async (code) => {
21
+ const resolved = await code;
22
+
23
+ if (typeof resolved !== "string") {
24
+ throw new TypeError(
25
+ `Cell code must resolve to a string; received ${typeof resolved}.`
26
+ );
27
+ }
28
+
29
+ return resolved;
30
+ };
31
+
32
+ export class UINotebook extends UIElement {
20
33
  constructor({
21
34
  cells: initialCells = [],
22
35
  importMap = {},
23
36
  runCells = true,
24
- codeMirrorConfig = {},
25
- codeMirrorPlugins = [],
26
37
  minLines = 3,
27
38
  maxCells = Infinity,
28
39
  showHeader = true,
29
- runtimePlugins = []
40
+ codeMirrorConfig = {},
41
+ codeMirrorPlugins = [],
42
+ runtimePlugins = [],
43
+ remarkPlugins = [],
44
+ rehypePlugins = [],
30
45
  } = {}) {
31
- super({ element : 'div'})
32
- this.setAttr({class : 'ziko-notebook'})
33
- this.id = crypto.randomUUID ? crypto.randomUUID() : 'nb_' + Math.random().toString(36).substring(2, 9);
34
-
46
+ super({ element: "div" });
47
+
48
+ this.setAttr({ class: "ziko-notebook" });
49
+
50
+ this.id = globalThis.crypto?.randomUUID
51
+ ? globalThis.crypto.randomUUID()
52
+ : "nb_" + Math.random().toString(36).substring(2, 9);
53
+
35
54
  if (!window.__notebook_scope_map) {
36
55
  window.__notebook_scope_map = new Map();
37
56
  }
57
+
38
58
  this.resetScope();
39
59
 
40
60
  this.importMap = importMap;
41
61
  this.runtimePlugins = runtimePlugins;
42
62
  this.codeMirrorConfig = codeMirrorConfig;
43
63
  this.codeMirrorPlugins = codeMirrorPlugins;
64
+ this.remarkPlugins = remarkPlugins;
65
+ this.rehypePlugins = rehypePlugins;
44
66
  this.minLines = minLines;
45
67
  this.maxCells = maxCells;
46
68
 
47
- const startingCells = maxCells !== Infinity ? initialCells.slice(0, maxCells) : initialCells;
48
- let nextCellId = startingCells.length ? Math.max(...startingCells.map(c => c.id || 0)) + 1 : 1;
69
+ this.nextCellId = 1;
49
70
 
50
71
  this.store = new NotebookStore({
51
- cells: startingCells.map(cell => ({
52
- id: cell.id || nextCellId++,
53
- type: cell.type || "code",
54
- code: cell.code || "",
55
- readonly: cell.readonly ?? false,
56
- isEditingMarkdown: cell.readonly ? false : (cell.isEditingMarkdown ?? true),
57
- execCount: cell.execCount ?? null,
58
- outputNode: cell.outputNode || null,
59
- hasDomOutput: cell.hasDomOutput ?? false,
60
- _copiedFlag: false,
61
- _dom: null, _editor: null, _actions: null,
62
- _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: null
63
- })),
64
- activeCellId: startingCells.length ? startingCells[0].id : null,
72
+ cells: [],
73
+ activeCellId: null,
65
74
  executionCounter: 1,
66
- executionQueueState: { isRunning: false, currentIndex: 0, total: 0 },
67
- maxCells: this.maxCells
75
+ executionQueueState: {
76
+ isRunning: false,
77
+ currentIndex: 0,
78
+ total: 0,
79
+ },
80
+ maxCells: this.maxCells,
68
81
  });
69
82
 
70
- this.nextCellId = nextCellId;
71
-
72
83
  this.notebookGlobalShortcuts = {
73
- deleteActive: () => this.deleteCell(this.store.get('activeCellId')),
74
- addMdBelow: () => this.addCell("markdown", "", this.store.get('activeCellId')),
75
- addCodeBelow: () => this.addCell("code", "", this.store.get('activeCellId'))
84
+ deleteActive: () =>
85
+ this.deleteCell(this.store.get("activeCellId")),
86
+ addMdBelow: () =>
87
+ this.addCell("markdown", "", this.store.get("activeCellId")),
88
+ addCodeBelow: () =>
89
+ this.addCell("code", "", this.store.get("activeCellId")),
76
90
  };
77
91
 
78
92
  this.cellsContainer = div({ class: "cells-container" }).element;
93
+
79
94
  this.header = Header({
80
95
  store: this.store,
81
96
  actions: {
82
97
  runAll: () => this.runAllCells(),
83
98
  clearOutputs: () => this.clearOutputs(),
84
- addCode: () => this.addCell("code", "", this.store.get('activeCellId')),
85
- addMarkdown: () => this.addCell("markdown", "", this.store.get('activeCellId')),
86
- deleteActive: () => this.deleteCell(this.store.get('activeCellId')),
87
- exportData: () => console.log("Notebook Data Export:", this.getNotebookData())
88
- }
99
+ addCode: () =>
100
+ this.addCell("code", "", this.store.get("activeCellId")),
101
+ addMarkdown: () =>
102
+ this.addCell("markdown", "", this.store.get("activeCellId")),
103
+ deleteActive: () =>
104
+ this.deleteCell(this.store.get("activeCellId")),
105
+ exportData: () =>
106
+ console.log("Notebook Data Export:", this.getNotebookData()),
107
+ },
89
108
  });
90
109
 
91
- if(!showHeader) this.header.style.display = 'none'
110
+ if (!showHeader) {
111
+ this.header.style.display = "none";
112
+ }
92
113
 
93
114
  this.append(
94
- this.header,
95
- div({ class: "notebook" },
96
- this.cellsContainer)
97
- )
98
-
99
- this._renderAllCells();
115
+ this.header,
116
+ div({ class: "notebook" }, this.cellsContainer)
117
+ );
100
118
 
101
- this.store.on('change:activeCellId', (newId, oldId) => {
119
+ this.store.on("change:activeCellId", (newId, oldId) => {
102
120
  if (oldId !== null) {
103
- const prev = this.store.get('cells').find(c => c.id === oldId);
104
- if (prev) this._refreshCell(prev);
121
+ const previous = this.store
122
+ .get("cells")
123
+ .find((cell) => cell.id === oldId);
124
+
125
+ if (previous?._dom) {
126
+ this._refreshCell(previous);
127
+ }
128
+ }
129
+
130
+ const next = this.store
131
+ .get("cells")
132
+ .find((cell) => cell.id === newId);
133
+
134
+ if (next?._dom) {
135
+ this._refreshCell(next);
105
136
  }
106
- const next = this.store.get('cells').find(c => c.id === newId);
107
- if (next) this._refreshCell(next);
108
137
  });
109
138
 
139
+ // Always expose one readiness Promise, whether the initial
140
+ // data is synchronous or asynchronous.
141
+ this.ready = this._initialize(initialCells);
142
+
143
+ // Prevent an unhandled-rejection warning when callers rely on
144
+ // runCells but don't explicitly await notebook.ready.
145
+ // Awaiting notebook.ready still rejects if the initial cells
146
+ // collection itself fails to load.
147
+ this.ready.catch(() => {});
148
+
110
149
  if (runCells) {
111
- setTimeout(() => { this.runAllCells(); }, 200);
150
+ this.ready
151
+ .then(() => this.runAllCells())
152
+ .catch((error) => {
153
+ console.error("Failed to initialize notebook:", error);
154
+ });
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Resolve the initial cells array, normalize it, render it,
160
+ * and resolve any Promise-based cell code.
161
+ */
162
+ async _initialize(initialCells) {
163
+ const resolvedCells = await initialCells;
164
+
165
+ if (!Array.isArray(resolvedCells)) {
166
+ throw new TypeError(
167
+ "Notebook cells must be an array or a Promise resolving to an array."
168
+ );
169
+ }
170
+
171
+ this._replaceInitialCells(resolvedCells);
172
+ await this._resolvePendingCellCode();
173
+
174
+ return this;
175
+ }
176
+
177
+ /**
178
+ * Normalize and replace initial cells.
179
+ * Used for both synchronous and asynchronous initial data.
180
+ */
181
+ _replaceInitialCells(initialCells) {
182
+ const startingCells =
183
+ this.maxCells !== Infinity
184
+ ? initialCells.slice(0, this.maxCells)
185
+ : initialCells;
186
+
187
+ const ids = startingCells
188
+ .map((cell) => cell.id)
189
+ .filter((id) => Number.isFinite(id));
190
+
191
+ this.nextCellId = ids.length ? Math.max(...ids) + 1 : 1;
192
+
193
+ const normalizedCells = startingCells.map((cell) => {
194
+ const id = cell.id ?? this.nextCellId++;
195
+ const source = cell.code ?? "";
196
+ const pendingCode = isPromiseLike(source)
197
+ ? Promise.resolve(source)
198
+ : null;
199
+
200
+ return {
201
+ id,
202
+ type: cell.type || "code",
203
+ code: pendingCode
204
+ ? ""
205
+ : typeof source === "string"
206
+ ? source
207
+ : "",
208
+ _codePromise: pendingCode,
209
+ _codeLoadError: null,
210
+
211
+ readonly: cell.readonly ?? false,
212
+ runtime: cell.runtime || null,
213
+ isEditingMarkdown: cell.readonly
214
+ ? false
215
+ : (cell.isEditingMarkdown ?? true),
216
+ execCount: cell.execCount ?? null,
217
+ outputNode: cell.outputNode || null,
218
+ hasDomOutput: cell.hasDomOutput ?? false,
219
+
220
+ _copiedFlag: false,
221
+ _dom: null,
222
+ _editor: null,
223
+ _actions: null,
224
+ _inPrompt: null,
225
+ _outPrompt: null,
226
+ _bodySlot: null,
227
+ _outputGrid: null,
228
+ _outputRef: null,
229
+ };
230
+ });
231
+
232
+ this.store.set("cells", normalizedCells);
233
+ this.store.set(
234
+ "activeCellId",
235
+ normalizedCells.length ? normalizedCells[0].id : null
236
+ );
237
+
238
+ this._renderAllCells();
239
+ }
240
+
241
+ /**
242
+ * Resolve every pending code source independently.
243
+ * A single failed code request does not block other cells.
244
+ */
245
+ async _resolvePendingCellCode() {
246
+ const cells = this.store.get("cells");
247
+
248
+ await Promise.all(
249
+ cells.map((cell) => this._resolveCellCode(cell))
250
+ );
251
+ }
252
+
253
+ async _resolveCellCode(cellData) {
254
+ if (!cellData._codePromise) {
255
+ return !cellData._codeLoadError;
256
+ }
257
+
258
+ const sourcePromise = cellData._codePromise;
259
+
260
+ try {
261
+ const code = await resolveCode(sourcePromise);
262
+
263
+ // Ignore stale results if the cell's source was replaced
264
+ // while the previous Promise was still pending.
265
+ if (cellData._codePromise !== sourcePromise) {
266
+ return false;
267
+ }
268
+
269
+ cellData.code = code;
270
+ cellData._codePromise = null;
271
+ cellData._codeLoadError = null;
272
+
273
+ if (cellData._dom) {
274
+ this._refreshCell(cellData);
275
+ }
276
+
277
+ return true;
278
+ } catch (error) {
279
+ if (cellData._codePromise !== sourcePromise) {
280
+ return false;
281
+ }
282
+
283
+ cellData._codePromise = null;
284
+ cellData._codeLoadError = error;
285
+
286
+ if (cellData._outputRef) {
287
+ cellData._outputRef.replaceChildren();
288
+ ErrorOutput(error).mount(cellData._outputRef);
289
+ cellData.hasDomOutput = true;
290
+
291
+ if (cellData._dom) {
292
+ this._refreshCell(cellData);
293
+ }
294
+ }
295
+
296
+ console.error(
297
+ `Failed to load code for notebook cell ${cellData.id}:`,
298
+ error
299
+ );
300
+
301
+ return false;
112
302
  }
113
303
  }
114
304
 
115
- _isMaxCellsReached() { return this.store.get('cells').length >= this.maxCells; }
305
+ _isMaxCellsReached() {
306
+ return this.store.get("cells").length >= this.maxCells;
307
+ }
116
308
 
117
309
  _cellActions(cellData) {
118
310
  const app = this;
311
+
119
312
  return {
120
313
  setActive: () => app.setActiveCell(cellData.id),
314
+
121
315
  runCode: async (autoNext = true) => {
316
+ // If this cell's code is still loading, wait for it.
317
+ await app._resolveCellCode(cellData);
318
+
319
+ if (cellData._codeLoadError) {
320
+ return;
321
+ }
322
+
122
323
  const code = cellData._editor.getValue();
123
324
  cellData.code = code;
124
325
 
125
326
  if (cellData.type === "markdown") {
126
327
  cellData.isEditingMarkdown = false;
127
- if (autoNext) app.nextCell(cellData.id, "markdown");
128
- else app._refreshCell(cellData);
328
+
329
+ if (autoNext) {
330
+ app.nextCell(cellData.id, "markdown");
331
+ } else {
332
+ app._refreshCell(cellData);
333
+ }
334
+
129
335
  return;
130
336
  }
131
337
 
132
338
  if (!code.trim()) return;
133
339
 
134
- cellData._outputRef.innerHTML = "";
340
+ cellData._outputRef.replaceChildren();
341
+
135
342
  let hasError = false;
136
343
 
137
344
  try {
138
345
  const scope = app.getScope();
346
+
139
347
  await evaluateCodeAsync(
140
348
  code,
141
349
  cellData._outputRef,
142
350
  {
143
351
  importMap: app.importMap,
144
352
  plugins: app.runtimePlugins,
353
+ runtime: cellData.runtime,
145
354
  },
146
- scope,
355
+ scope
147
356
  );
148
- } catch (err) {
357
+ } catch (error) {
149
358
  hasError = true;
150
- ErrorOutput(err).mount(cellData._outputRef)
359
+ ErrorOutput(error).mount(cellData._outputRef);
151
360
  }
152
361
 
153
- const counter = this.store.get('executionCounter');
154
- this.store.set('executionCounter', counter + 1);
362
+ const counter = app.store.get("executionCounter");
363
+
364
+ app.store.set("executionCounter", counter + 1);
155
365
  cellData.execCount = counter;
156
- cellData.hasDomOutput = cellData._outputRef.childNodes.length > 0 || hasError;
366
+ cellData.hasDomOutput =
367
+ cellData._outputRef.childNodes.length > 0 || hasError;
157
368
 
158
- const container = div({ class: "output-area" }).element;
159
- while (cellData._outputRef.firstChild) container.appendChild(cellData._outputRef.firstChild);
160
- cellData.outputNode = container;
161
- cellData._outputRef.appendChild(container);
369
+ // Output mounting remains handled by the existing component
370
+ // lifecycle / autoMount behavior.
371
+ if (autoNext && !hasError) {
372
+ app.nextCell(cellData.id, "code");
373
+ }
162
374
 
163
- if (autoNext && !hasError) app.nextCell(cellData.id, "code");
164
375
  app._refreshCell(cellData);
165
376
  },
377
+
166
378
  copyCode: async () => {
167
- const textToCopy = cellData._editor.getValue() || cellData.code;
379
+ const textToCopy =
380
+ cellData._editor.getValue() || cellData.code;
381
+
168
382
  try {
169
383
  await navigator.clipboard.writeText(textToCopy);
170
- } catch (err) {
171
- const temporary = tags.textarea(textToCopy).mount(document.body)
172
- temporary.element.select()
384
+ } catch {
385
+ const temporary = tags.textarea(textToCopy).mount(
386
+ document.body
387
+ );
388
+
389
+ temporary.element.select();
173
390
  document.execCommand("copy");
174
- temporary.unmount()
391
+ temporary.unmount();
175
392
  }
176
393
  },
394
+
177
395
  toggleReadonly: () => {
178
396
  cellData.readonly = !cellData.readonly;
179
- if (cellData.readonly && cellData.type === "markdown") cellData.isEditingMarkdown = false;
397
+
398
+ if (
399
+ cellData.readonly &&
400
+ cellData.type === "markdown"
401
+ ) {
402
+ cellData.isEditingMarkdown = false;
403
+ }
404
+
180
405
  app._refreshCell(cellData);
181
406
  },
407
+
182
408
  toggleType: () => {
183
409
  if (cellData.readonly) return;
184
- cellData.type = cellData.type === "code" ? "markdown" : "code";
410
+
411
+ cellData.type =
412
+ cellData.type === "code" ? "markdown" : "code";
413
+
185
414
  cellData.isEditingMarkdown = true;
186
415
  cellData.hasDomOutput = false;
416
+
187
417
  app._refreshCell(cellData);
188
418
  },
419
+
189
420
  editMarkdown: () => {
190
421
  if (cellData.readonly) return;
422
+
191
423
  cellData.isEditingMarkdown = true;
192
424
  app._refreshCell(cellData);
193
425
  },
194
- addBelow: () => app.addCell(cellData.type, null, cellData.id),
195
- deleteCell: () => app.deleteCell(cellData.id)
426
+
427
+ addBelow: () =>
428
+ app.addCell(cellData.type, null, cellData.id),
429
+
430
+ deleteCell: () => app.deleteCell(cellData.id),
431
+
432
+ setRuntime: (newRuntime) => {
433
+ cellData.runtime = newRuntime;
434
+ app._refreshCell(cellData);
435
+ },
196
436
  };
197
437
  }
198
438
 
199
439
  _buildCellDom(cellData) {
200
- return CellItem({ cellData, app: this, store: this.store });
440
+ return CellItem({
441
+ cellData,
442
+ app: this,
443
+ store: this.store,
444
+ });
201
445
  }
202
446
 
203
447
  _refreshCell(cellData) {
204
- const activeId = this.store.get('activeCellId');
448
+ if (!cellData._dom) return;
449
+
450
+ const activeId = this.store.get("activeCellId");
205
451
  const isActive = activeId === cellData.id;
206
- cellData._dom.className = `cell ${isActive ? "active" : ""} ${cellData.readonly ? "readonly" : ""}`.trim();
207
452
 
208
- const countText = cellData.execCount ? `[${cellData.execCount}]` : "[ ]";
209
- const isMarkdownView = cellData.type === "markdown" && !cellData.isEditingMarkdown;
453
+ cellData._dom.className =
454
+ `cell ${isActive ? "active" : ""} ${
455
+ cellData.readonly ? "readonly" : ""
456
+ }`.trim();
457
+
458
+ const countText = cellData.execCount
459
+ ? `[${cellData.execCount}]`
460
+ : "[ ]";
461
+
462
+ const isMarkdownView =
463
+ cellData.type === "markdown" &&
464
+ !cellData.isEditingMarkdown;
210
465
 
211
- cellData._inPrompt.textContent = cellData.type === "code" ? `In ${countText}:` : "";
466
+ cellData._inPrompt.textContent =
467
+ cellData.type === "code" ? `In ${countText}:` : "";
212
468
 
213
469
  cellData._editor.sync(isActive, isMarkdownView);
470
+
214
471
  cellData._bodySlot.replaceChildren(
215
- isMarkdownView ? MarkdownView({ cellData, actions: cellData._actions }) : cellData._editor.dom
472
+ isMarkdownView
473
+ ? MarkdownView({
474
+ cellData,
475
+ actions: cellData._actions,
476
+ remarkPlugins: this.remarkPlugins,
477
+ rehypePlugins: this.rehypePlugins,
478
+ })
479
+ : cellData._editor.dom
216
480
  );
217
481
 
218
- const showOutput = cellData.type === "code" && cellData.hasDomOutput;
219
- cellData._outputGrid.style.display = showOutput ? "grid" : "none";
482
+ const showOutput =
483
+ cellData.type === "code" && cellData.hasDomOutput;
484
+
485
+ cellData._outputGrid.style.display =
486
+ showOutput ? "grid" : "none";
487
+
220
488
  cellData._outPrompt.textContent = `Out ${countText}:`;
221
489
  }
222
490
 
223
491
  _renderAllCells() {
224
- const cells = this.store.get('cells');
225
- this.cellsContainer.replaceChildren(...cells.map(c => c._dom || this._buildCellDom(c)));
492
+ const cells = this.store.get("cells");
493
+
494
+ this.cellsContainer.replaceChildren(
495
+ ...cells.map(
496
+ (cell) => cell._dom || this._buildCellDom(cell)
497
+ )
498
+ );
226
499
  }
227
500
 
228
501
  setActiveCell(id) {
229
- const currentActiveId = this.store.get('activeCellId');
502
+ const cells = this.store.get("cells");
503
+ const target = cells.find((cell) => cell.id === id);
504
+
505
+ if (!target) return;
506
+
507
+ const currentActiveId = this.store.get("activeCellId");
508
+
230
509
  if (currentActiveId === id) return;
231
510
 
232
- // Check all cells and close editing mode for any markdown cells that are losing focus
233
- this.store.get('cells').forEach(c => {
234
- if (c.type === "markdown" && c.isEditingMarkdown) {
235
- c.isEditingMarkdown = false;
236
- this._refreshCell(c);
511
+ cells.forEach((cell) => {
512
+ if (cell.id !== id && cell.type === "markdown") {
513
+ cell.isEditingMarkdown = false;
237
514
  }
238
515
  });
239
516
 
240
- this.store.set('activeCellId', id);
517
+ this.store.set("activeCellId", id);
518
+
519
+ cells.forEach((cell) => {
520
+ if (cell._dom) {
521
+ this._refreshCell(cell);
522
+ }
523
+ });
241
524
  }
242
525
 
243
526
  focusCell(targetId) {
244
- // Close editing on other markdown cells before focusing
245
- this.store.get('cells').forEach(c => {
246
- if (c.type === "markdown" && c.isEditingMarkdown && c.id !== targetId) {
247
- c.isEditingMarkdown = false;
248
- this._refreshCell(c);
527
+ this.store.get("cells").forEach((cell) => {
528
+ if (
529
+ cell.type === "markdown" &&
530
+ cell.isEditingMarkdown &&
531
+ cell.id !== targetId
532
+ ) {
533
+ cell.isEditingMarkdown = false;
534
+ this._refreshCell(cell);
249
535
  }
250
536
  });
251
537
 
252
- const target = this.store.get('cells').find(c => c.id === targetId);
253
- if (target && target.type === "markdown" && !target.readonly) {
538
+ const target = this.store
539
+ .get("cells")
540
+ .find((cell) => cell.id === targetId);
541
+
542
+ if (
543
+ target &&
544
+ target.type === "markdown" &&
545
+ !target.readonly
546
+ ) {
254
547
  target.isEditingMarkdown = true;
255
548
  }
256
- this.store.set('activeCellId', targetId);
549
+
550
+ this.store.set("activeCellId", targetId);
257
551
  }
258
552
 
259
- addCell(type = "code", initialCode = null, afterId = null, readonly = false) {
260
- const cells = [...this.store.get('cells')];
553
+ addCell(
554
+ type = "code",
555
+ initialCode = null,
556
+ afterId = null,
557
+ readonly = false
558
+ ) {
559
+ const cells = [...this.store.get("cells")];
560
+
261
561
  if (cells.length >= this.maxCells) {
262
- console.warn(`Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`);
562
+ console.warn(
563
+ `Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`
564
+ );
565
+
263
566
  return null;
264
567
  }
265
568
 
266
569
  const newId = this.nextCellId++;
267
- const defaultText = type === "markdown" ? "### New Markdown Cell\nClick to edit..." : "";
570
+
571
+ const defaultText =
572
+ type === "markdown"
573
+ ? "### New Markdown Cell\nClick to edit..."
574
+ : "";
575
+
576
+ const source =
577
+ initialCode !== null ? initialCode : defaultText;
578
+
579
+ const pendingCode = isPromiseLike(source)
580
+ ? Promise.resolve(source)
581
+ : null;
582
+
268
583
  const newCell = {
269
- id: newId, type, code: initialCode !== null ? initialCode : defaultText, readonly,
270
- isEditingMarkdown: !readonly && type === "markdown" && initialCode === null,
271
- execCount: null, outputNode: null, hasDomOutput: false, _copiedFlag: false,
272
- _dom: null, _editor: null, _actions: null,
273
- _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: null
584
+ id: newId,
585
+ type,
586
+ code: pendingCode
587
+ ? ""
588
+ : typeof source === "string"
589
+ ? source
590
+ : "",
591
+ _codePromise: pendingCode,
592
+ _codeLoadError: null,
593
+
594
+ readonly,
595
+ isEditingMarkdown:
596
+ !readonly &&
597
+ type === "markdown" &&
598
+ initialCode === null,
599
+ runtime: null,
600
+ execCount: null,
601
+ outputNode: null,
602
+ hasDomOutput: false,
603
+
604
+ _copiedFlag: false,
605
+ _dom: null,
606
+ _editor: null,
607
+ _actions: null,
608
+ _inPrompt: null,
609
+ _outPrompt: null,
610
+ _bodySlot: null,
611
+ _outputGrid: null,
612
+ _outputRef: null,
274
613
  };
275
614
 
276
615
  if (afterId === null) {
277
616
  cells.push(newCell);
278
617
  } else {
279
- const index = cells.findIndex(c => c.id === afterId);
618
+ const index = cells.findIndex(
619
+ (cell) => cell.id === afterId
620
+ );
621
+
280
622
  cells.splice(index + 1, 0, newCell);
281
623
  }
282
624
 
283
- this.store.set('cells', cells);
625
+ this.store.set("cells", cells);
284
626
  this._renderAllCells();
285
627
  this.focusCell(newId);
628
+
629
+ // Resolve newly added Promise-based code without blocking
630
+ // the synchronous addCell() API.
631
+ if (pendingCode) {
632
+ this._resolveCellCode(newCell);
633
+ }
634
+
286
635
  return newId;
287
636
  }
288
637
 
289
638
  deleteCell(id) {
290
- const cells = this.store.get('cells');
639
+ const cells = this.store.get("cells");
640
+
291
641
  if (cells.length <= 1) return;
292
- const index = cells.findIndex(c => c.id === id);
293
- const updatedCells = cells.filter(c => c.id !== id);
294
-
295
- this.store.set('cells', updatedCells);
642
+
643
+ const index = cells.findIndex((cell) => cell.id === id);
644
+ const updatedCells = cells.filter(
645
+ (cell) => cell.id !== id
646
+ );
647
+
648
+ this.store.set("cells", updatedCells);
296
649
  this._renderAllCells();
297
- const nextActiveIndex = Math.min(index, updatedCells.length - 1);
650
+
651
+ const nextActiveIndex = Math.min(
652
+ Math.max(index, 0),
653
+ updatedCells.length - 1
654
+ );
655
+
298
656
  this.focusCell(updatedCells[nextActiveIndex].id);
299
657
  }
300
658
 
301
659
  nextCell(currentId, defaultType) {
302
- const cells = this.store.get('cells');
303
- const idx = cells.findIndex(c => c.id === currentId);
304
- if (idx === cells.length - 1) this.addCell(defaultType, null, currentId);
305
- else this.focusCell(cells[idx + 1].id);
660
+ const cells = this.store.get("cells");
661
+ const index = cells.findIndex(
662
+ (cell) => cell.id === currentId
663
+ );
664
+
665
+ if (index === cells.length - 1) {
666
+ this.addCell(defaultType, null, currentId);
667
+ } else if (index >= 0) {
668
+ this.focusCell(cells[index + 1].id);
669
+ }
306
670
  }
307
671
 
308
672
  async runCell(id) {
309
- const cell = this.store.get('cells').find(c => c.id === id);
310
- if (cell && cell._actions) await cell._actions.runCode(false);
673
+ await this.ready;
674
+
675
+ const cell = this.store
676
+ .get("cells")
677
+ .find((item) => item.id === id);
678
+
679
+ if (cell?._actions) {
680
+ await cell._actions.runCode(false);
681
+ }
311
682
  }
312
683
 
313
684
  async runAllCells() {
314
- const cells = this.store.get('cells');
315
- const codeCells = cells.filter(c => c.type === "code");
685
+ await this.ready;
686
+
687
+ const cells = this.store.get("cells");
688
+ const codeCells = cells.filter(
689
+ (cell) => cell.type === "code"
690
+ );
691
+
316
692
  if (codeCells.length === 0) return;
317
693
 
318
- this.store.set('executionQueueState', { isRunning: true, currentIndex: 0, total: codeCells.length });
694
+ this.store.set("executionQueueState", {
695
+ isRunning: true,
696
+ currentIndex: 0,
697
+ total: codeCells.length,
698
+ });
699
+
700
+ try {
701
+ for (const cell of cells) {
702
+ if (cell.type !== "code" || !cell._actions) {
703
+ continue;
704
+ }
319
705
 
320
- for (const cell of cells) {
321
- if (cell.type === "code" && cell._actions) {
322
- this.store.set('executionQueueState', {
323
- isRunning: true,
324
- currentIndex: codeCells.indexOf(cell),
325
- total: codeCells.length
706
+ this.store.set("executionQueueState", {
707
+ isRunning: true,
708
+ currentIndex: codeCells.indexOf(cell),
709
+ total: codeCells.length,
326
710
  });
711
+
327
712
  await cell._actions.runCode(false);
328
713
  }
714
+ } finally {
715
+ this.store.set("executionQueueState", {
716
+ isRunning: false,
717
+ currentIndex: 0,
718
+ total: 0,
719
+ });
329
720
  }
330
-
331
- this.store.set('executionQueueState', { isRunning: false, currentIndex: 0, total: 0 });
332
721
  }
333
722
 
334
723
  clearOutputs() {
335
- this.store.get('cells').forEach(cell => {
724
+ this.store.get("cells").forEach((cell) => {
336
725
  cell.execCount = null;
337
726
  cell.outputNode = null;
338
727
  cell.hasDomOutput = false;
339
- if (cell._outputRef) cell._outputRef.innerHTML = "";
728
+
729
+ if (cell._outputRef) {
730
+ cell._outputRef.replaceChildren();
731
+ }
732
+ });
733
+
734
+ this.store.set("executionCounter", 1);
735
+
736
+ this.store.get("cells").forEach((cell) => {
737
+ this._refreshCell(cell);
340
738
  });
341
- this.store.set('executionCounter', 1);
342
- this.store.get('cells').forEach(c => this._refreshCell(c));
343
739
  }
344
740
 
345
741
  resetScope() {
346
742
  if (!window.__notebook_scope_map) {
347
743
  window.__notebook_scope_map = new Map();
348
744
  }
745
+
349
746
  const newScope = Object.create(null);
747
+
350
748
  window.__notebook_scope_map.set(this.id, newScope);
749
+
351
750
  return newScope;
352
751
  }
353
752
 
354
753
  getScope() {
355
- if (!window.__notebook_scope_map || !window.__notebook_scope_map.has(this.id)) {
754
+ if (
755
+ !window.__notebook_scope_map ||
756
+ !window.__notebook_scope_map.has(this.id)
757
+ ) {
356
758
  return this.resetScope();
357
759
  }
760
+
358
761
  return window.__notebook_scope_map.get(this.id);
359
762
  }
360
763
 
361
- getNotebookData({ inputs = true, outputs = true } = {}) {
362
- return this.store.get('cells').map(cell => {
363
- const exportedCell = { id: cell.id, type: cell.type, readonly: cell.readonly, order: cell.execCount };
364
- if (inputs) exportedCell.code = cell.code;
365
- if (outputs) exportedCell.outputHtml = cell.outputNode ? cell.outputNode.innerHTML : null;
764
+ getNotebookData({
765
+ inputs = true,
766
+ outputs = true,
767
+ } = {}) {
768
+ return this.store.get("cells").map((cell) => {
769
+ const exportedCell = {
770
+ id: cell.id,
771
+ type: cell.type,
772
+ readonly: cell.readonly,
773
+ order: cell.execCount,
774
+ };
775
+
776
+ if (inputs) {
777
+ exportedCell.code = cell.code;
778
+ }
779
+
780
+ if (outputs) {
781
+ exportedCell.outputHtml = cell.outputNode
782
+ ? cell.outputNode.innerHTML
783
+ : null;
784
+ }
785
+
366
786
  return exportedCell;
367
787
  });
368
788
  }
369
789
  }
370
790
 
371
- export const Notebook = call_with_optional_props(UINotebook)
791
+ export const Notebook = call_with_optional_props(UINotebook);