@marimo-team/islands 0.23.17-dev32 → 0.23.17-dev33
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/{common-XU-hyhVj.js → common-C2Coc2Bh.js} +1 -1
- package/dist/main.js +2 -2
- package/dist/{reveal-component-DEpX3cZP.js → reveal-component--Ln8gdbm.js} +1 -1
- package/package.json +1 -1
- package/src/components/editor/actions/export-dialog/export-command.ts +4 -9
- package/src/core/network/__tests__/api.test.ts +132 -17
- package/src/core/network/__tests__/export-filename.test.ts +43 -0
- package/src/core/network/api.ts +47 -12
- package/src/core/network/export-filename.ts +37 -0
- package/src/core/network/markdown-export.ts +19 -0
- package/src/core/network/requests-network.ts +29 -5
- package/src/core/network/types.ts +3 -0
|
@@ -36338,7 +36338,7 @@ ${d}`,
|
|
|
36338
36338
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
36339
36339
|
}
|
|
36340
36340
|
}
|
|
36341
|
-
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.17-
|
|
36341
|
+
marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.17-dev33");
|
|
36342
36342
|
showCodeInRunModeAtom = atom(true);
|
|
36343
36343
|
atom(null);
|
|
36344
36344
|
var import_compiler_runtime = require_compiler_runtime();
|
package/dist/main.js
CHANGED
|
@@ -26,7 +26,7 @@ import { $ as notebookOutline, $t as isUninstantiated, A as ChevronDownIcon, An
|
|
|
26
26
|
import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-B9WoXK1R.js";
|
|
27
27
|
import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist-DgzzIPS4.js";
|
|
28
28
|
import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-C7rMyZ_8.js";
|
|
29
|
-
import { $ as CommandSeparator, $t as Kbd, A as ErrorState, At as getPageIndexForRow, B as ContextMenuTrigger, Bt as dateToLocalISODate, C as downloadHTMLAsImage, Ct as inferFieldTypes, D as ColumnName, Dt as DelayMount, E as downloadSizeLimitAtom, Et as ColumnChartSpecModel, F as getUserColumnVisibilityCounts, Ft as TOO_MANY_ROWS, G as DateRangePicker, Gt as TabsList, H as useSelectList, Ht as dateToLocalISOTime, I as ContextMenu, It as toFieldTypes, J as Command, Jt as ChartInfoState, K as Combobox, Kt as TabsTrigger, L as ContextMenuContent, Lt as getMimeValues, M as prettifyRowColumnCount, Mt as loadTableData, N as prettifyRowCount, Nt as INDEX_COLUMN_NAME, O as ColumnPreviewContainer, Ot as useIntersectionObserver, P as getColumnCountForDisplay, Pt as SELECT_COLUMN_ID, Q as CommandList, Qt as RenderTextWithLinks, R as ContextMenuItem, Rt as isNullishFilter, S as downloadExportedFile, St as generateColumns, T as Filenames, Tt as ColumnChartContext, U as CompactChipRow, Ut as Tabs, V as useInternalStateWithSync, Vt as dateToLocalISODateTime, W as DatePicker, Wt as TabsContent, X as CommandInput, Xt as LazyVegaEmbed, Y as CommandEmpty, Yt as ChartLoadingState, Z as CommandItem, Zt as useOverflowDetection, _ as DataTable, _n as ChevronLeft, _t as TableCell, an as TextWrap, at as contextAwarePanelType, b as downloadBlob, bt as TableRow, c as SLIDE_TYPE_OPTIONS_BY_VALUE, cn as EyeOff, ct as slotsController, d as Switch, dn as Download, en as HtmlOutput, et as smartMatch, f as RadioGroup, fn as Code, g as OutputRenderer, gn as ChevronsDownUp, gt as TableBody, h as OutputArea, hn as ChevronsLeft, ht as Table, i as showCodeInRunModeAtom, in as $fae977aafc393c5c$export$6b862160d295c8e, it as contextAwarePanelOwner, j as LoadingState, jt as loadTableAndRawData, k as EmptyState, kt as usePrevious$1, lt as Toggle, m as JsonOutput, mn as ChevronsRight, mt as Provider$1, n as useNotebookCodeAvailable, nn as safeLocale, nt as PANEL_TYPES, on as GripHorizontal, ot as isCellAwareAtom, p as RadioGroupItem, pn as ChevronsUpDown, pt as Fill, q as ComboboxItem, qt as ChartErrorState, r as marimoVersionAtom, rn as $fae977aafc393c5c$export$588937bcd60ade55, rt as contextAwarePanelOpen, sn as Funnel, st as SlotNames, t as cellDomProps, tn as EmotionCacheProvider, tt as ContextAwarePanelItem, u as Slide, un as Ellipsis, v as InstallPackageButton, vn as ArrowDownWideNarrow, vt as TableHead, w as Progress, wt as renderCellValue, x as downloadByURL, xt as NAMELESS_COLUMN_PREFIX, y as ADD_PRINTING_CLASS, yt as TableHeader, z as ContextMenuSeparator, zt as Maps, __tla as __tla_2 } from "./common-
|
|
29
|
+
import { $ as CommandSeparator, $t as Kbd, A as ErrorState, At as getPageIndexForRow, B as ContextMenuTrigger, Bt as dateToLocalISODate, C as downloadHTMLAsImage, Ct as inferFieldTypes, D as ColumnName, Dt as DelayMount, E as downloadSizeLimitAtom, Et as ColumnChartSpecModel, F as getUserColumnVisibilityCounts, Ft as TOO_MANY_ROWS, G as DateRangePicker, Gt as TabsList, H as useSelectList, Ht as dateToLocalISOTime, I as ContextMenu, It as toFieldTypes, J as Command, Jt as ChartInfoState, K as Combobox, Kt as TabsTrigger, L as ContextMenuContent, Lt as getMimeValues, M as prettifyRowColumnCount, Mt as loadTableData, N as prettifyRowCount, Nt as INDEX_COLUMN_NAME, O as ColumnPreviewContainer, Ot as useIntersectionObserver, P as getColumnCountForDisplay, Pt as SELECT_COLUMN_ID, Q as CommandList, Qt as RenderTextWithLinks, R as ContextMenuItem, Rt as isNullishFilter, S as downloadExportedFile, St as generateColumns, T as Filenames, Tt as ColumnChartContext, U as CompactChipRow, Ut as Tabs, V as useInternalStateWithSync, Vt as dateToLocalISODateTime, W as DatePicker, Wt as TabsContent, X as CommandInput, Xt as LazyVegaEmbed, Y as CommandEmpty, Yt as ChartLoadingState, Z as CommandItem, Zt as useOverflowDetection, _ as DataTable, _n as ChevronLeft, _t as TableCell, an as TextWrap, at as contextAwarePanelType, b as downloadBlob, bt as TableRow, c as SLIDE_TYPE_OPTIONS_BY_VALUE, cn as EyeOff, ct as slotsController, d as Switch, dn as Download, en as HtmlOutput, et as smartMatch, f as RadioGroup, fn as Code, g as OutputRenderer, gn as ChevronsDownUp, gt as TableBody, h as OutputArea, hn as ChevronsLeft, ht as Table, i as showCodeInRunModeAtom, in as $fae977aafc393c5c$export$6b862160d295c8e, it as contextAwarePanelOwner, j as LoadingState, jt as loadTableAndRawData, k as EmptyState, kt as usePrevious$1, lt as Toggle, m as JsonOutput, mn as ChevronsRight, mt as Provider$1, n as useNotebookCodeAvailable, nn as safeLocale, nt as PANEL_TYPES, on as GripHorizontal, ot as isCellAwareAtom, p as RadioGroupItem, pn as ChevronsUpDown, pt as Fill, q as ComboboxItem, qt as ChartErrorState, r as marimoVersionAtom, rn as $fae977aafc393c5c$export$588937bcd60ade55, rt as contextAwarePanelOpen, sn as Funnel, st as SlotNames, t as cellDomProps, tn as EmotionCacheProvider, tt as ContextAwarePanelItem, u as Slide, un as Ellipsis, v as InstallPackageButton, vn as ArrowDownWideNarrow, vt as TableHead, w as Progress, wt as renderCellValue, x as downloadByURL, xt as NAMELESS_COLUMN_PREFIX, y as ADD_PRINTING_CLASS, yt as TableHeader, z as ContextMenuSeparator, zt as Maps, __tla as __tla_2 } from "./common-C2Coc2Bh.js";
|
|
30
30
|
import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-CLPBATCv.js";
|
|
31
31
|
import { t as Check } from "./check-CCvJTB2E.js";
|
|
32
32
|
import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-CITcoFNY.js";
|
|
@@ -38214,7 +38214,7 @@ ${c}
|
|
|
38214
38214
|
function _temp2$2(e) {
|
|
38215
38215
|
e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
|
|
38216
38216
|
}
|
|
38217
|
-
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component
|
|
38217
|
+
var LazySlidesComponent = import_react.lazy(() => import("./reveal-component--Ln8gdbm.js"));
|
|
38218
38218
|
const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
|
|
38219
38219
|
var _a5;
|
|
38220
38220
|
let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
|
|
@@ -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-Bgb6aaXi.js";
|
|
10
10
|
import "./chunk-5FQGJX7Z-B9WoXK1R.js";
|
|
11
11
|
import { u as createLucideIcon } from "./dist-DgzzIPS4.js";
|
|
12
|
-
import { a as DEFAULT_DECK_TRANSITION, cn as EyeOff, dt as PanelGroup, fn as Code, ft as PanelResizeHandle, l as SlideSidebar, ln as Expand, n as useNotebookCodeAvailable, o as DEFAULT_DECK_VERTICAL_ALIGN, s as DEFAULT_SLIDE_TYPE, t as cellDomProps, u as Slide, ut as Panel } from "./common-
|
|
12
|
+
import { a as DEFAULT_DECK_TRANSITION, cn as EyeOff, dt as PanelGroup, fn as Code, ft as PanelResizeHandle, l as SlideSidebar, ln as Expand, n as useNotebookCodeAvailable, o as DEFAULT_DECK_VERTICAL_ALIGN, s as DEFAULT_SLIDE_TYPE, t as cellDomProps, u as Slide, ut as Panel } from "./common-C2Coc2Bh.js";
|
|
13
13
|
import { R as ChevronDown } from "./strings-CITcoFNY.js";
|
|
14
14
|
import { X as useDebouncedCallback } from "./input-DtTqU0xh.js";
|
|
15
15
|
import { v as kioskModeAtom } from "./toDate-kxcuDOWd.js";
|
package/package.json
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
MARKDOWN_EXTENSIONS,
|
|
5
|
+
MARKDOWN_SUFFIXES,
|
|
6
|
+
} from "@/core/network/markdown-export";
|
|
3
7
|
import { Filenames } from "@/utils/filenames";
|
|
4
8
|
import { shellQuote } from "@/utils/shell";
|
|
5
9
|
import type { ExportFormat, ExportOptions, MarkdownFlavor } from "./state";
|
|
6
10
|
|
|
7
|
-
const MARKDOWN_EXTENSIONS: Record<MarkdownFlavor, string> = {
|
|
8
|
-
pymdown: "md",
|
|
9
|
-
qmd: "qmd",
|
|
10
|
-
mystmd: "myst.md",
|
|
11
|
-
mdx: "mdx",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const MARKDOWN_SUFFIXES = [".myst.md", ".markdown", ".qmd", ".mdx", ".md"];
|
|
15
|
-
|
|
16
11
|
function inferMarkdownFlavor(filename: string): MarkdownFlavor {
|
|
17
12
|
if (filename.endsWith(".myst.md")) {
|
|
18
13
|
return "mystmd";
|
|
@@ -105,7 +105,10 @@ describe("API", () => {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
await expect(
|
|
108
|
-
API.handleExportResponse(
|
|
108
|
+
API.handleExportResponse(
|
|
109
|
+
{ data: "markdown", response },
|
|
110
|
+
{ defaultFilename: "download.md" },
|
|
111
|
+
),
|
|
109
112
|
).resolves.toEqual({
|
|
110
113
|
contents: "markdown",
|
|
111
114
|
filename: "résumé.qmd",
|
|
@@ -113,27 +116,139 @@ describe("API", () => {
|
|
|
113
116
|
});
|
|
114
117
|
});
|
|
115
118
|
|
|
116
|
-
it("
|
|
119
|
+
it("parses quoted Content-Disposition filenames", async () => {
|
|
120
|
+
const response = new Response("html", {
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Disposition": 'inline; filename="notebook.html"',
|
|
123
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
117
127
|
await expect(
|
|
118
|
-
API.handleExportResponse(
|
|
119
|
-
data: "
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
API.handleExportResponse(
|
|
129
|
+
{ data: "html", response },
|
|
130
|
+
{ defaultFilename: "download.html" },
|
|
131
|
+
),
|
|
132
|
+
).resolves.toEqual({
|
|
133
|
+
contents: "html",
|
|
134
|
+
filename: "notebook.html",
|
|
135
|
+
mediaType: "text/html; charset=utf-8",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("parses unquoted Content-Disposition filenames", async () => {
|
|
140
|
+
const response = new Response("pdf", {
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Disposition": "attachment; filename=notebook.pdf",
|
|
143
|
+
"Content-Type": "application/pdf",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
API.handleExportResponse(
|
|
149
|
+
{ data: "pdf", response },
|
|
150
|
+
{ defaultFilename: "download.pdf" },
|
|
151
|
+
),
|
|
152
|
+
).resolves.toEqual({
|
|
153
|
+
contents: "pdf",
|
|
154
|
+
filename: "notebook.pdf",
|
|
155
|
+
mediaType: "application/pdf",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("prefers RFC 5987 filenames over quoted fallbacks", async () => {
|
|
160
|
+
const response = new Response("markdown", {
|
|
161
|
+
headers: {
|
|
162
|
+
"Content-Disposition":
|
|
163
|
+
"attachment; filename=\"wrong.md\"; filename*=UTF-8''notebook.md",
|
|
164
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
API.handleExportResponse(
|
|
170
|
+
{ data: "markdown", response },
|
|
171
|
+
{ defaultFilename: "download.md" },
|
|
172
|
+
),
|
|
173
|
+
).resolves.toEqual({
|
|
174
|
+
contents: "markdown",
|
|
175
|
+
filename: "notebook.md",
|
|
176
|
+
mediaType: "text/plain; charset=utf-8",
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("falls back to a default filename when headers are missing", async () => {
|
|
181
|
+
const response = new Response("markdown", {
|
|
182
|
+
headers: {
|
|
183
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await expect(
|
|
188
|
+
API.handleExportResponse(
|
|
189
|
+
{ data: "markdown", response },
|
|
190
|
+
{ defaultFilename: "download.md" },
|
|
191
|
+
),
|
|
192
|
+
).resolves.toEqual({
|
|
193
|
+
contents: "markdown",
|
|
194
|
+
filename: "download.md",
|
|
195
|
+
mediaType: "text/plain; charset=utf-8",
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("falls back to a default filename when Content-Disposition is unparsable", async () => {
|
|
200
|
+
const response = new Response("markdown", {
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Disposition": "attachment",
|
|
203
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
API.handleExportResponse(
|
|
209
|
+
{ data: "markdown", response },
|
|
210
|
+
{ defaultFilename: "download.md" },
|
|
211
|
+
),
|
|
212
|
+
).resolves.toEqual({
|
|
213
|
+
contents: "markdown",
|
|
214
|
+
filename: "download.md",
|
|
215
|
+
mediaType: "text/plain; charset=utf-8",
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("falls back to a default filename when RFC 5987 encoding is malformed", async () => {
|
|
220
|
+
const response = new Response("markdown", {
|
|
221
|
+
headers: {
|
|
222
|
+
"Content-Disposition": "attachment; filename*=UTF-8''%E0%A4%A",
|
|
223
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await expect(
|
|
228
|
+
API.handleExportResponse(
|
|
229
|
+
{ data: "markdown", response },
|
|
230
|
+
{ defaultFilename: "download.md" },
|
|
231
|
+
),
|
|
232
|
+
).resolves.toEqual({
|
|
233
|
+
contents: "markdown",
|
|
234
|
+
filename: "download.md",
|
|
235
|
+
mediaType: "text/plain; charset=utf-8",
|
|
236
|
+
});
|
|
125
237
|
});
|
|
126
238
|
|
|
127
239
|
it("rejects export responses without a media type", async () => {
|
|
128
240
|
await expect(
|
|
129
|
-
API.handleExportResponse(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
241
|
+
API.handleExportResponse(
|
|
242
|
+
{
|
|
243
|
+
data: "markdown",
|
|
244
|
+
response: new Response(null, {
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Disposition": "attachment; filename*=UTF-8''notebook.md",
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
{ defaultFilename: "download.md" },
|
|
251
|
+
),
|
|
137
252
|
).rejects.toThrow("Export response is missing a media type");
|
|
138
253
|
});
|
|
139
254
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
5
|
+
import { store } from "@/core/state/jotai";
|
|
6
|
+
import {
|
|
7
|
+
getDefaultExportFilename,
|
|
8
|
+
getDefaultMarkdownExportFilename,
|
|
9
|
+
} from "../export-filename";
|
|
10
|
+
|
|
11
|
+
describe("default export filenames", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
store.set(filenameAtom, "folder/my_notebook.py");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("derives filenames from the notebook name", () => {
|
|
17
|
+
expect(getDefaultExportFilename("html")).toBe("my_notebook.html");
|
|
18
|
+
expect(getDefaultExportFilename("pdf")).toBe("my_notebook.pdf");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("handles Windows-style notebook paths", () => {
|
|
22
|
+
store.set(filenameAtom, "C:\\Users\\me\\my_notebook.py");
|
|
23
|
+
expect(getDefaultExportFilename("html")).toBe("my_notebook.html");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("derives markdown filenames from the selected flavor", () => {
|
|
27
|
+
expect(getDefaultMarkdownExportFilename("qmd")).toBe("my_notebook.qmd");
|
|
28
|
+
expect(getDefaultMarkdownExportFilename("mystmd")).toBe(
|
|
29
|
+
"my_notebook.myst.md",
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("derives script export filenames from the notebook name", () => {
|
|
34
|
+
expect(getDefaultExportFilename("script.py")).toBe("my_notebook.script.py");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("falls back to download.* when the notebook is unnamed", () => {
|
|
38
|
+
store.set(filenameAtom, null);
|
|
39
|
+
expect(getDefaultExportFilename("html")).toBe("download.html");
|
|
40
|
+
expect(getDefaultMarkdownExportFilename("qmd")).toBe("download.qmd");
|
|
41
|
+
expect(getDefaultExportFilename("script.py")).toBe("download.script.py");
|
|
42
|
+
});
|
|
43
|
+
});
|
package/src/core/network/api.ts
CHANGED
|
@@ -16,13 +16,45 @@ function getBaseUriWithoutQueryParams(): string {
|
|
|
16
16
|
return url.toString();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!
|
|
23
|
-
|
|
19
|
+
function parseContentDispositionFilename(
|
|
20
|
+
contentDisposition: string | null,
|
|
21
|
+
): string | undefined {
|
|
22
|
+
if (!contentDisposition) {
|
|
23
|
+
return undefined;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
const rfc5987Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
27
|
+
if (rfc5987Match) {
|
|
28
|
+
try {
|
|
29
|
+
return decodeURIComponent(rfc5987Match[1]);
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const quotedMatch = contentDisposition.match(/filename="([^"]+)"/i);
|
|
36
|
+
if (quotedMatch) {
|
|
37
|
+
return quotedMatch[1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const unquotedMatch = contentDisposition.match(/filename=([^;]+)/i);
|
|
41
|
+
if (unquotedMatch) {
|
|
42
|
+
return unquotedMatch[1].trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getExportFilename(response: Response): string | undefined {
|
|
49
|
+
const filename = parseContentDispositionFilename(
|
|
50
|
+
response.headers.get("Content-Disposition"),
|
|
51
|
+
);
|
|
52
|
+
if (!filename) {
|
|
53
|
+
Logger.warn(
|
|
54
|
+
"Export response is missing a readable Content-Disposition filename",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return filename;
|
|
26
58
|
}
|
|
27
59
|
|
|
28
60
|
/**
|
|
@@ -129,11 +161,14 @@ export const API = {
|
|
|
129
161
|
}
|
|
130
162
|
return Promise.resolve(response.data as T);
|
|
131
163
|
},
|
|
132
|
-
handleExportResponse: async <T extends BlobPart>(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
handleExportResponse: async <T extends BlobPart>(
|
|
165
|
+
response: {
|
|
166
|
+
data?: T | undefined;
|
|
167
|
+
error?: Record<string, unknown>;
|
|
168
|
+
response: Response;
|
|
169
|
+
},
|
|
170
|
+
opts: { defaultFilename: string },
|
|
171
|
+
): Promise<ExportedFile<T>> => {
|
|
137
172
|
const contents = await API.handleResponse<T>(response);
|
|
138
173
|
const mediaType = response.response.headers.get("Content-Type");
|
|
139
174
|
if (!mediaType) {
|
|
@@ -142,7 +177,7 @@ export const API = {
|
|
|
142
177
|
|
|
143
178
|
return {
|
|
144
179
|
contents,
|
|
145
|
-
filename: getExportFilename(response.response),
|
|
180
|
+
filename: getExportFilename(response.response) ?? opts.defaultFilename,
|
|
146
181
|
mediaType,
|
|
147
182
|
};
|
|
148
183
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
4
|
+
import { store } from "@/core/state/jotai";
|
|
5
|
+
import { Filenames } from "@/utils/filenames";
|
|
6
|
+
import { Paths } from "@/utils/paths";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MARKDOWN_FLAVOR,
|
|
9
|
+
MARKDOWN_EXTENSIONS,
|
|
10
|
+
} from "./markdown-export";
|
|
11
|
+
import type { ExportAsMarkdownRequest } from "./types";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_EXPORT_FILENAME = "download";
|
|
14
|
+
|
|
15
|
+
function getNotebookStem(): string | undefined {
|
|
16
|
+
const filename = store.get(filenameAtom);
|
|
17
|
+
if (!filename) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const basename = Paths.basename(filename);
|
|
21
|
+
return Filenames.withoutExtension(basename);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getDefaultExportFilename(extension: string): string {
|
|
25
|
+
const stem = getNotebookStem();
|
|
26
|
+
if (stem) {
|
|
27
|
+
return `${stem}.${extension}`;
|
|
28
|
+
}
|
|
29
|
+
return `${DEFAULT_EXPORT_FILENAME}.${extension}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getDefaultMarkdownExportFilename(
|
|
33
|
+
flavor: ExportAsMarkdownRequest["flavor"],
|
|
34
|
+
): string {
|
|
35
|
+
const extension = MARKDOWN_EXTENSIONS[flavor ?? DEFAULT_MARKDOWN_FLAVOR];
|
|
36
|
+
return getDefaultExportFilename(extension);
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { MarkdownExportFlavor } from "./types";
|
|
4
|
+
|
|
5
|
+
export const MARKDOWN_EXTENSIONS = {
|
|
6
|
+
pymdown: "md",
|
|
7
|
+
qmd: "qmd",
|
|
8
|
+
mystmd: "myst.md",
|
|
9
|
+
mdx: "mdx",
|
|
10
|
+
} satisfies Record<MarkdownExportFlavor, string>;
|
|
11
|
+
export const MARKDOWN_SUFFIXES = [
|
|
12
|
+
".myst.md",
|
|
13
|
+
".markdown",
|
|
14
|
+
".qmd",
|
|
15
|
+
".mdx",
|
|
16
|
+
".md",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_MARKDOWN_FLAVOR: MarkdownExportFlavor = "pymdown";
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { once } from "@/utils/once";
|
|
4
4
|
import { getRuntimeManager } from "../runtime/config";
|
|
5
5
|
import { API, createClientWithRuntimeManager } from "./api";
|
|
6
|
+
import {
|
|
7
|
+
getDefaultExportFilename,
|
|
8
|
+
getDefaultMarkdownExportFilename,
|
|
9
|
+
} from "./export-filename";
|
|
6
10
|
import {
|
|
7
11
|
waitForConnectionOpen,
|
|
8
12
|
waitForConnectionOpenIfNotebook,
|
|
@@ -426,7 +430,11 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
426
430
|
parseAs: "text",
|
|
427
431
|
params: getParams(),
|
|
428
432
|
})
|
|
429
|
-
.then(
|
|
433
|
+
.then((response) =>
|
|
434
|
+
handleExportResponse(response, {
|
|
435
|
+
defaultFilename: getDefaultExportFilename("html"),
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
430
438
|
},
|
|
431
439
|
exportAsMarkdown: async (request) => {
|
|
432
440
|
return getClient()
|
|
@@ -435,7 +443,11 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
435
443
|
parseAs: "text",
|
|
436
444
|
params: getParams(),
|
|
437
445
|
})
|
|
438
|
-
.then(
|
|
446
|
+
.then((response) =>
|
|
447
|
+
handleExportResponse(response, {
|
|
448
|
+
defaultFilename: getDefaultMarkdownExportFilename(request.flavor),
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
439
451
|
},
|
|
440
452
|
exportAsScript: async (request) => {
|
|
441
453
|
return getClient()
|
|
@@ -444,7 +456,11 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
444
456
|
parseAs: "text",
|
|
445
457
|
params: getParams(),
|
|
446
458
|
})
|
|
447
|
-
.then(
|
|
459
|
+
.then((response) =>
|
|
460
|
+
handleExportResponse(response, {
|
|
461
|
+
defaultFilename: getDefaultExportFilename("script.py"),
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
448
464
|
},
|
|
449
465
|
exportAsIPYNB: async (request) => {
|
|
450
466
|
return getClient()
|
|
@@ -453,7 +469,11 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
453
469
|
parseAs: "text",
|
|
454
470
|
params: getParams(),
|
|
455
471
|
})
|
|
456
|
-
.then(
|
|
472
|
+
.then((response) =>
|
|
473
|
+
handleExportResponse(response, {
|
|
474
|
+
defaultFilename: getDefaultExportFilename("ipynb"),
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
457
477
|
},
|
|
458
478
|
exportAsPDF: async (request) => {
|
|
459
479
|
return getClient()
|
|
@@ -462,7 +482,11 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
462
482
|
parseAs: "blob",
|
|
463
483
|
params: getParams(),
|
|
464
484
|
})
|
|
465
|
-
.then(
|
|
485
|
+
.then((response) =>
|
|
486
|
+
handleExportResponse(response, {
|
|
487
|
+
defaultFilename: getDefaultExportFilename("pdf"),
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
466
490
|
},
|
|
467
491
|
autoExportAsHTML: async (request) => {
|
|
468
492
|
return getClient()
|
|
@@ -22,6 +22,9 @@ export type AutoExportAsMarkdownRequest =
|
|
|
22
22
|
schemas["AutoExportAsMarkdownRequest"];
|
|
23
23
|
export type ExportAsHTMLRequest = schemas["ExportAsHTMLRequest"];
|
|
24
24
|
export type ExportAsMarkdownRequest = schemas["ExportAsMarkdownRequest"];
|
|
25
|
+
export type MarkdownExportFlavor = NonNullable<
|
|
26
|
+
ExportAsMarkdownRequest["flavor"]
|
|
27
|
+
>;
|
|
25
28
|
export type ExportAsIPYNBRequest = schemas["ExportAsIPYNBRequest"];
|
|
26
29
|
export type ExportAsScriptRequest = schemas["ExportAsScriptRequest"];
|
|
27
30
|
export type ExportAsPDFRequest = schemas["ExportAsPDFRequest"];
|