@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zikojs/notebook",
3
- "version": "0.10.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": "latest",
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": "latest"
56
+ "ziko": "2.0.0-beta.17.1"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -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
- languageFor(cellData.type)
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(
@@ -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
- scope || Object.create(null);
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
- transformImportsAndScope(
59
- compiledCode,
60
- importMap,
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 () => {
@@ -1,77 +1,110 @@
1
- export const resolveImport = (
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
- const resolved = plugin.resolveImport(
15
- source,
16
- );
30
+ if (typeof plugin.resolveImport !== "function") {
31
+ continue;
32
+ }
17
33
 
18
- if (resolved) {
19
- return resolved;
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
- if (
25
- source.startsWith("http://") ||
26
- source.startsWith("https://") ||
27
- source.startsWith("./") ||
28
- source.startsWith("../")
29
- ) {
30
- return source;
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 `https://esm.sh/${source}`;
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("${source}");`;
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.forEach((spec) => {
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: spec.imported.name,
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 `__scope__.${namespaceId} = await import("${source}");`;
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("${source}");`,
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
- namedImports.forEach(({ imported, local }) => {
117
+ for (const { imported, local } of namedImports) {
85
118
  lines.push(
86
119
  `__scope__.${local} = ` +
87
- `${tmp}.${imported} !== undefined ` +
88
- `? ${tmp}.${imported} ` +
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
- export const transformImportsAndScope = (
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.forEach((node) => {
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
- return;
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
- return;
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
- return;
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
- modifications.forEach((modification) => {
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
- modification.end,
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
- : Button({
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",
@@ -37,6 +37,8 @@ export function Header({ store, actions }) {
37
37
  addMdBtn.disabled = q.isRunning || maxReached;
38
38
  });
39
39
 
40
+ statusBadge.unmount()
41
+
40
42
  return div({ class: 'header' },
41
43
  // div({ class: 'brand' }, '</>', statusBadge),
42
44
  div({ class: 'toolbar' },
@@ -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 { UINotebook } from './notebook.js';
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.addCell(cellData.type, null, cellData.id),
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(targetId) {
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);
535
- }
536
- });
528
+ focusCell(id) {
529
+ const cells = this.store.get("cells");
530
+ const cell = cells.find((c) => c.id === id);
537
531
 
538
- const target = this.store
539
- .get("cells")
540
- .find((cell) => cell.id === targetId);
532
+ if (!cell) return;
541
533
 
542
- if (
543
- target &&
544
- target.type === "markdown" &&
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
- this.store.set("activeCellId", targetId);
540
+ if (cell.type === "markdown" && !cell.readonly) {
541
+ cell.isEditingMarkdown = true;
551
542
  }
552
543
 
553
- addCell(
554
- type = "code",
555
- initialCode = null,
556
- afterId = null,
557
- readonly = false
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
- return null;
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
- const newId = this.nextCellId++;
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
-
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
- if (afterId === null) {
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
- this.store.set("cells", cells);
626
- this._renderAllCells();
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
- }
627
+ this.store.set("cells", cells);
628
+ this._renderAllCells();
629
+ this.focusCell(newId);
634
630
 
635
- return newId;
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
- nextCell(currentId, defaultType) {
660
- const cells = this.store.get("cells");
661
- const index = cells.findIndex(
662
- (cell) => cell.id === currentId
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
- if (index === cells.length - 1) {
666
- this.addCell(defaultType, null, currentId);
667
- } else if (index >= 0) {
668
- this.focusCell(cells[index + 1].id);
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
- 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
- };
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
- if (inputs) {
777
- exportedCell.code = cell.code;
778
- }
778
+ if (inputs) {
779
+ exportedCell.code = cell._editor?.getValue() ?? cell.code;
780
+ }
779
781
 
780
- if (outputs) {
781
- exportedCell.outputHtml = cell.outputNode
782
- ? cell.outputNode.innerHTML
783
- : null;
784
- }
782
+ if (outputs) {
783
+ exportedCell.outputHtml = cell.outputNode
784
+ ? cell.outputNode.innerHTML
785
+ : cell._outputRef?.innerHTML ?? null;
786
+ }
785
787
 
786
- return exportedCell;
787
- });
788
- }
788
+ return exportedCell;
789
+ });
790
+ }
789
791
  }
790
792
 
791
793
  export const Notebook = call_with_optional_props(UINotebook);