@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 +3 -2
- package/src/compiler/evaluate.js +12 -4
- package/src/index.d.ts +95 -3
- package/src/notebook.js +565 -176
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zikojs/notebook",
|
|
3
|
-
"version": "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": "
|
|
56
|
+
"ziko": "latest"
|
|
56
57
|
},
|
|
57
58
|
"scripts": {
|
|
58
59
|
"test": "echo \"Error: no test specified\" && exit 1",
|
package/src/compiler/evaluate.js
CHANGED
|
@@ -64,11 +64,19 @@ export const evaluateCodeAsync = async (
|
|
|
64
64
|
"TARGET",
|
|
65
65
|
"__scope__",
|
|
66
66
|
`
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
const { div } = tags
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
this.
|
|
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
|
-
|
|
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:
|
|
57
|
-
|
|
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: {
|
|
73
|
-
|
|
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: () =>
|
|
80
|
-
|
|
81
|
-
|
|
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: () =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
110
|
+
if (!showHeader) {
|
|
111
|
+
this.header.style.display = "none";
|
|
112
|
+
}
|
|
98
113
|
|
|
99
114
|
this.append(
|
|
100
|
-
this.header,
|
|
101
|
-
div({ class: "notebook" },
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
this._renderAllCells();
|
|
115
|
+
this.header,
|
|
116
|
+
div({ class: "notebook" }, this.cellsContainer)
|
|
117
|
+
);
|
|
106
118
|
|
|
107
|
-
this.store.on(
|
|
119
|
+
this.store.on("change:activeCellId", (newId, oldId) => {
|
|
108
120
|
if (oldId !== null) {
|
|
109
|
-
const
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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.
|
|
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
|
|
353
|
+
runtime: cellData.runtime,
|
|
152
354
|
},
|
|
153
|
-
scope
|
|
355
|
+
scope
|
|
154
356
|
);
|
|
155
|
-
} catch (
|
|
357
|
+
} catch (error) {
|
|
156
358
|
hasError = true;
|
|
157
|
-
ErrorOutput(
|
|
359
|
+
ErrorOutput(error).mount(cellData._outputRef);
|
|
158
360
|
}
|
|
159
361
|
|
|
160
|
-
const counter =
|
|
161
|
-
|
|
362
|
+
const counter = app.store.get("executionCounter");
|
|
363
|
+
|
|
364
|
+
app.store.set("executionCounter", counter + 1);
|
|
162
365
|
cellData.execCount = counter;
|
|
163
|
-
cellData.hasDomOutput =
|
|
366
|
+
cellData.hasDomOutput =
|
|
367
|
+
cellData._outputRef.childNodes.length > 0 || hasError;
|
|
164
368
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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 =
|
|
379
|
+
const textToCopy =
|
|
380
|
+
cellData._editor.getValue() || cellData.code;
|
|
381
|
+
|
|
175
382
|
try {
|
|
176
383
|
await navigator.clipboard.writeText(textToCopy);
|
|
177
|
-
} catch
|
|
178
|
-
const temporary = tags.textarea(textToCopy).mount(
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
440
|
+
return CellItem({
|
|
441
|
+
cellData,
|
|
442
|
+
app: this,
|
|
443
|
+
store: this.store,
|
|
444
|
+
});
|
|
212
445
|
}
|
|
213
446
|
|
|
214
447
|
_refreshCell(cellData) {
|
|
215
|
-
|
|
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
|
-
|
|
220
|
-
|
|
453
|
+
cellData._dom.className =
|
|
454
|
+
`cell ${isActive ? "active" : ""} ${
|
|
455
|
+
cellData.readonly ? "readonly" : ""
|
|
456
|
+
}`.trim();
|
|
221
457
|
|
|
222
|
-
|
|
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
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 =
|
|
235
|
-
|
|
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(
|
|
241
|
-
|
|
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
|
-
|
|
246
|
-
|
|
502
|
+
const cells = this.store.get("cells");
|
|
503
|
+
const target = cells.find((cell) => cell.id === id);
|
|
247
504
|
|
|
248
|
-
|
|
505
|
+
if (!target) return;
|
|
249
506
|
|
|
250
|
-
|
|
507
|
+
const currentActiveId = this.store.get("activeCellId");
|
|
251
508
|
|
|
252
|
-
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
509
|
+
if (currentActiveId === id) return;
|
|
255
510
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
});
|
|
511
|
+
cells.forEach((cell) => {
|
|
512
|
+
if (cell.id !== id && cell.type === "markdown") {
|
|
513
|
+
cell.isEditingMarkdown = false;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
262
516
|
|
|
263
|
-
|
|
517
|
+
this.store.set("activeCellId", id);
|
|
264
518
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
283
|
-
|
|
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
|
-
|
|
549
|
+
|
|
550
|
+
this.store.set("activeCellId", targetId);
|
|
287
551
|
}
|
|
288
552
|
|
|
289
|
-
addCell(
|
|
290
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
300
|
-
|
|
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,
|
|
303
|
-
|
|
304
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
639
|
+
const cells = this.store.get("cells");
|
|
640
|
+
|
|
322
641
|
if (cells.length <= 1) return;
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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(
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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(
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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(
|
|
724
|
+
this.store.get("cells").forEach((cell) => {
|
|
367
725
|
cell.execCount = null;
|
|
368
726
|
cell.outputNode = null;
|
|
369
727
|
cell.hasDomOutput = false;
|
|
370
|
-
|
|
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 (
|
|
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({
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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);
|