@marimo-team/islands 0.23.6-dev9 → 0.23.6
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/{ConnectedDataExplorerComponent-CWU3Az6F.js → ConnectedDataExplorerComponent-PmilQqXR.js} +4 -4
- package/dist/assets/__vite-browser-external-rrUYDKRl.js +1 -0
- package/dist/assets/{worker-D-EdLKct.js → worker-Bfy15ViQ.js} +2 -2
- package/dist/{chat-ui-Cyca6aKX.js → chat-ui-B-gbqk_F.js} +6 -6
- package/dist/{code-visibility-B0kwrVA6.js → code-visibility-DNiCvIcQ.js} +678 -564
- package/dist/{formats-Dh5M1ZRs.js → formats-CgaK7Gmx.js} +1 -1
- package/dist/{glide-data-editor-DXti2axL.js → glide-data-editor-CvlvtPWJ.js} +2 -2
- package/dist/{html-to-image-6VI69paz.js → html-to-image-hMMPiNe_.js} +2120 -2103
- package/dist/{input-Drx1pguW.js → input-BAOe64zx.js} +1 -1
- package/dist/main.js +19 -19
- package/dist/{mermaid-BagLPXm9.js → mermaid-DJ1NyBGw.js} +2 -2
- package/dist/{process-output-SkNR_Omd.js → process-output-Bza_GK7Q.js} +1 -1
- package/dist/{reveal-component-DlCLweHo.js → reveal-component-BSwl7P64.js} +13 -13
- package/dist/{spec-BKWq0wn2.js → spec-DSIuqd3f.js} +1 -1
- package/dist/toDate-CHtl9vts.js +662 -0
- package/dist/{useAsyncData-CKYzhCis.js → useAsyncData-B6hCGywC.js} +1 -1
- package/dist/{useDeepCompareMemoize-je76AJS_.js → useDeepCompareMemoize-CmwDuYUH.js} +1 -1
- package/dist/{useLifecycle-smVfjLNI.js → useLifecycle-CjMjllqy.js} +1 -1
- package/dist/{useTheme-CX9pPLUH.js → useTheme-CByZUW0p.js} +1 -0
- package/dist/{vega-component-BnCQmtxw.js → vega-component-CC8TqWWV.js} +5 -5
- package/package.json +5 -5
- package/src/components/ai/ai-provider-icon.tsx +1 -0
- package/src/components/ai/ai-utils.ts +1 -0
- package/src/components/app-config/ai-config.tsx +30 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +47 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +2 -0
- package/src/components/editor/renderers/cell-array.tsx +14 -7
- package/src/components/slides/slide-form.tsx +43 -0
- package/src/components/terminal/terminal.tsx +16 -0
- package/src/components/ui/links.tsx +2 -1
- package/src/core/ai/ids/ids.ts +1 -0
- package/src/core/codemirror/markdown/__tests__/commands.test.ts +36 -0
- package/src/core/codemirror/markdown/commands.ts +4 -1
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/edit-app.tsx +1 -0
- package/src/core/run-app.tsx +9 -2
- package/src/core/runtime/runtime.ts +3 -2
- package/src/core/static/static-state.ts +5 -1
- package/src/core/wasm/PyodideLoader.tsx +54 -16
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +72 -0
- package/src/core/wasm/__tests__/bridge.test.ts +26 -1
- package/src/core/wasm/bridge.ts +24 -6
- package/src/core/wasm/state.ts +3 -0
- package/src/core/wasm/worker/getController.ts +7 -0
- package/src/core/wasm/worker/save-worker.ts +2 -1
- package/src/core/wasm/worker/worker.ts +2 -1
- package/src/plugins/core/RenderHTML.tsx +49 -3
- package/src/plugins/core/__test__/RenderHTML.test.ts +54 -0
- package/src/plugins/impl/common/labeled.tsx +1 -1
- package/dist/assets/__vite-browser-external-C4JkHbyY.js +0 -1
- package/dist/toDate-yqOcZ_tY.js +0 -638
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { shouldShowSpinner } from "../PyodideLoader";
|
|
5
|
+
|
|
6
|
+
describe("shouldShowSpinner", () => {
|
|
7
|
+
it("shows the spinner when there are no cells yet (Pyodide hasn't parsed)", () => {
|
|
8
|
+
expect(
|
|
9
|
+
shouldShowSpinner({
|
|
10
|
+
hasCells: false,
|
|
11
|
+
hasOutput: false,
|
|
12
|
+
mode: "read",
|
|
13
|
+
codeHidden: false,
|
|
14
|
+
}),
|
|
15
|
+
).toBe(true);
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
shouldShowSpinner({
|
|
19
|
+
hasCells: false,
|
|
20
|
+
hasOutput: true,
|
|
21
|
+
mode: "edit",
|
|
22
|
+
codeHidden: false,
|
|
23
|
+
}),
|
|
24
|
+
).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders children once cells exist with code visible", () => {
|
|
28
|
+
// run mode, code visible, no outputs yet — the user can read the code
|
|
29
|
+
expect(
|
|
30
|
+
shouldShowSpinner({
|
|
31
|
+
hasCells: true,
|
|
32
|
+
hasOutput: false,
|
|
33
|
+
mode: "read",
|
|
34
|
+
codeHidden: false,
|
|
35
|
+
}),
|
|
36
|
+
).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("renders children once cells exist with cached outputs (snapshot case)", () => {
|
|
40
|
+
expect(
|
|
41
|
+
shouldShowSpinner({
|
|
42
|
+
hasCells: true,
|
|
43
|
+
hasOutput: true,
|
|
44
|
+
mode: "read",
|
|
45
|
+
codeHidden: true,
|
|
46
|
+
}),
|
|
47
|
+
).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("keeps the spinner up in headless run mode with no outputs", () => {
|
|
51
|
+
// read mode + code hidden + no outputs = nothing visible to render
|
|
52
|
+
expect(
|
|
53
|
+
shouldShowSpinner({
|
|
54
|
+
hasCells: true,
|
|
55
|
+
hasOutput: false,
|
|
56
|
+
mode: "read",
|
|
57
|
+
codeHidden: true,
|
|
58
|
+
}),
|
|
59
|
+
).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("never blocks edit mode once cells exist", () => {
|
|
63
|
+
expect(
|
|
64
|
+
shouldShowSpinner({
|
|
65
|
+
hasCells: true,
|
|
66
|
+
hasOutput: false,
|
|
67
|
+
mode: "edit",
|
|
68
|
+
codeHidden: true,
|
|
69
|
+
}),
|
|
70
|
+
).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -58,7 +58,7 @@ vi.mock("@/core/wasm/store", () => ({
|
|
|
58
58
|
// Import after all mocks are set up
|
|
59
59
|
import { store } from "@/core/state/jotai";
|
|
60
60
|
import { initialModeAtom } from "@/core/mode";
|
|
61
|
-
import { PyodideBridge } from "../bridge";
|
|
61
|
+
import { getWasmWorkerName, PyodideBridge } from "../bridge";
|
|
62
62
|
|
|
63
63
|
// Access INSTANCE once at module level so the constructor runs (and
|
|
64
64
|
// addMessageListener populates rpcListeners) before any test executes.
|
|
@@ -111,3 +111,28 @@ describe("PyodideBridge.readCode", () => {
|
|
|
111
111
|
expect(mockNotebookReadFile).not.toHaveBeenCalled();
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
describe("getWasmWorkerName", () => {
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
delete (window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean })
|
|
118
|
+
.__MARIMO_HAS_WASM_CONTROLLER__;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns the version without suffix by default", () => {
|
|
122
|
+
expect(getWasmWorkerName()).toBe("0.0.0-test");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("appends ::controller when the host opts in", () => {
|
|
126
|
+
(
|
|
127
|
+
window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean }
|
|
128
|
+
).__MARIMO_HAS_WASM_CONTROLLER__ = true;
|
|
129
|
+
expect(getWasmWorkerName()).toBe("0.0.0-test::controller");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("does not append the suffix for non-true values", () => {
|
|
133
|
+
(
|
|
134
|
+
window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: unknown }
|
|
135
|
+
).__MARIMO_HAS_WASM_CONTROLLER__ = "true";
|
|
136
|
+
expect(getWasmWorkerName()).toBe("0.0.0-test");
|
|
137
|
+
});
|
|
138
|
+
});
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -37,7 +37,7 @@ import type { IConnectionTransport } from "../websocket/transports/transport";
|
|
|
37
37
|
import { PyodideRouter } from "./router";
|
|
38
38
|
import { getWorkerRPC } from "./rpc";
|
|
39
39
|
import { createShareableLink } from "./share";
|
|
40
|
-
import { wasmInitializationAtom } from "./state";
|
|
40
|
+
import { wasmInitializationAtom, wasmInitStatusAtom } from "./state";
|
|
41
41
|
import { fallbackFileStore, notebookFileStore } from "./store";
|
|
42
42
|
import { isWasm } from "./utils";
|
|
43
43
|
import type { SaveWorkerSchema } from "./worker/save-worker";
|
|
@@ -81,9 +81,9 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
81
81
|
new URL("./worker/save-worker.ts", import.meta.url),
|
|
82
82
|
{
|
|
83
83
|
type: "module",
|
|
84
|
-
// Pass the version to the worker
|
|
84
|
+
// Pass the version (and optional capability suffix) to the worker
|
|
85
85
|
/* @vite-ignore */
|
|
86
|
-
name:
|
|
86
|
+
name: getWasmWorkerName(),
|
|
87
87
|
},
|
|
88
88
|
);
|
|
89
89
|
|
|
@@ -101,9 +101,9 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
101
101
|
new URL("./worker/worker.ts", import.meta.url),
|
|
102
102
|
{
|
|
103
103
|
type: "module",
|
|
104
|
-
// Pass the version to the worker
|
|
104
|
+
// Pass the version (and optional capability suffix) to the worker
|
|
105
105
|
/* @vite-ignore */
|
|
106
|
-
name:
|
|
106
|
+
name: getWasmWorkerName(),
|
|
107
107
|
},
|
|
108
108
|
);
|
|
109
109
|
|
|
@@ -119,13 +119,15 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
119
119
|
// By initializing after, we get hits on cached network requests
|
|
120
120
|
this.saveRpc = this.getSaveWorker();
|
|
121
121
|
this.setInterruptBuffer();
|
|
122
|
+
store.set(wasmInitStatusAtom, "ready");
|
|
122
123
|
this.initialized.resolve();
|
|
123
124
|
});
|
|
124
125
|
this.rpc.addMessageListener("initializingMessage", ({ message }) => {
|
|
125
126
|
store.set(wasmInitializationAtom, message);
|
|
126
127
|
});
|
|
127
128
|
this.rpc.addMessageListener("initializedError", ({ error }) => {
|
|
128
|
-
// If already
|
|
129
|
+
// If already initialized, surface as a toast and leave the deferred /
|
|
130
|
+
// init status alone — the worker is healthy, this is a runtime error.
|
|
129
131
|
if (this.initialized.status === "resolved") {
|
|
130
132
|
Logger.error(error);
|
|
131
133
|
toast({
|
|
@@ -133,7 +135,9 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
133
135
|
description: error,
|
|
134
136
|
variant: "danger",
|
|
135
137
|
});
|
|
138
|
+
return;
|
|
136
139
|
}
|
|
140
|
+
store.set(wasmInitStatusAtom, "error");
|
|
137
141
|
this.initialized.reject(new Error(error));
|
|
138
142
|
});
|
|
139
143
|
this.rpc.addMessageListener("kernelMessage", ({ message }) => {
|
|
@@ -634,3 +638,17 @@ export function createPyodideConnection(): IConnectionTransport {
|
|
|
634
638
|
PyodideBridge.INSTANCE.attachMessageConsumer(callback);
|
|
635
639
|
});
|
|
636
640
|
}
|
|
641
|
+
|
|
642
|
+
// Compose the worker name. The version prefix is read by getMarimoVersion()
|
|
643
|
+
// in the worker; the optional "::controller" suffix tells getController.ts
|
|
644
|
+
// that the host page provides a custom /wasm/controller.js and that the
|
|
645
|
+
// dynamic import should be attempted. Hosts opt in by setting
|
|
646
|
+
// `window.__MARIMO_HAS_WASM_CONTROLLER__ = true` before
|
|
647
|
+
// PyodideBridge/worker initialization.
|
|
648
|
+
export function getWasmWorkerName(): string {
|
|
649
|
+
const hasCustomController =
|
|
650
|
+
typeof window !== "undefined" &&
|
|
651
|
+
(window as unknown as { __MARIMO_HAS_WASM_CONTROLLER__?: boolean })
|
|
652
|
+
.__MARIMO_HAS_WASM_CONTROLLER__ === true;
|
|
653
|
+
return getMarimoVersion() + (hasCustomController ? "::controller" : "");
|
|
654
|
+
}
|
package/src/core/wasm/state.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { isOutputEmpty } from "../cells/outputs";
|
|
|
5
5
|
|
|
6
6
|
export const wasmInitializationAtom = atom<string>("Initializing...");
|
|
7
7
|
|
|
8
|
+
export type WasmInitStatus = "loading" | "ready" | "error";
|
|
9
|
+
export const wasmInitStatusAtom = atom<WasmInitStatus>("loading");
|
|
10
|
+
|
|
8
11
|
export const hasAnyOutputAtom = atom<boolean>((get) => {
|
|
9
12
|
const notebook = get(notebookAtom);
|
|
10
13
|
const runtimeStates = Object.values(notebook.cellRuntime);
|
|
@@ -5,6 +5,13 @@ import type { WasmController } from "./types";
|
|
|
5
5
|
// Load the controller
|
|
6
6
|
// Falls back to the default controller
|
|
7
7
|
export async function getController(version: string): Promise<WasmController> {
|
|
8
|
+
// Hosts that provide a custom /wasm/controller.js opt in via the worker
|
|
9
|
+
// name (see bridge.ts). Default: skip the dynamic import to avoid a
|
|
10
|
+
// guaranteed-404 round trip on the standard build.
|
|
11
|
+
const hasCustomController = self.name?.includes("::controller") ?? false;
|
|
12
|
+
if (!hasCustomController) {
|
|
13
|
+
return new DefaultWasmController();
|
|
14
|
+
}
|
|
8
15
|
try {
|
|
9
16
|
const controller = await import(
|
|
10
17
|
/* @vite-ignore */ `/wasm/controller.js?version=${version}`
|
|
@@ -103,5 +103,6 @@ const rpc = createRPC<SaveWorkerSchema, ParentSchema>({
|
|
|
103
103
|
rpc.send("ready", {});
|
|
104
104
|
|
|
105
105
|
function getMarimoVersion() {
|
|
106
|
-
|
|
106
|
+
// Worker name is "<version>" or "<version>::<capability>" — see bridge.ts.
|
|
107
|
+
return self.name.split("::")[0];
|
|
107
108
|
}
|
|
@@ -375,7 +375,8 @@ const namesThatRequireSync = new Set<keyof RawBridge>([
|
|
|
375
375
|
]);
|
|
376
376
|
|
|
377
377
|
function getMarimoVersion() {
|
|
378
|
-
|
|
378
|
+
// Worker name is "<version>" or "<version>::<capability>" — see bridge.ts.
|
|
379
|
+
return self.name.split("::")[0];
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
const pyodideReadyPromise = t.wrapAsync(loadPyodideAndPackages)();
|
|
@@ -6,6 +6,7 @@ import parse, {
|
|
|
6
6
|
type HTMLReactParserOptions,
|
|
7
7
|
} from "html-react-parser";
|
|
8
8
|
import React, {
|
|
9
|
+
cloneElement,
|
|
9
10
|
isValidElement,
|
|
10
11
|
type JSX,
|
|
11
12
|
type ReactNode,
|
|
@@ -169,6 +170,35 @@ const addCopyButtonToCodehilite: TransformFn = (
|
|
|
169
170
|
}
|
|
170
171
|
};
|
|
171
172
|
|
|
173
|
+
// Decorator (not a match-and-replace transform): applies a src-based key
|
|
174
|
+
// to <img> elements so they remount on src change. Reusing an <img> across
|
|
175
|
+
// src changes can leave the previous image painted (e.g. when the new
|
|
176
|
+
// request is slow/blocked, served stale by a CDN, or fails CORS), so the
|
|
177
|
+
// user sees the old image even though the HTML source is up to date.
|
|
178
|
+
//
|
|
179
|
+
// Runs unconditionally after the match-and-replace transforms so it still
|
|
180
|
+
// applies when an <img> was already wrapped by, say, wrapTooltipTargets.
|
|
181
|
+
const keyImagesBySrc: TransformFn = (
|
|
182
|
+
reactNode: ReactNode,
|
|
183
|
+
domNode: DOMNode,
|
|
184
|
+
index: number,
|
|
185
|
+
): JSX.Element | undefined => {
|
|
186
|
+
if (!(domNode instanceof Element) || domNode.name !== "img") {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const src = domNode.attribs?.src;
|
|
190
|
+
if (!src || !isValidElement(reactNode)) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
// data: URIs are inline — no network fetch — so they can't go stale.
|
|
194
|
+
// Skip to avoid bloating the React key with a megabyte base64 payload.
|
|
195
|
+
// URI schemes are case-insensitive per RFC 3986.
|
|
196
|
+
if (/^data:/i.test(src)) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
return cloneElement(reactNode, { key: `${src}-${index}` });
|
|
200
|
+
};
|
|
201
|
+
|
|
172
202
|
// Wrap elements with data-marimo-doc attribute in a DocHoverTarget
|
|
173
203
|
const wrapDocHoverTargets: TransformFn = (
|
|
174
204
|
reactNode: ReactNode,
|
|
@@ -281,6 +311,8 @@ function parseHtml({
|
|
|
281
311
|
...additionalReplacements,
|
|
282
312
|
];
|
|
283
313
|
|
|
314
|
+
// Match-and-replace transforms: the first one that returns a value wins
|
|
315
|
+
// (short-circuits the rest).
|
|
284
316
|
const transformFunctions: TransformFn[] = [
|
|
285
317
|
addCopyButtonToCodehilite,
|
|
286
318
|
preserveQueryParamsInAnchorLinks,
|
|
@@ -290,6 +322,12 @@ function parseHtml({
|
|
|
290
322
|
removeWrappingHtmlTags,
|
|
291
323
|
];
|
|
292
324
|
|
|
325
|
+
// Decorators: run unconditionally on the result of the transform pipeline
|
|
326
|
+
// and may further wrap/clone it. Used for cross-cutting concerns that
|
|
327
|
+
// should apply regardless of which (if any) match-and-replace transform
|
|
328
|
+
// ran above.
|
|
329
|
+
const decoratorFunctions: TransformFn[] = [keyImagesBySrc];
|
|
330
|
+
|
|
293
331
|
return parse(html, {
|
|
294
332
|
replace: (domNode: DOMNode, index: number) => {
|
|
295
333
|
for (const renderFunction of renderFunctions) {
|
|
@@ -301,13 +339,21 @@ function parseHtml({
|
|
|
301
339
|
return domNode;
|
|
302
340
|
},
|
|
303
341
|
transform: (reactNode: ReactNode, domNode: DOMNode, index: number) => {
|
|
342
|
+
let result: ReactNode = reactNode as JSX.Element;
|
|
304
343
|
for (const transformFunction of transformFunctions) {
|
|
305
|
-
const transformed = transformFunction(
|
|
344
|
+
const transformed = transformFunction(result, domNode, index);
|
|
306
345
|
if (transformed) {
|
|
307
|
-
|
|
346
|
+
result = transformed;
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const decorate of decoratorFunctions) {
|
|
351
|
+
const decorated = decorate(result, domNode, index);
|
|
352
|
+
if (decorated) {
|
|
353
|
+
result = decorated;
|
|
308
354
|
}
|
|
309
355
|
}
|
|
310
|
-
return
|
|
356
|
+
return result as JSX.Element;
|
|
311
357
|
},
|
|
312
358
|
});
|
|
313
359
|
}
|
|
@@ -60,6 +60,60 @@ describe("parseHtml", () => {
|
|
|
60
60
|
`);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("img has key derived from src so React remounts on src change", () => {
|
|
64
|
+
const html = '<img src="https://cdn.example.com/a.png" alt="a">';
|
|
65
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
66
|
+
expect(result.key).toBe("https://cdn.example.com/a.png-0");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("multiple imgs each get distinct keys", () => {
|
|
70
|
+
const html =
|
|
71
|
+
'<div><img src="https://cdn.example.com/a.png"><img src="https://cdn.example.com/b.png"></div>';
|
|
72
|
+
const result = parseHtml({ html }) as React.ReactElement<{
|
|
73
|
+
children: React.ReactElement[];
|
|
74
|
+
}>;
|
|
75
|
+
const children = result.props.children;
|
|
76
|
+
expect(children[0].key).toBe("https://cdn.example.com/a.png-0");
|
|
77
|
+
expect(children[1].key).toBe("https://cdn.example.com/b.png-1");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("img without src is left alone", () => {
|
|
81
|
+
const html = "<img>";
|
|
82
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
83
|
+
expect(result.key).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("img with data: URI is not keyed (inline, no network fetch)", () => {
|
|
87
|
+
const longPayload = "A".repeat(10_000);
|
|
88
|
+
const html = `<img src="data:image/png;base64,${longPayload}">`;
|
|
89
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
90
|
+
// No remount-on-src needed for inline images, so we leave the key
|
|
91
|
+
// unset rather than bloat it with the base64 payload.
|
|
92
|
+
expect(result.key).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("img with uppercase DATA: URI is also skipped (scheme is case-insensitive)", () => {
|
|
96
|
+
const html = `<img src="DATA:image/png;base64,${"A".repeat(100)}">`;
|
|
97
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
98
|
+
expect(result.key).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("img wrapped by data-tooltip is still keyed by src", () => {
|
|
102
|
+
const html =
|
|
103
|
+
'<img src="https://cdn.example.com/a.png" data-tooltip="hi" alt="a">';
|
|
104
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
105
|
+
// Outer Tooltip carries the src-based key so it remounts on src change,
|
|
106
|
+
// forcing the inner <img> to remount as well.
|
|
107
|
+
expect(result.key).toBe("https://cdn.example.com/a.png-0");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("img wrapped by data-marimo-doc is still keyed by src", () => {
|
|
111
|
+
const html =
|
|
112
|
+
'<img src="https://cdn.example.com/b.png" data-marimo-doc="foo.bar">';
|
|
113
|
+
const result = parseHtml({ html }) as React.ReactElement;
|
|
114
|
+
expect(result.key).toBe("https://cdn.example.com/b.png-0");
|
|
115
|
+
});
|
|
116
|
+
|
|
63
117
|
test("codehilite with copy button", () => {
|
|
64
118
|
const html =
|
|
65
119
|
'<div class="codehilite"><pre><code>console.log("Hello");</code></pre></div>';
|
|
@@ -44,7 +44,7 @@ export const Labeled: React.FC<PropsWithChildren<Props>> = ({
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const labelElement = (
|
|
47
|
-
<div part="label" className="m-0 p-0">
|
|
47
|
+
<div part="label" className="inline-flex items-center m-0 p-0">
|
|
48
48
|
<Label htmlFor={id} className={cn("font-prose", labelClassName)}>
|
|
49
49
|
{renderHTML({ html: label })}
|
|
50
50
|
</Label>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./worker-D-EdLKct.js";var t=e(((e,t)=>{t.exports={}}));export default t();
|