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