@marimo-team/islands 0.23.6-dev15 → 0.23.6-dev16

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/main.js CHANGED
@@ -22,18 +22,18 @@ import { _ as Logger, c as Objects, g as cn, h as Events, i as NOT_SET, l as use
22
22
  import { t as require_react } from "./react-DA-nE2FX.js";
23
23
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
24
24
  import { n as Copy, r as toast, t as copyToClipboard } from "./copy-TGGAUEWp.js";
25
- import { $ as useCellActions, At as DeferredRequestRegistry, B as safeExtractSetUIElementMessageBuffers, Bn as Braces, Bt as getDataTypeColor, C as AccordionContent, Cn as Root2$1, Ct as customPythonLanguageSupport, Dn as Table2, Dt as Paths, E as BorderAllIcon, En as Trash2, Et as PathBuilder, F as base64ToDataView, Fn as Eye, Ft as jotaiJsonStorage, Gt as convertStatsName, H as getMarimoExportContext, In as Database, J as getCellNames, Jt as useRequestClient, K as createActions, Kt as getRequestClient, L as dataViewToBase64, Ln as Columns2, Mn as Layers, Nn as Info, Nt as repl, On as PaintRoller, Pn as FileText, Q as reducer, Rt as PluralWords, S as Accordion, Sn as Item$1, St as Checkbox, T as AccordionTrigger, U as hasTrustedExportContext, V as renderHTML, Vt as require_client, W as hasRunAnyCellAtom, X as notebookOutline, Y as notebookAtom, Yt as isUninstantiated, Z as numColumnsAtom, _n as atomWithStorage, a as useLastFocusedCellId, an as parseInitialValue, bt as isInternalCellName, ct as kioskModeAtom, dn as OBJECT_ID_ATTR, dt as outputIsLoading, en as NotebookScopedLocalStorage, et as useCellIds, f as isOutputEmpty, fn as RANDOM_ID_ATTR, ft as outputIsStale, gn as atomWithReducer, i as useCellFocusActions, in as parseDataset, jn as LoaderCircle, jt as generateUUID, k as ChevronDownIcon, ln as UIElementId, mt as headingToIdentifier, nt as createCell, o as maybeAddAltairImport, ot as getInitialAppMode, p as useExpandedConsoleOutput, pn as jsonParseWithSpecialChar, pt as isErrorMime, qt as requestClientAtom, r as LazyAnyLanguageCodeMirror, rn as parseAttrValue, s as Spinner, sn as HTMLCellId, st as initialModeAtom, un as findCellId, vn as selectAtom, w as AccordionItem, wn as Trigger2, wt as MarkdownLanguageAdapter, xn as Content2, xt as normalizeName, yt as getValidName, zn as CircleAlert, zt as DATA_TYPE_ICON, __tla as __tla_0 } from "./html-to-image-6VI69paz.js";
25
+ import { $ as useCellActions, At as DeferredRequestRegistry, B as safeExtractSetUIElementMessageBuffers, Bn as Braces, Bt as getDataTypeColor, C as AccordionContent, Cn as Root2$1, Ct as customPythonLanguageSupport, Dn as Table2, Dt as Paths, E as BorderAllIcon, En as Trash2, Et as PathBuilder, F as base64ToDataView, Fn as Eye, Ft as jotaiJsonStorage, Gt as convertStatsName, H as getMarimoExportContext, In as Database, J as getCellNames, Jt as useRequestClient, K as createActions, Kt as getRequestClient, L as dataViewToBase64, Ln as Columns2, Mn as Layers, Nn as Info, Nt as repl, On as PaintRoller, Pn as FileText, Q as reducer, Rt as PluralWords, S as Accordion, Sn as Item$1, St as Checkbox, T as AccordionTrigger, U as hasTrustedExportContext, V as renderHTML, Vt as require_client, W as hasRunAnyCellAtom, X as notebookOutline, Y as notebookAtom, Yt as isUninstantiated, Z as numColumnsAtom, _n as atomWithStorage, a as useLastFocusedCellId, an as parseInitialValue, bt as isInternalCellName, ct as kioskModeAtom, dn as OBJECT_ID_ATTR, dt as outputIsLoading, en as NotebookScopedLocalStorage, et as useCellIds, f as isOutputEmpty, fn as RANDOM_ID_ATTR, ft as outputIsStale, gn as atomWithReducer, i as useCellFocusActions, in as parseDataset, jn as LoaderCircle, jt as generateUUID, k as ChevronDownIcon, ln as UIElementId, mt as headingToIdentifier, nt as createCell, o as maybeAddAltairImport, ot as getInitialAppMode, p as useExpandedConsoleOutput, pn as jsonParseWithSpecialChar, pt as isErrorMime, qt as requestClientAtom, r as LazyAnyLanguageCodeMirror, rn as parseAttrValue, s as Spinner, sn as HTMLCellId, st as initialModeAtom, un as findCellId, vn as selectAtom, w as AccordionItem, wn as Trigger2, wt as MarkdownLanguageAdapter, xn as Content2, xt as normalizeName, yt as getValidName, zn as CircleAlert, zt as DATA_TYPE_ICON, __tla as __tla_0 } from "./html-to-image-uzALXlch.js";
26
26
  import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-CO1e63h_.js";
27
27
  import { o as useSize, s as Root$3, u as createLucideIcon } from "./dist-ESg7xyoD.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$1, 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-BKWq0wn2.js";
29
- import { $ as getPageIndexForRow, A as contextAwarePanelType, At as GripHorizontal, B as TableHead, Bt as ChevronsDownUp, C as useInternalStateWithSync, Ct as RenderTextWithLinks, D as PANEL_TYPES, Dt as getStaticVirtualFiles, E as ContextAwarePanelItem, Et as EmotionCacheProvider, F as Fill, Ft as Download, G as inferFieldTypes, H as TableRow, Ht as ArrowDownWideNarrow, I as Provider$1, It as Code, J as ColumnChartSpecModel, K as renderCellValue, L as Table, Lt as ChevronsUpDown, M as SlotNames, Mt as EyeOff, N as slotsController, O as contextAwarePanelOpen, Ot as isStaticNotebook, P as Toggle, Pt as Ellipsis, Q as filtersToFilterGroup, R as TableBody, Rt as ChevronsRight, S as prettifyRowCount, St as useOverflowDetection, T as ComboboxItem, Tt as HtmlOutput, U as NAMELESS_COLUMN_PREFIX, V as TableHeader, Vt as ChevronLeft, W as generateColumns, X as useIntersectionObserver, Y as DelayMount, Z as usePrevious$1, _ as downloadBlob, _t as TabsTrigger, at as toFieldTypes, b as Filenames, bt as ChartLoadingState, c as Slide, ct as CommandEmpty, d as JsonOutput, dt as CommandList, et as loadTableAndRawData, f as OutputArea, ft as CommandSeparator, g as ADD_PRINTING_CLASS, gt as TabsList, h as InstallPackageButton, ht as TabsContent, it as TOO_MANY_ROWS, j as isCellAwareAtom, jt as Funnel, k as contextAwarePanelOwner, kt as TextWrap, l as RadioGroup, lt as CommandInput, m as DataTable, mt as Tabs, n as marimoVersionAtom, nt as INDEX_COLUMN_NAME, o as SLIDE_TYPE_OPTIONS_BY_VALUE, ot as getMimeValues, p as OutputRenderer, pt as Maps, q as ColumnChartContext, r as showCodeInRunModeAtom, rt as SELECT_COLUMN_ID, st as Command, t as useNotebookCodeAvailable, tt as loadTableData, u as RadioGroupItem, ut as CommandItem, v as downloadByURL, vt as ChartErrorState, w as Combobox, wt as Kbd, x as prettifyRowColumnCount, xt as LazyVegaEmbed, y as downloadHTMLAsImage, yt as ChartInfoState, z as TableCell, zt as ChevronsLeft, __tla as __tla_2 } from "./code-visibility-Ch6utETw.js";
29
+ import { $ as getPageIndexForRow, A as contextAwarePanelType, At as GripHorizontal, B as TableHead, Bt as ChevronsDownUp, C as useInternalStateWithSync, Ct as RenderTextWithLinks, D as PANEL_TYPES, Dt as getStaticVirtualFiles, E as ContextAwarePanelItem, Et as EmotionCacheProvider, F as Fill, Ft as Download, G as inferFieldTypes, H as TableRow, Ht as ArrowDownWideNarrow, I as Provider$1, It as Code, J as ColumnChartSpecModel, K as renderCellValue, L as Table, Lt as ChevronsUpDown, M as SlotNames, Mt as EyeOff, N as slotsController, O as contextAwarePanelOpen, Ot as isStaticNotebook, P as Toggle, Pt as Ellipsis, Q as filtersToFilterGroup, R as TableBody, Rt as ChevronsRight, S as prettifyRowCount, St as useOverflowDetection, T as ComboboxItem, Tt as HtmlOutput, U as NAMELESS_COLUMN_PREFIX, V as TableHeader, Vt as ChevronLeft, W as generateColumns, X as useIntersectionObserver, Y as DelayMount, Z as usePrevious$1, _ as downloadBlob, _t as TabsTrigger, at as toFieldTypes, b as Filenames, bt as ChartLoadingState, c as Slide, ct as CommandEmpty, d as JsonOutput, dt as CommandList, et as loadTableAndRawData, f as OutputArea, ft as CommandSeparator, g as ADD_PRINTING_CLASS, gt as TabsList, h as InstallPackageButton, ht as TabsContent, it as TOO_MANY_ROWS, j as isCellAwareAtom, jt as Funnel, k as contextAwarePanelOwner, kt as TextWrap, l as RadioGroup, lt as CommandInput, m as DataTable, mt as Tabs, n as marimoVersionAtom, nt as INDEX_COLUMN_NAME, o as SLIDE_TYPE_OPTIONS_BY_VALUE, ot as getMimeValues, p as OutputRenderer, pt as Maps, q as ColumnChartContext, r as showCodeInRunModeAtom, rt as SELECT_COLUMN_ID, st as Command, t as useNotebookCodeAvailable, tt as loadTableData, u as RadioGroupItem, ut as CommandItem, v as downloadByURL, vt as ChartErrorState, w as Combobox, wt as Kbd, x as prettifyRowColumnCount, xt as LazyVegaEmbed, y as downloadHTMLAsImage, yt as ChartInfoState, z as TableCell, zt as ChevronsLeft, __tla as __tla_2 } from "./code-visibility-CUj4voDP.js";
30
30
  import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-smVfjLNI.js";
31
31
  import { n as $fb18d541ea1ad717$export$ad991b66133851cf, r as $5a387cc49350e6db$export$722debc0e56fea39, t as $896ba0a80a8f4d36$export$85fd5fdf27bacc79 } from "./useDateFormatter-B3mCQMP3.js";
32
32
  import { t as Check } from "./check-CFM2mVDr.js";
33
33
  import { A as Trigger$1, C as $a916eb452884faea$export$b7a616150fdb9f44, D as $b5e257d569688ac6$export$535bd6ca7f90a273, E as $18f2051aff69b9bf$export$a54013f0d02a8f82, F as X, L as ChevronDown, M as usePrevious$2, N as useDirection, P as createCollection, S as logNever, T as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, a as SelectGroup, c as SelectSeparator, d as NativeSelect, f as selectStyles, i as SelectContent, j as clamp$2, k as Icon, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $488c6ddbf4ef74c2$export$cc77c4ff7e8673c5, x as assertNever } from "./strings-B_FOH6eV.js";
34
34
  import { $ as $e5be200c675c3b3a$export$aca958c65c314e6c, A as $d2b4bc8c273e7be6$export$24d547caef80ccd1, At as $c87311424ea30a05$export$fedb369cb70207f1, B as $64fa3d84918910a7$export$c62b8e45d58ddad9, Bt as $431fbd86ca7dc216$export$f21a1ffae260145a, C as $a049562f99e7db0e$export$eb2fcfdbd7ba97d4, Ct as $8ae05eaa5c114e9c$export$7f54fc3180508a52, D as $ee014567cb39d3f0$export$ff05c3ac10437e03, Dt as $c87311424ea30a05$export$78551043582a6a98, E as $ee014567cb39d3f0$export$f551688fc98f2e09, Et as $c87311424ea30a05$export$6446a186d09e379e, F as $64fa3d84918910a7$export$2881499e37b75b9a, Ft as $d4ee10de306f2510$export$b4f377a2b6254582, H as $64fa3d84918910a7$export$ef03459518577ad4, Ht as $bdb11010cef70236$export$b4cc09c592e8fdb8, I as $64fa3d84918910a7$export$29f1550f4b0d4415, It as $d4ee10de306f2510$export$cd4e5573fbe2b576, J as $d2e8511e6f209edf$export$e908e06f4b8e3402, K as useDebounceControlledState, L as $64fa3d84918910a7$export$4d86445c2cf5e3, Lt as $d4ee10de306f2510$export$e58f029f0fbfdb29, M as $01b77f81d0f07f68$export$75b6ee27786ba447, Mt as $65484d02dcb7eb3e$export$457c3d6518dd4c6f, N as $01b77f81d0f07f68$export$b04be29aa201d4f5, Nt as $3ef42575df84b30b$export$9d1611c77c2fe928, O as $514c0188e459b4c0$export$5f1af8db9871e1d6, Ot as $c87311424ea30a05$export$9ac100e40613ea10, P as $f39a9eba43920ace$export$b5d7cc18bb8d2b59, Pt as $d4ee10de306f2510$export$4282f70798064fe0, Q as $e5be200c675c3b3a$export$a763b9476acd3eb, R as $64fa3d84918910a7$export$9d4c57ee4c6ffdd8, Rt as $f4e2df6bd15f8569$export$98658e8c59125e6a, S as $3985021b0ad6602f$export$f5b8910cec6cf069, St as $e9faafb641e167db$export$90fc3a17d93f704c, T as $d3e0e05bdfcf66bd$export$c24727297075ec6a, Tt as $313b98861ee5dd6c$export$d6875122194c7b44, U as $64fa3d84918910a7$export$fabf2dc03a41866e, Ut as $bdb11010cef70236$export$f680877a34711e37, V as $64fa3d84918910a7$export$df3a06d6289f983e, Vt as $ff5963eb1fccf552$export$e08e3b67e392101e, Wt as $f0a04ccd8dbdd83b$export$e5c5a5f917a5871c, X as $e93e671b31057976$export$b8473d3665f3a75a, Y as $2baaea4c71418dea$export$294aa081a6c6f55d, Z as $e5be200c675c3b3a$export$75ee7c75d68f5b0e, _t as $9446cca9a3875146$export$7d15b64cf5a3a4c4, a as NumberField, at as $6c7bd7858deea686$export$cd11ab140839f11d, b as DropdownMenuTrigger, bt as $b4b717babfbb907b$export$bebd5a1431fec25d, c as prettyNumber, ct as $6db58dc88e78b024$export$2f817fcdc4b89ae0, d as DropdownMenuContent, dt as $9ab94262bd0047c7$export$420e68273165f4ec, et as $e5be200c675c3b3a$export$dad6ae84456c676a, f as DropdownMenuGroup, fn as Circle, ft as $3ad3f6e1647bc98d$export$80f3e147d781571c, g as DropdownMenuSeparator, gt as $ae1eeba8b9eafd08$export$5165eccb35aaadb5, ht as _class_private_field_init, i as OnBlurredInput, it as $701a24aa0da5b062$export$ea18c227d4417cc3, j as $d2b4bc8c273e7be6$export$353f5b6fc5456de1, jt as $7215afc6de606d6b$export$de79e2c695e052f3, k as $514c0188e459b4c0$export$9afb8bc826b033ea, kt as $c87311424ea30a05$export$a11b0059900ceec8, l as prettyScientificNumber, lt as $5b160d28a433310d$export$c17fa47878dc55b6, m as DropdownMenuLabel, mt as $f6c31cce2adf654f$export$45712eceda6fad21, n as DebouncedNumberInput, nt as $319e236875307eab$export$a9b970dcc4ae71a9, ot as $fca6afa0e843324b$export$87b761675e8eaa10, p as DropdownMenuItem, pn as ChevronRight, pt as $507fabe10e71c6fb$export$630ff653c5ada6a9, q as useDebouncedCallback, r as Input, rt as $f7dceffc5ad7768b$export$4e328f61c538687f, st as $fca6afa0e843324b$export$f12b703ca79dfbb1, t as DebouncedInput, tt as $e5be200c675c3b3a$export$fc1a364ae1f3ff10, u as DropdownMenu, ut as $6179b936705e76d3$export$ae780daf29e6d456, vt as $458b0a5536c1a7cf$export$40bfa8c7b0832715, w as $a049562f99e7db0e$export$f9c6924e160136d1, wt as $df56164dff5785e2$export$4338b53315abf666, x as $3985021b0ad6602f$export$37fb8590cf2c088c, xt as $99facab73266f662$export$5add1d006293d136, yt as $b4b717babfbb907b$export$4c063cf1350e6fed, z as $64fa3d84918910a7$export$c245e6201fed2f75, zt as $431fbd86ca7dc216$export$b204af158042fbac } from "./input-Drx1pguW.js";
35
35
  import { c as asRemoteURL, f as require_cuid2, g as CircleQuestionMark, h as isWasm, m as Deferred, u as appendQueryParams } from "./toDate-yqOcZ_tY.js";
36
- import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, d as File, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-SkNR_Omd.js";
36
+ import { a as MarimoIncomingMessageEvent, c as MarimoValueUpdateEvent, d as File, i as PythonIcon, l as createInputEvent, n as blobToString, o as MarimoValueInputEvent, r as filesToBase64, s as MarimoValueReadyEvent, t as processOutput, u as deserializeBlob } from "./process-output-CD5QiwBb.js";
37
37
  import { i as Pencil, n as Trash, r as Plus, t as BulkEdit } from "./types-DBtDeUKD.js";
38
38
  import { t as require_react_dom } from "./react-dom-BWRJ_g_k.js";
39
39
  import { t as require_jsx_runtime } from "./jsx-runtime-COBk7ree.js";
@@ -13655,7 +13655,7 @@ Defaulting to \`null\`.`;
13655
13655
  };
13656
13656
  }
13657
13657
  };
13658
- var LazyChatbot = import_react.lazy(() => import("./chat-ui-Cyca6aKX.js").then((e) => ({
13658
+ var LazyChatbot = import_react.lazy(() => import("./chat-ui-CtIeZD6j.js").then((e) => ({
13659
13659
  default: e.Chatbot
13660
13660
  }))), messageSchema = array(object({
13661
13661
  id: string(),
@@ -44649,7 +44649,7 @@ ${c}
44649
44649
  if (l && l !== "slide") return l;
44650
44650
  if (c == null ? void 0 : c.has(e)) return "skip";
44651
44651
  }
44652
- var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CIvwF-9l.js"));
44652
+ var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CwPAMS35.js"));
44653
44653
  const SlidesLayoutPlugin = {
44654
44654
  type: "slides",
44655
44655
  name: "Slides",
@@ -1,6 +1,6 @@
1
1
  import { s as __toESM } from "./chunk-BNovOVIE.js";
2
2
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
3
- import { it as parseHtmlContent, rt as ansiToPlainText } from "./html-to-image-6VI69paz.js";
3
+ import { it as parseHtmlContent, rt as ansiToPlainText } from "./html-to-image-uzALXlch.js";
4
4
  import { u as createLucideIcon } from "./dist-ESg7xyoD.js";
5
5
  import { t as Strings } from "./strings-B_FOH6eV.js";
6
6
  import { t as require_jsx_runtime } from "./jsx-runtime-COBk7ree.js";
@@ -6,9 +6,9 @@ import { s as __toESM } from "./chunk-BNovOVIE.js";
6
6
  import { _ as Logger, g as cn, h as Events, l as useEventListener, t as Button } from "./button-CA5pI2YF.js";
7
7
  import { t as require_react } from "./react-DA-nE2FX.js";
8
8
  import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
- import "./html-to-image-6VI69paz.js";
9
+ import "./html-to-image-uzALXlch.js";
10
10
  import "./chunk-5FQGJX7Z-CO1e63h_.js";
11
- import { It as Code, Mt as EyeOff, Nt as Expand, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-Ch6utETw.js";
11
+ import { It as Code, Mt as EyeOff, Nt as Expand, a as DEFAULT_SLIDE_TYPE, c as Slide, i as DEFAULT_DECK_TRANSITION, s as SlideSidebar, t as useNotebookCodeAvailable } from "./code-visibility-CUj4voDP.js";
12
12
  import "./input-Drx1pguW.js";
13
13
  import "./toDate-yqOcZ_tY.js";
14
14
  import "./react-dom-BWRJ_g_k.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.6-dev15",
3
+ "version": "0.23.6-dev16",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -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(reactNode, domNode, index);
344
+ const transformed = transformFunction(result, domNode, index);
306
345
  if (transformed) {
307
- return transformed;
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 reactNode as JSX.Element;
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>';