@zikojs/notebook 0.8.0 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zikojs/notebook",
3
- "version": "0.8.0",
4
- "description": "",
3
+ "version": "0.9.0",
4
+ "description": "A lightweight, extensible JavaScript notebook component with Markdown, executable cells, persistent state, and pluggable runtimes.",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -1,6 +1,4 @@
1
- import {
2
- transformImportsAndScope,
3
- } from "./transform.js";
1
+ import { transformImportsAndScope } from "./transform.js";
4
2
 
5
3
  export const evaluateCodeAsync = async (
6
4
  code,
@@ -8,6 +6,7 @@ export const evaluateCodeAsync = async (
8
6
  {
9
7
  importMap = {},
10
8
  plugins = [],
9
+ runtime = null,
11
10
  } = {},
12
11
  scope,
13
12
  ) => {
@@ -19,11 +18,34 @@ export const evaluateCodeAsync = async (
19
18
  scope: validScope,
20
19
  importMap,
21
20
  plugins,
21
+ runtime,
22
22
  };
23
23
 
24
+ const selectedPlugin = plugins.find(
25
+ p => p.name === runtime,
26
+ );
27
+
28
+ // Runtime-specific evaluator
29
+ if (
30
+ selectedPlugin &&
31
+ selectedPlugin.evaluate
32
+ ) {
33
+ return await selectedPlugin.evaluate(
34
+ code,
35
+ context,
36
+ );
37
+ }
38
+
24
39
  let compiledCode = code;
25
40
 
26
41
  for (const plugin of plugins) {
42
+ if (
43
+ selectedPlugin &&
44
+ plugin.name !== runtime
45
+ ) {
46
+ continue;
47
+ }
48
+
27
49
  if (plugin.transform) {
28
50
  compiledCode = await plugin.transform(
29
51
  compiledCode,
@@ -32,10 +54,11 @@ export const evaluateCodeAsync = async (
32
54
  }
33
55
  }
34
56
 
35
- compiledCode = transformImportsAndScope(
36
- compiledCode,
37
- importMap,
38
- );
57
+ compiledCode =
58
+ transformImportsAndScope(
59
+ compiledCode,
60
+ importMap,
61
+ );
39
62
 
40
63
  const runCell = new Function(
41
64
  "TARGET",
@@ -55,6 +78,13 @@ export const evaluateCodeAsync = async (
55
78
  );
56
79
 
57
80
  for (const plugin of plugins) {
81
+ if (
82
+ selectedPlugin &&
83
+ plugin.name !== runtime
84
+ ) {
85
+ continue;
86
+ }
87
+
58
88
  if (plugin.afterEvaluate) {
59
89
  result = await plugin.afterEvaluate(
60
90
  result,
@@ -29,13 +29,21 @@ export function CellItem({ cellData, app, store }) {
29
29
  const actions = app._cellActions(cellData);
30
30
  cellData._actions = actions;
31
31
  cellData._editor = createCodeEditor(app, cellData, actions);
32
+
32
33
  /* Check ziko */
33
- const [In, setIn] = useState(0)
34
- globalThis.setIn = setIn
34
+ const [In, setIn] = useState(0);
35
+ globalThis.setIn = setIn;
35
36
  const inPrompt = span({ class: "prompt" }, In);
36
37
  const outPrompt = span({ class: "prompt out" });
37
- const bodySlot = div().style({ display : 'contents'});
38
- const controlsEl = CellControls({ cellData, store, actions });
38
+ const bodySlot = div().style({ display: 'contents' });
39
+
40
+ // Pass runtimePlugins from app into CellControls
41
+ const controlsEl = CellControls({
42
+ cellData,
43
+ store,
44
+ actions,
45
+ runtimePlugins: app.runtimePlugins
46
+ });
39
47
 
40
48
  const outputGrid = div({ class: "cell-output-grid" }, outPrompt, div({ class: "output-wrapper" }, outputRef));
41
49
  const inputGrid = div({ class: "cell-input-grid" }, inPrompt, bodySlot, controlsEl);
@@ -47,13 +55,12 @@ export function CellItem({ cellData, app, store }) {
47
55
  cellData._outPrompt = outPrompt.element;
48
56
  cellData._bodySlot = bodySlot.element;
49
57
  cellData._outputGrid = outputGrid.element;
50
- cellData._setIn = setIn
58
+ cellData._setIn = setIn;
51
59
 
52
60
  app._refreshCell(cellData);
53
61
  return root.element;
54
62
  }
55
-
56
- export function CellControls({ cellData, store, actions }) {
63
+ export function CellControls({ cellData, store, actions, runtimePlugins = [] }) {
57
64
  const controlsContainer = div({ class: "cell-controls" }).element;
58
65
 
59
66
  const renderButtons = () => {
@@ -61,7 +68,38 @@ export function CellControls({ cellData, store, actions }) {
61
68
  const isMaxCellsReached = store.get('cells').length >= store.get('maxCells');
62
69
  const isCopied = cellData._copiedFlag === true;
63
70
 
64
- controlsContainer.replaceChildren(
71
+ // Build the runtime selector dropdown for code cells using native DOM options
72
+ let runtimeSelectEl = null;
73
+ if (cellData.type === "code") {
74
+ const selectElement = document.createElement("select");
75
+ selectElement.className = "runtime-select";
76
+ selectElement.title = "Select Cell Runtime";
77
+ selectElement.disabled = cellData.readonly;
78
+
79
+ selectElement.onchange = (e) => {
80
+ actions.setRuntime(e.target.value === "" ? null : e.target.value);
81
+ };
82
+
83
+ // Default JS option
84
+ const optJs = document.createElement("option");
85
+ optJs.value = "";
86
+ optJs.textContent = "JS";
87
+ selectElement.appendChild(optJs);
88
+
89
+ // Runtime plugin options
90
+ for (const p of runtimePlugins) {
91
+ const opt = document.createElement("option");
92
+ opt.value = p.name;
93
+ opt.textContent = p.name.charAt(0).toUpperCase() + p.name.slice(1);
94
+ selectElement.appendChild(opt);
95
+ }
96
+
97
+ // Explicitly set the active value
98
+ selectElement.value = cellData.runtime || "";
99
+ runtimeSelectEl = selectElement;
100
+ }
101
+
102
+ const children = [
65
103
  Button({
66
104
  class: `btn-icon ${isCopied ? "btn-active" : ""}`,
67
105
  title: isCopied ? "Copied!" : "Copy Cell Content",
@@ -117,7 +155,13 @@ export function CellControls({ cellData, store, actions }) {
117
155
  disabled: isQueueRunning,
118
156
  onclick: actions.deleteCell
119
157
  }, Trash2())
120
- );
158
+ ];
159
+
160
+ if (runtimeSelectEl) {
161
+ children.unshift(runtimeSelectEl);
162
+ }
163
+
164
+ controlsContainer.replaceChildren(...children);
121
165
  };
122
166
 
123
167
  renderButtons();
@@ -1,4 +1,4 @@
1
- import van from "vanjs-core";
1
+ import van from 'vanjs-core';
2
2
 
3
3
  import {
4
4
  FileText,
@@ -16,20 +16,20 @@ import { tags } from 'ziko/dom'
16
16
 
17
17
  const { div, span } = tags
18
18
 
19
- export const Button = ({ class: className = "", title = "", disabled = false, onclick }, ...children) =>
19
+ export const Button = ({ class: className = '', title = '', disabled = false, onclick }, ...children) =>
20
20
  button({ class: `btn ${className}`, title, disabled, onclick }, ...children);
21
21
 
22
22
  export function Header({ store, actions }) {
23
- const statusBadge = span({ class: "status-badge" }, Loader2(), span('Loading ...'));
24
- const runAllBtn = Button({ class: "btn-primary", onclick: actions.runAll }, PlayForward(), "Run All");
25
- const clearBtn = Button({ onclick: actions.clearOutputs }, Eraser(), "Clear Outputs");
26
- const addCodeBtn = Button({ onclick: actions.addCode }, Plus(), "Add Code");
27
- const addMdBtn = Button({ onclick: actions.addMarkdown }, FileText(), "Add Markdown");
28
- const deleteActiveBtn = Button({ onclick: actions.deleteActive }, Trash2(), "Delete Active");
29
- const exportBtn = Button({ onclick: actions.exportData }, Download(), "Export Data");
23
+ const statusBadge = span({ class: 'status-badge' }, Loader2(), span('Loading ...'));
24
+ const runAllBtn = Button({ class: 'btn-primary', onclick: actions.runAll, title : 'Run All' }, PlayForward());
25
+ const clearBtn = Button({ onclick: actions.clearOutputs, title : 'Clear Outputs' }, Eraser());
26
+ const addCodeBtn = Button({ onclick: actions.addCode, title : 'Add Code'}, Plus());
27
+ const addMdBtn = Button({ onclick: actions.addMarkdown, title : 'Add Markdown' }, FileText());
28
+ const deleteActiveBtn = Button({ onclick: actions.deleteActive, title : 'Delete Active cell' }, Trash2());
29
+ const exportBtn = Button({ onclick: actions.exportData, title : 'Export Data' }, Download());
30
30
 
31
31
  store.on('change:executionQueueState', (q) => {
32
- statusBadge.style({display :q.isRunning ? "inline-flex" : "none" })
32
+ statusBadge.style({display :q.isRunning ? 'inline-flex' : 'none' })
33
33
  statusBadge.element.lastChild.textContent = `Executing Queue (${q.currentIndex + 1}/${q.total})`;
34
34
  [runAllBtn, clearBtn, deleteActiveBtn].forEach(b => { b.disabled = q.isRunning; });
35
35
  const maxReached = store.get('cells').length >= store.get('maxCells');
@@ -37,9 +37,9 @@ export function Header({ store, actions }) {
37
37
  addMdBtn.disabled = q.isRunning || maxReached;
38
38
  });
39
39
 
40
- return div({ class: "header" },
41
- div({ class: "brand" }, "</>", statusBadge),
42
- div({ class: "toolbar" },
40
+ return div({ class: 'header' },
41
+ // div({ class: 'brand' }, '</>', statusBadge),
42
+ div({ class: 'toolbar' },
43
43
  runAllBtn, clearBtn, addCodeBtn, addMdBtn, deleteActiveBtn, exportBtn
44
44
  )
45
45
  ).element;
@@ -2,27 +2,132 @@ import { remark } from "remark";
2
2
  import remarkRehype from "remark-rehype";
3
3
  import rehypeStringify from "rehype-stringify";
4
4
 
5
+ import { tags } from "ziko/dom";
5
6
 
6
- import { tags } from 'ziko/dom'
7
+ const { div } = tags;
7
8
 
8
- const { div } = tags
9
-
10
- export function MarkdownView({ cellData, actions }) {
9
+ export function MarkdownView({
10
+ cellData,
11
+ actions,
12
+ remarkPlugins = [],
13
+ rehypePlugins = [],
14
+ }) {
11
15
  const content = div();
12
-
13
- // Process Markdown asynchronously using Remark + Rehype
16
+
14
17
  remark()
18
+ .use(remarkPlugins)
15
19
  .use(remarkRehype, { allowDangerousHtml: true })
20
+ .use(rehypePlugins)
16
21
  .use(rehypeStringify)
17
22
  .process(cellData.code || "*Empty Markdown Cell*")
18
23
  .then((file) => {
19
24
  content.element.innerHTML = String(file);
20
25
  })
21
26
  .catch((err) => {
22
- content.element.innerHTML = `<div class="error-output">Markdown rendering error: ${err.message}</div>`;
27
+ content.element.innerHTML =
28
+ `<div class="error-output">Markdown rendering error: ${err.message}</div>`;
23
29
  });
24
-
25
- const wrap = div({ class: "markdown-rendered-cell markdown-body" }, content);
26
- wrap.onClick(() => actions.editMarkdown())
30
+
31
+ const wrap = div(
32
+ { class: "markdown-rendered-cell markdown-body" },
33
+ content
34
+ );
35
+
36
+ wrap.onClick(() => actions.editMarkdown());
37
+
27
38
  return wrap.element;
28
- }
39
+ }
40
+
41
+
42
+
43
+ // import { remark } from "remark";
44
+ // import remarkRehype from "remark-rehype";
45
+ // import rehypeStringify from "rehype-stringify";
46
+
47
+ // import { tags } from "ziko/dom";
48
+
49
+ // const { div } = tags;
50
+
51
+ // /**
52
+ // * Activate elements inserted through innerHTML.
53
+ // *
54
+ // * Scripts created by innerHTML are inert, so recreate them
55
+ // * as real DOM nodes. This also handles external scripts.
56
+ // */
57
+ // async function activateHTML(container) {
58
+ // const scripts = [
59
+ // ...container.querySelectorAll("script"),
60
+ // ];
61
+
62
+ // for (const oldScript of scripts) {
63
+ // const script = document.createElement("script");
64
+
65
+ // for (const attribute of oldScript.attributes) {
66
+ // script.setAttribute(
67
+ // attribute.name,
68
+ // attribute.value
69
+ // );
70
+ // }
71
+
72
+ // script.textContent = oldScript.textContent;
73
+
74
+ // oldScript.replaceWith(script);
75
+
76
+ // if (script.src) {
77
+ // await new Promise((resolve, reject) => {
78
+ // script.addEventListener(
79
+ // "load",
80
+ // resolve,
81
+ // { once: true }
82
+ // );
83
+
84
+ // script.addEventListener(
85
+ // "error",
86
+ // reject,
87
+ // { once: true }
88
+ // );
89
+ // });
90
+ // }
91
+ // }
92
+ // }
93
+
94
+ // export function MarkdownView({
95
+ // cellData,
96
+ // actions,
97
+ // remarkPlugins = [],
98
+ // rehypePlugins = [],
99
+ // }) {
100
+ // const content = div();
101
+
102
+ // remark()
103
+ // .use(remarkPlugins)
104
+ // .use(remarkRehype, {
105
+ // allowDangerousHtml: true,
106
+ // })
107
+ // .use(rehypePlugins)
108
+ // .use(rehypeStringify)
109
+ // .process(cellData.code || "*Empty Markdown Cell*")
110
+ // .then(async (file) => {
111
+ // content.element.innerHTML = String(file);
112
+
113
+ // await activateHTML(content.element);
114
+ // })
115
+ // .catch((err) => {
116
+ // content.element.innerHTML = `
117
+ // <div class="error-output">
118
+ // Markdown rendering error: ${err.message}
119
+ // </div>
120
+ // `;
121
+ // });
122
+
123
+ // const wrap = div(
124
+ // {
125
+ // class: "markdown-rendered-cell markdown-body",
126
+ // },
127
+ // content
128
+ // );
129
+
130
+ // wrap.onClick(() => actions.editMarkdown());
131
+
132
+ // return wrap.element;
133
+ // }
@@ -1,4 +1,21 @@
1
1
  @import url('./ErrorOutput/index.css');
2
2
  @import url('./Button/index.css');
3
3
  @import url('./Header/index.css');
4
- @import url('./MarkdownView//index.css');
4
+ @import url('./MarkdownView//index.css');
5
+
6
+ .ziko-notebook .runtime-select {
7
+ font-family: inherit;
8
+ font-size: 11px;
9
+ padding: 2px 4px;
10
+ border: 1px solid var(--border-color);
11
+ border-radius: 4px;
12
+ background: #ffffff;
13
+ color: #1e293b;
14
+ cursor: pointer;
15
+ height: 24px;
16
+ }
17
+
18
+ .ziko-notebook .runtime-select:focus {
19
+ outline: none;
20
+ border-color: var(--primary);
21
+ }
package/src/index.css CHANGED
@@ -141,7 +141,7 @@
141
141
 
142
142
  .ziko-notebook .toolbar {
143
143
  width: 100%;
144
- justify-content: flex-start;
144
+ justify-content: center;
145
145
  }
146
146
 
147
147
  .ziko-notebook .cell-input-grid {
package/src/notebook.js CHANGED
@@ -21,12 +21,15 @@ export class UINotebook extends UIElement{
21
21
  cells: initialCells = [],
22
22
  importMap = {},
23
23
  runCells = true,
24
- codeMirrorConfig = {},
25
- codeMirrorPlugins = [],
26
24
  minLines = 3,
27
25
  maxCells = Infinity,
28
26
  showHeader = true,
29
- runtimePlugins = []
27
+ codeMirrorConfig = {},
28
+ codeMirrorPlugins = [],
29
+ runtimePlugins = [],
30
+ remarkPlugins = [],
31
+ rehypePlugins = []
32
+
30
33
  } = {}) {
31
34
  super({ element : 'div'})
32
35
  this.setAttr({class : 'ziko-notebook'})
@@ -41,6 +44,8 @@ export class UINotebook extends UIElement{
41
44
  this.runtimePlugins = runtimePlugins;
42
45
  this.codeMirrorConfig = codeMirrorConfig;
43
46
  this.codeMirrorPlugins = codeMirrorPlugins;
47
+ this.remarkPlugins = remarkPlugins;
48
+ this.rehypePlugins = rehypePlugins;
44
49
  this.minLines = minLines;
45
50
  this.maxCells = maxCells;
46
51
 
@@ -53,6 +58,7 @@ export class UINotebook extends UIElement{
53
58
  type: cell.type || "code",
54
59
  code: cell.code || "",
55
60
  readonly: cell.readonly ?? false,
61
+ runtime: cell.runtime || null,
56
62
  isEditingMarkdown: cell.readonly ? false : (cell.isEditingMarkdown ?? true),
57
63
  execCount: cell.execCount ?? null,
58
64
  outputNode: cell.outputNode || null,
@@ -142,12 +148,13 @@ export class UINotebook extends UIElement{
142
148
  {
143
149
  importMap: app.importMap,
144
150
  plugins: app.runtimePlugins,
151
+ runtime: cellData.runtime // <-- Passed dynamically from the cell configuration
145
152
  },
146
153
  scope,
147
154
  );
148
155
  } catch (err) {
149
156
  hasError = true;
150
- ErrorOutput(err).mount(cellData._outputRef)
157
+ ErrorOutput(err).mount(cellData._outputRef);
151
158
  }
152
159
 
153
160
  const counter = this.store.get('executionCounter');
@@ -192,7 +199,11 @@ export class UINotebook extends UIElement{
192
199
  app._refreshCell(cellData);
193
200
  },
194
201
  addBelow: () => app.addCell(cellData.type, null, cellData.id),
195
- deleteCell: () => app.deleteCell(cellData.id)
202
+ deleteCell: () => app.deleteCell(cellData.id),
203
+ setRuntime: (newRuntime) => {
204
+ cellData.runtime = newRuntime;
205
+ app._refreshCell(cellData);
206
+ },
196
207
  };
197
208
  }
198
209
 
@@ -212,7 +223,12 @@ export class UINotebook extends UIElement{
212
223
 
213
224
  cellData._editor.sync(isActive, isMarkdownView);
214
225
  cellData._bodySlot.replaceChildren(
215
- 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
216
232
  );
217
233
 
218
234
  const showOutput = cellData.type === "code" && cellData.hasDomOutput;
@@ -226,20 +242,34 @@ export class UINotebook extends UIElement{
226
242
  }
227
243
 
228
244
  setActiveCell(id) {
229
- const currentActiveId = this.store.get('activeCellId');
230
- if (currentActiveId === id) return;
245
+ const cells = this.store.get('cells');
246
+ const target = cells.find(c => c.id === id);
231
247
 
232
- // Check all cells and close editing mode for any markdown cells that are losing focus
233
- this.store.get('cells').forEach(c => {
234
- if (c.type === "markdown" && c.isEditingMarkdown) {
235
- c.isEditingMarkdown = false;
236
- this._refreshCell(c);
237
- }
238
- });
248
+ if (!target) return;
249
+
250
+ const currentActiveId = this.store.get('activeCellId');
239
251
 
240
- this.store.set('activeCellId', id);
252
+ if (currentActiveId === id) {
253
+ return;
241
254
  }
242
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
+
243
273
  focusCell(targetId) {
244
274
  // Close editing on other markdown cells before focusing
245
275
  this.store.get('cells').forEach(c => {
@@ -268,6 +298,7 @@ export class UINotebook extends UIElement{
268
298
  const newCell = {
269
299
  id: newId, type, code: initialCode !== null ? initialCode : defaultText, readonly,
270
300
  isEditingMarkdown: !readonly && type === "markdown" && initialCode === null,
301
+ runtime: null,
271
302
  execCount: null, outputNode: null, hasDomOutput: false, _copiedFlag: false,
272
303
  _dom: null, _editor: null, _actions: null,
273
304
  _inPrompt: null, _outPrompt: null, _bodySlot: null, _outputGrid: null, _outputRef: null