@marimo-team/frontend 0.19.7-dev41 → 0.19.7-dev45
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/{CellStatus-DhGipVU-.js → CellStatus-BFim6YKS.js} +1 -1
- package/dist/assets/{JsonOutput-B3se7Juu.js → JsonOutput-DkVhF91W.js} +1 -1
- package/dist/assets/{MarimoErrorOutput-Cci2wITc.js → MarimoErrorOutput-DWQ55i9r.js} +1 -1
- package/dist/assets/{RenderHTML-Co6iQEvR.js → RenderHTML-a-roMXlk.js} +1 -1
- package/dist/assets/{add-cell-with-ai-LP5p0BtF.js → add-cell-with-ai-BR7IVCSW.js} +1 -1
- package/dist/assets/{add-database-form-_pTLtiHN.js → add-database-form-ClfjsEtC.js} +1 -1
- package/dist/assets/{agent-panel-DreQAGPX.js → agent-panel-DSORiKrx.js} +1 -1
- package/dist/assets/{ai-model-dropdown-CJGHNqP_.js → ai-model-dropdown-JY5c7nJK.js} +1 -1
- package/dist/assets/{app-config-button-DDLHriXt.js → app-config-button-Jz087HD-.js} +1 -1
- package/dist/assets/{cell-editor-Bio_oQvd.js → cell-editor-B6FY0jdx.js} +2 -2
- package/dist/assets/{cell-link-_-mIiddP.js → cell-link-D2K7clF4.js} +1 -1
- package/dist/assets/{cells-BW_4R0Qw.js → cells-Dxpbusw1.js} +37 -37
- package/dist/assets/{chat-components-ZGfi_TyH.js → chat-components-B6qBxIzc.js} +1 -1
- package/dist/assets/{chat-display-C7-tFQht.js → chat-display-CPulKLCm.js} +1 -1
- package/dist/assets/{chat-panel-CfvDUHSP.js → chat-panel-COGD6xs1.js} +1 -1
- package/dist/assets/{column-preview-DpVMSlSk.js → column-preview-D3y9pwSu.js} +1 -1
- package/dist/assets/{command-B5H3BrRg.js → command-BqcYTnlg.js} +1 -1
- package/dist/assets/{command-palette-B0A78_e0.js → command-palette-BaR-SOA3.js} +1 -1
- package/dist/assets/{common-DxKcMlJZ.js → common-DuZx4dBP.js} +1 -1
- package/dist/assets/{datasource-CpcDqjf_.js → datasource-BA7iQQaN.js} +1 -1
- package/dist/assets/{dependency-graph-panel-CLjSJI0K.js → dependency-graph-panel-C27DHbPF.js} +1 -1
- package/dist/assets/{documentation-panel-BK_YuaI4.js → documentation-panel-COvuCjB2.js} +1 -1
- package/dist/assets/download-DZGL4UAN.js +9 -0
- package/dist/assets/{edit-page-CXtTEw1V.js → edit-page-CoNmmXUy.js} +3 -3
- package/dist/assets/{error-panel-DulelhA-.js → error-panel-BHPn8JVQ.js} +1 -1
- package/dist/assets/{file-explorer-panel-B7hxTwzM.js → file-explorer-panel-CozR65iO.js} +1 -1
- package/dist/assets/{floating-outline-Ni_RT38T.js → floating-outline-CY6lTY5s.js} +1 -1
- package/dist/assets/{focus-CsiV5LZR.js → focus-BLcChWyg.js} +1 -1
- package/dist/assets/{form-B1n-e_X0.js → form-CKnYm6ml.js} +1 -1
- package/dist/assets/{globals-ols5LsZP.js → globals-Dd-Dr3p2.js} +1 -1
- package/dist/assets/{home-page-MttpX4Nz.js → home-page-Ds6etoNv.js} +1 -1
- package/dist/assets/{hooks-DaqNvfLy.js → hooks-B1vW9UPB.js} +1 -1
- package/dist/assets/{html-to-image-BVbOFO17.js → html-to-image-DSMiAoU4.js} +1 -1
- package/dist/assets/{index-XWeGeEmy.js → index-B_qRlPME.js} +3 -3
- package/dist/assets/{kiosk-mode-vMIC4Cbh.js → kiosk-mode-x5QGVyC6.js} +1 -1
- package/dist/assets/{layout-CrAbikmI.js → layout-BVZJ0wNL.js} +1 -1
- package/dist/assets/{logs-panel-DeNFSwJb.js → logs-panel-W6lnhN9B.js} +1 -1
- package/dist/assets/{markdown-renderer-YZb6nuwv.js → markdown-renderer-Bx2DfaSd.js} +1 -1
- package/dist/assets/{mode-BCr7l3Th.js → mode-BQL1qb7F.js} +1 -1
- package/dist/assets/{name-cell-input-D6uaGwg7.js → name-cell-input-B3FIAxBj.js} +1 -1
- package/dist/assets/{outline-panel-DpbnlPhw.js → outline-panel-AJlQUjZ_.js} +1 -1
- package/dist/assets/{packages-panel-BkOmsLKm.js → packages-panel-BJsvZ-68.js} +1 -1
- package/dist/assets/{panels-Khoe5vWe.js → panels-CZFKs6IJ.js} +1 -1
- package/dist/assets/{process-output-TMO5uCHT.js → process-output-DK8a3ADA.js} +1 -1
- package/dist/assets/{readonly-python-code-Dv4ZvZ6I.js → readonly-python-code-tAbt2ya8.js} +1 -1
- package/dist/assets/{run-page-D7t4ZVs2.js → run-page-BpOFopIQ.js} +1 -1
- package/dist/assets/{scratchpad-panel-BzDqHgmy.js → scratchpad-panel-DfCsTT2f.js} +1 -1
- package/dist/assets/{session-panel-C9DYV18k.js → session-panel-Dd73AAIv.js} +1 -1
- package/dist/assets/{snippets-panel-ypIwat9G.js → snippets-panel-HEdYHO0z.js} +1 -1
- package/dist/assets/{state-BrsyJBHJ.js → state-BMdr2nMy.js} +1 -1
- package/dist/assets/{switch-Cch0bLxo.js → switch-uE7-SGTN.js} +1 -1
- package/dist/assets/{textarea-DSR2T2ft.js → textarea-Dnc9oNCJ.js} +1 -1
- package/dist/assets/{tracing-vnJxVAtK.js → tracing-CsZXkSMG.js} +1 -1
- package/dist/assets/{tracing-panel-57WGQhyd.js → tracing-panel-DzL1Eutt.js} +2 -2
- package/dist/assets/{types-fTSozrNJ.js → types-BjS1POBx.js} +1 -1
- package/dist/assets/{useAddCell-CLnCtSpX.js → useAddCell-fK8ZFPPQ.js} +1 -1
- package/dist/assets/{useCellActionButton-B1CEV7yf.js → useCellActionButton-BBXD0ZNf.js} +1 -1
- package/dist/assets/{useDeleteCell-CR3IczUk.js → useDeleteCell-BFvljUOH.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CJoiLtsW.js → useDependencyPanelTab-4_zo1zmo.js} +1 -1
- package/dist/assets/{useNotebookActions-BG12cF9Q.js → useNotebookActions---KuaMmt.js} +1 -1
- package/dist/assets/{useRunCells-C50mliNM.js → useRunCells-ChluEx0W.js} +1 -1
- package/dist/assets/{useSplitCell-IQsKBoRj.js → useSplitCell-BYSgH1JP.js} +1 -1
- package/dist/assets/{utilities.esm-DK107uYJ.js → utilities.esm-CgPvBw6L.js} +1 -1
- package/dist/index.html +31 -31
- package/package.json +1 -1
- package/src/core/ai/tools/__tests__/edit-notebook-tool.test.ts +1 -0
- package/src/core/codemirror/cm.ts +1 -0
- package/src/core/codemirror/config/extension.ts +3 -0
- package/src/core/codemirror/copilot/__tests__/getCodes.test.ts +1 -0
- package/src/core/codemirror/facet.ts +4 -1
- package/src/core/codemirror/language/__tests__/extension.test.ts +1 -0
- package/src/core/codemirror/language/__tests__/utils.test.ts +1 -0
- package/src/core/codemirror/language/extension.ts +12 -0
- package/src/utils/__tests__/download.test.tsx +89 -45
- package/src/utils/__tests__/iframe.test.ts +9 -14
- package/src/utils/download.ts +37 -44
- package/src/utils/iframe.ts +3 -10
- package/dist/assets/download-BN9map2G.js +0 -9
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
LSPConfig,
|
|
16
16
|
} from "@/core/config/config-schema";
|
|
17
17
|
import type { HotkeyProvider } from "@/core/hotkeys/hotkeys";
|
|
18
|
+
import { Logger } from "@/utils/Logger";
|
|
18
19
|
import { clamp } from "@/utils/math";
|
|
19
20
|
import {
|
|
20
21
|
cellIdState,
|
|
@@ -310,6 +311,17 @@ export function reconfigureLanguageEffect(
|
|
|
310
311
|
const language = view.state.field(languageAdapterState);
|
|
311
312
|
const placeholderType = view.state.facet(placeholderState);
|
|
312
313
|
const cellId = view.state.facet(cellIdState);
|
|
314
|
+
|
|
315
|
+
if (cellId === undefined) {
|
|
316
|
+
Logger.error("Cell ID is undefined in reconfigureLanguageEffect");
|
|
317
|
+
}
|
|
318
|
+
if (placeholderType === undefined) {
|
|
319
|
+
Logger.error("Placeholder type is undefined in reconfigureLanguageEffect");
|
|
320
|
+
}
|
|
321
|
+
if (completionConfig === undefined) {
|
|
322
|
+
Logger.error("Completion config is undefined in reconfigureLanguageEffect");
|
|
323
|
+
}
|
|
324
|
+
|
|
313
325
|
return languageCompartment.reconfigure(
|
|
314
326
|
language.getExtension(
|
|
315
327
|
cellId,
|
|
@@ -166,13 +166,63 @@ describe("getImageDataUrlForCell", () => {
|
|
|
166
166
|
);
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
it("should
|
|
169
|
+
it("should pass style options to prevent clipping", async () => {
|
|
170
|
+
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
171
|
+
|
|
172
|
+
await getImageDataUrlForCell("cell-1" as CellId);
|
|
173
|
+
|
|
174
|
+
expect(toPng).toHaveBeenCalledWith(
|
|
175
|
+
mockElement,
|
|
176
|
+
expect.objectContaining({
|
|
177
|
+
style: {
|
|
178
|
+
maxHeight: "none",
|
|
179
|
+
overflow: "visible",
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should pass scrollHeight as height option", async () => {
|
|
186
|
+
// Set up element with scrollHeight
|
|
187
|
+
Object.defineProperty(mockElement, "scrollHeight", {
|
|
188
|
+
value: 500,
|
|
189
|
+
configurable: true,
|
|
190
|
+
});
|
|
191
|
+
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
192
|
+
|
|
193
|
+
await getImageDataUrlForCell("cell-1" as CellId);
|
|
194
|
+
|
|
195
|
+
expect(toPng).toHaveBeenCalledWith(
|
|
196
|
+
mockElement,
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
height: 500,
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should pass scrollbar hiding styles via extraStyleContent", async () => {
|
|
204
|
+
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
205
|
+
|
|
206
|
+
await getImageDataUrlForCell("cell-1" as CellId);
|
|
207
|
+
|
|
208
|
+
expect(toPng).toHaveBeenCalledWith(
|
|
209
|
+
mockElement,
|
|
210
|
+
expect.objectContaining({
|
|
211
|
+
extraStyleContent: expect.stringContaining("scrollbar-width: none"),
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should not modify the live DOM element", async () => {
|
|
170
217
|
mockElement.style.overflow = "hidden";
|
|
218
|
+
mockElement.style.maxHeight = "100px";
|
|
171
219
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
172
220
|
|
|
173
221
|
await getImageDataUrlForCell("cell-1" as CellId);
|
|
174
222
|
|
|
223
|
+
// DOM should remain unchanged
|
|
175
224
|
expect(mockElement.style.overflow).toBe("hidden");
|
|
225
|
+
expect(mockElement.style.maxHeight).toBe("100px");
|
|
176
226
|
});
|
|
177
227
|
|
|
178
228
|
it("should throw error on failure", async () => {
|
|
@@ -183,34 +233,20 @@ describe("getImageDataUrlForCell", () => {
|
|
|
183
233
|
);
|
|
184
234
|
});
|
|
185
235
|
|
|
186
|
-
it("should cleanup even on failure", async () => {
|
|
187
|
-
mockElement.style.overflow = "scroll";
|
|
188
|
-
vi.mocked(toPng).mockRejectedValue(new Error("Capture failed"));
|
|
189
|
-
|
|
190
|
-
await expect(getImageDataUrlForCell("cell-1" as CellId)).rejects.toThrow();
|
|
191
|
-
|
|
192
|
-
expect(document.body.classList.contains("printing")).toBe(false);
|
|
193
|
-
expect(mockElement.style.overflow).toBe("scroll");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
236
|
it("should handle concurrent captures correctly", async () => {
|
|
197
237
|
// Create a second element
|
|
198
238
|
const mockElement2 = document.createElement("div");
|
|
199
239
|
mockElement2.id = CellOutputId.create("cell-2" as CellId);
|
|
200
240
|
document.body.append(mockElement2);
|
|
201
241
|
|
|
202
|
-
vi.mocked(toPng).
|
|
203
|
-
// body.printing should not be added during cell captures
|
|
204
|
-
expect(document.body.classList.contains("printing")).toBe(false);
|
|
205
|
-
return mockDataUrl;
|
|
206
|
-
});
|
|
242
|
+
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
207
243
|
|
|
208
244
|
const capture1 = getImageDataUrlForCell("cell-1" as CellId);
|
|
209
245
|
const capture2 = getImageDataUrlForCell("cell-2" as CellId);
|
|
210
246
|
|
|
211
247
|
await Promise.all([capture1, capture2]);
|
|
212
248
|
|
|
213
|
-
expect(
|
|
249
|
+
expect(toPng).toHaveBeenCalledTimes(2);
|
|
214
250
|
|
|
215
251
|
mockElement2.remove();
|
|
216
252
|
});
|
|
@@ -266,23 +302,6 @@ describe("downloadHTMLAsImage", () => {
|
|
|
266
302
|
expect(mockAnchor.click).toHaveBeenCalled();
|
|
267
303
|
});
|
|
268
304
|
|
|
269
|
-
it("should add body.printing class without prepare function", async () => {
|
|
270
|
-
vi.mocked(toPng).mockImplementation(async () => {
|
|
271
|
-
expect(document.body.classList.contains("printing")).toBe(true);
|
|
272
|
-
return mockDataUrl;
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
await downloadHTMLAsImage({ element: mockElement, filename: "test" });
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("should remove body.printing class after download without prepare", async () => {
|
|
279
|
-
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
280
|
-
|
|
281
|
-
await downloadHTMLAsImage({ element: mockElement, filename: "test" });
|
|
282
|
-
|
|
283
|
-
expect(document.body.classList.contains("printing")).toBe(false);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
305
|
it("should use prepare function when provided", async () => {
|
|
287
306
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
288
307
|
const cleanup = vi.fn();
|
|
@@ -382,13 +401,30 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
382
401
|
vi.restoreAllMocks();
|
|
383
402
|
});
|
|
384
403
|
|
|
385
|
-
it("should
|
|
404
|
+
it("should show error toast if element not found", async () => {
|
|
386
405
|
await downloadCellOutputAsImage("nonexistent" as CellId, "test");
|
|
387
406
|
|
|
388
407
|
expect(toPng).not.toHaveBeenCalled();
|
|
389
408
|
expect(Logger.error).toHaveBeenCalledWith(
|
|
390
409
|
"Output element not found for cell nonexistent",
|
|
391
410
|
);
|
|
411
|
+
expect(toast).toHaveBeenCalledWith({
|
|
412
|
+
title: "Failed to download PNG",
|
|
413
|
+
description: expect.any(String),
|
|
414
|
+
variant: "danger",
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should show error toast if toPng fails", async () => {
|
|
419
|
+
vi.mocked(toPng).mockRejectedValue(new Error("Screenshot failed"));
|
|
420
|
+
|
|
421
|
+
await downloadCellOutputAsImage("cell-1" as CellId, "result");
|
|
422
|
+
|
|
423
|
+
expect(toast).toHaveBeenCalledWith({
|
|
424
|
+
title: "Failed to download PNG",
|
|
425
|
+
description: expect.stringContaining("Screenshot failed"),
|
|
426
|
+
variant: "danger",
|
|
427
|
+
});
|
|
392
428
|
});
|
|
393
429
|
|
|
394
430
|
it("should download cell output as image", async () => {
|
|
@@ -406,24 +442,32 @@ describe("downloadCellOutputAsImage", () => {
|
|
|
406
442
|
expect(mockAnchor.download).toBe("result.png");
|
|
407
443
|
});
|
|
408
444
|
|
|
409
|
-
it("should
|
|
410
|
-
vi.mocked(toPng).
|
|
411
|
-
// Check that cell-specific classes are applied
|
|
412
|
-
expect(mockElement.style.overflow).toBe("visible");
|
|
413
|
-
return mockDataUrl;
|
|
414
|
-
});
|
|
445
|
+
it("should pass style options to toPng for full content capture", async () => {
|
|
446
|
+
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
415
447
|
|
|
416
448
|
await downloadCellOutputAsImage("cell-1" as CellId, "result");
|
|
449
|
+
|
|
450
|
+
expect(toPng).toHaveBeenCalledWith(
|
|
451
|
+
mockElement,
|
|
452
|
+
expect.objectContaining({
|
|
453
|
+
style: {
|
|
454
|
+
maxHeight: "none",
|
|
455
|
+
overflow: "visible",
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
417
459
|
});
|
|
418
460
|
|
|
419
|
-
it("should
|
|
420
|
-
mockElement.style.overflow = "
|
|
461
|
+
it("should not modify the live DOM element", async () => {
|
|
462
|
+
mockElement.style.overflow = "hidden";
|
|
463
|
+
mockElement.style.maxHeight = "100px";
|
|
421
464
|
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
422
465
|
|
|
423
466
|
await downloadCellOutputAsImage("cell-1" as CellId, "result");
|
|
424
467
|
|
|
425
|
-
|
|
426
|
-
expect(mockElement.style.overflow).toBe("
|
|
468
|
+
// DOM should remain unchanged
|
|
469
|
+
expect(mockElement.style.overflow).toBe("hidden");
|
|
470
|
+
expect(mockElement.style.maxHeight).toBe("100px");
|
|
427
471
|
});
|
|
428
472
|
});
|
|
429
473
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
3
|
+
import { captureExternalIframes } from "../iframe";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
vi.mock("html-to-image", () => ({
|
|
7
|
-
toPng: vi.fn().mockResolvedValue("data:image/png;base64,mock"),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
describe("captureIframeAsImage", () => {
|
|
5
|
+
describe("captureExternalIframes", () => {
|
|
11
6
|
const originalCreateElement = document.createElement.bind(document);
|
|
12
7
|
|
|
13
8
|
beforeEach(() => {
|
|
@@ -53,7 +48,7 @@ describe("captureIframeAsImage", () => {
|
|
|
53
48
|
const element = document.createElement("div");
|
|
54
49
|
element.innerHTML = "<p>No iframe here</p>";
|
|
55
50
|
|
|
56
|
-
const result = await
|
|
51
|
+
const result = await captureExternalIframes(element);
|
|
57
52
|
expect(result).toBeNull();
|
|
58
53
|
});
|
|
59
54
|
|
|
@@ -61,7 +56,7 @@ describe("captureIframeAsImage", () => {
|
|
|
61
56
|
const element = document.createElement("div");
|
|
62
57
|
element.innerHTML = '<iframe src="https://external.com/page"></iframe>';
|
|
63
58
|
|
|
64
|
-
const result = await
|
|
59
|
+
const result = await captureExternalIframes(element);
|
|
65
60
|
|
|
66
61
|
expect(result).not.toBeNull();
|
|
67
62
|
expect(result).toMatch(/^data:image\/png;base64,/);
|
|
@@ -72,7 +67,7 @@ describe("captureIframeAsImage", () => {
|
|
|
72
67
|
element.innerHTML =
|
|
73
68
|
'<iframe src="https://www.openstreetmap.org/export/embed.html"></iframe>';
|
|
74
69
|
|
|
75
|
-
const result = await
|
|
70
|
+
const result = await captureExternalIframes(element);
|
|
76
71
|
|
|
77
72
|
expect(result).not.toBeNull();
|
|
78
73
|
expect(result).toMatch(/^data:image\/png;base64,/);
|
|
@@ -85,7 +80,7 @@ describe("captureIframeAsImage", () => {
|
|
|
85
80
|
element.append(iframe);
|
|
86
81
|
|
|
87
82
|
// The iframe has no body accessible in jsdom
|
|
88
|
-
const result = await
|
|
83
|
+
const result = await captureExternalIframes(element);
|
|
89
84
|
|
|
90
85
|
// In jsdom, contentDocument may not be accessible
|
|
91
86
|
expect(result).toBeNull();
|
|
@@ -96,7 +91,7 @@ describe("captureIframeAsImage", () => {
|
|
|
96
91
|
element.innerHTML = '<iframe src="./@file/123.html"></iframe>';
|
|
97
92
|
|
|
98
93
|
// Same-origin relative URL, but contentDocument not accessible in jsdom
|
|
99
|
-
const result = await
|
|
94
|
+
const result = await captureExternalIframes(element);
|
|
100
95
|
|
|
101
96
|
// Returns null because jsdom can't access contentDocument for file URLs
|
|
102
97
|
expect(result).toBeNull();
|
|
@@ -113,7 +108,7 @@ describe("captureIframeAsImage", () => {
|
|
|
113
108
|
const element = document.createElement("div");
|
|
114
109
|
element.innerHTML = `<iframe src="${url}"></iframe>`;
|
|
115
110
|
|
|
116
|
-
const result = await
|
|
111
|
+
const result = await captureExternalIframes(element);
|
|
117
112
|
|
|
118
113
|
expect(result).not.toBeNull();
|
|
119
114
|
expect(result).toMatch(/^data:image\/png;base64,/);
|
|
@@ -128,7 +123,7 @@ describe("captureIframeAsImage", () => {
|
|
|
128
123
|
const element = document.createElement("div");
|
|
129
124
|
element.innerHTML = `<iframe src="${url}"></iframe>`;
|
|
130
125
|
|
|
131
|
-
const result = await
|
|
126
|
+
const result = await captureExternalIframes(element);
|
|
132
127
|
|
|
133
128
|
// In jsdom, these return null because contentDocument isn't accessible
|
|
134
129
|
// but they should NOT return a placeholder (which would indicate external detection)
|
package/src/utils/download.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Filenames } from "@/utils/filenames";
|
|
|
8
8
|
import { Paths } from "@/utils/paths";
|
|
9
9
|
import { prettyError } from "./errors";
|
|
10
10
|
import { toPng } from "./html-to-image";
|
|
11
|
-
import {
|
|
11
|
+
import { captureExternalIframes } from "./iframe";
|
|
12
12
|
import { Logger } from "./Logger";
|
|
13
13
|
import { ProgressState } from "./progress";
|
|
14
14
|
import { ToastProgress } from "./toast-progress";
|
|
@@ -46,24 +46,6 @@ function findElementForCell(cellId: CellId): HTMLElement | undefined {
|
|
|
46
46
|
return element;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* Prepare a cell element for screenshot capture.
|
|
51
|
-
*
|
|
52
|
-
* @param element - The cell output element to prepare
|
|
53
|
-
* @returns A cleanup function to restore the element's original state
|
|
54
|
-
*/
|
|
55
|
-
function prepareCellElementForScreenshot(element: HTMLElement) {
|
|
56
|
-
const originalOverflow = element.style.overflow;
|
|
57
|
-
const maxHeight = element.style.maxHeight;
|
|
58
|
-
element.style.overflow = "visible";
|
|
59
|
-
element.style.maxHeight = "none";
|
|
60
|
-
|
|
61
|
-
return () => {
|
|
62
|
-
element.style.overflow = originalOverflow;
|
|
63
|
-
element.style.maxHeight = maxHeight;
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
49
|
const THRESHOLD_TIME_MS = 500;
|
|
68
50
|
const HIDE_SCROLLBAR_STYLES = `
|
|
69
51
|
* { scrollbar-width: none; -ms-overflow-style: none; }
|
|
@@ -84,30 +66,33 @@ export async function getImageDataUrlForCell(
|
|
|
84
66
|
return;
|
|
85
67
|
}
|
|
86
68
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
// TODO: This doesn't handle external iframes + normal elements together (eg. in vstack).
|
|
70
|
+
// It will return the iframe only
|
|
71
|
+
const externalIframeDataUrl = await captureExternalIframes(element);
|
|
72
|
+
if (externalIframeDataUrl) {
|
|
73
|
+
return externalIframeDataUrl;
|
|
90
74
|
}
|
|
91
75
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
const dataUrl = await toPng(element, {
|
|
78
|
+
extraStyleContent: HIDE_SCROLLBAR_STYLES,
|
|
79
|
+
// Add these styles so the element output is not clipped
|
|
80
|
+
// Width can be clipped since pdf has limited width
|
|
81
|
+
style: {
|
|
82
|
+
maxHeight: "none",
|
|
83
|
+
overflow: "visible",
|
|
84
|
+
},
|
|
85
|
+
height: element.scrollHeight,
|
|
86
|
+
});
|
|
87
|
+
const timeTaken = Date.now() - startTime;
|
|
88
|
+
if (timeTaken > THRESHOLD_TIME_MS) {
|
|
89
|
+
Logger.debug(
|
|
90
|
+
"toPng operation for element",
|
|
91
|
+
element,
|
|
92
|
+
`took ${timeTaken} ms (exceeds threshold)`,
|
|
93
|
+
);
|
|
110
94
|
}
|
|
95
|
+
return dataUrl;
|
|
111
96
|
}
|
|
112
97
|
|
|
113
98
|
/**
|
|
@@ -117,11 +102,19 @@ export async function downloadCellOutputAsImage(
|
|
|
117
102
|
cellId: CellId,
|
|
118
103
|
filename: string,
|
|
119
104
|
) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
105
|
+
try {
|
|
106
|
+
const dataUrl = await getImageDataUrlForCell(cellId);
|
|
107
|
+
if (!dataUrl) {
|
|
108
|
+
throw new Error("Failed to get image data URL");
|
|
109
|
+
}
|
|
110
|
+
downloadByURL(dataUrl, Filenames.toPNG(filename));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
toast({
|
|
113
|
+
title: "Failed to download PNG",
|
|
114
|
+
description: prettyError(error),
|
|
115
|
+
variant: "danger",
|
|
116
|
+
});
|
|
123
117
|
}
|
|
124
|
-
downloadByURL(dataUrl, Filenames.toPNG(filename));
|
|
125
118
|
}
|
|
126
119
|
|
|
127
120
|
export const ADD_PRINTING_CLASS = (): (() => void) => {
|
package/src/utils/iframe.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { toPng } from "./html-to-image";
|
|
4
|
-
|
|
5
3
|
const PLACEHOLDER_WIDTH = 320;
|
|
6
4
|
const PLACEHOLDER_HEIGHT = 180;
|
|
7
5
|
|
|
@@ -95,11 +93,11 @@ function createPlaceholderImage(url: string | null): string {
|
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
/**
|
|
98
|
-
* Capture
|
|
96
|
+
* Capture external iframes as a PNG image. External iframes are not supported by html-to-image.
|
|
99
97
|
* @param element - The element to capture the iframe from
|
|
100
98
|
* @returns The image data URL of the iframe, or a placeholder image if the iframe is external
|
|
101
99
|
*/
|
|
102
|
-
export async function
|
|
100
|
+
export async function captureExternalIframes(
|
|
103
101
|
element: HTMLElement,
|
|
104
102
|
): Promise<string | null> {
|
|
105
103
|
const iframe = element.querySelector("iframe");
|
|
@@ -133,10 +131,5 @@ export async function captureIframeAsImage(
|
|
|
133
131
|
}
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
return await toPng(doc.body);
|
|
139
|
-
} catch {
|
|
140
|
-
return createPlaceholderImage(null);
|
|
141
|
-
}
|
|
134
|
+
return null;
|
|
142
135
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
var ie=Object.defineProperty;var le=(t,e,r)=>e in t?ie(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var D=(t,e,r)=>le(t,typeof e!="symbol"?e+"":e,r);import{s as I,t as V}from"./chunk-LvLJmgfZ.js";import{t as G}from"./react-BGmjiNul.js";import{ii as se,zt as ue}from"./cells-BW_4R0Qw.js";import{t as H}from"./compiler-runtime-DeeZ7FnK.js";import{d as q}from"./hotkeys-BHHWjLlp.js";import{t as ce}from"./jsx-runtime-ZmTK25f3.js";import{t as B}from"./cn-BKtXLv3a.js";import{t as de}from"./requests-BsVD4CdD.js";import{t as fe}from"./createLucideIcon-CnW3RofX.js";import{t as k}from"./use-toast-rmUWldD_.js";import{d as me}from"./popover-Gz-GJzym.js";import{r as pe}from"./errors-2SszdW9t.js";import{t as W}from"./dist-CdxIjAOP.js";import{t as O}from"./html-to-image-BVbOFO17.js";var ve=fe("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),z=t=>{let e,r=new Set,a=(l,s)=>{let c=typeof l=="function"?l(e):l;if(!Object.is(c,e)){let d=e;e=s??(typeof c!="object"||!c)?c:Object.assign({},e,c),r.forEach(u=>u(e,d))}},n=()=>e,i={setState:a,getState:n,getInitialState:()=>o,subscribe:l=>(r.add(l),()=>r.delete(l)),destroy:()=>{r.clear()}},o=e=t(a,n,i);return i},he=t=>t?z(t):z,ge=V((t=>{var e=G(),r=me();function a(d,u){return d===u&&(d!==0||1/d==1/u)||d!==d&&u!==u}var n=typeof Object.is=="function"?Object.is:a,i=r.useSyncExternalStore,o=e.useRef,l=e.useEffect,s=e.useMemo,c=e.useDebugValue;t.useSyncExternalStoreWithSelector=function(d,u,f,g,m){var p=o(null);if(p.current===null){var w={hasValue:!1,value:null};p.current=w}else w=p.current;p=s(function(){function A(y){if(!U){if(U=!0,S=y,y=g(y),m!==void 0&&w.hasValue){var N=w.value;if(m(N,y))return $=N}return $=y}if(N=$,n(S,y))return N;var F=g(y);return m!==void 0&&m(N,F)?(S=y,N):(S=y,$=F)}var U=!1,S,$,C=f===void 0?null:f;return[function(){return A(u())},C===null?void 0:function(){return A(C())}]},[u,f,g,m]);var b=i(d,p[0],p[1]);return l(function(){w.hasValue=!0,w.value=b},[b]),c(b),b}})),ye=V(((t,e)=>{e.exports=ge()}));const h={toMarkdown:t=>h.replace(t,"md"),toHTML:t=>h.replace(t,"html"),toPNG:t=>h.replace(t,"png"),toPDF:t=>h.replace(t,"pdf"),toPY:t=>h.replace(t,"py"),withoutExtension:t=>{let e=t.split(".");return e.length===1?t:e.slice(0,-1).join(".")},replace:(t,e)=>t.endsWith(`.${e}`)?t:`${h.withoutExtension(t)}.${e}`};var P=320,j=180;function X(t){if(!t||t==="about:blank")return null;try{let e=new URL(t,window.location.href);return e.origin===window.location.origin?null:e.href}catch{return t}}function xe(t,e,r){let a=[],n="";for(let i of e){let o=n+i;t.measureText(o).width<=r?n=o:(n&&a.push(n),n=i)}return n&&a.push(n),a}function L(t){let e=window.devicePixelRatio||1,r=document.createElement("canvas");r.width=P*e,r.height=j*e;let a=r.getContext("2d");if(!a)return r.toDataURL("image/png");a.scale(e,e),a.fillStyle="#f3f4f6",a.fillRect(0,0,P,j),a.strokeStyle="#d1d5db",a.strokeRect(.5,.5,P-1,j-1),a.fillStyle="#6b7280",a.font="8px sans-serif",a.textAlign="center",a.textBaseline="middle";let n=P-32,i=t?xe(a,t,n):[],o=(j-(1+i.length)*14)/2+14/2;a.fillText("External iframe",P/2,o),o+=14;for(let l of i)a.fillText(l,P/2,o),o+=14;return r.toDataURL("image/png")}async function we(t){var n;let e=t.querySelector("iframe");if(!e)return null;let r=X(e.getAttribute("src"));if(r)return L(r);let a;try{let i=e.contentDocument||((n=e.contentWindow)==null?void 0:n.document);if(!(i!=null&&i.body))return null;a=i}catch{return L(null)}for(let i of a.querySelectorAll("iframe")){let o=X(i.getAttribute("src"));if(o)return L(o)}try{return await O(a.body)}catch{return L(null)}}var Y=class oe{constructor(e){D(this,"progress",0);D(this,"listeners",new Set);this.total=e}static indeterminate(){return new oe("indeterminate")}addTotal(e){this.total==="indeterminate"?this.total=e:this.total+=e,this.notifyListeners()}increment(e){this.progress+=e,this.notifyListeners()}getProgress(){return this.total==="indeterminate"?"indeterminate":this.progress/this.total*100}subscribe(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}notifyListeners(){let e=this.getProgress();for(let r of this.listeners)r(e)}},v=I(G(),1),x=I(ce(),1);function be(t,e=[]){let r=[];function a(i,o){let l=v.createContext(o);l.displayName=i+"Context";let s=r.length;r=[...r,o];let c=u=>{var b;let{scope:f,children:g,...m}=u,p=((b=f==null?void 0:f[t])==null?void 0:b[s])||l,w=v.useMemo(()=>m,Object.values(m));return(0,x.jsx)(p.Provider,{value:w,children:g})};c.displayName=i+"Provider";function d(u,f){var p;let g=((p=f==null?void 0:f[t])==null?void 0:p[s])||l,m=v.useContext(g);if(m)return m;if(o!==void 0)return o;throw Error(`\`${u}\` must be used within \`${i}\``)}return[c,d]}let n=()=>{let i=r.map(o=>v.createContext(o));return function(o){let l=(o==null?void 0:o[t])||i;return v.useMemo(()=>({[`__scope${t}`]:{...o,[t]:l}}),[o,l])}};return n.scopeName=t,[a,Ne(n,...e)]}function Ne(...t){let e=t[0];if(t.length===1)return e;let r=()=>{let a=t.map(n=>({useScope:n(),scopeName:n.scopeName}));return function(n){let i=a.reduce((o,{useScope:l,scopeName:s})=>{let c=l(n)[`__scope${s}`];return{...o,...c}},{});return v.useMemo(()=>({[`__scope${e.scopeName}`]:i}),[i])}};return r.scopeName=e.scopeName,r}var _="Progress",M=100,[Pe,nt]=be(_),[Se,$e]=Pe(_),J=v.forwardRef((t,e)=>{let{__scopeProgress:r,value:a=null,max:n,getValueLabel:i=je,...o}=t;(n||n===0)&&!ee(n)&&console.error(Le(`${n}`,"Progress"));let l=ee(n)?n:M;a!==null&&!te(a,l)&&console.error(Ee(`${a}`,"Progress"));let s=te(a,l)?a:null,c=E(s)?i(s,l):void 0;return(0,x.jsx)(Se,{scope:r,value:s,max:l,children:(0,x.jsx)(W.div,{"aria-valuemax":l,"aria-valuemin":0,"aria-valuenow":E(s)?s:void 0,"aria-valuetext":c,role:"progressbar","data-state":Z(s,l),"data-value":s??void 0,"data-max":l,...o,ref:e})})});J.displayName=_;var K="ProgressIndicator",Q=v.forwardRef((t,e)=>{let{__scopeProgress:r,...a}=t,n=$e(K,r);return(0,x.jsx)(W.div,{"data-state":Z(n.value,n.max),"data-value":n.value??void 0,"data-max":n.max,...a,ref:e})});Q.displayName=K;function je(t,e){return`${Math.round(t/e*100)}%`}function Z(t,e){return t==null?"indeterminate":t===e?"complete":"loading"}function E(t){return typeof t=="number"}function ee(t){return E(t)&&!isNaN(t)&&t>0}function te(t,e){return E(t)&&!isNaN(t)&&t<=e&&t>=0}function Le(t,e){return`Invalid prop \`max\` of value \`${t}\` supplied to \`${e}\`. Only numbers greater than 0 are valid max values. Defaulting to \`${M}\`.`}function Ee(t,e){return`Invalid prop \`value\` of value \`${t}\` supplied to \`${e}\`. The \`value\` prop must be:
|
|
2
|
-
- a positive number
|
|
3
|
-
- less than the value passed to \`max\` (or ${M} if no \`max\` prop is set)
|
|
4
|
-
- \`null\` or \`undefined\` if the progress is indeterminate.
|
|
5
|
-
|
|
6
|
-
Defaulting to \`null\`.`}var re=J,Re=Q,De=H(),T=v.forwardRef((t,e)=>{let r=(0,De.c)(20),a,n,i,o;r[0]===t?(a=r[1],n=r[2],i=r[3],o=r[4]):({className:a,value:o,indeterminate:n,...i}=t,r[0]=t,r[1]=a,r[2]=n,r[3]=i,r[4]=o);let l;r[5]===a?l=r[6]:(l=B("relative h-2 w-full overflow-hidden rounded-full bg-primary/20",a),r[5]=a,r[6]=l);let s=n?"w-1/3 animate-progress-indeterminate":"w-full transition-transform duration-300 ease-out",c;r[7]===s?c=r[8]:(c=B("h-full flex-1 bg-primary",s),r[7]=s,r[8]=c);let d;r[9]!==n||r[10]!==o?(d=n?void 0:{transform:`translateX(-${100-(o||0)}%)`},r[9]=n,r[10]=o,r[11]=d):d=r[11];let u;r[12]!==c||r[13]!==d?(u=(0,x.jsx)(Re,{className:c,style:d}),r[12]=c,r[13]=d,r[14]=u):u=r[14];let f;return r[15]!==i||r[16]!==e||r[17]!==l||r[18]!==u?(f=(0,x.jsx)(re,{ref:e,className:l,...i,children:u}),r[15]=i,r[16]=e,r[17]=l,r[18]=u,r[19]=f):f=r[19],f});T.displayName=re.displayName;var ke=H();const Oe=t=>{let e=(0,ke.c)(13),{progress:r,showPercentage:a}=t,n=a===void 0?!1:a,i,o;e[0]===r?(i=e[1],o=e[2]):(i=g=>r.subscribe(g),o=()=>r.getProgress(),e[0]=r,e[1]=i,e[2]=o);let l=(0,v.useSyncExternalStore)(i,o),s=l==="indeterminate"||l===100,c=s?void 0:l,d;e[3]!==s||e[4]!==c?(d=(0,x.jsx)(T,{value:c,indeterminate:s}),e[3]=s,e[4]=c,e[5]=d):d=e[5];let u;e[6]!==s||e[7]!==n||e[8]!==l?(u=!s&&n&&(0,x.jsxs)("div",{className:"mt-1 text-xs text-muted-foreground text-right",children:[Math.round(l),"%"]}),e[6]=s,e[7]=n,e[8]=l,e[9]=u):u=e[9];let f;return e[10]!==d||e[11]!==u?(f=(0,x.jsxs)("div",{className:"mt-2 w-full min-w-[200px]",children:[d,u]}),e[10]=d,e[11]=u,e[12]=f):f=e[12],f};async function _e(t,e){let r=Y.indeterminate(),a=k({title:t,description:v.createElement(Oe,{progress:r}),duration:1/0});try{let n=await e(r);return a.dismiss(),n}catch(n){throw a.dismiss(),n}}function Me(t){let e=document.getElementById(se.create(t));if(!e){q.error(`Output element not found for cell ${t}`);return}return e}function Te(t){let e=t.style.overflow,r=t.style.maxHeight;return t.style.overflow="visible",t.style.maxHeight="none",()=>{t.style.overflow=e,t.style.maxHeight=r}}var Ae=500,Ue=`
|
|
7
|
-
* { scrollbar-width: none; -ms-overflow-style: none; }
|
|
8
|
-
*::-webkit-scrollbar { display: none; }
|
|
9
|
-
`;async function ne(t){let e=Me(t);if(!e)return;let r=await we(e);if(r)return r;let a=Te(e);try{let n=Date.now(),i=await O(e,{extraStyleContent:Ue}),o=Date.now()-n;return o>Ae&&q.debug("toPng operation for element",e,`took ${o} ms (exceeds threshold)`),i}finally{a()}}async function Ce(t,e){let r=await ne(t);r&&R(r,h.toPNG(e))}const Fe=()=>(document.body.classList.add("printing"),()=>{document.body.classList.remove("printing")});async function Ie(t){let{element:e,filename:r,prepare:a}=t,n=document.getElementById("App"),i=(n==null?void 0:n.scrollTop)??0,o;a&&(o=a(e));try{R(await O(e),h.toPNG(r))}catch{k({title:"Error",description:"Failed to download as PNG.",variant:"danger"})}finally{o==null||o(),document.body.classList.contains("printing")&&document.body.classList.remove("printing"),requestAnimationFrame(()=>{n==null||n.scrollTo(0,i)})}}function R(t,e){let r=document.createElement("a");r.href=t,r.download=e,r.click(),r.remove()}function ae(t,e){let r=URL.createObjectURL(t);R(r,e),URL.revokeObjectURL(r)}async function Ve(t){let e=de(),{filename:r,webpdf:a}=t;try{let n=await e.exportAsPDF({webpdf:a}),i=ue.basename(r);ae(n,h.toPDF(i))}catch(n){throw k({title:"Failed to download",description:pe(n),variant:"danger"}),n}}export{Ce as a,_e as c,h as d,ye as f,R as i,T as l,ve as m,Ve as n,Ie as o,he as p,ae as r,ne as s,Fe as t,Y as u};
|