@marimo-team/frontend 0.23.1-dev9 → 0.23.1
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/dist/assets/{JsonOutput-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
- package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
- package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
- package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
- package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
- package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
- package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
- package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
- package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
- package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
- package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
- package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
- package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
- package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
- package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
- package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
- package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
- package/dist/assets/index-y6osgSWB.js +42 -0
- package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
- package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
- package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
- package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
- package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
- package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
- package/dist/assets/state-D1n-olwf.js +3 -0
- package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +4 -4
- package/src/core/islands/__tests__/bridge.test.ts +2 -12
- package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
- package/src/core/islands/__tests__/parse.test.ts +466 -24
- package/src/core/islands/__tests__/test-utils.tsx +263 -0
- package/src/core/islands/bootstrap.ts +265 -0
- package/src/core/islands/bridge.ts +154 -75
- package/src/core/islands/components/IslandControls.tsx +103 -0
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
- package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
- package/src/core/islands/components/output-wrapper.tsx +76 -93
- package/src/core/islands/components/useIslandControls.ts +60 -0
- package/src/core/islands/components/web-components.tsx +168 -40
- package/src/core/islands/constants.ts +28 -0
- package/src/core/islands/main.ts +7 -205
- package/src/core/islands/parse.ts +73 -26
- package/src/core/islands/worker-factory.ts +86 -0
- package/src/plugins/core/RenderHTML.tsx +9 -0
- package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
- package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
- package/src/plugins/core/registerReactComponent.tsx +11 -8
- package/src/plugins/core/trusted-url.ts +20 -0
- package/src/plugins/impl/ButtonPlugin.tsx +4 -6
- package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
- package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
- package/src/plugins/impl/DataTablePlugin.tsx +8 -9
- package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
- package/src/plugins/impl/FormPlugin.tsx +2 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
- package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
- package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
- package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
- package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
- package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
- package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
- package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
- package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
- package/dist/assets/index-Bm25ctN7.js +0 -42
- package/dist/assets/state-BvnlMKdT.js +0 -3
|
@@ -11,73 +11,172 @@ import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
|
|
|
11
11
|
import { LocaleProvider } from "@/core/i18n/locale-provider";
|
|
12
12
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
13
13
|
import { invariant } from "@/utils/invariant";
|
|
14
|
-
import type { CellId } from "../../cells/ids";
|
|
14
|
+
import type { CellId, UIElementId } from "../../cells/ids";
|
|
15
15
|
import { store } from "../../state/jotai";
|
|
16
|
+
import {
|
|
17
|
+
ISLAND_CSS_CLASSES,
|
|
18
|
+
ISLAND_DATA_ATTRIBUTES,
|
|
19
|
+
ISLAND_TAG_NAMES,
|
|
20
|
+
} from "../constants";
|
|
16
21
|
import { extractIslandCodeFromEmbed } from "../parse";
|
|
17
22
|
import { MarimoOutputWrapper } from "./output-wrapper";
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
|
-
*
|
|
25
|
+
* Configuration for rendering a marimo island
|
|
26
|
+
*/
|
|
27
|
+
export interface IslandRenderConfig {
|
|
28
|
+
html: string;
|
|
29
|
+
codeCallback: () => string;
|
|
30
|
+
editor: JSX.Element | null;
|
|
31
|
+
cellId: CellId | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A custom element that renders the output of a marimo cell.
|
|
36
|
+
*
|
|
37
|
+
* This web component wraps marimo cell outputs and provides interactive
|
|
38
|
+
* functionality like re-running cells and copying code.
|
|
21
39
|
*/
|
|
22
40
|
export class MarimoIslandElement extends HTMLElement {
|
|
23
41
|
private root?: Root;
|
|
24
42
|
|
|
25
|
-
public static readonly tagName =
|
|
26
|
-
public static readonly outputTagName =
|
|
27
|
-
public static readonly codeTagName =
|
|
28
|
-
public static readonly editorTagName =
|
|
29
|
-
public static readonly styleNamespace =
|
|
43
|
+
public static readonly tagName = ISLAND_TAG_NAMES.ISLAND;
|
|
44
|
+
public static readonly outputTagName = ISLAND_TAG_NAMES.CELL_OUTPUT;
|
|
45
|
+
public static readonly codeTagName = ISLAND_TAG_NAMES.CELL_CODE;
|
|
46
|
+
public static readonly editorTagName = ISLAND_TAG_NAMES.CODE_EDITOR;
|
|
47
|
+
public static readonly styleNamespace = ISLAND_CSS_CLASSES.NAMESPACE;
|
|
30
48
|
|
|
31
49
|
constructor() {
|
|
32
50
|
super();
|
|
33
51
|
this.classList.add(MarimoIslandElement.styleNamespace);
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Gets the app ID from the element's data attribute
|
|
56
|
+
*/
|
|
36
57
|
get appId(): string {
|
|
37
|
-
|
|
38
|
-
|
|
58
|
+
const appId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
|
|
59
|
+
invariant(appId, "Missing data-app-id attribute");
|
|
60
|
+
return appId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether this island is reactive (has code sent to Python for execution)
|
|
65
|
+
*/
|
|
66
|
+
get isReactive(): boolean {
|
|
67
|
+
return this.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets the cell ID by looking up the cell index in the notebook state.
|
|
72
|
+
* Returns undefined for non-reactive islands (they have no corresponding cell).
|
|
73
|
+
*/
|
|
74
|
+
get cellId(): CellId | undefined {
|
|
75
|
+
const cellId = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_ID);
|
|
76
|
+
if (cellId) {
|
|
77
|
+
return cellId as CellId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const cellIdx = this.getAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX);
|
|
81
|
+
if (!cellIdx) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return this.getCellIdFromIndex(Number.parseInt(cellIdx, 10));
|
|
39
85
|
}
|
|
40
86
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Gets the code for this island cell
|
|
89
|
+
*/
|
|
90
|
+
get code(): string {
|
|
91
|
+
return extractIslandCodeFromEmbed(this);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Looks up a cell ID from the notebook state by index
|
|
96
|
+
*/
|
|
97
|
+
private getCellIdFromIndex(idx: number): CellId {
|
|
44
98
|
const { cellIds } = store.get(notebookAtom);
|
|
45
|
-
const idx = Number.parseInt(this.dataset.cellIdx, 10);
|
|
46
99
|
const cellId = cellIds.inOrderIds.at(idx);
|
|
47
|
-
invariant(cellId,
|
|
100
|
+
invariant(cellId, `Missing cell ID at index ${idx}`);
|
|
48
101
|
return cellId;
|
|
49
102
|
}
|
|
50
103
|
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Called when the element is added to the DOM.
|
|
106
|
+
*
|
|
107
|
+
* Deferred to a microtask because `defineCustomElement` during
|
|
108
|
+
* `kernel-ready` upgrades all existing elements synchronously,
|
|
109
|
+
* which can happen inside a React render cycle. Rendering
|
|
110
|
+
* synchronously from there causes "unmount during render" warnings.
|
|
111
|
+
*/
|
|
112
|
+
connectedCallback(): void {
|
|
113
|
+
// Capture config synchronously (before children get cleared by createRoot)
|
|
114
|
+
const config = this.extractRenderConfig();
|
|
115
|
+
queueMicrotask(() => {
|
|
116
|
+
// Guard against disconnect between connectedCallback and microtask
|
|
117
|
+
if (!this.isConnected) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.root = ReactDOM.createRoot(this);
|
|
121
|
+
this.renderIsland(config);
|
|
122
|
+
});
|
|
53
123
|
}
|
|
54
124
|
|
|
55
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Extracts configuration needed for rendering
|
|
127
|
+
*/
|
|
128
|
+
private extractRenderConfig(): IslandRenderConfig {
|
|
56
129
|
const output = this.querySelectorOrThrow(MarimoIslandElement.outputTagName);
|
|
57
130
|
const initialOutput = output.innerHTML;
|
|
58
|
-
|
|
59
131
|
const optionalEditor = this.getOptionalEditor();
|
|
60
132
|
const code = this.code;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
const cellId = this.cellId;
|
|
134
|
+
|
|
135
|
+
// Read objectId directly from the DOM before createRoot clears children.
|
|
136
|
+
// optionalEditor is a <RenderHTML> wrapper, so its .props don't carry the
|
|
137
|
+
// underlying element's attributes — we must grab objectId here instead.
|
|
138
|
+
const editorElement = this.querySelector(MarimoIslandElement.editorTagName);
|
|
139
|
+
const editorObjectId = (
|
|
140
|
+
editorElement?.parentElement as Element | null
|
|
141
|
+
)?.getAttribute(OBJECT_ID_ATTR) as UIElementId | null;
|
|
142
|
+
|
|
143
|
+
const codeCallback: () => string =
|
|
144
|
+
optionalEditor && editorObjectId
|
|
145
|
+
? () => {
|
|
146
|
+
const val = UI_ELEMENT_REGISTRY.lookupValue(editorObjectId);
|
|
147
|
+
return val !== undefined ? String(val) : code;
|
|
148
|
+
}
|
|
149
|
+
: () => code;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
html: initialOutput,
|
|
153
|
+
codeCallback,
|
|
154
|
+
editor: optionalEditor,
|
|
155
|
+
cellId,
|
|
156
|
+
};
|
|
70
157
|
}
|
|
71
158
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Renders the island with React
|
|
161
|
+
*/
|
|
162
|
+
private renderIsland(config: IslandRenderConfig): void {
|
|
163
|
+
const { html, codeCallback, editor, cellId } = config;
|
|
77
164
|
const alwaysShowRun = !!editor;
|
|
78
|
-
|
|
79
|
-
const isEmpty =
|
|
80
|
-
const initialHtml = isEmpty ? null : renderHTML({ html });
|
|
165
|
+
const trimmedHtml = html.trim();
|
|
166
|
+
const isEmpty = trimmedHtml === "<span></span>" || trimmedHtml === "";
|
|
167
|
+
const initialHtml = isEmpty ? null : renderHTML({ html: trimmedHtml });
|
|
168
|
+
|
|
169
|
+
// Non-reactive islands have no cell in the kernel — just render static HTML
|
|
170
|
+
if (!cellId) {
|
|
171
|
+
this.root?.render(
|
|
172
|
+
<ErrorBoundary>
|
|
173
|
+
<Provider store={store}>
|
|
174
|
+
<LocaleProvider>{initialHtml}</LocaleProvider>
|
|
175
|
+
</Provider>
|
|
176
|
+
</ErrorBoundary>,
|
|
177
|
+
);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
81
180
|
|
|
82
181
|
this.root?.render(
|
|
83
182
|
<ErrorBoundary>
|
|
@@ -85,7 +184,7 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
85
184
|
<LocaleProvider>
|
|
86
185
|
<TooltipProvider>
|
|
87
186
|
<MarimoOutputWrapper
|
|
88
|
-
cellId={
|
|
187
|
+
cellId={cellId}
|
|
89
188
|
codeCallback={codeCallback}
|
|
90
189
|
alwaysShowRun={alwaysShowRun}
|
|
91
190
|
>
|
|
@@ -99,16 +198,30 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
99
198
|
);
|
|
100
199
|
}
|
|
101
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Attempts to find and render an optional code editor.
|
|
203
|
+
*
|
|
204
|
+
* The DOM structure is:
|
|
205
|
+
* <marimo-cell-output>
|
|
206
|
+
* <div data-marimo-element> ← parent wrapper from Python render
|
|
207
|
+
* <marimo-code-editor .../>
|
|
208
|
+
* </div>
|
|
209
|
+
* </marimo-cell-output>
|
|
210
|
+
*
|
|
211
|
+
* We take the parent's outerHTML (the wrapper div) so that the rendered
|
|
212
|
+
* React element includes the UI element registration attributes.
|
|
213
|
+
*
|
|
214
|
+
* @returns A React element for the editor, or null if not found
|
|
215
|
+
*/
|
|
102
216
|
private getOptionalEditor(): JSX.Element | null {
|
|
103
|
-
// TODO: Maybe add specificity with a [editor=island] selector or something.
|
|
104
217
|
const optionalElement = this.querySelector(
|
|
105
218
|
MarimoIslandElement.editorTagName,
|
|
106
219
|
);
|
|
107
220
|
const html = (optionalElement?.parentNode as Element)?.outerHTML;
|
|
108
221
|
if (html) {
|
|
109
|
-
//
|
|
222
|
+
// Convert HTML to virtual DOM
|
|
110
223
|
const virtualDom = renderHTML({ html });
|
|
111
|
-
//
|
|
224
|
+
// Verify it's a valid React element
|
|
112
225
|
if (isValidElement(virtualDom)) {
|
|
113
226
|
return virtualDom;
|
|
114
227
|
}
|
|
@@ -116,9 +229,24 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
116
229
|
return null;
|
|
117
230
|
}
|
|
118
231
|
|
|
119
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Queries for an element and throws if not found
|
|
234
|
+
*/
|
|
235
|
+
private querySelectorOrThrow(selector: string): Element {
|
|
120
236
|
const element = this.querySelector(selector);
|
|
121
237
|
invariant(element, `Missing ${selector} element`);
|
|
122
238
|
return element;
|
|
123
239
|
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Cleanup when element is removed from DOM
|
|
243
|
+
*/
|
|
244
|
+
disconnectedCallback(): void {
|
|
245
|
+
const root = this.root;
|
|
246
|
+
this.root = undefined;
|
|
247
|
+
// Defer unmount to avoid "unmount during render" race
|
|
248
|
+
if (root) {
|
|
249
|
+
queueMicrotask(() => root.unmount());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
124
252
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom element tag names for islands
|
|
5
|
+
*/
|
|
6
|
+
export const ISLAND_TAG_NAMES = {
|
|
7
|
+
ISLAND: "marimo-island",
|
|
8
|
+
CELL_OUTPUT: "marimo-cell-output",
|
|
9
|
+
CELL_CODE: "marimo-cell-code",
|
|
10
|
+
CODE_EDITOR: "marimo-code-editor",
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Data attributes for islands
|
|
15
|
+
*/
|
|
16
|
+
export const ISLAND_DATA_ATTRIBUTES = {
|
|
17
|
+
APP_ID: "data-app-id",
|
|
18
|
+
CELL_IDX: "data-cell-idx",
|
|
19
|
+
CELL_ID: "data-cell-id",
|
|
20
|
+
REACTIVE: "data-reactive",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CSS classes for islands
|
|
25
|
+
*/
|
|
26
|
+
export const ISLAND_CSS_CLASSES = {
|
|
27
|
+
NAMESPACE: "marimo",
|
|
28
|
+
} as const;
|
package/src/core/islands/main.ts
CHANGED
|
@@ -11,216 +11,18 @@ import "../../css/table.css";
|
|
|
11
11
|
|
|
12
12
|
import "iconify-icon";
|
|
13
13
|
|
|
14
|
-
import { toast } from "@/components/ui/use-toast";
|
|
15
|
-
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
16
|
-
import {
|
|
17
|
-
handleWidgetMessage,
|
|
18
|
-
MODEL_MANAGER,
|
|
19
|
-
} from "@/plugins/impl/anywidget/model";
|
|
20
|
-
import { initializePlugins } from "@/plugins/plugins";
|
|
21
|
-
import { logNever } from "@/utils/assertNever";
|
|
22
|
-
import { Functions } from "@/utils/functions";
|
|
23
|
-
import { safeExtractSetUIElementMessageBuffers } from "@/utils/json/base64";
|
|
24
|
-
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
25
14
|
import { Logger } from "@/utils/Logger";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
notebookAtom,
|
|
29
|
-
notebookReducer,
|
|
30
|
-
} from "../cells/cells";
|
|
31
|
-
import { defineCustomElement } from "../dom/defineCustomElement";
|
|
32
|
-
import { MarimoValueInputEvent } from "../dom/events";
|
|
33
|
-
import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
|
|
34
|
-
import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
|
|
35
|
-
import {
|
|
36
|
-
handleCellNotificationeration,
|
|
37
|
-
handleKernelReady,
|
|
38
|
-
handleRemoveUIElements,
|
|
39
|
-
} from "../kernel/handlers";
|
|
40
|
-
import { queryParamHandlers } from "../kernel/queryParamHandlers";
|
|
41
|
-
import { RuntimeState } from "../kernel/RuntimeState";
|
|
42
|
-
import { initialModeAtom } from "../mode";
|
|
43
|
-
import { requestClientAtom } from "../network/requests";
|
|
44
|
-
import { store } from "../state/jotai";
|
|
45
|
-
import { IslandsPyodideBridge } from "./bridge";
|
|
46
|
-
import { MarimoIslandElement } from "./components/web-components";
|
|
47
|
-
import {
|
|
48
|
-
shouldShowIslandsWarningIndicatorAtom,
|
|
49
|
-
userTriedToInteractWithIslandsAtom,
|
|
50
|
-
} from "./state";
|
|
51
|
-
import { dismissIslandsLoadingToast, toastIslandsLoading } from "./toast";
|
|
15
|
+
import { initializeIslands } from "./bootstrap";
|
|
16
|
+
import { getGlobalBridge } from "./bridge";
|
|
52
17
|
|
|
53
18
|
/**
|
|
54
19
|
* Main entry point for the js bundle for embedded marimo apps.
|
|
55
20
|
*/
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Initialize the Marimo app.
|
|
59
|
-
*/
|
|
60
21
|
export async function initialize() {
|
|
61
|
-
|
|
62
|
-
store.set(requestClientAtom, IslandsPyodideBridge.INSTANCE);
|
|
63
|
-
store.set(initialModeAtom, "read");
|
|
64
|
-
|
|
65
|
-
// This will display all the static HTML content.
|
|
66
|
-
initializePlugins();
|
|
67
|
-
|
|
68
|
-
// Find all `marimo-island` elements.
|
|
69
|
-
const islands = document.querySelectorAll<HTMLElement>(
|
|
70
|
-
MarimoIslandElement.tagName,
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// If no islands are found, we can skip the rest of the initialization.
|
|
74
|
-
if (islands.length === 0) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Add 'marimo' class name to all `marimo-island` elements.
|
|
79
|
-
// This makes our styles apply to the islands.
|
|
80
|
-
for (const island of islands) {
|
|
81
|
-
island.classList.add(MarimoIslandElement.styleNamespace);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const actions = createNotebookActions((action) => {
|
|
85
|
-
store.set(notebookAtom, (state) => notebookReducer(state, action));
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// If the user has interacted with the islands before they are initialized,
|
|
89
|
-
// we show the loading toast.
|
|
90
|
-
store.sub(shouldShowIslandsWarningIndicatorAtom, () => {
|
|
91
|
-
const showing = store.get(shouldShowIslandsWarningIndicatorAtom);
|
|
92
|
-
if (showing) {
|
|
93
|
-
toastIslandsLoading();
|
|
94
|
-
// For each island, set the opacity to 0.5
|
|
95
|
-
for (const island of islands) {
|
|
96
|
-
island.style.setProperty("opacity", "0.5");
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
dismissIslandsLoadingToast();
|
|
100
|
-
// For each island, remove the opacity
|
|
101
|
-
for (const island of islands) {
|
|
102
|
-
island.style.removeProperty("opacity");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Consume messages from the kernel
|
|
108
|
-
IslandsPyodideBridge.INSTANCE.consumeMessages((message) => {
|
|
109
|
-
const msg = jsonParseWithSpecialChar(message);
|
|
110
|
-
switch (msg.data.op) {
|
|
111
|
-
case "banner":
|
|
112
|
-
case "missing-package-alert":
|
|
113
|
-
case "installing-package-alert":
|
|
114
|
-
case "completion-result":
|
|
115
|
-
case "reload":
|
|
116
|
-
case "focus-cell":
|
|
117
|
-
case "variables":
|
|
118
|
-
case "variable-values":
|
|
119
|
-
case "data-column-preview":
|
|
120
|
-
case "sql-table-preview":
|
|
121
|
-
case "sql-table-list-preview":
|
|
122
|
-
case "sql-schema-list-preview":
|
|
123
|
-
case "datasets":
|
|
124
|
-
case "data-source-connections":
|
|
125
|
-
case "validate-sql-result":
|
|
126
|
-
case "storage-namespaces":
|
|
127
|
-
case "storage-entries":
|
|
128
|
-
case "storage-download-ready":
|
|
129
|
-
case "secret-keys-result":
|
|
130
|
-
case "startup-logs":
|
|
131
|
-
// Unsupported
|
|
132
|
-
return;
|
|
133
|
-
case "kernel-ready":
|
|
134
|
-
handleKernelReady(msg.data, {
|
|
135
|
-
autoInstantiate: true,
|
|
136
|
-
setCells: actions.setCells,
|
|
137
|
-
setLayoutData: Functions.NOOP,
|
|
138
|
-
setAppConfig: Functions.NOOP,
|
|
139
|
-
setCapabilities: Functions.NOOP,
|
|
140
|
-
setKernelState: Functions.NOOP,
|
|
141
|
-
onError: Logger.error,
|
|
142
|
-
});
|
|
143
|
-
// Define the custom element for the marimo-island tag.
|
|
144
|
-
// This comes after initializing since this reads from the store.
|
|
145
|
-
defineCustomElement(MarimoIslandElement.tagName, MarimoIslandElement);
|
|
146
|
-
return;
|
|
147
|
-
case "completed-run":
|
|
148
|
-
return;
|
|
149
|
-
case "interrupted":
|
|
150
|
-
return;
|
|
151
|
-
case "send-ui-element-message":
|
|
152
|
-
UI_ELEMENT_REGISTRY.broadcastMessage(
|
|
153
|
-
msg.data.ui_element,
|
|
154
|
-
msg.data.message,
|
|
155
|
-
safeExtractSetUIElementMessageBuffers(msg.data),
|
|
156
|
-
);
|
|
157
|
-
return;
|
|
158
|
-
|
|
159
|
-
case "remove-ui-elements":
|
|
160
|
-
handleRemoveUIElements(msg.data);
|
|
161
|
-
return;
|
|
162
|
-
case "function-call-result":
|
|
163
|
-
FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
|
|
164
|
-
return;
|
|
165
|
-
case "cell-op":
|
|
166
|
-
handleCellNotificationeration(msg.data, actions.handleCellMessage);
|
|
167
|
-
return;
|
|
168
|
-
case "alert":
|
|
169
|
-
// TODO: support toast with islands
|
|
170
|
-
toast({
|
|
171
|
-
title: msg.data.title,
|
|
172
|
-
description: renderHTML({
|
|
173
|
-
html: msg.data.description,
|
|
174
|
-
}),
|
|
175
|
-
variant: msg.data.variant,
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
case "query-params-append":
|
|
179
|
-
queryParamHandlers.append(msg.data);
|
|
180
|
-
return;
|
|
181
|
-
case "query-params-set":
|
|
182
|
-
queryParamHandlers.set(msg.data);
|
|
183
|
-
return;
|
|
184
|
-
case "query-params-delete":
|
|
185
|
-
queryParamHandlers.delete(msg.data);
|
|
186
|
-
return;
|
|
187
|
-
case "query-params-clear":
|
|
188
|
-
queryParamHandlers.clear();
|
|
189
|
-
return;
|
|
190
|
-
case "reconnected":
|
|
191
|
-
return;
|
|
192
|
-
case "cache-cleared":
|
|
193
|
-
return;
|
|
194
|
-
case "cache-info":
|
|
195
|
-
return;
|
|
196
|
-
case "kernel-startup-error":
|
|
197
|
-
return;
|
|
198
|
-
case "notebook-document-transaction":
|
|
199
|
-
return;
|
|
200
|
-
case "model-lifecycle":
|
|
201
|
-
handleWidgetMessage(MODEL_MANAGER, msg.data);
|
|
202
|
-
return;
|
|
203
|
-
default:
|
|
204
|
-
logNever(msg.data);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Set the user tried to interact with islands
|
|
209
|
-
// before they are initialized.
|
|
210
|
-
document.addEventListener(
|
|
211
|
-
MarimoValueInputEvent.TYPE,
|
|
212
|
-
() => {
|
|
213
|
-
store.set(userTriedToInteractWithIslandsAtom, true);
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
once: true,
|
|
217
|
-
},
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Start the runtime
|
|
221
|
-
RuntimeState.INSTANCE.start(
|
|
222
|
-
IslandsPyodideBridge.INSTANCE.sendComponentValues,
|
|
223
|
-
);
|
|
22
|
+
await initializeIslands({ bridge: getGlobalBridge() });
|
|
224
23
|
}
|
|
225
24
|
|
|
226
|
-
initialize
|
|
25
|
+
// Auto-initialize on module load
|
|
26
|
+
void initialize().catch((error) => {
|
|
27
|
+
Logger.error("Failed to initialize islands:", error);
|
|
28
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ISLAND_DATA_ATTRIBUTES,
|
|
5
|
+
ISLAND_TAG_NAMES,
|
|
6
|
+
} from "@/core/islands/constants";
|
|
4
7
|
import { Logger } from "@/utils/Logger";
|
|
5
8
|
|
|
6
9
|
/**
|
|
@@ -41,53 +44,94 @@ interface MarimoIslandCell {
|
|
|
41
44
|
idx: number;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Parses marimo island apps from the DOM
|
|
49
|
+
* @param root - Root element to search within (defaults to document)
|
|
50
|
+
*/
|
|
51
|
+
export function parseMarimoIslandApps(
|
|
52
|
+
root: Document | Element = document,
|
|
53
|
+
): MarimoIslandApp[] {
|
|
54
|
+
const embeds = root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND);
|
|
50
55
|
if (embeds.length === 0) {
|
|
51
56
|
Logger.warn("No embedded marimo apps found.");
|
|
52
57
|
return [];
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
// eslint-disable-next-line prefer-spread
|
|
61
|
+
return parseIslandElementsIntoApps(Array.from(embeds));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pure function to parse island elements into app structures
|
|
66
|
+
* @param embeds - Array of island HTML elements
|
|
67
|
+
*/
|
|
68
|
+
export function parseIslandElementsIntoApps(
|
|
69
|
+
embeds: HTMLElement[],
|
|
70
|
+
): MarimoIslandApp[] {
|
|
71
|
+
const apps = new Map<string, MarimoIslandApp>();
|
|
72
|
+
|
|
55
73
|
for (const embed of embeds) {
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
74
|
+
const appId = embed.getAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID);
|
|
75
|
+
if (!appId) {
|
|
58
76
|
Logger.warn("Embedded marimo cell missing data-app-id attribute.");
|
|
59
77
|
continue;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
// Non-reactive islands are static — they don't participate in the kernel
|
|
81
|
+
const reactive =
|
|
82
|
+
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
|
|
83
|
+
if (!reactive) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
const cellData = parseIslandElement(embed);
|
|
88
|
+
if (!cellData) {
|
|
89
|
+
Logger.warn(`Embedded marimo app ${appId} missing cell output or code.`);
|
|
69
90
|
continue;
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
if (!apps.has(
|
|
73
|
-
apps.set(
|
|
93
|
+
if (!apps.has(appId)) {
|
|
94
|
+
apps.set(appId, { id: appId, cells: [] });
|
|
74
95
|
}
|
|
75
|
-
|
|
76
|
-
const app = apps.get(
|
|
96
|
+
|
|
97
|
+
const app = apps.get(appId)!;
|
|
77
98
|
const idx = app.cells.length;
|
|
78
99
|
app.cells.push({
|
|
79
|
-
output:
|
|
80
|
-
code: code,
|
|
100
|
+
output: cellData.output,
|
|
101
|
+
code: cellData.code,
|
|
81
102
|
idx: idx,
|
|
82
103
|
});
|
|
83
104
|
|
|
84
105
|
// Add data-cell-idx attribute to the island element
|
|
85
|
-
embed.
|
|
106
|
+
embed.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, idx.toString());
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
return [...apps.values()];
|
|
89
110
|
}
|
|
90
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Parses a single island element into cell data
|
|
114
|
+
* @param embed - The island HTML element
|
|
115
|
+
* @returns Cell data or null if invalid
|
|
116
|
+
*/
|
|
117
|
+
export function parseIslandElement(
|
|
118
|
+
embed: HTMLElement,
|
|
119
|
+
): { output: string; code: string } | null {
|
|
120
|
+
const cellOutput = embed.querySelector<HTMLElement>(
|
|
121
|
+
ISLAND_TAG_NAMES.CELL_OUTPUT,
|
|
122
|
+
);
|
|
123
|
+
const code = extractIslandCodeFromEmbed(embed);
|
|
124
|
+
|
|
125
|
+
if (!cellOutput || !code) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
output: cellOutput.innerHTML,
|
|
131
|
+
code: code,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
91
135
|
export function createMarimoFile(app: { cells: { code: string }[] }): string {
|
|
92
136
|
const lines = [
|
|
93
137
|
"import marimo",
|
|
@@ -134,7 +178,8 @@ export function parseIslandCode(code: string | undefined | null): string {
|
|
|
134
178
|
}
|
|
135
179
|
|
|
136
180
|
export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
|
|
137
|
-
const reactive =
|
|
181
|
+
const reactive =
|
|
182
|
+
embed.getAttribute(ISLAND_DATA_ATTRIBUTES.REACTIVE) === "true";
|
|
138
183
|
// Non-reactive cells are not guaranteed to have code, and should be treated as
|
|
139
184
|
// such.
|
|
140
185
|
if (!reactive) {
|
|
@@ -142,17 +187,19 @@ export function extractIslandCodeFromEmbed(embed: HTMLElement): string {
|
|
|
142
187
|
}
|
|
143
188
|
|
|
144
189
|
const cellCodeElement = embed.querySelector<HTMLElement>(
|
|
145
|
-
|
|
190
|
+
ISLAND_TAG_NAMES.CELL_CODE,
|
|
146
191
|
);
|
|
147
192
|
if (cellCodeElement) {
|
|
148
193
|
return parseIslandCode(cellCodeElement.textContent);
|
|
149
194
|
}
|
|
150
195
|
|
|
151
196
|
const editorCodeElement = embed.querySelector<HTMLElement>(
|
|
152
|
-
|
|
197
|
+
ISLAND_TAG_NAMES.CODE_EDITOR,
|
|
153
198
|
);
|
|
154
199
|
if (editorCodeElement) {
|
|
155
|
-
return parseIslandEditor(
|
|
200
|
+
return parseIslandEditor(
|
|
201
|
+
editorCodeElement.getAttribute("data-initial-value"),
|
|
202
|
+
);
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
return "";
|