@zikojs/notebook 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/notebook.js CHANGED
@@ -1,20 +1,35 @@
1
1
  import { NotebookStore } from "./store.js";
2
- import { Header, CellItem, MarkdownView } from "./components.js";
3
- import { evaluateCodeAsync } from "./transform.js";
2
+ import {
3
+ Header,
4
+ CellItem,
5
+ MarkdownView,
6
+ ErrorOutput
7
+ } from "./components/index.js";
8
+ import { evaluateCodeAsync } from "./compiler/index.js";
4
9
  import { tags, UIElement } from "ziko/dom";
5
10
  import { call_with_optional_props } from 'ziko/dom/internal-utils'
6
-
11
+ import { useState, useDerived } from "ziko/hooks";
7
12
  const { div } = tags
13
+ // const [st, setSt] = useState('hh')
14
+ // globalThis.mm = useDerived((n)=>n, [st])
15
+ // globalThis.pp = tags.p({}, mm).mount(document.body)
16
+ // globalThis.st = st
17
+ // globalThis.setSt = setSt
18
+
8
19
  export class UINotebook extends UIElement{
9
20
  constructor({
10
21
  cells: initialCells = [],
11
22
  importMap = {},
12
23
  runCells = true,
13
- codeMirrorConfig = {},
14
- codeMirrorPlugins = [],
15
24
  minLines = 3,
16
25
  maxCells = Infinity,
17
- showHeader = true
26
+ showHeader = true,
27
+ codeMirrorConfig = {},
28
+ codeMirrorPlugins = [],
29
+ runtimePlugins = [],
30
+ remarkPlugins = [],
31
+ rehypePlugins = []
32
+
18
33
  } = {}) {
19
34
  super({ element : 'div'})
20
35
  this.setAttr({class : 'ziko-notebook'})
@@ -26,8 +41,11 @@ export class UINotebook extends UIElement{
26
41
  this.resetScope();
27
42
 
28
43
  this.importMap = importMap;
44
+ this.runtimePlugins = runtimePlugins;
29
45
  this.codeMirrorConfig = codeMirrorConfig;
30
46
  this.codeMirrorPlugins = codeMirrorPlugins;
47
+ this.remarkPlugins = remarkPlugins;
48
+ this.rehypePlugins = rehypePlugins;
31
49
  this.minLines = minLines;
32
50
  this.maxCells = maxCells;
33
51
 
@@ -40,6 +58,7 @@ export class UINotebook extends UIElement{
40
58
  type: cell.type || "code",
41
59
  code: cell.code || "",
42
60
  readonly: cell.readonly ?? false,
61
+ runtime: cell.runtime || null,
43
62
  isEditingMarkdown: cell.readonly ? false : (cell.isEditingMarkdown ?? true),
44
63
  execCount: cell.execCount ?? null,
45
64
  outputNode: cell.outputNode || null,
@@ -123,10 +142,19 @@ export class UINotebook extends UIElement{
123
142
 
124
143
  try {
125
144
  const scope = app.getScope();
126
- await evaluateCodeAsync(code, cellData._outputRef, app.importMap, scope);
145
+ await evaluateCodeAsync(
146
+ code,
147
+ cellData._outputRef,
148
+ {
149
+ importMap: app.importMap,
150
+ plugins: app.runtimePlugins,
151
+ runtime: cellData.runtime // <-- Passed dynamically from the cell configuration
152
+ },
153
+ scope,
154
+ );
127
155
  } catch (err) {
128
156
  hasError = true;
129
- div({ class: "error-output" }, err.toString()).mount(cellData._outputRef)
157
+ ErrorOutput(err).mount(cellData._outputRef);
130
158
  }
131
159
 
132
160
  const counter = this.store.get('executionCounter');
@@ -140,19 +168,17 @@ export class UINotebook extends UIElement{
140
168
  cellData._outputRef.appendChild(container);
141
169
 
142
170
  if (autoNext && !hasError) app.nextCell(cellData.id, "code");
143
- else app._refreshCell(cellData);
171
+ app._refreshCell(cellData);
144
172
  },
145
173
  copyCode: async () => {
146
174
  const textToCopy = cellData._editor.getValue() || cellData.code;
147
175
  try {
148
176
  await navigator.clipboard.writeText(textToCopy);
149
177
  } catch (err) {
150
- const textArea = document.createElement("textarea");
151
- textArea.value = textToCopy;
152
- document.body.appendChild(textArea);
153
- textArea.select();
178
+ const temporary = tags.textarea(textToCopy).mount(document.body)
179
+ temporary.element.select()
154
180
  document.execCommand("copy");
155
- document.body.removeChild(textArea);
181
+ temporary.unmount()
156
182
  }
157
183
  },
158
184
  toggleReadonly: () => {
@@ -173,7 +199,11 @@ export class UINotebook extends UIElement{
173
199
  app._refreshCell(cellData);
174
200
  },
175
201
  addBelow: () => app.addCell(cellData.type, null, cellData.id),
176
- deleteCell: () => app.deleteCell(cellData.id)
202
+ deleteCell: () => app.deleteCell(cellData.id),
203
+ setRuntime: (newRuntime) => {
204
+ cellData.runtime = newRuntime;
205
+ app._refreshCell(cellData);
206
+ },
177
207
  };
178
208
  }
179
209
 
@@ -193,7 +223,12 @@ export class UINotebook extends UIElement{
193
223
 
194
224
  cellData._editor.sync(isActive, isMarkdownView);
195
225
  cellData._bodySlot.replaceChildren(
196
- isMarkdownView ? MarkdownView({ cellData, actions: cellData._actions }) : cellData._editor.dom
226
+ isMarkdownView ? MarkdownView({
227
+ cellData,
228
+ actions: cellData._actions,
229
+ remarkPlugins : this.remarkPlugins,
230
+ rehypePlugins : this.rehypePlugins
231
+ }) : cellData._editor.dom
197
232
  );
198
233
 
199
234
  const showOutput = cellData.type === "code" && cellData.hasDomOutput;
@@ -207,20 +242,34 @@ export class UINotebook extends UIElement{
207
242
  }
208
243
 
209
244
  setActiveCell(id) {
210
- const currentActiveId = this.store.get('activeCellId');
211
- if (currentActiveId === id) return;
245
+ const cells = this.store.get('cells');
246
+ const target = cells.find(c => c.id === id);
212
247
 
213
- // Check all cells and close editing mode for any markdown cells that are losing focus
214
- this.store.get('cells').forEach(c => {
215
- if (c.type === "markdown" && c.isEditingMarkdown) {
216
- c.isEditingMarkdown = false;
217
- this._refreshCell(c);
218
- }
219
- });
248
+ if (!target) return;
249
+
250
+ const currentActiveId = this.store.get('activeCellId');
220
251
 
221
- this.store.set('activeCellId', id);
252
+ if (currentActiveId === id) {
253
+ return;
222
254
  }
223
255
 
256
+ // Only one cell can be active.
257
+ cells.forEach(cell => {
258
+ if (cell.id !== id && cell.type === "markdown") {
259
+ cell.isEditingMarkdown = false;
260
+ }
261
+ });
262
+
263
+ this.store.set('activeCellId', id);
264
+
265
+ // Refresh every cell so the previous active cell loses "active".
266
+ cells.forEach(cell => {
267
+ if (cell._dom) {
268
+ this._refreshCell(cell);
269
+ }
270
+ });
271
+ }
272
+
224
273
  focusCell(targetId) {
225
274
  // Close editing on other markdown cells before focusing
226
275
  this.store.get('cells').forEach(c => {
@@ -249,6 +298,7 @@ export class UINotebook extends UIElement{
249
298
  const newCell = {
250
299
  id: newId, type, code: initialCode !== null ? initialCode : defaultText, readonly,
251
300
  isEditingMarkdown: !readonly && type === "markdown" && initialCode === null,
301
+ runtime: null,
252
302
  execCount: null, outputNode: null, hasDomOutput: false, _copiedFlag: false,
253
303
  _dom: null, _editor: null, _actions: null,
254
304
  _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: null
package/src/transform.js DELETED
@@ -1,116 +0,0 @@
1
- import * as acorn from "acorn";
2
-
3
- export const rewriteImportNode = (node, code, importMapConfig) => {
4
- let source = node.source.value;
5
- if (importMapConfig && importMapConfig[source]) {
6
- source = importMapConfig[source];
7
- } else if (!source.startsWith("http://") && !source.startsWith("https://") && !source.startsWith("./") && !source.startsWith("../")) {
8
- source = `https://esm.sh/${source}`;
9
- }
10
-
11
- if (node.specifiers.length === 0) return `await import("${source}");`;
12
-
13
- let defaultImportName = null;
14
- let namespaceId = null;
15
- let namedImports = [];
16
-
17
- node.specifiers.forEach(spec => {
18
- if (spec.type === "ImportNamespaceSpecifier") namespaceId = spec.local.name;
19
- else if (spec.type === "ImportDefaultSpecifier") defaultImportName = spec.local.name;
20
- else if (spec.type === "ImportSpecifier") namedImports.push({ imported: spec.imported.name, local: spec.local.name });
21
- });
22
-
23
- if (namespaceId) return `__scope__.${namespaceId} = await import("${source}");`;
24
-
25
- const tmp = `__mod_${node.start}`;
26
- let lines = [`const ${tmp} = await import("${source}");`];
27
-
28
- if (defaultImportName && namedImports.length === 0) {
29
- lines.push(`__scope__.${defaultImportName} = ${tmp}.default ?? ${tmp};`);
30
- return lines.join("\n");
31
- }
32
-
33
- if (defaultImportName) lines.push(`__scope__.${defaultImportName} = ${tmp}.default ?? ${tmp};`);
34
- namedImports.forEach(({ imported, local }) => {
35
- lines.push(`__scope__.${local} = ${tmp}.${imported} !== undefined ? ${tmp}.${imported} : (${tmp}.default ?? ${tmp});`);
36
- });
37
-
38
- return lines.join("\n");
39
- };
40
-
41
- export const transformImportsAndScope = (code, importMapConfig) => {
42
- try {
43
- const ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
44
- let modifications = [];
45
-
46
- ast.body.forEach(node => {
47
- if (node.type === "ImportDeclaration") {
48
- modifications.push({ start: node.start, end: node.end, replacement: rewriteImportNode(node, code, importMapConfig) });
49
- } else if (node.type === "VariableDeclaration") {
50
- let replacementCode = "";
51
- node.declarations.forEach(decl => {
52
- if (decl.id.type === "Identifier") {
53
- const varName = decl.id.name;
54
- const initCode = decl.init ? code.slice(decl.init.start, decl.init.end) : "undefined";
55
- replacementCode += `__scope__.${varName} = ${initCode};\n`;
56
- } else if (decl.id.type === "ObjectPattern") {
57
- const initCode = decl.init ? code.slice(decl.init.start, decl.init.end) : "undefined";
58
- const tempVar = `__destruct_obj_${decl.start}`;
59
- replacementCode += `(() => { const ${tempVar} = ${initCode}; `;
60
- decl.id.properties.forEach(prop => {
61
- if (prop.value && prop.value.type === "Identifier") {
62
- const localName = prop.value.name;
63
- const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.value;
64
- replacementCode += `__scope__.${localName} = ${tempVar}.${keyName}; `;
65
- }
66
- });
67
- replacementCode += `})();\n`;
68
- } else if (decl.id.type === "ArrayPattern") {
69
- const initCode = decl.init ? code.slice(decl.init.start, decl.init.end) : "undefined";
70
- const tempVar = `__destruct_arr_${decl.start}`;
71
- replacementCode += `(() => { const ${tempVar} = ${initCode}; `;
72
- decl.id.elements.forEach((elem, index) => {
73
- if (elem && elem.type === "Identifier") {
74
- const localName = elem.name;
75
- replacementCode += `__scope__.${localName} = ${tempVar}[${index}]; `;
76
- }
77
- });
78
- replacementCode += `})();\n`;
79
- }
80
- });
81
- modifications.push({ start: node.start, end: node.end, replacement: replacementCode });
82
- } else if (node.type === "FunctionDeclaration" && node.id) {
83
- const funcName = node.id.name;
84
- const funcBody = code.slice(node.start, node.end);
85
- modifications.push({ start: node.start, end: node.end, replacement: `__scope__.${funcName} = ${funcBody.replace(/^function\s+\w+/, "function")}` });
86
- } else if (node.type === "ClassDeclaration" && node.id) {
87
- const className = node.id.name;
88
- const classBody = code.slice(node.start, node.end);
89
- modifications.push({ start: node.start, end: node.end, replacement: `__scope__.${className} = ${classBody.replace(/^class\s+\w+/, "class")}` });
90
- }
91
- });
92
-
93
- modifications.sort((a, b) => b.start - a.start);
94
- let transformedCode = code;
95
- modifications.forEach(mod => { transformedCode = transformedCode.slice(0, mod.start) + mod.replacement + transformedCode.slice(mod.end); });
96
- return transformedCode;
97
- } catch (e) {
98
- if (code.trim().startsWith("import ") || code.includes("\nimport ")) throw new SyntaxError("Invalid code module layout syntax.");
99
- return code;
100
- }
101
- };
102
-
103
- export const evaluateCodeAsync = async (code, TARGET, importMapConfig, scope) => {
104
- const validScope = scope || Object.create(null);
105
- const compiledCode = transformImportsAndScope(code, importMapConfig);
106
-
107
- const runCell = new Function("TARGET", "__scope__", `
108
- return (async () => {
109
- with (__scope__) {
110
- ${compiledCode}
111
- }
112
- })();
113
- `);
114
-
115
- const result = await runCell(TARGET, validScope);
116
- };
File without changes
File without changes