@marimo-team/islands 0.23.16-dev23 → 0.23.16-dev25
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/{code-visibility-Bi6qrSJV.js → code-visibility-Btz9Lj-7.js} +778 -770
- package/dist/main.js +937 -942
- package/dist/{reveal-component-BFiF5alf.js → reveal-component-ByqpHkkW.js} +18 -18
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +20 -4
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/requests-network.test.ts +1 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/requests-network.ts +5 -5
- package/src/core/network/types.ts +15 -4
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +3 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
|
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
|
|
|
9
9
|
import { ut as outputIsStale } from "./html-to-image-27MFfyOf.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-B9WoXK1R.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-DIlxLFXG.js";
|
|
12
|
-
import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar,
|
|
12
|
+
import { a as DEFAULT_DECK_VERTICAL_ALIGN, c as SlideSidebar, dt as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, l as Slide, ln as EyeOff, lt as Panel, o as DEFAULT_SLIDE_TYPE, pn as Code, t as useNotebookCodeAvailable, un as Expand, ut as PanelGroup } from "./code-visibility-Btz9Lj-7.js";
|
|
13
13
|
import { R as ChevronDown } from "./strings-DIy9NAmg.js";
|
|
14
14
|
import { X as useDebouncedCallback } from "./input-C5K4VBvB.js";
|
|
15
15
|
import { v as kioskModeAtom } from "./toDate-CqhBWjvO.js";
|
|
@@ -7328,10 +7328,10 @@ function resolveSlideContentStyle(e5) {
|
|
|
7328
7328
|
var SubslideView = (e5) => {
|
|
7329
7329
|
let t = (0, import_compiler_runtime.c)(24), { subslide: n, resolveShowCode: r, isEditable: i, slideConfigs: a, contentStyle: o } = e5, s, l, u, f, go, _o;
|
|
7330
7330
|
if (t[0] !== o || t[1] !== i || t[2] !== r || t[3] !== a || t[4] !== n) {
|
|
7331
|
-
let { slideLevel: e6, cumulativeByBlock:
|
|
7331
|
+
let { slideLevel: e6, cumulativeByBlock: vo2 } = buildSubslideNotes(n, a);
|
|
7332
7332
|
u = e6;
|
|
7333
|
-
let
|
|
7334
|
-
l = w, s = SlideScrollContainer, f =
|
|
7333
|
+
let yo2 = n.blocks.some((e7) => e7.cells.some((e8) => r(e8.id)));
|
|
7334
|
+
l = w, s = SlideScrollContainer, f = yo2 ? "mo-slide-content flex flex-col gap-3" : "mo-slide-content", go = o, _o = n.blocks.map((e7, t2) => {
|
|
7335
7335
|
let n2 = e7.cells.map((e8) => r(e8.id) ? i ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideCellReadOnlyView, { cell: e8 }, e8.id) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Slide, {
|
|
7336
7336
|
cellId: e8.id,
|
|
7337
7337
|
status: e8.status,
|
|
@@ -7339,7 +7339,7 @@ var SubslideView = (e5) => {
|
|
|
7339
7339
|
stale: outputIsStale(e8, false)
|
|
7340
7340
|
}, e8.id));
|
|
7341
7341
|
if (e7.isFragment) {
|
|
7342
|
-
let e8 =
|
|
7342
|
+
let e8 = vo2.get(t2);
|
|
7343
7343
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Bt, {
|
|
7344
7344
|
as: "div",
|
|
7345
7345
|
children: [n2, e8 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: e8 })]
|
|
@@ -7348,18 +7348,18 @@ var SubslideView = (e5) => {
|
|
|
7348
7348
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: n2 }, t2);
|
|
7349
7349
|
}), t[0] = o, t[1] = i, t[2] = r, t[3] = a, t[4] = n, t[5] = s, t[6] = l, t[7] = u, t[8] = f, t[9] = go, t[10] = _o;
|
|
7350
7350
|
} else s = t[5], l = t[6], u = t[7], f = t[8], go = t[9], _o = t[10];
|
|
7351
|
-
let
|
|
7352
|
-
t[11] !== f || t[12] !== go || t[13] !== _o ? (
|
|
7351
|
+
let vo;
|
|
7352
|
+
t[11] !== f || t[12] !== go || t[13] !== _o ? (vo = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7353
7353
|
className: f,
|
|
7354
7354
|
style: go,
|
|
7355
7355
|
children: _o
|
|
7356
|
-
}), t[11] = f, t[12] = go, t[13] = _o, t[14] =
|
|
7357
|
-
let
|
|
7358
|
-
t[15] !== s || t[16] !==
|
|
7356
|
+
}), t[11] = f, t[12] = go, t[13] = _o, t[14] = vo) : vo = t[14];
|
|
7357
|
+
let yo;
|
|
7358
|
+
t[15] !== s || t[16] !== vo ? (yo = /* @__PURE__ */ (0, import_jsx_runtime.jsx)(s, { children: vo }), t[15] = s, t[16] = vo, t[17] = yo) : yo = t[17];
|
|
7359
7359
|
let bo;
|
|
7360
7360
|
t[18] === u ? bo = t[19] : (bo = u && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NotesAside, { text: u }), t[18] = u, t[19] = bo);
|
|
7361
7361
|
let xo;
|
|
7362
|
-
return t[20] !== l || t[21] !==
|
|
7362
|
+
return t[20] !== l || t[21] !== yo || t[22] !== bo ? (xo = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(l, { children: [yo, bo] }), t[20] = l, t[21] = yo, t[22] = bo, t[23] = xo) : xo = t[23], xo;
|
|
7363
7363
|
};
|
|
7364
7364
|
function parkedRendersSource(e5) {
|
|
7365
7365
|
let { isNoOutputPreview: t, isEditable: n, showCode: r } = e5;
|
|
@@ -7384,7 +7384,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7384
7384
|
output: l,
|
|
7385
7385
|
stale: u
|
|
7386
7386
|
}), t[5] = n.id, t[6] = n.output, t[7] = n.status, t[8] = u, t[9] = f) : f = t[9], f;
|
|
7387
|
-
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: c, activeIndex: l, onSlideChange: u, mode:
|
|
7387
|
+
}, reveal_component_default = ({ slideCells: e5, layout: o, setLayout: s, noOutputIds: c, activeIndex: l, onSlideChange: u, mode: _o, configWidth: p, isEditable: bo = false }) => {
|
|
7388
7388
|
var _a2, _b2, _c;
|
|
7389
7389
|
let To = (0, import_react.useRef)(null), Eo = (0, import_react.useRef)(null), { width: Oo, height: Po } = useSlideDimensions(To), Fo = useFullScreenElement() != null, Lo = useAtomValue(kioskModeAtom), Ro = (0, import_react.useMemo)(() => Lo ? [] : [ce], [Lo]), [zo, Bo] = (0, import_react.useState)(() => /* @__PURE__ */ new Set()), Vo = useNotebookCodeAvailable(e5), Ho = !isIslands() && Vo, Uo = l == null ? void 0 : e5[l], Wo = Uo ?? e5.at(0), { parkedPreviewCell: Go, isHeldEdit: Ko, isNoOutputPreview: qo, heldEditCellId: Jo, heldShowsCode: Yo, toggleHeldShowsCode: Xo } = useParkedPreview({
|
|
7390
7390
|
activeCell: Uo,
|
|
@@ -7488,7 +7488,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7488
7488
|
useEventListener(document, "keydown", ms, { capture: true });
|
|
7489
7489
|
let gs = Ko ? null : qo ? "Hidden as there is no output" : "Skipped in presentation", _s = Ko ? Yo : Zo(Go == null ? void 0 : Go.id), vs = parkedRendersSource({
|
|
7490
7490
|
isNoOutputPreview: qo,
|
|
7491
|
-
isEditable:
|
|
7491
|
+
isEditable: bo,
|
|
7492
7492
|
showCode: _s
|
|
7493
7493
|
}), ys = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7494
7494
|
ref: To,
|
|
@@ -7512,13 +7512,13 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7512
7512
|
children: rs.stacks.map((e6, t) => e6.subslides.length === 1 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SubslideView, {
|
|
7513
7513
|
subslide: e6.subslides[0],
|
|
7514
7514
|
resolveShowCode: Zo,
|
|
7515
|
-
isEditable:
|
|
7515
|
+
isEditable: bo,
|
|
7516
7516
|
slideConfigs: o.cells,
|
|
7517
7517
|
contentStyle: ss
|
|
7518
7518
|
}, t) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ne, { children: e6.subslides.map((e7, t2) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SubslideView, {
|
|
7519
7519
|
subslide: e7,
|
|
7520
7520
|
resolveShowCode: Zo,
|
|
7521
|
-
isEditable:
|
|
7521
|
+
isEditable: bo,
|
|
7522
7522
|
slideConfigs: o.cells,
|
|
7523
7523
|
contentStyle: ss
|
|
7524
7524
|
}, t2)) }, t))
|
|
@@ -7537,7 +7537,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7537
7537
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ParkedPreviewContent, {
|
|
7538
7538
|
cell: Go,
|
|
7539
7539
|
isNoOutputPreview: qo,
|
|
7540
|
-
isEditable:
|
|
7540
|
+
isEditable: bo,
|
|
7541
7541
|
showCode: _s
|
|
7542
7542
|
})
|
|
7543
7543
|
})
|
|
@@ -7579,7 +7579,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7579
7579
|
]
|
|
7580
7580
|
})
|
|
7581
7581
|
});
|
|
7582
|
-
return
|
|
7582
|
+
return _o === "read" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
7583
7583
|
className: "flex-1 min-w-0 flex flex-row gap-3",
|
|
7584
7584
|
children: ys
|
|
7585
7585
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -7615,7 +7615,7 @@ var ParkedPreviewContent = (e5) => {
|
|
|
7615
7615
|
})
|
|
7616
7616
|
]
|
|
7617
7617
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlideSidebar, {
|
|
7618
|
-
configWidth:
|
|
7618
|
+
configWidth: p,
|
|
7619
7619
|
layout: o,
|
|
7620
7620
|
setLayout: s,
|
|
7621
7621
|
activeConfigCell: Wo
|
package/package.json
CHANGED
|
@@ -64,10 +64,26 @@ export const MockRequestClient = {
|
|
|
64
64
|
getWorkspaceFiles: vi.fn().mockResolvedValue({ files: [] }),
|
|
65
65
|
getRunningNotebooks: vi.fn().mockResolvedValue({ files: [] }),
|
|
66
66
|
shutdownSession: vi.fn().mockResolvedValue({}),
|
|
67
|
-
exportAsHTML: vi.fn().mockResolvedValue({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
exportAsHTML: vi.fn().mockResolvedValue({
|
|
68
|
+
contents: "",
|
|
69
|
+
filename: "notebook.html",
|
|
70
|
+
mediaType: "text/html; charset=utf-8",
|
|
71
|
+
}),
|
|
72
|
+
exportAsIPYNB: vi.fn().mockResolvedValue({
|
|
73
|
+
contents: "",
|
|
74
|
+
filename: "notebook.ipynb",
|
|
75
|
+
mediaType: "text/plain; charset=utf-8",
|
|
76
|
+
}),
|
|
77
|
+
exportAsMarkdown: vi.fn().mockResolvedValue({
|
|
78
|
+
contents: "",
|
|
79
|
+
filename: "notebook.md",
|
|
80
|
+
mediaType: "text/plain; charset=utf-8",
|
|
81
|
+
}),
|
|
82
|
+
exportAsPDF: vi.fn().mockResolvedValue({
|
|
83
|
+
contents: new Blob(),
|
|
84
|
+
filename: "notebook.pdf",
|
|
85
|
+
mediaType: "application/pdf",
|
|
86
|
+
}),
|
|
71
87
|
autoExportAsHTML: vi.fn().mockResolvedValue({}),
|
|
72
88
|
autoExportAsMarkdown: vi.fn().mockResolvedValue({}),
|
|
73
89
|
autoExportAsIPYNB: vi.fn().mockResolvedValue({}),
|
|
@@ -8,13 +8,11 @@ describe("runServerSidePDFDownload", () => {
|
|
|
8
8
|
const downloadPDF = vi.fn().mockResolvedValue(undefined);
|
|
9
9
|
|
|
10
10
|
await runServerSidePDFDownload({
|
|
11
|
-
filename: "slides.py",
|
|
12
11
|
preset: "document",
|
|
13
12
|
downloadPDF,
|
|
14
13
|
});
|
|
15
14
|
|
|
16
15
|
expect(downloadPDF).toHaveBeenCalledWith({
|
|
17
|
-
filename: "slides.py",
|
|
18
16
|
webpdf: false,
|
|
19
17
|
preset: "document",
|
|
20
18
|
includeInputs: true,
|
|
@@ -26,13 +24,11 @@ describe("runServerSidePDFDownload", () => {
|
|
|
26
24
|
const downloadPDF = vi.fn().mockResolvedValue(undefined);
|
|
27
25
|
|
|
28
26
|
await runServerSidePDFDownload({
|
|
29
|
-
filename: "slides.py",
|
|
30
27
|
preset: "slides",
|
|
31
28
|
downloadPDF,
|
|
32
29
|
});
|
|
33
30
|
|
|
34
31
|
expect(downloadPDF).toHaveBeenCalledWith({
|
|
35
|
-
filename: "slides.py",
|
|
36
32
|
webpdf: false,
|
|
37
33
|
preset: "slides",
|
|
38
34
|
includeInputs: true,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
type Preset = "document" | "slides";
|
|
4
4
|
type DownloadPDF = (opts: {
|
|
5
|
-
filename: string;
|
|
6
5
|
webpdf: boolean;
|
|
7
6
|
preset: Preset;
|
|
8
7
|
includeInputs: boolean;
|
|
@@ -10,14 +9,12 @@ type DownloadPDF = (opts: {
|
|
|
10
9
|
}) => Promise<void>;
|
|
11
10
|
|
|
12
11
|
export async function runServerSidePDFDownload(opts: {
|
|
13
|
-
filename: string;
|
|
14
12
|
preset: Preset;
|
|
15
13
|
downloadPDF: DownloadPDF;
|
|
16
14
|
}): Promise<void> {
|
|
17
|
-
const {
|
|
15
|
+
const { preset, downloadPDF } = opts;
|
|
18
16
|
|
|
19
17
|
await downloadPDF({
|
|
20
|
-
filename,
|
|
21
18
|
webpdf: false,
|
|
22
19
|
preset,
|
|
23
20
|
includeInputs: true,
|
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
ADD_PRINTING_CLASS,
|
|
84
84
|
downloadAsPDF,
|
|
85
85
|
downloadBlob,
|
|
86
|
+
downloadExportedFile,
|
|
86
87
|
downloadHTMLAsImage,
|
|
87
88
|
withLoadingToast,
|
|
88
89
|
} from "@/utils/download";
|
|
@@ -197,7 +198,6 @@ export function useNotebookActions() {
|
|
|
197
198
|
updateCellOutputs,
|
|
198
199
|
});
|
|
199
200
|
await runServerSidePDFDownload({
|
|
200
|
-
filename,
|
|
201
201
|
preset,
|
|
202
202
|
downloadPDF: downloadAsPDF,
|
|
203
203
|
});
|
|
@@ -231,11 +231,8 @@ export function useNotebookActions() {
|
|
|
231
231
|
takeScreenshots: () => takeScreenshots({ progress }),
|
|
232
232
|
updateCellOutputs,
|
|
233
233
|
});
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
new Blob([ipynb], { type: "application/x-ipynb+json" }),
|
|
237
|
-
Filenames.toIPYNB(document.title),
|
|
238
|
-
);
|
|
234
|
+
const exportedFile = await exportAsIPYNB({ download: false });
|
|
235
|
+
downloadExportedFile(exportedFile);
|
|
239
236
|
};
|
|
240
237
|
|
|
241
238
|
await withLoadingToast("Downloading IPYNB...", runDownload);
|
|
@@ -255,7 +252,7 @@ export function useNotebookActions() {
|
|
|
255
252
|
toastNotebookMustBeNamed();
|
|
256
253
|
return;
|
|
257
254
|
}
|
|
258
|
-
await downloadAsHTML({
|
|
255
|
+
await downloadAsHTML({ includeCode: true });
|
|
259
256
|
},
|
|
260
257
|
},
|
|
261
258
|
{
|
|
@@ -266,7 +263,7 @@ export function useNotebookActions() {
|
|
|
266
263
|
toastNotebookMustBeNamed();
|
|
267
264
|
return;
|
|
268
265
|
}
|
|
269
|
-
await downloadAsHTML({
|
|
266
|
+
await downloadAsHTML({ includeCode: false });
|
|
270
267
|
},
|
|
271
268
|
},
|
|
272
269
|
{
|
|
@@ -275,11 +272,8 @@ export function useNotebookActions() {
|
|
|
275
272
|
),
|
|
276
273
|
label: "Download as Markdown",
|
|
277
274
|
handle: async () => {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
new Blob([md], { type: "text/plain" }),
|
|
281
|
-
Filenames.toMarkdown(document.title),
|
|
282
|
-
);
|
|
275
|
+
const exportedFile = await exportAsMarkdown({ download: false });
|
|
276
|
+
downloadExportedFile(exportedFile);
|
|
283
277
|
},
|
|
284
278
|
},
|
|
285
279
|
{
|
|
@@ -40,7 +40,7 @@ export const ShareStaticNotebookModal: React.FC<{
|
|
|
40
40
|
e.preventDefault();
|
|
41
41
|
|
|
42
42
|
onClose();
|
|
43
|
-
const html = await exportAsHTML({
|
|
43
|
+
const { contents: html } = await exportAsHTML({
|
|
44
44
|
download: false,
|
|
45
45
|
includeCode: true,
|
|
46
46
|
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
@@ -94,4 +94,46 @@ describe("API", () => {
|
|
|
94
94
|
}),
|
|
95
95
|
).rejects.toBeInstanceOf(HTTPError);
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
it("preserves export metadata from response headers", async () => {
|
|
99
|
+
const response = new Response("markdown", {
|
|
100
|
+
headers: {
|
|
101
|
+
"Content-Disposition":
|
|
102
|
+
"attachment; filename*=UTF-8''r%C3%A9sum%C3%A9.qmd",
|
|
103
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
API.handleExportResponse({ data: "markdown", response }),
|
|
109
|
+
).resolves.toEqual({
|
|
110
|
+
contents: "markdown",
|
|
111
|
+
filename: "résumé.qmd",
|
|
112
|
+
mediaType: "text/plain; charset=utf-8",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("rejects export responses without a filename", async () => {
|
|
117
|
+
await expect(
|
|
118
|
+
API.handleExportResponse({
|
|
119
|
+
data: "markdown",
|
|
120
|
+
response: new Response(null, {
|
|
121
|
+
headers: { "Content-Type": "text/plain" },
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow("Export response is missing a filename");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("rejects export responses without a media type", async () => {
|
|
128
|
+
await expect(
|
|
129
|
+
API.handleExportResponse({
|
|
130
|
+
data: "markdown",
|
|
131
|
+
response: new Response(null, {
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
).rejects.toThrow("Export response is missing a media type");
|
|
138
|
+
});
|
|
97
139
|
});
|
package/src/core/network/api.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Logger } from "../../utils/Logger";
|
|
|
6
6
|
import { Strings } from "../../utils/strings";
|
|
7
7
|
import { getRuntimeManager } from "../runtime/config";
|
|
8
8
|
import type { RuntimeManager } from "../runtime/runtime";
|
|
9
|
+
import type { ExportedFile } from "./types";
|
|
9
10
|
|
|
10
11
|
function getBaseUriWithoutQueryParams(): string {
|
|
11
12
|
// Remove query params and hash
|
|
@@ -15,6 +16,15 @@ function getBaseUriWithoutQueryParams(): string {
|
|
|
15
16
|
return url.toString();
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function getExportFilename(response: Response): string {
|
|
20
|
+
const contentDisposition = response.headers.get("Content-Disposition");
|
|
21
|
+
const match = contentDisposition?.match(/filename\*=UTF-8''([^;]+)/i);
|
|
22
|
+
if (!match) {
|
|
23
|
+
throw new Error("Export response is missing a filename");
|
|
24
|
+
}
|
|
25
|
+
return decodeURIComponent(match[1]);
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Wrapper around fetch that adds XSRF token and session ID to the request and
|
|
20
30
|
* strong types.
|
|
@@ -119,6 +129,23 @@ export const API = {
|
|
|
119
129
|
}
|
|
120
130
|
return Promise.resolve(response.data as T);
|
|
121
131
|
},
|
|
132
|
+
handleExportResponse: async <T extends BlobPart>(response: {
|
|
133
|
+
data?: T | undefined;
|
|
134
|
+
error?: Record<string, unknown>;
|
|
135
|
+
response: Response;
|
|
136
|
+
}): Promise<ExportedFile<T>> => {
|
|
137
|
+
const contents = await API.handleResponse<T>(response);
|
|
138
|
+
const mediaType = response.response.headers.get("Content-Type");
|
|
139
|
+
if (!mediaType) {
|
|
140
|
+
throw new Error("Export response is missing a media type");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
contents,
|
|
145
|
+
filename: getExportFilename(response.response),
|
|
146
|
+
mediaType,
|
|
147
|
+
};
|
|
148
|
+
},
|
|
122
149
|
handleResponseReturnNull: (response: {
|
|
123
150
|
error?: Record<string, unknown>;
|
|
124
151
|
response: Response;
|
|
@@ -22,7 +22,7 @@ function multipartInit(formData: FormData) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const { handleResponse, handleResponseReturnNull } = API;
|
|
25
|
+
const { handleExportResponse, handleResponse, handleResponseReturnNull } = API;
|
|
26
26
|
|
|
27
27
|
export function createNetworkRequests(): EditRequests & RunRequests {
|
|
28
28
|
const getClient = once(() => {
|
|
@@ -415,7 +415,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
415
415
|
parseAs: "text",
|
|
416
416
|
params: getParams(),
|
|
417
417
|
})
|
|
418
|
-
.then(
|
|
418
|
+
.then(handleExportResponse);
|
|
419
419
|
},
|
|
420
420
|
exportAsMarkdown: async (request) => {
|
|
421
421
|
return getClient()
|
|
@@ -424,7 +424,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
424
424
|
parseAs: "text",
|
|
425
425
|
params: getParams(),
|
|
426
426
|
})
|
|
427
|
-
.then(
|
|
427
|
+
.then(handleExportResponse);
|
|
428
428
|
},
|
|
429
429
|
exportAsIPYNB: async (request) => {
|
|
430
430
|
return getClient()
|
|
@@ -433,7 +433,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
433
433
|
parseAs: "text",
|
|
434
434
|
params: getParams(),
|
|
435
435
|
})
|
|
436
|
-
.then(
|
|
436
|
+
.then(handleExportResponse);
|
|
437
437
|
},
|
|
438
438
|
exportAsPDF: async (request) => {
|
|
439
439
|
return getClient()
|
|
@@ -442,7 +442,7 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
442
442
|
parseAs: "blob",
|
|
443
443
|
params: getParams(),
|
|
444
444
|
})
|
|
445
|
-
.then(
|
|
445
|
+
.then(handleExportResponse);
|
|
446
446
|
},
|
|
447
447
|
autoExportAsHTML: async (request) => {
|
|
448
448
|
return getClient()
|
|
@@ -23,6 +23,13 @@ export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
|
23
23
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
24
24
|
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|
|
25
25
|
export type UpdateCellOutputsRequest = schemas["UpdateCellOutputsRequest"];
|
|
26
|
+
|
|
27
|
+
export interface ExportedFile<T extends BlobPart = BlobPart> {
|
|
28
|
+
contents: T;
|
|
29
|
+
filename: string;
|
|
30
|
+
mediaType: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export type FileCopyRequest = schemas["FileCopyRequest"];
|
|
27
34
|
export type FileCopyResponse = schemas["FileCopyResponse"];
|
|
28
35
|
export type FileCreateRequest = schemas["FileCreateRequest"];
|
|
@@ -205,10 +212,14 @@ export interface EditRequests {
|
|
|
205
212
|
request: ShutdownSessionRequest,
|
|
206
213
|
) => Promise<RunningNotebooksResponse>;
|
|
207
214
|
// Export requests
|
|
208
|
-
exportAsHTML: (request: ExportAsHTMLRequest) => Promise<string
|
|
209
|
-
exportAsIPYNB: (
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
exportAsHTML: (request: ExportAsHTMLRequest) => Promise<ExportedFile<string>>;
|
|
216
|
+
exportAsIPYNB: (
|
|
217
|
+
request: ExportAsIPYNBRequest,
|
|
218
|
+
) => Promise<ExportedFile<string>>;
|
|
219
|
+
exportAsMarkdown: (
|
|
220
|
+
request: ExportAsMarkdownRequest,
|
|
221
|
+
) => Promise<ExportedFile<string>>;
|
|
222
|
+
exportAsPDF: (request: ExportAsPDFRequest) => Promise<ExportedFile<Blob>>;
|
|
212
223
|
autoExportAsHTML: (request: ExportAsHTMLRequest) => Promise<null>;
|
|
213
224
|
autoExportAsMarkdown: (request: ExportAsMarkdownRequest) => Promise<null>;
|
|
214
225
|
autoExportAsIPYNB: (request: ExportAsIPYNBRequest) => Promise<null>;
|
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { Filenames } from "@/utils/filenames";
|
|
5
|
-
import { Paths } from "@/utils/paths";
|
|
3
|
+
import { downloadExportedFile } from "@/utils/download";
|
|
6
4
|
import { getRequestClient } from "../network/requests";
|
|
7
5
|
import { VirtualFileTracker } from "./virtual-file-tracker";
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
8
|
* Downloads the current notebook as an HTML file.
|
|
11
9
|
*/
|
|
12
|
-
export async function downloadAsHTML(opts: {
|
|
13
|
-
filename: string;
|
|
14
|
-
includeCode: boolean;
|
|
15
|
-
}) {
|
|
10
|
+
export async function downloadAsHTML(opts: { includeCode: boolean }) {
|
|
16
11
|
const client = getRequestClient();
|
|
17
|
-
const {
|
|
18
|
-
const
|
|
12
|
+
const { includeCode } = opts;
|
|
13
|
+
const exportedFile = await client.exportAsHTML({
|
|
19
14
|
download: true,
|
|
20
15
|
includeCode: includeCode,
|
|
21
16
|
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
22
17
|
});
|
|
23
|
-
const filenameWithoutPath = Paths.basename(filename) ?? "notebook.py";
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
new Blob([html], { type: "text/html" }),
|
|
27
|
-
Filenames.toHTML(filenameWithoutPath),
|
|
28
|
-
);
|
|
19
|
+
downloadExportedFile(exportedFile);
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
function updateAssetUrl(existingUrl: string, assetBaseUrl: string) {
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
EnvironmentInfo,
|
|
20
20
|
ExportAsHTMLRequest,
|
|
21
21
|
ExportAsMarkdownRequest,
|
|
22
|
+
ExportedFile,
|
|
22
23
|
FileCopyResponse,
|
|
23
24
|
FileCreateResponse,
|
|
24
25
|
FileDeleteResponse,
|
|
@@ -520,7 +521,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
520
521
|
functionName: "export_html",
|
|
521
522
|
payload: request,
|
|
522
523
|
});
|
|
523
|
-
return response as string
|
|
524
|
+
return response as ExportedFile<string>;
|
|
524
525
|
};
|
|
525
526
|
|
|
526
527
|
exportAsMarkdown: EditRequests["exportAsMarkdown"] = async (
|
|
@@ -530,7 +531,7 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
530
531
|
functionName: "export_markdown",
|
|
531
532
|
payload: request,
|
|
532
533
|
});
|
|
533
|
-
return response as string
|
|
534
|
+
return response as ExportedFile<string>;
|
|
534
535
|
};
|
|
535
536
|
|
|
536
537
|
previewDatasetColumn: EditRequests["previewDatasetColumn"] = async (
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
EnvironmentInfo,
|
|
13
13
|
ExportAsHTMLRequest,
|
|
14
14
|
ExportAsMarkdownRequest,
|
|
15
|
+
ExportedFile,
|
|
15
16
|
FileCopyRequest,
|
|
16
17
|
FileCopyResponse,
|
|
17
18
|
FileCreateRequest,
|
|
@@ -96,8 +97,10 @@ export interface RawBridge {
|
|
|
96
97
|
load_packages(request: string): Promise<string>;
|
|
97
98
|
read_file(request: string): Promise<string>;
|
|
98
99
|
set_interrupt_buffer(request: Uint8Array): Promise<string>;
|
|
99
|
-
export_html(request: ExportAsHTMLRequest): Promise<string
|
|
100
|
-
export_markdown(
|
|
100
|
+
export_html(request: ExportAsHTMLRequest): Promise<ExportedFile<string>>;
|
|
101
|
+
export_markdown(
|
|
102
|
+
request: ExportAsMarkdownRequest,
|
|
103
|
+
): Promise<ExportedFile<string>>;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
export type BridgePayload<T extends keyof RawBridge> = T extends keyof RawBridge
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
downloadAsPDF,
|
|
9
9
|
downloadByURL,
|
|
10
10
|
downloadCellOutputAsImage,
|
|
11
|
+
downloadExportedFile,
|
|
11
12
|
downloadHTMLAsImage,
|
|
12
13
|
getImageDataUrlForCell,
|
|
13
14
|
withLoadingToast,
|
|
@@ -195,7 +196,6 @@ describe("downloadAsPDF", () => {
|
|
|
195
196
|
|
|
196
197
|
await expect(
|
|
197
198
|
downloadAsPDF({
|
|
198
|
-
filename: "path/to/notebook.py",
|
|
199
199
|
webpdf: false,
|
|
200
200
|
preset: "slides",
|
|
201
201
|
}),
|
|
@@ -572,6 +572,7 @@ describe("downloadByURL", () => {
|
|
|
572
572
|
|
|
573
573
|
afterEach(() => {
|
|
574
574
|
vi.restoreAllMocks();
|
|
575
|
+
vi.unstubAllGlobals();
|
|
575
576
|
});
|
|
576
577
|
|
|
577
578
|
it("should create anchor, set attributes, click, and remove", () => {
|
|
@@ -583,4 +584,26 @@ describe("downloadByURL", () => {
|
|
|
583
584
|
expect(mockAnchor.click).toHaveBeenCalled();
|
|
584
585
|
expect(mockAnchor.remove).toHaveBeenCalled();
|
|
585
586
|
});
|
|
587
|
+
|
|
588
|
+
it("downloads an exported file with its returned metadata", async () => {
|
|
589
|
+
let downloadedBlob: Blob | undefined;
|
|
590
|
+
vi.stubGlobal("URL", {
|
|
591
|
+
createObjectURL: vi.fn((blob: Blob) => {
|
|
592
|
+
downloadedBlob = blob;
|
|
593
|
+
return "blob:export";
|
|
594
|
+
}),
|
|
595
|
+
revokeObjectURL: vi.fn(),
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
downloadExportedFile({
|
|
599
|
+
contents: "notebook",
|
|
600
|
+
filename: "résumé.qmd",
|
|
601
|
+
mediaType: "text/plain; charset=utf-8",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
expect(downloadedBlob?.type).toBe("text/plain; charset=utf-8");
|
|
605
|
+
expect(await downloadedBlob?.text()).toBe("notebook");
|
|
606
|
+
expect(mockAnchor.download).toBe("résumé.qmd");
|
|
607
|
+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:export");
|
|
608
|
+
});
|
|
586
609
|
});
|
package/src/utils/download.ts
CHANGED
|
@@ -4,8 +4,8 @@ import React from "react";
|
|
|
4
4
|
import { toast } from "@/components/ui/use-toast";
|
|
5
5
|
import { type CellId, CellOutputId } from "@/core/cells/ids";
|
|
6
6
|
import { getRequestClient } from "@/core/network/requests";
|
|
7
|
+
import type { ExportedFile } from "@/core/network/types";
|
|
7
8
|
import { Filenames } from "@/utils/filenames";
|
|
8
|
-
import { Paths } from "@/utils/paths";
|
|
9
9
|
import { prettyError } from "./errors";
|
|
10
10
|
import { toPng } from "./html-to-image";
|
|
11
11
|
import { captureExternalIframes } from "./iframe";
|
|
@@ -191,6 +191,14 @@ export function downloadBlob(blob: Blob, filename: string) {
|
|
|
191
191
|
URL.revokeObjectURL(url);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
export function downloadExportedFile({
|
|
195
|
+
contents,
|
|
196
|
+
filename,
|
|
197
|
+
mediaType,
|
|
198
|
+
}: ExportedFile) {
|
|
199
|
+
downloadBlob(new Blob([contents], { type: mediaType }), filename);
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
export type PDFExportPreset = "document" | "slides";
|
|
195
203
|
|
|
196
204
|
/**
|
|
@@ -200,7 +208,6 @@ export type PDFExportPreset = "document" | "slides";
|
|
|
200
208
|
* Standard PDF requires Pandoc & TeX (~few GBs) but is of higher quality.
|
|
201
209
|
*/
|
|
202
210
|
export async function downloadAsPDF(opts: {
|
|
203
|
-
filename: string;
|
|
204
211
|
webpdf: boolean;
|
|
205
212
|
preset?: PDFExportPreset;
|
|
206
213
|
includeInputs?: boolean;
|
|
@@ -210,7 +217,6 @@ export async function downloadAsPDF(opts: {
|
|
|
210
217
|
}) {
|
|
211
218
|
const client = getRequestClient();
|
|
212
219
|
const {
|
|
213
|
-
filename,
|
|
214
220
|
webpdf,
|
|
215
221
|
preset = "document",
|
|
216
222
|
includeInputs = true,
|
|
@@ -220,7 +226,7 @@ export async function downloadAsPDF(opts: {
|
|
|
220
226
|
} = opts;
|
|
221
227
|
|
|
222
228
|
try {
|
|
223
|
-
const
|
|
229
|
+
const exportedFile = await client.exportAsPDF({
|
|
224
230
|
webpdf,
|
|
225
231
|
preset,
|
|
226
232
|
includeInputs,
|
|
@@ -229,8 +235,7 @@ export async function downloadAsPDF(opts: {
|
|
|
229
235
|
rasterServer,
|
|
230
236
|
});
|
|
231
237
|
|
|
232
|
-
|
|
233
|
-
downloadBlob(pdfBlob, Filenames.toPDF(filenameWithoutPath));
|
|
238
|
+
downloadExportedFile(exportedFile);
|
|
234
239
|
} catch (error) {
|
|
235
240
|
toast({
|
|
236
241
|
title: "Failed to download",
|