@zikojs/notebook 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zikojs/notebook",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A lightweight, extensible JavaScript notebook component with Markdown, executable cells, persistent state, and pluggable runtimes.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -44,6 +44,7 @@
44
44
  "@codemirror/state": "^6.7.4",
45
45
  "@codemirror/view": "^6.43.11",
46
46
  "@zikojs/lucide": "^0.2.2",
47
+ "@zikojs/console": "latest",
47
48
  "acorn": "^8.18.0",
48
49
  "codemirror": "^6.0.2",
49
50
  "lucide": "^1.44.0",
@@ -52,7 +53,7 @@
52
53
  "remark-rehype": "^11.1.2",
53
54
  "vanjs-core": "^1.6.1",
54
55
  "zextra": "^1.15.0",
55
- "ziko": "^2.0.0-beta.16.0"
56
+ "ziko": "latest"
56
57
  },
57
58
  "scripts": {
58
59
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -64,11 +64,19 @@ export const evaluateCodeAsync = async (
64
64
  "TARGET",
65
65
  "__scope__",
66
66
  `
67
- return (async () => {
68
- with (__scope__) {
67
+ // Bind the active TARGET for this cell evaluation
68
+ if (typeof __Ziko__ !== "undefined" && __Ziko__.__Config__) {
69
+ __Ziko__.__Config__.default.target = TARGET;
70
+ __Ziko__.__Config__.default.autoMount = true;
71
+ }
72
+
73
+ return (async () => {
74
+ with (__scope__) {
75
+ return await (async () => {
69
76
  ${compiledCode}
70
- }
71
- })();
77
+ })();
78
+ }
79
+ })();
72
80
  `,
73
81
  );
74
82
 
package/src/index.d.ts CHANGED
@@ -1,5 +1,97 @@
1
- import type { UIElement } from 'ziko/dom/UIElement'
2
- export declare class UINotebook extends UIElement{
3
1
 
2
+ import type { UIElement } from "ziko/dom/UIElement";
3
+
4
+ export type MaybePromise<T> = T | PromiseLike<T>;
5
+
6
+ export interface NotebookCell {
7
+ id?: number;
8
+ type?: "code" | "markdown";
9
+ code?: MaybePromise<string>;
10
+ readonly?: boolean;
11
+ runtime?: string | null;
12
+ isEditingMarkdown?: boolean;
13
+ execCount?: number | null;
14
+ outputNode?: HTMLElement | null;
15
+ hasDomOutput?: boolean;
16
+ }
17
+
18
+ export type NotebookCells = MaybePromise<NotebookCell[]>;
19
+
20
+ export interface NotebookOptions {
21
+ /**
22
+ * Initial cells, or a Promise resolving to the cells.
23
+ */
24
+ cells?: NotebookCells;
25
+
26
+ importMap?: Record<string, string>;
27
+ runCells?: boolean;
28
+ minLines?: number;
29
+ maxCells?: number;
30
+ showHeader?: boolean;
31
+ codeMirrorConfig?: unknown[] | Record<string, unknown>;
32
+ codeMirrorPlugins?: unknown[];
33
+ runtimePlugins?: Array<{
34
+ name: string;
35
+ [key: string]: unknown;
36
+ }>;
37
+ remarkPlugins?: unknown[];
38
+ rehypePlugins?: unknown[];
39
+ }
40
+
41
+ export declare class UINotebook extends UIElement {
42
+ constructor(options?: NotebookOptions);
43
+
44
+ /**
45
+ * Resolves when initial cells and Promise-based cell code
46
+ * have finished loading.
47
+ *
48
+ * Rejects if the initial cells source fails or does not
49
+ * resolve to an array. Individual code-source failures are
50
+ * displayed in their respective cells.
51
+ */
52
+ readonly ready: Promise<this>;
53
+
54
+ readonly id: string;
55
+
56
+ importMap: Record<string, string>;
57
+ runtimePlugins: NotebookOptions["runtimePlugins"];
58
+ codeMirrorConfig: NotebookOptions["codeMirrorConfig"];
59
+ codeMirrorPlugins: unknown[];
60
+ remarkPlugins: unknown[];
61
+ rehypePlugins: unknown[];
62
+ minLines: number;
63
+ maxCells: number;
64
+
65
+ runCell(id: number): Promise<void>;
66
+ runAllCells(): Promise<void>;
67
+
68
+ addCell(
69
+ type?: "code" | "markdown",
70
+ initialCode?: MaybePromise<string> | null,
71
+ afterId?: number | null,
72
+ readonly?: boolean
73
+ ): number | null;
74
+
75
+ deleteCell(id: number): void;
76
+ setActiveCell(id: number): void;
77
+ focusCell(id: number): void;
78
+ clearOutputs(): void;
79
+ resetScope(): Record<string, unknown>;
80
+ getScope(): Record<string, unknown>;
81
+
82
+ getNotebookData(options?: {
83
+ inputs?: boolean;
84
+ outputs?: boolean;
85
+ }): Array<{
86
+ id: number;
87
+ type: "code" | "markdown";
88
+ readonly: boolean;
89
+ order: number | null;
90
+ code?: string;
91
+ outputHtml?: string | null;
92
+ }>;
4
93
  }
5
- export declare function Notebook() : UINotebook
94
+
95
+ export declare function Notebook(
96
+ options?: NotebookOptions
97
+ ): UINotebook;
package/src/notebook.js CHANGED
@@ -1,22 +1,35 @@
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 = {},
@@ -28,16 +41,20 @@ export class UINotebook extends UIElement{
28
41
  codeMirrorPlugins = [],
29
42
  runtimePlugins = [],
30
43
  remarkPlugins = [],
31
- rehypePlugins = []
32
-
44
+ rehypePlugins = [],
33
45
  } = {}) {
34
- super({ element : 'div'})
35
- this.setAttr({class : 'ziko-notebook'})
36
- this.id = crypto.randomUUID ? crypto.randomUUID() : 'nb_' + Math.random().toString(36).substring(2, 9);
37
-
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
+
38
54
  if (!window.__notebook_scope_map) {
39
55
  window.__notebook_scope_map = new Map();
40
56
  }
57
+
41
58
  this.resetScope();
42
59
 
43
60
  this.importMap = importMap;
@@ -49,157 +66,369 @@ export class UINotebook extends UIElement{
49
66
  this.minLines = minLines;
50
67
  this.maxCells = maxCells;
51
68
 
52
- const startingCells = maxCells !== Infinity ? initialCells.slice(0, maxCells) : initialCells;
53
- let nextCellId = startingCells.length ? Math.max(...startingCells.map(c => c.id || 0)) + 1 : 1;
69
+ this.nextCellId = 1;
54
70
 
55
71
  this.store = new NotebookStore({
56
- cells: startingCells.map(cell => ({
57
- id: cell.id || nextCellId++,
58
- type: cell.type || "code",
59
- code: cell.code || "",
60
- readonly: cell.readonly ?? false,
61
- runtime: cell.runtime || null,
62
- isEditingMarkdown: cell.readonly ? false : (cell.isEditingMarkdown ?? true),
63
- execCount: cell.execCount ?? null,
64
- outputNode: cell.outputNode || null,
65
- hasDomOutput: cell.hasDomOutput ?? false,
66
- _copiedFlag: false,
67
- _dom: null, _editor: null, _actions: null,
68
- _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: null
69
- })),
70
- activeCellId: startingCells.length ? startingCells[0].id : null,
72
+ cells: [],
73
+ activeCellId: null,
71
74
  executionCounter: 1,
72
- executionQueueState: { isRunning: false, currentIndex: 0, total: 0 },
73
- maxCells: this.maxCells
75
+ executionQueueState: {
76
+ isRunning: false,
77
+ currentIndex: 0,
78
+ total: 0,
79
+ },
80
+ maxCells: this.maxCells,
74
81
  });
75
82
 
76
- this.nextCellId = nextCellId;
77
-
78
83
  this.notebookGlobalShortcuts = {
79
- deleteActive: () => this.deleteCell(this.store.get('activeCellId')),
80
- addMdBelow: () => this.addCell("markdown", "", this.store.get('activeCellId')),
81
- 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")),
82
90
  };
83
91
 
84
92
  this.cellsContainer = div({ class: "cells-container" }).element;
93
+
85
94
  this.header = Header({
86
95
  store: this.store,
87
96
  actions: {
88
97
  runAll: () => this.runAllCells(),
89
98
  clearOutputs: () => this.clearOutputs(),
90
- addCode: () => this.addCell("code", "", this.store.get('activeCellId')),
91
- addMarkdown: () => this.addCell("markdown", "", this.store.get('activeCellId')),
92
- deleteActive: () => this.deleteCell(this.store.get('activeCellId')),
93
- exportData: () => console.log("Notebook Data Export:", this.getNotebookData())
94
- }
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
+ },
95
108
  });
96
109
 
97
- if(!showHeader) this.header.style.display = 'none'
110
+ if (!showHeader) {
111
+ this.header.style.display = "none";
112
+ }
98
113
 
99
114
  this.append(
100
- this.header,
101
- div({ class: "notebook" },
102
- this.cellsContainer)
103
- )
104
-
105
- this._renderAllCells();
115
+ this.header,
116
+ div({ class: "notebook" }, this.cellsContainer)
117
+ );
106
118
 
107
- this.store.on('change:activeCellId', (newId, oldId) => {
119
+ this.store.on("change:activeCellId", (newId, oldId) => {
108
120
  if (oldId !== null) {
109
- const prev = this.store.get('cells').find(c => c.id === oldId);
110
- 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);
111
136
  }
112
- const next = this.store.get('cells').find(c => c.id === newId);
113
- if (next) this._refreshCell(next);
114
137
  });
115
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
+
116
149
  if (runCells) {
117
- setTimeout(() => { this.runAllCells(); }, 200);
150
+ this.ready
151
+ .then(() => this.runAllCells())
152
+ .catch((error) => {
153
+ console.error("Failed to initialize notebook:", error);
154
+ });
118
155
  }
119
156
  }
120
157
 
121
- _isMaxCellsReached() { return this.store.get('cells').length >= this.maxCells; }
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;
302
+ }
303
+ }
304
+
305
+ _isMaxCellsReached() {
306
+ return this.store.get("cells").length >= this.maxCells;
307
+ }
122
308
 
123
309
  _cellActions(cellData) {
124
310
  const app = this;
311
+
125
312
  return {
126
313
  setActive: () => app.setActiveCell(cellData.id),
314
+
127
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
+
128
323
  const code = cellData._editor.getValue();
129
324
  cellData.code = code;
130
325
 
131
326
  if (cellData.type === "markdown") {
132
327
  cellData.isEditingMarkdown = false;
133
- if (autoNext) app.nextCell(cellData.id, "markdown");
134
- else app._refreshCell(cellData);
328
+
329
+ if (autoNext) {
330
+ app.nextCell(cellData.id, "markdown");
331
+ } else {
332
+ app._refreshCell(cellData);
333
+ }
334
+
135
335
  return;
136
336
  }
137
337
 
138
338
  if (!code.trim()) return;
139
339
 
140
- cellData._outputRef.innerHTML = "";
340
+ cellData._outputRef.replaceChildren();
341
+
141
342
  let hasError = false;
142
343
 
143
344
  try {
144
345
  const scope = app.getScope();
346
+
145
347
  await evaluateCodeAsync(
146
348
  code,
147
349
  cellData._outputRef,
148
350
  {
149
351
  importMap: app.importMap,
150
352
  plugins: app.runtimePlugins,
151
- runtime: cellData.runtime // <-- Passed dynamically from the cell configuration
353
+ runtime: cellData.runtime,
152
354
  },
153
- scope,
355
+ scope
154
356
  );
155
- } catch (err) {
357
+ } catch (error) {
156
358
  hasError = true;
157
- ErrorOutput(err).mount(cellData._outputRef);
359
+ ErrorOutput(error).mount(cellData._outputRef);
158
360
  }
159
361
 
160
- const counter = this.store.get('executionCounter');
161
- this.store.set('executionCounter', counter + 1);
362
+ const counter = app.store.get("executionCounter");
363
+
364
+ app.store.set("executionCounter", counter + 1);
162
365
  cellData.execCount = counter;
163
- cellData.hasDomOutput = cellData._outputRef.childNodes.length > 0 || hasError;
366
+ cellData.hasDomOutput =
367
+ cellData._outputRef.childNodes.length > 0 || hasError;
164
368
 
165
- const container = div({ class: "output-area" }).element;
166
- while (cellData._outputRef.firstChild) container.appendChild(cellData._outputRef.firstChild);
167
- cellData.outputNode = container;
168
- 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
+ }
169
374
 
170
- if (autoNext && !hasError) app.nextCell(cellData.id, "code");
171
375
  app._refreshCell(cellData);
172
376
  },
377
+
173
378
  copyCode: async () => {
174
- const textToCopy = cellData._editor.getValue() || cellData.code;
379
+ const textToCopy =
380
+ cellData._editor.getValue() || cellData.code;
381
+
175
382
  try {
176
383
  await navigator.clipboard.writeText(textToCopy);
177
- } catch (err) {
178
- const temporary = tags.textarea(textToCopy).mount(document.body)
179
- temporary.element.select()
384
+ } catch {
385
+ const temporary = tags.textarea(textToCopy).mount(
386
+ document.body
387
+ );
388
+
389
+ temporary.element.select();
180
390
  document.execCommand("copy");
181
- temporary.unmount()
391
+ temporary.unmount();
182
392
  }
183
393
  },
394
+
184
395
  toggleReadonly: () => {
185
396
  cellData.readonly = !cellData.readonly;
186
- 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
+
187
405
  app._refreshCell(cellData);
188
406
  },
407
+
189
408
  toggleType: () => {
190
409
  if (cellData.readonly) return;
191
- cellData.type = cellData.type === "code" ? "markdown" : "code";
410
+
411
+ cellData.type =
412
+ cellData.type === "code" ? "markdown" : "code";
413
+
192
414
  cellData.isEditingMarkdown = true;
193
415
  cellData.hasDomOutput = false;
416
+
194
417
  app._refreshCell(cellData);
195
418
  },
419
+
196
420
  editMarkdown: () => {
197
421
  if (cellData.readonly) return;
422
+
198
423
  cellData.isEditingMarkdown = true;
199
424
  app._refreshCell(cellData);
200
425
  },
201
- addBelow: () => app.addCell(cellData.type, null, cellData.id),
426
+
427
+ addBelow: () =>
428
+ app.addCell(cellData.type, null, cellData.id),
429
+
202
430
  deleteCell: () => app.deleteCell(cellData.id),
431
+
203
432
  setRuntime: (newRuntime) => {
204
433
  cellData.runtime = newRuntime;
205
434
  app._refreshCell(cellData);
@@ -208,195 +437,355 @@ export class UINotebook extends UIElement{
208
437
  }
209
438
 
210
439
  _buildCellDom(cellData) {
211
- return CellItem({ cellData, app: this, store: this.store });
440
+ return CellItem({
441
+ cellData,
442
+ app: this,
443
+ store: this.store,
444
+ });
212
445
  }
213
446
 
214
447
  _refreshCell(cellData) {
215
- const activeId = this.store.get('activeCellId');
448
+ if (!cellData._dom) return;
449
+
450
+ const activeId = this.store.get("activeCellId");
216
451
  const isActive = activeId === cellData.id;
217
- cellData._dom.className = `cell ${isActive ? "active" : ""} ${cellData.readonly ? "readonly" : ""}`.trim();
218
452
 
219
- const countText = cellData.execCount ? `[${cellData.execCount}]` : "[ ]";
220
- const isMarkdownView = cellData.type === "markdown" && !cellData.isEditingMarkdown;
453
+ cellData._dom.className =
454
+ `cell ${isActive ? "active" : ""} ${
455
+ cellData.readonly ? "readonly" : ""
456
+ }`.trim();
221
457
 
222
- cellData._inPrompt.textContent = cellData.type === "code" ? `In ${countText}:` : "";
458
+ const countText = cellData.execCount
459
+ ? `[${cellData.execCount}]`
460
+ : "[ ]";
461
+
462
+ const isMarkdownView =
463
+ cellData.type === "markdown" &&
464
+ !cellData.isEditingMarkdown;
465
+
466
+ cellData._inPrompt.textContent =
467
+ cellData.type === "code" ? `In ${countText}:` : "";
223
468
 
224
469
  cellData._editor.sync(isActive, isMarkdownView);
470
+
225
471
  cellData._bodySlot.replaceChildren(
226
- isMarkdownView ? MarkdownView({
227
- cellData,
228
- actions: cellData._actions,
229
- remarkPlugins : this.remarkPlugins,
230
- rehypePlugins : this.rehypePlugins
231
- }) : 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
232
480
  );
233
481
 
234
- const showOutput = cellData.type === "code" && cellData.hasDomOutput;
235
- 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
+
236
488
  cellData._outPrompt.textContent = `Out ${countText}:`;
237
489
  }
238
490
 
239
491
  _renderAllCells() {
240
- const cells = this.store.get('cells');
241
- 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
+ );
242
499
  }
243
500
 
244
501
  setActiveCell(id) {
245
- const cells = this.store.get('cells');
246
- const target = cells.find(c => c.id === id);
502
+ const cells = this.store.get("cells");
503
+ const target = cells.find((cell) => cell.id === id);
247
504
 
248
- if (!target) return;
505
+ if (!target) return;
249
506
 
250
- const currentActiveId = this.store.get('activeCellId');
507
+ const currentActiveId = this.store.get("activeCellId");
251
508
 
252
- if (currentActiveId === id) {
253
- return;
254
- }
509
+ if (currentActiveId === id) return;
255
510
 
256
- // Only one cell can be active.
257
- cells.forEach(cell => {
258
- if (cell.id !== id && cell.type === "markdown") {
259
- cell.isEditingMarkdown = false;
260
- }
261
- });
511
+ cells.forEach((cell) => {
512
+ if (cell.id !== id && cell.type === "markdown") {
513
+ cell.isEditingMarkdown = false;
514
+ }
515
+ });
262
516
 
263
- this.store.set('activeCellId', id);
517
+ this.store.set("activeCellId", id);
264
518
 
265
- // Refresh every cell so the previous active cell loses "active".
266
- cells.forEach(cell => {
267
- if (cell._dom) {
268
- this._refreshCell(cell);
269
- }
270
- });
271
- }
519
+ cells.forEach((cell) => {
520
+ if (cell._dom) {
521
+ this._refreshCell(cell);
522
+ }
523
+ });
524
+ }
272
525
 
273
526
  focusCell(targetId) {
274
- // Close editing on other markdown cells before focusing
275
- this.store.get('cells').forEach(c => {
276
- if (c.type === "markdown" && c.isEditingMarkdown && c.id !== targetId) {
277
- c.isEditingMarkdown = false;
278
- 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);
279
535
  }
280
536
  });
281
537
 
282
- const target = this.store.get('cells').find(c => c.id === targetId);
283
- 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
+ ) {
284
547
  target.isEditingMarkdown = true;
285
548
  }
286
- this.store.set('activeCellId', targetId);
549
+
550
+ this.store.set("activeCellId", targetId);
287
551
  }
288
552
 
289
- addCell(type = "code", initialCode = null, afterId = null, readonly = false) {
290
- 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
+
291
561
  if (cells.length >= this.maxCells) {
292
- 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
+
293
566
  return null;
294
567
  }
295
568
 
296
569
  const newId = this.nextCellId++;
297
- 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
+
298
583
  const newCell = {
299
- id: newId, type, code: initialCode !== null ? initialCode : defaultText, readonly,
300
- isEditingMarkdown: !readonly && type === "markdown" && initialCode === 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,
301
599
  runtime: null,
302
- execCount: null, outputNode: null, hasDomOutput: false, _copiedFlag: false,
303
- _dom: null, _editor: null, _actions: null,
304
- _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: 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,
305
613
  };
306
614
 
307
615
  if (afterId === null) {
308
616
  cells.push(newCell);
309
617
  } else {
310
- const index = cells.findIndex(c => c.id === afterId);
618
+ const index = cells.findIndex(
619
+ (cell) => cell.id === afterId
620
+ );
621
+
311
622
  cells.splice(index + 1, 0, newCell);
312
623
  }
313
624
 
314
- this.store.set('cells', cells);
625
+ this.store.set("cells", cells);
315
626
  this._renderAllCells();
316
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
+
317
635
  return newId;
318
636
  }
319
637
 
320
638
  deleteCell(id) {
321
- const cells = this.store.get('cells');
639
+ const cells = this.store.get("cells");
640
+
322
641
  if (cells.length <= 1) return;
323
- const index = cells.findIndex(c => c.id === id);
324
- const updatedCells = cells.filter(c => c.id !== id);
325
-
326
- 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);
327
649
  this._renderAllCells();
328
- 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
+
329
656
  this.focusCell(updatedCells[nextActiveIndex].id);
330
657
  }
331
658
 
332
659
  nextCell(currentId, defaultType) {
333
- const cells = this.store.get('cells');
334
- const idx = cells.findIndex(c => c.id === currentId);
335
- if (idx === cells.length - 1) this.addCell(defaultType, null, currentId);
336
- 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
+ }
337
670
  }
338
671
 
339
672
  async runCell(id) {
340
- const cell = this.store.get('cells').find(c => c.id === id);
341
- 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
+ }
342
682
  }
343
683
 
344
684
  async runAllCells() {
345
- const cells = this.store.get('cells');
346
- 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
+
347
692
  if (codeCells.length === 0) return;
348
693
 
349
- 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
+ }
350
705
 
351
- for (const cell of cells) {
352
- if (cell.type === "code" && cell._actions) {
353
- this.store.set('executionQueueState', {
354
- isRunning: true,
355
- currentIndex: codeCells.indexOf(cell),
356
- total: codeCells.length
706
+ this.store.set("executionQueueState", {
707
+ isRunning: true,
708
+ currentIndex: codeCells.indexOf(cell),
709
+ total: codeCells.length,
357
710
  });
711
+
358
712
  await cell._actions.runCode(false);
359
713
  }
714
+ } finally {
715
+ this.store.set("executionQueueState", {
716
+ isRunning: false,
717
+ currentIndex: 0,
718
+ total: 0,
719
+ });
360
720
  }
361
-
362
- this.store.set('executionQueueState', { isRunning: false, currentIndex: 0, total: 0 });
363
721
  }
364
722
 
365
723
  clearOutputs() {
366
- this.store.get('cells').forEach(cell => {
724
+ this.store.get("cells").forEach((cell) => {
367
725
  cell.execCount = null;
368
726
  cell.outputNode = null;
369
727
  cell.hasDomOutput = false;
370
- 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);
371
738
  });
372
- this.store.set('executionCounter', 1);
373
- this.store.get('cells').forEach(c => this._refreshCell(c));
374
739
  }
375
740
 
376
741
  resetScope() {
377
742
  if (!window.__notebook_scope_map) {
378
743
  window.__notebook_scope_map = new Map();
379
744
  }
745
+
380
746
  const newScope = Object.create(null);
747
+
381
748
  window.__notebook_scope_map.set(this.id, newScope);
749
+
382
750
  return newScope;
383
751
  }
384
752
 
385
753
  getScope() {
386
- 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
+ ) {
387
758
  return this.resetScope();
388
759
  }
760
+
389
761
  return window.__notebook_scope_map.get(this.id);
390
762
  }
391
763
 
392
- getNotebookData({ inputs = true, outputs = true } = {}) {
393
- return this.store.get('cells').map(cell => {
394
- const exportedCell = { id: cell.id, type: cell.type, readonly: cell.readonly, order: cell.execCount };
395
- if (inputs) exportedCell.code = cell.code;
396
- 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
+
397
786
  return exportedCell;
398
787
  });
399
788
  }
400
789
  }
401
790
 
402
- export const Notebook = call_with_optional_props(UINotebook)
791
+ export const Notebook = call_with_optional_props(UINotebook);