@zikojs/notebook 0.11.0 → 0.13.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.11.0",
3
+ "version": "0.13.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)
@@ -166,62 +187,51 @@ export function createCodeEditor(app, cellData, actions) {
166
187
  ? view.state.doc.toString()
167
188
  : cellData.code,
168
189
 
169
-
170
- sync: (isActive, isMarkdownView) => {
171
- if (!isMarkdownView) {
172
- if (!view) {
173
- requestAnimationFrame(() => {
174
- initEditor();
175
-
176
- // The editor may only become available after initialization.
177
- // Check the current active cell, not a potentially stale
178
- // isActive value captured by this callback.
179
- if (
180
- app.store.get("activeCellId") === cellData.id &&
181
- !cellData.readonly &&
182
- view
183
- ) {
184
- view.focus();
185
- }
186
- });
187
- } else {
188
- view.dispatch({
189
- effects: [
190
- languageCompartment.reconfigure(
191
- languageFor(cellData.type)
192
- ),
193
- readOnlyCompartment.reconfigure(
194
- EditorState.readOnly.of(cellData.readonly)
195
- ),
196
- editableCompartment.reconfigure(
197
- EditorView.editable.of(!cellData.readonly)
198
- )
199
- ]
200
- });
201
-
202
- const currentValue = view.state.doc.toString();
203
-
204
- if (currentValue !== cellData.code) {
205
- view.dispatch({
206
- changes: {
207
- from: 0,
208
- to: currentValue.length,
209
- insert: cellData.code
190
+ sync: (isActive, isMarkdownView) => {
191
+ if (!isMarkdownView) {
192
+ if (!view) {
193
+ requestAnimationFrame(() => initEditor());
194
+ } else {
195
+ view.dispatch({
196
+ effects: [
197
+ languageCompartment.reconfigure(
198
+ languageFor(cellData.type, cellData.runtime, app.runtimePlugins)
199
+ ),
200
+
201
+ themeCompartment.reconfigure(
202
+ themeFor(cellData.runtime, app.runtimePlugins)
203
+ ),
204
+
205
+ readOnlyCompartment.reconfigure(
206
+ EditorState.readOnly.of(cellData.readonly)
207
+ ),
208
+
209
+ editableCompartment.reconfigure(
210
+ EditorView.editable.of(!cellData.readonly)
211
+ )
212
+ ]
213
+ });
214
+
215
+ const currentValue = view.state.doc.toString();
216
+
217
+ if (currentValue !== cellData.code) {
218
+ view.dispatch({
219
+ changes: {
220
+ from: 0,
221
+ to: currentValue.length,
222
+ insert: cellData.code
223
+ }
224
+ });
210
225
  }
211
- });
212
- }
226
+ }
213
227
 
214
- if (
215
- isActive &&
216
- !cellData.readonly
217
- ) {
218
- requestAnimationFrame(() => view?.focus());
228
+ if (isActive && view && !cellData.readonly) {
229
+ requestAnimationFrame(() => view.focus());
230
+ }
231
+ } else if (view) {
232
+ view.destroy();
233
+ view = null;
219
234
  }
220
235
  }
221
- } else if (view) {
222
- view.destroy();
223
- view = null;
224
- }
225
- }
226
236
  };
227
237
  }
@@ -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/index.css CHANGED
@@ -108,10 +108,14 @@
108
108
  .ziko-notebook .cell-controls {
109
109
  display: flex;
110
110
  align-items: center;
111
+ justify-content: flex-end;
112
+ grid-column: 1 / -1;
111
113
  gap: 4px;
112
114
  flex-shrink: 0;
113
115
  flex-wrap: wrap;
114
- max-width: 60px;
116
+ /* border-bottom: 1px solid #f1f5f9; */
117
+ padding-bottom: 6px;
118
+ margin-bottom: 2px;
115
119
  }
116
120
 
117
121
  .ziko-notebook .cm-line{
@@ -149,15 +153,6 @@
149
153
  grid-template-rows: auto auto;
150
154
  }
151
155
 
152
- .ziko-notebook .cell-controls {
153
- grid-column: 1 / -1;
154
- justify-content: flex-end;
155
- border-bottom: 1px solid #f1f5f9;
156
- padding-bottom: 6px;
157
- margin-bottom: 2px;
158
- max-width: 100%;
159
- }
160
-
161
156
  .ziko-notebook .cell-output-grid {
162
157
  grid-template-columns: 42px 1fr;
163
158
  }
package/src/main.js CHANGED
@@ -1 +1 @@
1
- export { UINotebook } from './notebook.js';
1
+ export * from './notebook.js';