@marimo-team/islands 0.15.5 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-CBeIYi8p.js → ConnectedDataExplorerComponent-DyqLQGPc.js} +1567 -1544
- package/dist/{ImageComparisonComponent-Bk0a0xBq.js → ImageComparisonComponent-CQDGJfUA.js} +1 -1
- package/dist/{_baseUniq-utU5_Vu-.js → _baseUniq-B2Nna6Kt.js} +1 -1
- package/dist/{any-language-editor-PrUUh2lr.js → any-language-editor-D-wq0tOG.js} +1 -1
- package/dist/{architectureDiagram-W76B3OCA-D-vOp0UU.js → architectureDiagram-W76B3OCA-C6tdnMBf.js} +4 -4
- package/dist/assets/{worker-BcG8m3h5.js → worker-B0C57BK8.js} +40 -38
- package/dist/{blockDiagram-QIGZ2CNN-IG-z8q8A.js → blockDiagram-QIGZ2CNN-IagL8LCN.js} +5 -5
- package/dist/{c4Diagram-FPNF74CW-5AEXIX3t.js → c4Diagram-FPNF74CW-D3_lIWUP.js} +2 -2
- package/dist/{channel-ECVsTGGL.js → channel-DCJI_DKk.js} +1 -1
- package/dist/{chunk-4BX2VUAB-DfJcd9e-.js → chunk-4BX2VUAB-B2DrODwN.js} +1 -1
- package/dist/{chunk-55IACEB6-BwT8MejR.js → chunk-55IACEB6-BUWDsQ-t.js} +1 -1
- package/dist/{chunk-FMBD7UC4-DW7uxNR6.js → chunk-FMBD7UC4-BExPNFv1.js} +1 -1
- package/dist/{chunk-K7UQS3LO-BGn2ZPDQ.js → chunk-K7UQS3LO-Cixi-Yko.js} +4 -4
- package/dist/{chunk-QN33PNHL-BcIbOumv.js → chunk-QN33PNHL-B83MtvER.js} +1 -1
- package/dist/{chunk-QZHKN3VN-CMSnhk6x.js → chunk-QZHKN3VN-CXvbu85X.js} +1 -1
- package/dist/{chunk-TVAH2DTR-CZF2JRya.js → chunk-TVAH2DTR-CpiumCHg.js} +3 -3
- package/dist/{chunk-TZMSLE5B-BHzN_BY6.js → chunk-TZMSLE5B-DIzaZjcI.js} +1 -1
- package/dist/{classDiagram-v2-RKCZMP56-2H7MseyB.js → classDiagram-KNZD7YFC-DyN5HPdk.js} +2 -2
- package/dist/{classDiagram-KNZD7YFC-2H7MseyB.js → classDiagram-v2-RKCZMP56-DyN5HPdk.js} +2 -2
- package/dist/{clone-DKQcSK7N.js → clone-DrJYap2i.js} +1 -1
- package/dist/{cose-bilkent-S5V4N54A-CgvKFxTr.js → cose-bilkent-S5V4N54A-D39b4WrQ.js} +2 -2
- package/dist/{dagre-5GWH7T2D-VNFIipzt.js → dagre-5GWH7T2D-BLjRxDpS.js} +6 -6
- package/dist/{data-grid-overlay-editor-XdqkKCVx.js → data-grid-overlay-editor-DTALqerV.js} +2 -2
- package/dist/{diagram-N5W7TBWH-D1s8h-eH.js → diagram-N5W7TBWH-MM8AIKGR.js} +5 -5
- package/dist/{diagram-QEK2KX5R-DOa-AstT.js → diagram-QEK2KX5R-BZGarWuJ.js} +3 -3
- package/dist/{diagram-S2PKOQOG-CFZ-Y2zi.js → diagram-S2PKOQOG-CnPinN9Q.js} +3 -3
- package/dist/{dockerfile-zE-2DWBS.js → dockerfile-U8DnCJ4X.js} +1 -1
- package/dist/{erDiagram-AWTI2OKA-WxUYJfbS.js → erDiagram-AWTI2OKA-CvDVbxOO.js} +4 -4
- package/dist/{flowDiagram-PVAE7QVJ-dDZH2O1W.js → flowDiagram-PVAE7QVJ-C2uuBTZS.js} +5 -5
- package/dist/{ganttDiagram-OWAHRB6G-D3CCqPQq.js → ganttDiagram-OWAHRB6G-BEff10RF.js} +4 -4
- package/dist/{gitGraphDiagram-NY62KEGX-BHFylEwc.js → gitGraphDiagram-NY62KEGX-wggu0kb2.js} +4 -4
- package/dist/{glide-data-editor-D0aJSGV_.js → glide-data-editor-Bqh5_dzJ.js} +3 -3
- package/dist/{graph-BPGEu6c8.js → graph-DKpp_wzf.js} +3 -3
- package/dist/{index-HtOEKQ3O.js → index-4XruEJkp.js} +1 -1
- package/dist/{index-eDB61tLS.js → index-DW0BCGJE.js} +1 -1
- package/dist/{index-DotQhzoN.js → index-DdfF_cLK.js} +1 -1
- package/dist/{index-Bx2b23rX.js → index-DzJ_YPCG.js} +3 -3
- package/dist/{infoDiagram-STP46IZ2-DWhhqGPi.js → infoDiagram-STP46IZ2-DF7KW-Op.js} +2 -2
- package/dist/{journeyDiagram-BIP6EPQ6-CU8FpryL.js → journeyDiagram-BIP6EPQ6-B_jmhmqd.js} +3 -3
- package/dist/{kanban-definition-6OIFK2YF-CWhF_a4g.js → kanban-definition-6OIFK2YF-B-M9FTyw.js} +2 -2
- package/dist/{layout-DGonEvAZ.js → layout-C4oVYZZD.js} +4 -4
- package/dist/{linear-Cww2a6nQ.js → linear-C-HCGr0T.js} +1 -1
- package/dist/{main-Bc0LY9fB.js → main-B9x2-9f2.js} +93798 -93495
- package/dist/main.js +1 -1
- package/dist/{mermaid-DpJuOhRr.js → mermaid-BE4cM3Qs.js} +30 -30
- package/dist/{min-CFQjsG4L.js → min-DTpHJ698.js} +2 -2
- package/dist/{mindmap-definition-Q6HEUPPD-K513Ef1t.js → mindmap-definition-Q6HEUPPD-Cpd-hO1E.js} +3 -3
- package/dist/{number-overlay-editor-DuSchUfE.js → number-overlay-editor-CvURA2Ud.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-DAIIUJJO.js → pieDiagram-ADFJNKIX-D9f_f6fn.js} +3 -3
- package/dist/{quadrantDiagram-LMRXKWRM-yuf-j7Os.js → quadrantDiagram-LMRXKWRM-DgllE7xw.js} +2 -2
- package/dist/{react-plotly-B378DZ9U.js → react-plotly-BU-JRJSi.js} +1 -1
- package/dist/{requirementDiagram-4UW4RH46-BBWvEl6q.js → requirementDiagram-4UW4RH46-Dk_G8eUb.js} +3 -3
- package/dist/{sankeyDiagram-GR3RE2ED-B_TwV-dS.js → sankeyDiagram-GR3RE2ED-BhLIhDc1.js} +1 -1
- package/dist/{sequenceDiagram-C3RYC4MD-BVC6lltp.js → sequenceDiagram-C3RYC4MD-DHoZdMFJ.js} +3 -3
- package/dist/{slides-component-CPX3S0Y9.js → slides-component-DXAgdf7K.js} +2 -2
- package/dist/{stateDiagram-KXAO66HF-BCU1tYTD.js → stateDiagram-KXAO66HF-C1Ie-7Xf.js} +4 -4
- package/dist/{stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js → stateDiagram-v2-UMBNRL4Z--CRuIHtM.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{time-CSIip6fV.js → time-yQjlGPwa.js} +2 -2
- package/dist/{timeline-definition-XQNQX7LJ-CCxCPNQI.js → timeline-definition-XQNQX7LJ-D_PjxB1B.js} +1 -1
- package/dist/{treemap-75Q7IDZK-Du6v0BzD.js → treemap-75Q7IDZK--NYqQjUZ.js} +134 -134
- package/dist/{vega-component-Da93sTnp.js → vega-component-CCUOMM5K.js} +2 -2
- package/dist/{xychartDiagram-6GGTOJPD-Oq6xaZKR.js → xychartDiagram-6GGTOJPD-WLKsEnzs.js} +2 -2
- package/package.json +10 -5
- package/src/__tests__/mocks.ts +43 -0
- package/src/components/app-config/user-config-form.tsx +78 -1
- package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +116 -65
- package/src/components/chat/acp/__tests__/atoms.test.ts +1 -1
- package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
- package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
- package/src/components/chat/acp/__tests__/state.test.ts +38 -42
- package/src/components/chat/acp/agent-docs.tsx +33 -6
- package/src/components/chat/acp/agent-panel.css +0 -18
- package/src/components/chat/acp/agent-panel.tsx +394 -72
- package/src/components/chat/acp/agent-selector.tsx +7 -1
- package/src/components/chat/acp/blocks.tsx +40 -10
- package/src/components/chat/acp/common.tsx +10 -2
- package/src/components/chat/acp/context-utils.ts +127 -0
- package/src/components/chat/acp/prompt.ts +96 -53
- package/src/components/chat/acp/state.ts +1 -1
- package/src/components/chat/acp/types.ts +8 -0
- package/src/components/chat/chat-panel.tsx +28 -89
- package/src/components/chat/chat-utils.ts +127 -1
- package/src/components/chat/markdown-renderer.css +39 -0
- package/src/components/chat/markdown-renderer.tsx +12 -47
- package/src/components/chat/tool-call-accordion.tsx +148 -26
- package/src/components/data-table/SearchBar.tsx +8 -7
- package/src/components/data-table/__tests__/column_formatting.test.ts +50 -35
- package/src/components/data-table/__tests__/data-table.test.tsx +39 -1
- package/src/components/data-table/cell-hover-template/feature.ts +14 -0
- package/src/components/data-table/cell-hover-template/types.ts +11 -0
- package/src/components/data-table/charts/components/form-fields.tsx +41 -37
- package/src/components/data-table/charts/forms/common-chart.tsx +2 -2
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +5 -2
- package/src/components/data-table/column-formatting/feature.ts +62 -29
- package/src/components/data-table/column-formatting/types.ts +1 -0
- package/src/components/data-table/column-header.tsx +3 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +24 -7
- package/src/components/data-table/column-summary/column-summary.tsx +18 -9
- package/src/components/data-table/columns.tsx +42 -18
- package/src/components/data-table/data-table.tsx +10 -2
- package/src/components/data-table/date-popover.tsx +85 -75
- package/src/components/data-table/filter-pills.tsx +14 -9
- package/src/components/data-table/header-items.tsx +5 -1
- package/src/components/data-table/pagination.tsx +20 -13
- package/src/components/data-table/renderers.tsx +28 -0
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +10 -8
- package/src/components/datasources/column-preview.tsx +6 -2
- package/src/components/datasources/datasources.tsx +8 -12
- package/src/components/editor/Cell.tsx +6 -0
- package/src/components/editor/actions/name-cell-input.tsx +6 -1
- package/src/components/editor/actions/useCellActionButton.tsx +3 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
- package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
- package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
- package/src/components/editor/ai/completion-handlers.tsx +44 -6
- package/src/components/editor/ai/completion-utils.ts +92 -0
- package/src/components/editor/ai/transport/chat-transport.tsx +39 -0
- package/src/components/editor/cell/CellStatus.tsx +23 -20
- package/src/components/editor/cell/CreateCellButton.tsx +3 -4
- package/src/components/editor/cell/StagedAICell.tsx +51 -0
- package/src/components/editor/cell/cell-actions.tsx +2 -1
- package/src/components/editor/cell/code/language-toggle.tsx +3 -4
- package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +39 -28
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
- package/src/components/editor/file-tree/requesting-tree.tsx +14 -8
- package/src/components/editor/renderers/CellArray.tsx +3 -4
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +3 -3
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/pages/home-page.tsx +4 -1
- package/src/components/slides/slides-component.tsx +1 -1
- package/src/components/slides/slides.css +6 -0
- package/src/components/terminal/__tests__/state.test.ts +207 -0
- package/src/components/terminal/hooks.ts +41 -0
- package/src/components/terminal/state.ts +75 -0
- package/src/components/terminal/terminal.tsx +334 -13
- package/src/components/terminal/theme.tsx +57 -0
- package/src/components/tracing/tracing-spec.ts +5 -4
- package/src/components/ui/range-slider.tsx +4 -2
- package/src/components/ui/slider.tsx +3 -1
- package/src/components/variables/variables-table.tsx +3 -0
- package/src/core/MarimoApp.tsx +9 -6
- package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
- package/src/core/ai/context/__tests__/registry.test.ts +6 -4
- package/src/core/ai/context/providers/cell-output.ts +3 -2
- package/src/core/ai/context/providers/error.ts +3 -1
- package/src/core/ai/context/providers/file.ts +7 -2
- package/src/core/ai/context/providers/tables.ts +3 -2
- package/src/core/ai/context/providers/variable.ts +6 -4
- package/src/core/ai/staged-cells.ts +241 -0
- package/src/core/cells/__tests__/add-missing-import.test.ts +67 -22
- package/src/core/cells/add-missing-import.ts +24 -7
- package/src/core/cells/cells.ts +27 -28
- package/src/core/cells/logs.ts +1 -1
- package/src/core/codemirror/find-replace/search-highlight.ts +3 -1
- package/src/core/codemirror/language/LanguageAdapters.ts +9 -3
- package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
- package/src/core/codemirror/lsp/notebook-lsp.ts +8 -2
- package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
- package/src/core/codemirror/rtc/loro/awareness.ts +52 -17
- package/src/core/codemirror/rtc/loro/sync.ts +12 -4
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +4 -0
- package/src/core/hotkeys/hotkeys.ts +8 -4
- package/src/core/i18n/__tests__/locale-provider.test.tsx +176 -0
- package/src/core/i18n/locale-provider.tsx +35 -0
- package/src/core/i18n/with-locale.tsx +12 -0
- package/src/core/islands/components/web-components.tsx +13 -10
- package/src/core/islands/main.ts +2 -2
- package/src/core/kernel/RuntimeState.ts +4 -1
- package/src/core/kernel/messages.ts +8 -12
- package/src/core/network/DeferredRequestRegistry.ts +16 -4
- package/src/core/runtime/runtime.ts +5 -4
- package/src/core/saving/__tests__/filename.test.ts +37 -0
- package/src/core/static/__tests__/download-html.test.ts +43 -1
- package/src/core/wasm/bridge.ts +5 -1
- package/src/core/wasm/store.ts +4 -1
- package/src/core/wasm/worker/message-buffer.ts +3 -2
- package/src/core/websocket/types.ts +22 -16
- package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
- package/src/css/app/Cell.css +11 -0
- package/src/hooks/useFormatting.ts +97 -0
- package/src/hooks/useTimer.ts +8 -5
- package/src/plugins/core/RenderHTML.tsx +36 -2
- package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
- package/src/plugins/core/registerReactComponent.tsx +44 -10
- package/src/plugins/impl/DataTablePlugin.tsx +4 -0
- package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
- package/src/plugins/impl/RangeSliderPlugin.tsx +5 -3
- package/src/plugins/impl/SliderPlugin.tsx +3 -1
- package/src/plugins/impl/anywidget/model.ts +16 -5
- package/src/plugins/impl/data-editor/types.ts +7 -5
- package/src/plugins/impl/data-explorer/components/column-summary.tsx +20 -13
- package/src/plugins/impl/panel/utils.ts +6 -4
- package/src/plugins/layout/OutlinePlugin.tsx +69 -0
- package/src/plugins/layout/StatPlugin.tsx +4 -1
- package/src/plugins/plugins.ts +2 -0
- package/src/stories/cell.stories.tsx +1 -1
- package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
- package/src/utils/__tests__/cell-urls.test.ts +29 -0
- package/src/utils/__tests__/dates.test.ts +45 -24
- package/src/utils/__tests__/filenames.test.ts +18 -0
- package/src/utils/__tests__/numbers.test.ts +42 -30
- package/src/utils/__tests__/once.test.ts +187 -0
- package/src/utils/__tests__/path.test.ts +38 -0
- package/src/utils/__tests__/urls.test.ts +56 -1
- package/src/utils/dates.ts +15 -10
- package/src/utils/edit-distance.ts +8 -6
- package/src/utils/errors.ts +9 -0
- package/src/utils/id-tree.tsx +21 -10
- package/src/utils/localStorage.ts +13 -4
- package/src/utils/numbers.ts +11 -11
- package/src/utils/once.ts +32 -0
- package/src/utils/paths.ts +4 -1
- package/src/utils/pluralize.ts +12 -5
- package/src/utils/python-poet/poet.ts +30 -15
- package/src/utils/time.ts +5 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { cleanup, render } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { userConfigAtom } from "@/core/config/config";
|
|
7
|
+
import { parseUserConfig } from "@/core/config/config-schema";
|
|
8
|
+
import { LocaleProvider } from "../locale-provider";
|
|
9
|
+
|
|
10
|
+
// Mock navigator.language with a getter
|
|
11
|
+
let mockNavigatorLanguage: string | undefined;
|
|
12
|
+
|
|
13
|
+
Object.defineProperty(window, "navigator", {
|
|
14
|
+
value: {
|
|
15
|
+
get language() {
|
|
16
|
+
return mockNavigatorLanguage;
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Mock react-aria-components I18nProvider
|
|
23
|
+
vi.mock("react-aria-components", () => ({
|
|
24
|
+
I18nProvider: ({
|
|
25
|
+
children,
|
|
26
|
+
locale,
|
|
27
|
+
}: {
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
locale: string;
|
|
30
|
+
}) => (
|
|
31
|
+
<div data-testid="i18n-provider" data-locale={locale}>
|
|
32
|
+
{children}
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe("LocaleProvider", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Reset the mock before each test
|
|
40
|
+
mockNavigatorLanguage = undefined;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
cleanup();
|
|
45
|
+
// Clear all mocks after each test
|
|
46
|
+
mockNavigatorLanguage = undefined;
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should render I18nProvider without locale when locale is null", () => {
|
|
51
|
+
const store = createStore();
|
|
52
|
+
const config = parseUserConfig({ display: { locale: null } });
|
|
53
|
+
store.set(userConfigAtom, config);
|
|
54
|
+
|
|
55
|
+
const { getByTestId } = render(
|
|
56
|
+
<Provider store={store}>
|
|
57
|
+
<LocaleProvider>
|
|
58
|
+
<div>Test content</div>
|
|
59
|
+
</LocaleProvider>
|
|
60
|
+
</Provider>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const i18nProvider = getByTestId("i18n-provider");
|
|
64
|
+
expect(i18nProvider).toBeInTheDocument();
|
|
65
|
+
expect(i18nProvider.dataset.locale).toBe(undefined);
|
|
66
|
+
expect(i18nProvider).toHaveTextContent("Test content");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should render I18nProvider without locale when locale is undefined", () => {
|
|
70
|
+
const store = createStore();
|
|
71
|
+
const config = parseUserConfig({ display: { locale: undefined } });
|
|
72
|
+
store.set(userConfigAtom, config);
|
|
73
|
+
|
|
74
|
+
const { getByTestId } = render(
|
|
75
|
+
<Provider store={store}>
|
|
76
|
+
<LocaleProvider>
|
|
77
|
+
<div>Test content</div>
|
|
78
|
+
</LocaleProvider>
|
|
79
|
+
</Provider>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const i18nProvider = getByTestId("i18n-provider");
|
|
83
|
+
expect(i18nProvider).toBeInTheDocument();
|
|
84
|
+
expect(i18nProvider.dataset.locale).toBe(undefined);
|
|
85
|
+
expect(i18nProvider).toHaveTextContent("Test content");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should render I18nProvider with locale when locale is provided", () => {
|
|
89
|
+
const store = createStore();
|
|
90
|
+
const testLocale = "es-ES";
|
|
91
|
+
const config = parseUserConfig({ display: { locale: testLocale } });
|
|
92
|
+
store.set(userConfigAtom, config);
|
|
93
|
+
|
|
94
|
+
const { getByTestId } = render(
|
|
95
|
+
<Provider store={store}>
|
|
96
|
+
<LocaleProvider>
|
|
97
|
+
<div>Test content</div>
|
|
98
|
+
</LocaleProvider>
|
|
99
|
+
</Provider>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const i18nProvider = getByTestId("i18n-provider");
|
|
103
|
+
expect(i18nProvider).toBeInTheDocument();
|
|
104
|
+
expect(i18nProvider.dataset.locale).toBe(testLocale);
|
|
105
|
+
expect(i18nProvider).toHaveTextContent("Test content");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should render I18nProvider with different locale values", () => {
|
|
109
|
+
const testCases = ["en-US", "fr-FR", "de-DE", "ja-JP"];
|
|
110
|
+
|
|
111
|
+
testCases.forEach((locale) => {
|
|
112
|
+
const store = createStore();
|
|
113
|
+
const config = parseUserConfig({ display: { locale } });
|
|
114
|
+
store.set(userConfigAtom, config);
|
|
115
|
+
|
|
116
|
+
const { getByTestId } = render(
|
|
117
|
+
<Provider store={store}>
|
|
118
|
+
<LocaleProvider>
|
|
119
|
+
<div>Test content for {locale}</div>
|
|
120
|
+
</LocaleProvider>
|
|
121
|
+
</Provider>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const i18nProvider = getByTestId("i18n-provider");
|
|
125
|
+
expect(i18nProvider.dataset.locale).toBe(locale);
|
|
126
|
+
expect(i18nProvider).toHaveTextContent(`Test content for ${locale}`);
|
|
127
|
+
|
|
128
|
+
// Clean up after each iteration
|
|
129
|
+
cleanup();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should render children correctly", () => {
|
|
134
|
+
const store = createStore();
|
|
135
|
+
const config = parseUserConfig({ display: { locale: "en-US" } });
|
|
136
|
+
store.set(userConfigAtom, config);
|
|
137
|
+
|
|
138
|
+
const { getByText, getByRole } = render(
|
|
139
|
+
<Provider store={store}>
|
|
140
|
+
<LocaleProvider>
|
|
141
|
+
<div>
|
|
142
|
+
<h1>Test Heading</h1>
|
|
143
|
+
<p>Test paragraph</p>
|
|
144
|
+
<button type="button">Test Button</button>
|
|
145
|
+
</div>
|
|
146
|
+
</LocaleProvider>
|
|
147
|
+
</Provider>,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(getByText("Test Heading")).toBeInTheDocument();
|
|
151
|
+
expect(getByText("Test paragraph")).toBeInTheDocument();
|
|
152
|
+
expect(getByRole("button", { name: "Test Button" })).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should auto-detect locale when no locale is set in config", () => {
|
|
156
|
+
mockNavigatorLanguage = "de-DE";
|
|
157
|
+
|
|
158
|
+
const store = createStore();
|
|
159
|
+
const config = parseUserConfig({});
|
|
160
|
+
store.set(userConfigAtom, config);
|
|
161
|
+
|
|
162
|
+
const { getByTestId } = render(
|
|
163
|
+
<Provider store={store}>
|
|
164
|
+
<LocaleProvider>
|
|
165
|
+
<div>Test content</div>
|
|
166
|
+
</LocaleProvider>
|
|
167
|
+
</Provider>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const i18nProvider = getByTestId("i18n-provider");
|
|
171
|
+
expect(i18nProvider).toBeInTheDocument();
|
|
172
|
+
// When no locale is specified in config, it should use navigator.language
|
|
173
|
+
expect(i18nProvider.dataset.locale).toBe("de-DE");
|
|
174
|
+
expect(i18nProvider).toHaveTextContent("Test content");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { I18nProvider } from "react-aria-components";
|
|
6
|
+
import { localeAtom } from "@/core/config/config";
|
|
7
|
+
|
|
8
|
+
interface LocaleProviderProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const LocaleProvider = ({ children }: LocaleProviderProps) => {
|
|
13
|
+
const locale = useAtomValue(localeAtom);
|
|
14
|
+
|
|
15
|
+
return <I18nProvider locale={safeLocale(locale)}>{children}</I18nProvider>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function safeLocale(locale: string | null | undefined) {
|
|
19
|
+
if (!locale) {
|
|
20
|
+
return navigator.language;
|
|
21
|
+
}
|
|
22
|
+
if (isValidLocale(locale)) {
|
|
23
|
+
return locale;
|
|
24
|
+
}
|
|
25
|
+
return navigator.language;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isValidLocale(locale: string) {
|
|
29
|
+
try {
|
|
30
|
+
new Intl.NumberFormat(locale);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useLocale } from "react-aria";
|
|
4
|
+
|
|
5
|
+
export const WithLocale = ({
|
|
6
|
+
children,
|
|
7
|
+
}: {
|
|
8
|
+
children: (locale: string) => React.ReactNode;
|
|
9
|
+
}) => {
|
|
10
|
+
const { locale } = useLocale();
|
|
11
|
+
return children(locale);
|
|
12
|
+
};
|
|
@@ -7,6 +7,7 @@ import { ErrorBoundary } from "@/components/editor/boundary/ErrorBoundary";
|
|
|
7
7
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
8
|
import { notebookAtom } from "@/core/cells/cells";
|
|
9
9
|
import { UI_ELEMENT_REGISTRY } from "@/core/dom/uiregistry";
|
|
10
|
+
import { LocaleProvider } from "@/core/i18n/locale-provider";
|
|
10
11
|
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
11
12
|
import { invariant } from "@/utils/invariant";
|
|
12
13
|
import type { CellId } from "../../cells/ids";
|
|
@@ -80,16 +81,18 @@ export class MarimoIslandElement extends HTMLElement {
|
|
|
80
81
|
this.root?.render(
|
|
81
82
|
<ErrorBoundary>
|
|
82
83
|
<Provider store={store}>
|
|
83
|
-
<
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
<LocaleProvider>
|
|
85
|
+
<TooltipProvider>
|
|
86
|
+
<MarimoOutputWrapper
|
|
87
|
+
cellId={this.cellId}
|
|
88
|
+
codeCallback={codeCallback}
|
|
89
|
+
alwaysShowRun={alwaysShowRun}
|
|
90
|
+
>
|
|
91
|
+
{initialHtml}
|
|
92
|
+
</MarimoOutputWrapper>
|
|
93
|
+
{editor}
|
|
94
|
+
</TooltipProvider>
|
|
95
|
+
</LocaleProvider>
|
|
93
96
|
</Provider>
|
|
94
97
|
</ErrorBoundary>,
|
|
95
98
|
);
|
package/src/core/islands/main.ts
CHANGED
|
@@ -103,7 +103,7 @@ export async function initialize() {
|
|
|
103
103
|
// Consume messages from the kernel
|
|
104
104
|
IslandsPyodideBridge.INSTANCE.consumeMessages((message) => {
|
|
105
105
|
const msg = jsonParseWithSpecialChar(message);
|
|
106
|
-
switch (msg.op) {
|
|
106
|
+
switch (msg.data.op) {
|
|
107
107
|
case "banner":
|
|
108
108
|
case "missing-package-alert":
|
|
109
109
|
case "installing-package-alert":
|
|
@@ -185,7 +185,7 @@ export async function initialize() {
|
|
|
185
185
|
case "reconnected":
|
|
186
186
|
return;
|
|
187
187
|
default:
|
|
188
|
-
logNever(msg);
|
|
188
|
+
logNever(msg.data);
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
|
|
@@ -29,8 +29,11 @@ export class RuntimeState {
|
|
|
29
29
|
* ObjectIds of UIElements whose values need to be updated in the kernel
|
|
30
30
|
*/
|
|
31
31
|
private _sendComponentValues: RunRequests["sendComponentValues"] | undefined;
|
|
32
|
+
private uiElementRegistry: UIElementRegistry;
|
|
32
33
|
|
|
33
|
-
constructor(
|
|
34
|
+
constructor(uiElementRegistry: UIElementRegistry) {
|
|
35
|
+
this.uiElementRegistry = uiElementRegistry;
|
|
36
|
+
}
|
|
34
37
|
|
|
35
38
|
private get sendComponentValues(): RunRequests["sendComponentValues"] {
|
|
36
39
|
if (!this._sendComponentValues) {
|
|
@@ -40,21 +40,17 @@ export type SQLTableListPreview =
|
|
|
40
40
|
OperationMessageData<"sql-table-list-preview">;
|
|
41
41
|
export type SecretKeysResult = OperationMessageData<"secret-keys-result">;
|
|
42
42
|
export type StartupLogs = OperationMessageData<"startup-logs">;
|
|
43
|
-
export type
|
|
43
|
+
export type CellMessage = OperationMessageData<"cell-op">;
|
|
44
|
+
export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
|
|
44
45
|
|
|
45
|
-
export type
|
|
46
|
-
export type OperationMessage = {
|
|
47
|
-
[Type in OperationMessageType]: {
|
|
48
|
-
op: Type;
|
|
49
|
-
data: Omit<Extract<MessageOperation, { op: Type }>, "op">;
|
|
50
|
-
};
|
|
51
|
-
}[OperationMessageType];
|
|
46
|
+
export type MessageOperationUnion = schemas["KnownUnions"]["operation"];
|
|
52
47
|
|
|
53
|
-
export type
|
|
48
|
+
export type OperationMessageType = MessageOperationUnion["op"];
|
|
49
|
+
export interface OperationMessage {
|
|
50
|
+
data: MessageOperationUnion;
|
|
51
|
+
}
|
|
54
52
|
|
|
55
53
|
export type OperationMessageData<T extends OperationMessageType> = Omit<
|
|
56
|
-
Extract<
|
|
54
|
+
Extract<MessageOperationUnion, { op: T }>,
|
|
57
55
|
"op"
|
|
58
56
|
>;
|
|
59
|
-
|
|
60
|
-
export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
|
|
@@ -18,17 +18,29 @@ export const RequestId = {
|
|
|
18
18
|
*/
|
|
19
19
|
export class DeferredRequestRegistry<REQ, RES> {
|
|
20
20
|
public requests = new Map<RequestId, Deferred<RES>>();
|
|
21
|
+
public operation: string;
|
|
22
|
+
private makeRequest: (id: RequestId, req: REQ) => Promise<void>;
|
|
23
|
+
private opts: {
|
|
24
|
+
/**
|
|
25
|
+
* Resolve existing requests with an empty response.
|
|
26
|
+
*/
|
|
27
|
+
resolveExistingRequests?: () => RES;
|
|
28
|
+
};
|
|
21
29
|
|
|
22
30
|
constructor(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
operation: string,
|
|
32
|
+
makeRequest: (id: RequestId, req: REQ) => Promise<void>,
|
|
33
|
+
opts: {
|
|
26
34
|
/**
|
|
27
35
|
* Resolve existing requests with an empty response.
|
|
28
36
|
*/
|
|
29
37
|
resolveExistingRequests?: () => RES;
|
|
30
38
|
} = {},
|
|
31
|
-
) {
|
|
39
|
+
) {
|
|
40
|
+
this.operation = operation;
|
|
41
|
+
this.makeRequest = makeRequest;
|
|
42
|
+
this.opts = opts;
|
|
43
|
+
}
|
|
32
44
|
|
|
33
45
|
async request(opts: REQ): Promise<RES> {
|
|
34
46
|
if (this.opts.resolveExistingRequests) {
|
|
@@ -10,11 +10,12 @@ import type { RuntimeConfig } from "./types";
|
|
|
10
10
|
|
|
11
11
|
export class RuntimeManager {
|
|
12
12
|
private initialHealthyCheck = new Deferred<void>();
|
|
13
|
+
private config: RuntimeConfig;
|
|
14
|
+
private lazy: boolean;
|
|
13
15
|
|
|
14
|
-
constructor(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
) {
|
|
16
|
+
constructor(config: RuntimeConfig, lazy = false) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.lazy = lazy;
|
|
18
19
|
// Validate the URL on construction
|
|
19
20
|
try {
|
|
20
21
|
new URL(this.config.url);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { EDGE_CASE_FILENAMES } from "../../../__tests__/mocks";
|
|
5
|
+
import { Paths } from "../../../utils/paths";
|
|
6
|
+
|
|
7
|
+
describe("filename handling logic", () => {
|
|
8
|
+
it.each(EDGE_CASE_FILENAMES)(
|
|
9
|
+
"should extract basename correctly for document title: %s",
|
|
10
|
+
(filename) => {
|
|
11
|
+
const basename = Paths.basename(filename);
|
|
12
|
+
expect(basename).toBe(filename); // Since no path separator
|
|
13
|
+
},
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
it("should handle full paths with unicode filenames", () => {
|
|
17
|
+
EDGE_CASE_FILENAMES.forEach((filename) => {
|
|
18
|
+
const fullPath = `/path/to/${filename}`;
|
|
19
|
+
|
|
20
|
+
const basename = Paths.basename(fullPath);
|
|
21
|
+
expect(basename).toBe(filename);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should handle document title setting with unicode", () => {
|
|
26
|
+
EDGE_CASE_FILENAMES.forEach((filename) => {
|
|
27
|
+
const originalTitle = document.title;
|
|
28
|
+
|
|
29
|
+
// In case this does any conversions, we want to simulate reading/writing the title
|
|
30
|
+
document.title = filename;
|
|
31
|
+
expect(document.title).toBe(filename);
|
|
32
|
+
|
|
33
|
+
// Restore
|
|
34
|
+
document.title = originalTitle;
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { EDGE_CASE_FILENAMES } from "../../../__tests__/mocks";
|
|
4
|
+
import { Filenames } from "../../../utils/filenames";
|
|
5
|
+
import { Paths } from "../../../utils/paths";
|
|
3
6
|
import { visibleForTesting } from "../download-html";
|
|
4
7
|
|
|
5
8
|
const { updateAssetUrl } = visibleForTesting;
|
|
@@ -39,3 +42,42 @@ describe("updateAssetUrl", () => {
|
|
|
39
42
|
expect(updateAssetUrl(existingUrl, assetBaseUrl)).toBe(existingUrl);
|
|
40
43
|
});
|
|
41
44
|
});
|
|
45
|
+
|
|
46
|
+
describe("filename handling for downloads", () => {
|
|
47
|
+
it.each(EDGE_CASE_FILENAMES)(
|
|
48
|
+
"should handle edge case filenames in download operations: %s",
|
|
49
|
+
(filename) => {
|
|
50
|
+
// Test that basename extraction works correctly for downloads
|
|
51
|
+
const basename = Paths.basename(filename);
|
|
52
|
+
expect(basename).toBe(filename);
|
|
53
|
+
|
|
54
|
+
// Test filename conversion for HTML downloads
|
|
55
|
+
const htmlFilename = Filenames.toHTML(filename);
|
|
56
|
+
expect(htmlFilename).toMatch(/\.html$/);
|
|
57
|
+
expect(htmlFilename).toContain(Filenames.withoutExtension(filename));
|
|
58
|
+
|
|
59
|
+
// Ensure unicode and spaces are preserved in basename
|
|
60
|
+
const withoutExt = Filenames.withoutExtension(filename);
|
|
61
|
+
expect(withoutExt).not.toBe("");
|
|
62
|
+
expect(typeof withoutExt).toBe("string");
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it("should handle blob download filename generation", () => {
|
|
67
|
+
// Mock URL.createObjectURL for blob testing
|
|
68
|
+
const mockCreateObjectURL = vi.fn().mockReturnValue("blob:mock-url");
|
|
69
|
+
global.URL.createObjectURL = mockCreateObjectURL;
|
|
70
|
+
|
|
71
|
+
EDGE_CASE_FILENAMES.forEach((filename) => {
|
|
72
|
+
const htmlFilename = Filenames.toHTML(filename);
|
|
73
|
+
|
|
74
|
+
// Verify blob can be created with unicode filenames
|
|
75
|
+
expect(() => new Blob(["test"], { type: "text/html" })).not.toThrow();
|
|
76
|
+
expect(htmlFilename).toMatch(/\.html$/);
|
|
77
|
+
|
|
78
|
+
// Verify filename maintains unicode characters
|
|
79
|
+
const baseName = Filenames.withoutExtension(filename);
|
|
80
|
+
expect(htmlFilename).toContain(baseName);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -576,7 +576,11 @@ export class PyodideWebsocket implements IReconnectingWebSocket {
|
|
|
576
576
|
messageSubscriptions = new Set<(event: MessageEvent) => void>();
|
|
577
577
|
errorSubscriptions = new Set<(event: Event) => void>();
|
|
578
578
|
|
|
579
|
-
|
|
579
|
+
private bridge: Pick<PyodideBridge, "consumeMessages">;
|
|
580
|
+
|
|
581
|
+
constructor(bridge: Pick<PyodideBridge, "consumeMessages">) {
|
|
582
|
+
this.bridge = bridge;
|
|
583
|
+
}
|
|
580
584
|
|
|
581
585
|
private consumeMessages() {
|
|
582
586
|
this.bridge.consumeMessages((message) => {
|
package/src/core/wasm/store.ts
CHANGED
|
@@ -87,7 +87,10 @@ const emptyFileStore: FileStore = {
|
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
export class CompositeFileStore implements FileStore {
|
|
90
|
-
|
|
90
|
+
private stores: FileStore[];
|
|
91
|
+
constructor(stores: FileStore[]) {
|
|
92
|
+
this.stores = stores;
|
|
93
|
+
}
|
|
91
94
|
|
|
92
95
|
insert(index: number, store: FileStore) {
|
|
93
96
|
this.stores.splice(index, 0, store);
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
export class MessageBuffer<T> {
|
|
8
8
|
private buffer: T[];
|
|
9
9
|
private started = false;
|
|
10
|
-
|
|
11
|
-
constructor(
|
|
10
|
+
private onMessage: (data: T) => void;
|
|
11
|
+
constructor(onMessage: (data: T) => void) {
|
|
12
|
+
this.onMessage = onMessage;
|
|
12
13
|
this.buffer = [];
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -2,22 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import type ReconnectingWebSocket from "partysocket/ws";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
CONNECTING
|
|
7
|
-
OPEN
|
|
8
|
-
CLOSING
|
|
9
|
-
CLOSED
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
export const WebSocketState = {
|
|
6
|
+
CONNECTING: "CONNECTING",
|
|
7
|
+
OPEN: "OPEN",
|
|
8
|
+
CLOSING: "CLOSING",
|
|
9
|
+
CLOSED: "CLOSED",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
export type WebSocketState =
|
|
13
|
+
(typeof WebSocketState)[keyof typeof WebSocketState];
|
|
14
|
+
|
|
15
|
+
export const WebSocketClosedReason = {
|
|
16
|
+
KERNEL_DISCONNECTED: "KERNEL_DISCONNECTED",
|
|
17
|
+
ALREADY_RUNNING: "ALREADY_RUNNING",
|
|
18
|
+
MALFORMED_QUERY: "MALFORMED_QUERY",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type WebSocketClosedReason =
|
|
22
|
+
(typeof WebSocketClosedReason)[keyof typeof WebSocketClosedReason];
|
|
17
23
|
|
|
18
24
|
export type ConnectionStatus =
|
|
19
25
|
| {
|
|
20
|
-
state: WebSocketState.CLOSED;
|
|
26
|
+
state: typeof WebSocketState.CLOSED;
|
|
21
27
|
code: WebSocketClosedReason;
|
|
22
28
|
/**
|
|
23
29
|
* Human-readable reason for closing the connection.
|
|
@@ -31,9 +37,9 @@ export type ConnectionStatus =
|
|
|
31
37
|
}
|
|
32
38
|
| {
|
|
33
39
|
state:
|
|
34
|
-
| WebSocketState.CONNECTING
|
|
35
|
-
| WebSocketState.OPEN
|
|
36
|
-
| WebSocketState.CLOSING;
|
|
40
|
+
| typeof WebSocketState.CONNECTING
|
|
41
|
+
| typeof WebSocketState.OPEN
|
|
42
|
+
| typeof WebSocketState.CLOSING;
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
type PublicInterface<T> = {
|
|
@@ -86,7 +86,7 @@ export function useMarimoWebSocket(opts: {
|
|
|
86
86
|
|
|
87
87
|
const handleMessage = (e: MessageEvent<JsonString<OperationMessage>>) => {
|
|
88
88
|
const msg = jsonParseWithSpecialChar(e.data);
|
|
89
|
-
switch (msg.op) {
|
|
89
|
+
switch (msg.data.op) {
|
|
90
90
|
case "reload":
|
|
91
91
|
reloadSafe();
|
|
92
92
|
return;
|
|
@@ -267,7 +267,7 @@ export function useMarimoWebSocket(opts: {
|
|
|
267
267
|
setCellIds({ cellIds: msg.data.cell_ids as CellId[] });
|
|
268
268
|
return;
|
|
269
269
|
default:
|
|
270
|
-
logNever(msg);
|
|
270
|
+
logNever(msg.data);
|
|
271
271
|
}
|
|
272
272
|
};
|
|
273
273
|
|
package/src/css/app/Cell.css
CHANGED
|
@@ -23,6 +23,17 @@
|
|
|
23
23
|
outline: none;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
&:has(.mo-ai-generated-cell)::before {
|
|
27
|
+
content: "";
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
border-radius: 10px;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
opacity: 0.5;
|
|
33
|
+
box-shadow: 0px 0px 10px 0px var(--cyan-9);
|
|
34
|
+
z-index: 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
&:focus-within {
|
|
27
38
|
z-index: 20;
|
|
28
39
|
}
|