@zikojs/notebook 0.10.0 → 0.12.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 -3
- package/src/code-editor/index.js +32 -7
- package/src/compiler/evaluate.js +23 -7
- package/src/compiler/imports.js +61 -28
- package/src/compiler/transform.js +19 -34
- package/src/components/CellItem/index.js +11 -2
- package/src/components/Header/index.js +2 -0
- package/src/components/icons/index.js +2 -0
- package/src/main.js +1 -1
- package/src/notebook.js +130 -128
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zikojs/notebook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,7 +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": "
|
|
47
|
+
"@zikojs/console": "^0.8.0",
|
|
48
48
|
"acorn": "^8.18.0",
|
|
49
49
|
"codemirror": "^6.0.2",
|
|
50
50
|
"lucide": "^1.44.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"remark-rehype": "^11.1.2",
|
|
54
54
|
"vanjs-core": "^1.6.1",
|
|
55
55
|
"zextra": "^1.15.0",
|
|
56
|
-
"ziko": "
|
|
56
|
+
"ziko": "2.0.0-beta.17.1"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"test": "echo \"Error: no test specified\" && exit 1",
|
package/src/code-editor/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EditorView, keymap, lineNumbers, highlightActiveLine } from "@codemirror/view";
|
|
2
2
|
import { EditorState, Compartment } from "@codemirror/state";
|
|
3
|
-
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
3
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
|
4
4
|
import { bracketMatching, indentUnit, syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language";
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete";
|
|
6
6
|
import { javascript } from "@codemirror/lang-javascript";
|
|
@@ -9,13 +9,28 @@ import { tags } from "ziko/dom";
|
|
|
9
9
|
|
|
10
10
|
const { div } = tags;
|
|
11
11
|
|
|
12
|
-
const languageFor = (type) =>
|
|
13
|
-
type === "markdown" ? markdown() : javascript();
|
|
14
12
|
|
|
13
|
+
const getRuntimePlugin = (runtime, runtimePlugins = []) =>
|
|
14
|
+
runtimePlugins.find((plugin) => plugin?.name === runtime);
|
|
15
|
+
|
|
16
|
+
const languageFor = (type, runtime, runtimePlugins = []) => {
|
|
17
|
+
if (type === "markdown") return markdown();
|
|
18
|
+
|
|
19
|
+
const plugin = getRuntimePlugin(runtime, runtimePlugins);
|
|
20
|
+
|
|
21
|
+
return plugin?.language ?? javascript();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const themeFor = (runtime, runtimePlugins = []) => {
|
|
25
|
+
const plugin = getRuntimePlugin(runtime, runtimePlugins);
|
|
26
|
+
|
|
27
|
+
return plugin?.theme ?? [];
|
|
28
|
+
};
|
|
15
29
|
export function createCodeEditor(app, cellData, actions) {
|
|
16
30
|
const editorDom = div({ class: "input-wrapper" });
|
|
17
31
|
|
|
18
32
|
const languageCompartment = new Compartment();
|
|
33
|
+
const themeCompartment = new Compartment();
|
|
19
34
|
const readOnlyCompartment = new Compartment();
|
|
20
35
|
const editableCompartment = new Compartment();
|
|
21
36
|
|
|
@@ -84,10 +99,12 @@ export function createCodeEditor(app, cellData, actions) {
|
|
|
84
99
|
return true;
|
|
85
100
|
}
|
|
86
101
|
},
|
|
102
|
+
indentWithTab,
|
|
87
103
|
...buildPluginKeymap(),
|
|
88
104
|
...closeBracketsKeymap,
|
|
89
105
|
...defaultKeymap,
|
|
90
|
-
...historyKeymap
|
|
106
|
+
...historyKeymap,
|
|
107
|
+
|
|
91
108
|
]);
|
|
92
109
|
|
|
93
110
|
const extraExtensions = Array.isArray(app.codeMirrorConfig)
|
|
@@ -114,8 +131,12 @@ export function createCodeEditor(app, cellData, actions) {
|
|
|
114
131
|
EditorState.tabSize.of(2),
|
|
115
132
|
|
|
116
133
|
languageCompartment.of(
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
languageFor(cellData.type, cellData.runtime, app.runtimePlugins)
|
|
135
|
+
),
|
|
136
|
+
|
|
137
|
+
themeCompartment.of(
|
|
138
|
+
themeFor(cellData.runtime, app.runtimePlugins)
|
|
139
|
+
),
|
|
119
140
|
|
|
120
141
|
readOnlyCompartment.of(
|
|
121
142
|
EditorState.readOnly.of(cellData.readonly)
|
|
@@ -174,7 +195,11 @@ export function createCodeEditor(app, cellData, actions) {
|
|
|
174
195
|
view.dispatch({
|
|
175
196
|
effects: [
|
|
176
197
|
languageCompartment.reconfigure(
|
|
177
|
-
languageFor(cellData.type)
|
|
198
|
+
languageFor(cellData.type, cellData.runtime, app.runtimePlugins)
|
|
199
|
+
),
|
|
200
|
+
|
|
201
|
+
themeCompartment.reconfigure(
|
|
202
|
+
themeFor(cellData.runtime, app.runtimePlugins)
|
|
178
203
|
),
|
|
179
204
|
|
|
180
205
|
readOnlyCompartment.reconfigure(
|
package/src/compiler/evaluate.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { transformImportsAndScope } from "./transform.js";
|
|
2
|
+
import { Console as _Console } from '@zikojs/console'
|
|
3
|
+
import { tags } from 'ziko/dom'
|
|
4
|
+
const { div } = tags
|
|
5
|
+
|
|
6
|
+
const Console = {
|
|
7
|
+
log : (...args) => div(_Console.log(...args)),
|
|
8
|
+
table : (...args) => div(_Console.table(...args)),
|
|
9
|
+
warn : (...args) => div(_Console.warn(...args)),
|
|
10
|
+
error : (...args) => div(_Console.error(...args))
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export const evaluateCodeAsync = async (
|
|
4
14
|
code,
|
|
@@ -10,8 +20,9 @@ export const evaluateCodeAsync = async (
|
|
|
10
20
|
} = {},
|
|
11
21
|
scope,
|
|
12
22
|
) => {
|
|
13
|
-
const validScope =
|
|
14
|
-
|
|
23
|
+
const validScope = scope || Object.create(null);
|
|
24
|
+
|
|
25
|
+
scope.console = Console
|
|
15
26
|
|
|
16
27
|
const context = {
|
|
17
28
|
TARGET,
|
|
@@ -54,11 +65,14 @@ export const evaluateCodeAsync = async (
|
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
compiledCode =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
compiledCode = await transformImportsAndScope(
|
|
69
|
+
compiledCode,
|
|
70
|
+
{
|
|
71
|
+
importMap,
|
|
72
|
+
plugins,
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
// console.log(compiledCode)
|
|
62
76
|
|
|
63
77
|
const runCell = new Function(
|
|
64
78
|
"TARGET",
|
|
@@ -70,6 +84,8 @@ export const evaluateCodeAsync = async (
|
|
|
70
84
|
__Ziko__.__Config__.default.autoMount = true;
|
|
71
85
|
}
|
|
72
86
|
|
|
87
|
+
|
|
88
|
+
|
|
73
89
|
return (async () => {
|
|
74
90
|
with (__scope__) {
|
|
75
91
|
return await (async () => {
|
package/src/compiler/imports.js
CHANGED
|
@@ -1,77 +1,110 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
const CDN = "https://esm.sh/";
|
|
3
|
+
|
|
4
|
+
const isURL = (source) =>
|
|
5
|
+
/^(?:https?:\/\/|data:|blob:)/i.test(source);
|
|
6
|
+
|
|
7
|
+
const isBareSpecifier = (source) =>
|
|
8
|
+
!source.startsWith(".") &&
|
|
9
|
+
!source.startsWith("/") &&
|
|
10
|
+
!source.startsWith("#") &&
|
|
11
|
+
!isURL(source);
|
|
12
|
+
|
|
13
|
+
export const resolveImport = async (
|
|
2
14
|
source,
|
|
3
15
|
{
|
|
4
16
|
importMap = {},
|
|
5
17
|
plugins = [],
|
|
18
|
+
resolverEndpoint = "/__notebook_resolve",
|
|
6
19
|
} = {},
|
|
7
20
|
) => {
|
|
8
21
|
if (importMap[source]) {
|
|
9
22
|
return importMap[source];
|
|
10
23
|
}
|
|
11
24
|
|
|
25
|
+
if (!isBareSpecifier(source)) {
|
|
26
|
+
return source;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
for (const plugin of plugins) {
|
|
13
|
-
if (plugin.resolveImport) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
);
|
|
30
|
+
if (typeof plugin.resolveImport !== "function") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
const resolved = await plugin.resolveImport(source);
|
|
35
|
+
|
|
36
|
+
if (typeof resolved === "string" && resolved) {
|
|
37
|
+
return resolved;
|
|
21
38
|
}
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
source
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
try {
|
|
42
|
+
const params = new URLSearchParams({ specifier: source });
|
|
43
|
+
|
|
44
|
+
const response = await fetch(
|
|
45
|
+
`${resolverEndpoint}?${params.toString()}`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
|
|
51
|
+
if (typeof result.url === "string" && result.url) {
|
|
52
|
+
return result.url;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// The Vite endpoint may not be available outside development.
|
|
31
57
|
}
|
|
32
58
|
|
|
33
|
-
return
|
|
59
|
+
return `${CDN}${source}`;
|
|
34
60
|
};
|
|
35
61
|
|
|
36
|
-
export const rewriteImportNode = (
|
|
62
|
+
export const rewriteImportNode = async (
|
|
37
63
|
node,
|
|
38
64
|
code,
|
|
39
65
|
context,
|
|
40
66
|
) => {
|
|
41
|
-
const source = resolveImport(
|
|
67
|
+
const source = await resolveImport(
|
|
42
68
|
node.source.value,
|
|
43
69
|
context,
|
|
44
70
|
);
|
|
45
71
|
|
|
72
|
+
// Safely quote the URL in generated JavaScript.
|
|
73
|
+
const url = JSON.stringify(source);
|
|
74
|
+
|
|
46
75
|
if (node.specifiers.length === 0) {
|
|
47
|
-
return `await import(
|
|
76
|
+
return `await import(${url});`;
|
|
48
77
|
}
|
|
49
78
|
|
|
50
79
|
let defaultImportName = null;
|
|
51
80
|
let namespaceId = null;
|
|
52
81
|
const namedImports = [];
|
|
53
82
|
|
|
54
|
-
node.specifiers
|
|
83
|
+
for (const spec of node.specifiers) {
|
|
55
84
|
if (spec.type === "ImportNamespaceSpecifier") {
|
|
56
85
|
namespaceId = spec.local.name;
|
|
57
86
|
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
58
87
|
defaultImportName = spec.local.name;
|
|
59
88
|
} else if (spec.type === "ImportSpecifier") {
|
|
60
89
|
namedImports.push({
|
|
61
|
-
imported:
|
|
90
|
+
imported:
|
|
91
|
+
spec.imported.name ?? spec.imported.value,
|
|
62
92
|
local: spec.local.name,
|
|
63
93
|
});
|
|
64
94
|
}
|
|
65
|
-
}
|
|
95
|
+
}
|
|
66
96
|
|
|
67
97
|
if (namespaceId) {
|
|
68
|
-
return
|
|
98
|
+
return (
|
|
99
|
+
`__scope__.${namespaceId} = ` +
|
|
100
|
+
`await import(${url});`
|
|
101
|
+
);
|
|
69
102
|
}
|
|
70
103
|
|
|
71
104
|
const tmp = `__mod_${node.start}`;
|
|
72
105
|
|
|
73
106
|
const lines = [
|
|
74
|
-
`const ${tmp} = await import(
|
|
107
|
+
`const ${tmp} = await import(${url});`,
|
|
75
108
|
];
|
|
76
109
|
|
|
77
110
|
if (defaultImportName) {
|
|
@@ -81,14 +114,14 @@ export const rewriteImportNode = (
|
|
|
81
114
|
);
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
|
|
117
|
+
for (const { imported, local } of namedImports) {
|
|
85
118
|
lines.push(
|
|
86
119
|
`__scope__.${local} = ` +
|
|
87
|
-
`${tmp}
|
|
88
|
-
`? ${tmp}
|
|
120
|
+
`${tmp}[${JSON.stringify(imported)}] !== undefined ` +
|
|
121
|
+
`? ${tmp}[${JSON.stringify(imported)}] ` +
|
|
89
122
|
`: (${tmp}.default ?? ${tmp});`,
|
|
90
123
|
);
|
|
91
|
-
}
|
|
124
|
+
}
|
|
92
125
|
|
|
93
126
|
return lines.join("\n");
|
|
94
127
|
};
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
rewriteClassDeclaration,
|
|
11
11
|
} from "./scope.js";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
export const transformImportsAndScope = async (
|
|
14
15
|
code,
|
|
15
16
|
{
|
|
16
17
|
importMap = {},
|
|
17
18
|
plugins = [],
|
|
19
|
+
resolverEndpoint,
|
|
18
20
|
} = {},
|
|
19
21
|
) => {
|
|
20
22
|
try {
|
|
@@ -27,36 +29,34 @@ export const transformImportsAndScope = (
|
|
|
27
29
|
const context = {
|
|
28
30
|
importMap,
|
|
29
31
|
plugins,
|
|
32
|
+
resolverEndpoint,
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
const modifications = [];
|
|
33
36
|
|
|
34
|
-
ast.body
|
|
37
|
+
for (const node of ast.body) {
|
|
35
38
|
if (node.type === "ImportDeclaration") {
|
|
36
39
|
modifications.push({
|
|
37
40
|
start: node.start,
|
|
38
41
|
end: node.end,
|
|
39
|
-
replacement: rewriteImportNode(
|
|
42
|
+
replacement: await rewriteImportNode(
|
|
40
43
|
node,
|
|
41
44
|
code,
|
|
42
45
|
context,
|
|
43
46
|
),
|
|
44
47
|
});
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
continue;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
if (node.type === "VariableDeclaration") {
|
|
50
53
|
modifications.push({
|
|
51
54
|
start: node.start,
|
|
52
55
|
end: node.end,
|
|
53
|
-
replacement: rewriteVariableDeclaration(
|
|
54
|
-
node,
|
|
55
|
-
code,
|
|
56
|
-
),
|
|
56
|
+
replacement: rewriteVariableDeclaration(node, code),
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
continue;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (
|
|
@@ -66,13 +66,10 @@ export const transformImportsAndScope = (
|
|
|
66
66
|
modifications.push({
|
|
67
67
|
start: node.start,
|
|
68
68
|
end: node.end,
|
|
69
|
-
replacement: rewriteFunctionDeclaration(
|
|
70
|
-
node,
|
|
71
|
-
code,
|
|
72
|
-
),
|
|
69
|
+
replacement: rewriteFunctionDeclaration(node, code),
|
|
73
70
|
});
|
|
74
71
|
|
|
75
|
-
|
|
72
|
+
continue;
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
if (
|
|
@@ -82,31 +79,21 @@ export const transformImportsAndScope = (
|
|
|
82
79
|
modifications.push({
|
|
83
80
|
start: node.start,
|
|
84
81
|
end: node.end,
|
|
85
|
-
replacement: rewriteClassDeclaration(
|
|
86
|
-
node,
|
|
87
|
-
code,
|
|
88
|
-
),
|
|
82
|
+
replacement: rewriteClassDeclaration(node, code),
|
|
89
83
|
});
|
|
90
84
|
}
|
|
91
|
-
}
|
|
85
|
+
}
|
|
92
86
|
|
|
93
|
-
modifications.sort(
|
|
94
|
-
(a, b) => b.start - a.start,
|
|
95
|
-
);
|
|
87
|
+
modifications.sort((a, b) => b.start - a.start);
|
|
96
88
|
|
|
97
89
|
let transformedCode = code;
|
|
98
90
|
|
|
99
|
-
|
|
91
|
+
for (const modification of modifications) {
|
|
100
92
|
transformedCode =
|
|
101
|
-
transformedCode.slice(
|
|
102
|
-
0,
|
|
103
|
-
modification.start,
|
|
104
|
-
) +
|
|
93
|
+
transformedCode.slice(0, modification.start) +
|
|
105
94
|
modification.replacement +
|
|
106
|
-
transformedCode.slice(
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
});
|
|
95
|
+
transformedCode.slice(modification.end);
|
|
96
|
+
}
|
|
110
97
|
|
|
111
98
|
return transformedCode;
|
|
112
99
|
} catch (error) {
|
|
@@ -116,9 +103,7 @@ export const transformImportsAndScope = (
|
|
|
116
103
|
) {
|
|
117
104
|
throw new SyntaxError(
|
|
118
105
|
"Invalid code module layout syntax.",
|
|
119
|
-
{
|
|
120
|
-
cause: error,
|
|
121
|
-
},
|
|
106
|
+
{ cause: error },
|
|
122
107
|
);
|
|
123
108
|
}
|
|
124
109
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
FileText,
|
|
11
11
|
Pencil,
|
|
12
12
|
Play,
|
|
13
|
+
Refresh,
|
|
13
14
|
Plus,
|
|
14
15
|
Trash2,
|
|
15
16
|
} from '../icons/index.js';
|
|
@@ -135,12 +136,20 @@ export function CellControls({ cellData, store, actions, runtimePlugins = [] })
|
|
|
135
136
|
disabled: isQueueRunning || cellData.readonly,
|
|
136
137
|
onclick: actions.editMarkdown
|
|
137
138
|
}, Pencil())
|
|
138
|
-
:
|
|
139
|
+
: div(
|
|
140
|
+
Button({
|
|
141
|
+
class: "btn-icon",
|
|
142
|
+
title: "Run (Shift+Enter)",
|
|
143
|
+
disabled: isQueueRunning,
|
|
144
|
+
onclick: () => actions.runCode(false)
|
|
145
|
+
}, Refresh()),
|
|
146
|
+
Button({
|
|
139
147
|
class: "btn-icon btn-primary",
|
|
140
148
|
title: "Run & Advance (Shift+Enter)",
|
|
141
149
|
disabled: isQueueRunning,
|
|
142
150
|
onclick: () => actions.runCode(true)
|
|
143
|
-
}, Play())
|
|
151
|
+
}, Play())
|
|
152
|
+
).style({ display : 'contents'}).element,
|
|
144
153
|
|
|
145
154
|
Button({
|
|
146
155
|
class: "btn-icon",
|
|
@@ -6,6 +6,7 @@ import _Code2 from '@zikojs/lucide/Code2'
|
|
|
6
6
|
import _FileText from '@zikojs/lucide/FileText'
|
|
7
7
|
import _Pencil from '@zikojs/lucide/Pencil'
|
|
8
8
|
import _Play from '@zikojs/lucide/Play'
|
|
9
|
+
import _Refresh from '@zikojs/lucide/RefreshCcw'
|
|
9
10
|
import _Plus from '@zikojs/lucide/Plus'
|
|
10
11
|
import _Trash2 from '@zikojs/lucide/Trash2'
|
|
11
12
|
import _PlayForward from '@zikojs/lucide/StepForward'
|
|
@@ -21,6 +22,7 @@ export const Code2 = () => _Code2({ class: 'icon', stroke : 'currentColor' }).el
|
|
|
21
22
|
export const FileText = () => _FileText({ class: 'icon', stroke : 'currentColor' }).element
|
|
22
23
|
export const Pencil = () => _Pencil({ class: 'icon', stroke : 'currentColor' }).element
|
|
23
24
|
export const Play = () => _Play({ class: 'icon', stroke : 'currentColor' }).element
|
|
25
|
+
export const Refresh = () => _Refresh({ class: 'icon', stroke : 'currentColor' }).element
|
|
24
26
|
export const Plus = () => _Plus({ class: 'icon', stroke : 'currentColor' }).element
|
|
25
27
|
export const Trash2 = () => _Trash2({ class: 'icon', stroke : 'currentColor' }).element
|
|
26
28
|
export const PlayForward = () => _PlayForward({ class: 'icon', stroke : 'currentColor' }).element
|
package/src/main.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './notebook.js';
|
package/src/notebook.js
CHANGED
|
@@ -424,8 +424,10 @@ export class UINotebook extends UIElement {
|
|
|
424
424
|
app._refreshCell(cellData);
|
|
425
425
|
},
|
|
426
426
|
|
|
427
|
-
addBelow: () =>
|
|
428
|
-
app.
|
|
427
|
+
addBelow: () => {
|
|
428
|
+
app.setActiveCell(cellData.id);
|
|
429
|
+
app.addCell(cellData.type, null, cellData.id);
|
|
430
|
+
},
|
|
429
431
|
|
|
430
432
|
deleteCell: () => app.deleteCell(cellData.id),
|
|
431
433
|
|
|
@@ -523,117 +525,111 @@ export class UINotebook extends UIElement {
|
|
|
523
525
|
});
|
|
524
526
|
}
|
|
525
527
|
|
|
526
|
-
focusCell(
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
cell.type === "markdown" &&
|
|
530
|
-
cell.isEditingMarkdown &&
|
|
531
|
-
cell.id !== targetId
|
|
532
|
-
) {
|
|
533
|
-
cell.isEditingMarkdown = false;
|
|
534
|
-
this._refreshCell(cell);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
528
|
+
focusCell(id) {
|
|
529
|
+
const cells = this.store.get("cells");
|
|
530
|
+
const cell = cells.find((c) => c.id === id);
|
|
537
531
|
|
|
538
|
-
|
|
539
|
-
.get("cells")
|
|
540
|
-
.find((cell) => cell.id === targetId);
|
|
532
|
+
if (!cell) return;
|
|
541
533
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
!target.readonly
|
|
546
|
-
) {
|
|
547
|
-
target.isEditingMarkdown = true;
|
|
534
|
+
for (const c of cells) {
|
|
535
|
+
if (c.type === "markdown" && c.id !== id) {
|
|
536
|
+
c.isEditingMarkdown = false;
|
|
548
537
|
}
|
|
538
|
+
}
|
|
549
539
|
|
|
550
|
-
|
|
540
|
+
if (cell.type === "markdown" && !cell.readonly) {
|
|
541
|
+
cell.isEditingMarkdown = true;
|
|
551
542
|
}
|
|
552
543
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const cells = [...this.store.get("cells")];
|
|
560
|
-
|
|
561
|
-
if (cells.length >= this.maxCells) {
|
|
562
|
-
console.warn(
|
|
563
|
-
`Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`
|
|
564
|
-
);
|
|
544
|
+
this.store.set("activeCellId", id);
|
|
545
|
+
this._refreshCell(cell);
|
|
546
|
+
|
|
547
|
+
// Scroll the target cell into view when it is outside the viewport.
|
|
548
|
+
requestAnimationFrame(() => {
|
|
549
|
+
const element = cell._dom;
|
|
565
550
|
|
|
566
|
-
|
|
551
|
+
if (!element?.isConnected) return;
|
|
552
|
+
|
|
553
|
+
const rect = element.getBoundingClientRect();
|
|
554
|
+
const viewportHeight = window.innerHeight;
|
|
555
|
+
|
|
556
|
+
if (rect.top < 0 || rect.bottom > viewportHeight) {
|
|
557
|
+
element.scrollIntoView({
|
|
558
|
+
behavior: "smooth",
|
|
559
|
+
block: rect.top < 0 ? "start" : "nearest",
|
|
560
|
+
});
|
|
567
561
|
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
568
564
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
583
|
-
const newCell = {
|
|
584
|
-
id: newId,
|
|
585
|
-
type,
|
|
586
|
-
code: pendingCode
|
|
587
|
-
? ""
|
|
588
|
-
: typeof source === "string"
|
|
589
|
-
? source
|
|
590
|
-
: "",
|
|
591
|
-
_codePromise: pendingCode,
|
|
592
|
-
_codeLoadError: null,
|
|
593
|
-
|
|
594
|
-
readonly,
|
|
595
|
-
isEditingMarkdown:
|
|
596
|
-
!readonly &&
|
|
597
|
-
type === "markdown" &&
|
|
598
|
-
initialCode === null,
|
|
599
|
-
runtime: null,
|
|
600
|
-
execCount: null,
|
|
601
|
-
outputNode: null,
|
|
602
|
-
hasDomOutput: false,
|
|
603
|
-
|
|
604
|
-
_copiedFlag: false,
|
|
605
|
-
_dom: null,
|
|
606
|
-
_editor: null,
|
|
607
|
-
_actions: null,
|
|
608
|
-
_inPrompt: null,
|
|
609
|
-
_outPrompt: null,
|
|
610
|
-
_bodySlot: null,
|
|
611
|
-
_outputGrid: null,
|
|
612
|
-
_outputRef: null,
|
|
613
|
-
};
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
addCell(type = "code", initialCode = null, afterId = null, readonly = false) {
|
|
568
|
+
const cells = [...this.store.get("cells")];
|
|
614
569
|
|
|
615
|
-
|
|
570
|
+
if (cells.length >= this.maxCells) {
|
|
571
|
+
console.warn(
|
|
572
|
+
`Cannot create cell: Maximum cell limit (${this.maxCells}) reached.`
|
|
573
|
+
);
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Prefer the cell associated with the clicked "+" button.
|
|
578
|
+
// Fall back to the last focused cell, then the last cell.
|
|
579
|
+
const sourceCell =
|
|
580
|
+
(afterId !== null
|
|
581
|
+
? cells.find((cell) => cell.id === afterId)
|
|
582
|
+
: null) ??
|
|
583
|
+
cells.find(
|
|
584
|
+
(cell) => cell.id === this.store.get("activeCellId")
|
|
585
|
+
) ??
|
|
586
|
+
cells.at(-1);
|
|
587
|
+
|
|
588
|
+
const newId = this.nextCellId++;
|
|
589
|
+
const defaultText =
|
|
590
|
+
type === "markdown"
|
|
591
|
+
? "### New Markdown Cell\nClick to edit..."
|
|
592
|
+
: "";
|
|
593
|
+
|
|
594
|
+
const newCell = {
|
|
595
|
+
id: newId,
|
|
596
|
+
type,
|
|
597
|
+
code: initialCode !== null ? initialCode : defaultText,
|
|
598
|
+
readonly,
|
|
599
|
+
isEditingMarkdown:
|
|
600
|
+
!readonly && type === "markdown" && initialCode === null,
|
|
601
|
+
runtime: sourceCell?.runtime ?? null,
|
|
602
|
+
execCount: null,
|
|
603
|
+
outputNode: null,
|
|
604
|
+
hasDomOutput: false,
|
|
605
|
+
_copiedFlag: false,
|
|
606
|
+
_dom: null,
|
|
607
|
+
_editor: null,
|
|
608
|
+
_actions: null,
|
|
609
|
+
_inPrompt: null,
|
|
610
|
+
_outPrompt: null,
|
|
611
|
+
_bodySlot: null,
|
|
612
|
+
_outputGrid: null,
|
|
613
|
+
_outputRef: null,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
if (afterId === null) {
|
|
617
|
+
cells.push(newCell);
|
|
618
|
+
} else {
|
|
619
|
+
const index = cells.findIndex((cell) => cell.id === afterId);
|
|
620
|
+
if (index === -1) {
|
|
616
621
|
cells.push(newCell);
|
|
617
622
|
} else {
|
|
618
|
-
const index = cells.findIndex(
|
|
619
|
-
(cell) => cell.id === afterId
|
|
620
|
-
);
|
|
621
|
-
|
|
622
623
|
cells.splice(index + 1, 0, newCell);
|
|
623
624
|
}
|
|
625
|
+
}
|
|
624
626
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
// Resolve newly added Promise-based code without blocking
|
|
630
|
-
// the synchronous addCell() API.
|
|
631
|
-
if (pendingCode) {
|
|
632
|
-
this._resolveCellCode(newCell);
|
|
633
|
-
}
|
|
627
|
+
this.store.set("cells", cells);
|
|
628
|
+
this._renderAllCells();
|
|
629
|
+
this.focusCell(newId);
|
|
634
630
|
|
|
635
|
-
|
|
636
|
-
|
|
631
|
+
return newId;
|
|
632
|
+
}
|
|
637
633
|
|
|
638
634
|
deleteCell(id) {
|
|
639
635
|
const cells = this.store.get("cells");
|
|
@@ -656,19 +652,26 @@ export class UINotebook extends UIElement {
|
|
|
656
652
|
this.focusCell(updatedCells[nextActiveIndex].id);
|
|
657
653
|
}
|
|
658
654
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
655
|
+
|
|
656
|
+
nextCell(currentId, defaultType) {
|
|
657
|
+
const cells = this.store.get("cells");
|
|
658
|
+
const index = cells.findIndex((cell) => cell.id === currentId);
|
|
659
|
+
|
|
660
|
+
if (index === -1) return;
|
|
664
661
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
662
|
+
if (index === cells.length - 1) {
|
|
663
|
+
const newId = this.addCell(defaultType, null, currentId);
|
|
664
|
+
|
|
665
|
+
if (newId !== null) {
|
|
666
|
+
this.focusCell(newId);
|
|
669
667
|
}
|
|
668
|
+
|
|
669
|
+
return;
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
this.focusCell(cells[index + 1].id);
|
|
673
|
+
}
|
|
674
|
+
|
|
672
675
|
async runCell(id) {
|
|
673
676
|
await this.ready;
|
|
674
677
|
|
|
@@ -761,31 +764,30 @@ export class UINotebook extends UIElement {
|
|
|
761
764
|
return window.__notebook_scope_map.get(this.id);
|
|
762
765
|
}
|
|
763
766
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
};
|
|
767
|
+
|
|
768
|
+
getNotebookData({ inputs = true, outputs = true } = {}) {
|
|
769
|
+
return this.store.get("cells").map((cell) => {
|
|
770
|
+
const exportedCell = {
|
|
771
|
+
id: cell.id,
|
|
772
|
+
type: cell.type,
|
|
773
|
+
readonly: cell.readonly,
|
|
774
|
+
runtime: cell.runtime ?? null,
|
|
775
|
+
order: cell.execCount,
|
|
776
|
+
};
|
|
775
777
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
778
|
+
if (inputs) {
|
|
779
|
+
exportedCell.code = cell._editor?.getValue() ?? cell.code;
|
|
780
|
+
}
|
|
779
781
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
782
|
+
if (outputs) {
|
|
783
|
+
exportedCell.outputHtml = cell.outputNode
|
|
784
|
+
? cell.outputNode.innerHTML
|
|
785
|
+
: cell._outputRef?.innerHTML ?? null;
|
|
786
|
+
}
|
|
785
787
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
return exportedCell;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
789
791
|
}
|
|
790
792
|
|
|
791
793
|
export const Notebook = call_with_optional_props(UINotebook);
|