@marimo-team/islands 0.18.2 → 0.18.4

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.
Files changed (43) hide show
  1. package/dist/{constants-DWBOe162.js → constants-D_G8vnDk.js} +5 -4
  2. package/dist/{formats-7RSCCoSI.js → formats-Bi_tbdwB.js} +21 -22
  3. package/dist/{glide-data-editor-D-Ia_Jsv.js → glide-data-editor-DXF8E-QD.js} +2 -2
  4. package/dist/main.js +280 -148
  5. package/dist/style.css +1 -1
  6. package/dist/{types-Dunk85GC.js → types-DclGb0Yh.js} +1 -1
  7. package/dist/{vega-component-kU4hFYYJ.js → vega-component-BFcH2SqR.js} +8 -8
  8. package/package.json +1 -1
  9. package/src/components/app-config/user-config-form.tsx +14 -1
  10. package/src/components/data-table/context-menu.tsx +7 -3
  11. package/src/components/data-table/filter-pills.tsx +2 -1
  12. package/src/components/data-table/filters.ts +11 -2
  13. package/src/components/editor/cell/CreateCellButton.tsx +5 -3
  14. package/src/components/editor/cell/collapse.tsx +2 -2
  15. package/src/components/editor/chrome/components/contribute-snippet-button.tsx +22 -103
  16. package/src/components/editor/controls/duplicate-shortcut-banner.tsx +50 -0
  17. package/src/components/editor/controls/keyboard-shortcuts.tsx +25 -2
  18. package/src/components/editor/notebook-banner.tsx +1 -1
  19. package/src/components/editor/notebook-cell.tsx +4 -3
  20. package/src/components/editor/output/__tests__/ansi-reduce.test.ts +6 -6
  21. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -3
  22. package/src/components/pages/home-page.tsx +6 -0
  23. package/src/components/scratchpad/scratchpad.tsx +2 -1
  24. package/src/core/constants.ts +10 -0
  25. package/src/core/layout/useTogglePresenting.ts +69 -25
  26. package/src/core/state/__mocks__/mocks.ts +1 -0
  27. package/src/hooks/__tests__/useDuplicateShortcuts.test.ts +449 -0
  28. package/src/hooks/useDuplicateShortcuts.ts +145 -0
  29. package/src/plugins/impl/NumberPlugin.tsx +1 -1
  30. package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +1 -1
  31. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +67 -47
  32. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +2 -57
  33. package/src/plugins/impl/anywidget/__tests__/model.test.ts +23 -19
  34. package/src/plugins/impl/anywidget/model.ts +68 -41
  35. package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +2 -0
  36. package/src/plugins/impl/data-frames/utils/operators.ts +1 -0
  37. package/src/plugins/impl/vega/vega.css +5 -0
  38. package/src/plugins/layout/NavigationMenuPlugin.tsx +24 -22
  39. package/src/plugins/layout/StatPlugin.tsx +43 -23
  40. package/src/utils/__tests__/data-views.test.ts +495 -13
  41. package/src/utils/__tests__/json-parser.test.ts +1 -1
  42. package/src/utils/data-views.ts +134 -16
  43. package/src/utils/json/base64.ts +8 -0
@@ -1,16 +1,23 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
- import { updateBufferPaths } from "../data-views";
3
+ import {
4
+ dataViewToBase64,
5
+ decodeFromWire,
6
+ isWireFormat,
7
+ serializeBuffersToBase64,
8
+ type WireFormat,
9
+ } from "../data-views";
10
+ import type { Base64String } from "../json/base64";
4
11
 
5
- describe("updateBufferPaths", () => {
6
- it("should return the original object if bufferPaths.length === 0", () => {
7
- const input = { a: 1, b: 2 };
8
- const result = updateBufferPaths(input, [], []);
9
- expect(result).toEqual(input);
12
+ describe("decodeFromWire with DataViews", () => {
13
+ it("should return the original state if bufferPaths.length === 0", () => {
14
+ const state = { a: 1, b: 2 };
15
+ const result = decodeFromWire({ state, bufferPaths: [] });
16
+ expect(result).toEqual(state);
10
17
  });
11
18
 
12
- it("should update buffer paths with provided buffers", () => {
13
- const input = {
19
+ it("should insert DataViews at specified buffer paths", () => {
20
+ const state = {
14
21
  a: 1,
15
22
  b: {
16
23
  c: "Hello",
@@ -25,7 +32,7 @@ describe("updateBufferPaths", () => {
25
32
  new TextEncoder().encode("Hello"),
26
33
  new TextEncoder().encode("World"),
27
34
  ].map((b) => new DataView(b.buffer));
28
- const result = updateBufferPaths(input, bufferPaths, buffers);
35
+ const result = decodeFromWire({ state, bufferPaths, buffers });
29
36
  expect(result).toMatchInlineSnapshot(`
30
37
  {
31
38
  "a": 1,
@@ -50,25 +57,500 @@ describe("updateBufferPaths", () => {
50
57
  });
51
58
 
52
59
  it("should throw error when buffers and paths length mismatch", () => {
53
- const input = { a: 1 };
60
+ const state = { a: 1 };
54
61
  const bufferPaths = [
55
62
  ["b", "c"],
56
63
  ["b", "d"],
57
64
  ];
58
65
  const buffers = [new DataView(new ArrayBuffer())]; // Only one buffer for two paths
59
66
 
60
- expect(() => updateBufferPaths(input, bufferPaths, buffers)).toThrow(
67
+ expect(() => decodeFromWire({ state, bufferPaths, buffers })).toThrow(
61
68
  "Buffers and buffer paths not the same length",
62
69
  );
63
70
  });
64
71
 
65
72
  it("should handle empty buffers array", () => {
66
- const input = { a: 1 };
73
+ const state = { a: 1 };
67
74
  const bufferPaths = [["b", "c"]];
68
75
  const buffers: DataView[] = [];
69
76
 
70
- expect(() => updateBufferPaths(input, bufferPaths, buffers)).toThrow(
77
+ expect(() => decodeFromWire({ state, bufferPaths, buffers })).toThrow(
71
78
  "Buffers and buffer paths not the same length",
72
79
  );
73
80
  });
74
81
  });
82
+
83
+ describe("Immutability Tests", () => {
84
+ it("serializeBuffersToBase64 should not mutate input", () => {
85
+ const encoder = new TextEncoder();
86
+ const dataView1 = new DataView(encoder.encode("data1").buffer);
87
+ const dataView2 = new DataView(encoder.encode("data2").buffer);
88
+
89
+ const input = {
90
+ buffer1: dataView1,
91
+ nested: {
92
+ buffer2: dataView2,
93
+ value: "test",
94
+ },
95
+ array: [1, 2, 3],
96
+ };
97
+
98
+ const clone = structuredClone(input);
99
+
100
+ serializeBuffersToBase64(input);
101
+
102
+ // Check deep equality
103
+ expect(input).toEqual(clone);
104
+
105
+ // Check references are unchanged
106
+ expect(input.buffer1).toBe(dataView1);
107
+ expect(input.nested.buffer2).toBe(dataView2);
108
+ expect(input.nested).toBe(input.nested); // Nested object reference unchanged
109
+ expect(input.array).toBe(input.array); // Array reference unchanged
110
+ });
111
+
112
+ it("decodeFromWire (wire format) should not mutate input", () => {
113
+ const encoder = new TextEncoder();
114
+ const data = encoder.encode("Hello");
115
+ const base64 = btoa(String.fromCharCode(...data)) as Base64String;
116
+
117
+ const input = {
118
+ state: { text: base64, number: 42, nested: { value: "test" } },
119
+ bufferPaths: [["text"]],
120
+ buffers: [base64],
121
+ };
122
+
123
+ const clone = structuredClone(input);
124
+
125
+ decodeFromWire(input);
126
+
127
+ // Check deep equality
128
+ expect(input).toEqual(clone);
129
+
130
+ // Check references are unchanged
131
+ expect(input.state).toBe(input.state);
132
+ expect(input.state.nested).toBe(input.state.nested);
133
+ });
134
+
135
+ it("decodeFromWire (with DataViews) should not mutate input", () => {
136
+ const encoder = new TextEncoder();
137
+ const dataView = new DataView(encoder.encode("data").buffer);
138
+
139
+ const state = {
140
+ placeholder: "value",
141
+ nested: { key: "test" },
142
+ array: [1, 2, 3],
143
+ };
144
+ const bufferPaths = [["data"]];
145
+ const buffers = [dataView];
146
+
147
+ const input = { state, bufferPaths, buffers };
148
+ const clone = structuredClone(input);
149
+
150
+ decodeFromWire(input);
151
+
152
+ // Check deep equality
153
+ expect(input).toEqual(clone);
154
+
155
+ // Check references are unchanged
156
+ expect(input.state.nested).toBe(state.nested);
157
+ expect(input.state.array).toBe(state.array);
158
+ expect(input.buffers?.[0]).toBe(dataView); // Buffer reference unchanged
159
+ });
160
+
161
+ it("decodeFromWire should return new object, not mutate", () => {
162
+ const encoder = new TextEncoder();
163
+ const dataView = new DataView(encoder.encode("data").buffer);
164
+
165
+ const state = { a: 1, b: 2 };
166
+ const bufferPaths = [["c"]];
167
+ const buffers = [dataView];
168
+
169
+ const result = decodeFromWire({ state, bufferPaths, buffers });
170
+
171
+ // Result should be different reference
172
+ expect(result).not.toBe(state);
173
+
174
+ // Input should not have new property
175
+ expect("c" in state).toBe(false);
176
+
177
+ // Result should have new property
178
+ expect("c" in result).toBe(true);
179
+ });
180
+
181
+ it("serializeBuffersToBase64 should return new state object", () => {
182
+ const encoder = new TextEncoder();
183
+ const dataView = new DataView(encoder.encode("data").buffer);
184
+
185
+ const input = {
186
+ buffer: dataView,
187
+ value: "test",
188
+ };
189
+
190
+ const result = serializeBuffersToBase64(input);
191
+
192
+ // State should be different reference
193
+ expect(result.state).not.toBe(input);
194
+
195
+ // Input should still have DataView
196
+ expect(input.buffer).toBeInstanceOf(DataView);
197
+
198
+ // Result state should have base64 string
199
+ expect(typeof result.state.buffer).toBe("string");
200
+ });
201
+
202
+ it("decodeFromWire with object input should not mutate state", () => {
203
+ const encoder = new TextEncoder();
204
+ const dataView = new DataView(encoder.encode("data").buffer);
205
+
206
+ const state = { a: 1, b: { c: 2 } };
207
+ const bufferPaths = [["d"]];
208
+ const buffers = [dataView];
209
+
210
+ const input = { state, bufferPaths, buffers };
211
+ const clone = structuredClone(input);
212
+
213
+ decodeFromWire(input);
214
+
215
+ // Check deep equality
216
+ expect(input).toEqual(clone);
217
+
218
+ // Check references are unchanged
219
+ expect(input.state.b).toBe(state.b);
220
+ });
221
+
222
+ it("nested objects should maintain independence after serialization", () => {
223
+ const encoder = new TextEncoder();
224
+ const dataView = new DataView(encoder.encode("data").buffer);
225
+
226
+ const nested = { buffer: dataView, value: "nested" };
227
+ const input = {
228
+ nested,
229
+ other: "value",
230
+ };
231
+
232
+ const result = serializeBuffersToBase64(input);
233
+
234
+ // Mutate result state
235
+ (result.state.nested as Record<string, unknown>).value = "changed";
236
+
237
+ // Original nested object should be unchanged
238
+ expect(nested.value).toBe("nested");
239
+ expect(input.nested.value).toBe("nested");
240
+ });
241
+
242
+ it("arrays should not be mutated during operations", () => {
243
+ const encoder = new TextEncoder();
244
+ const dataView = new DataView(encoder.encode("data").buffer);
245
+
246
+ const input = {
247
+ items: [dataView, "middle", { value: 1 }],
248
+ };
249
+
250
+ const originalItems = input.items;
251
+ const originalMiddle = input.items[1];
252
+ const originalObject = input.items[2];
253
+
254
+ serializeBuffersToBase64(input);
255
+
256
+ // Array reference should be unchanged
257
+ expect(input.items).toBe(originalItems);
258
+ expect(input.items[1]).toBe(originalMiddle);
259
+ expect(input.items[2]).toBe(originalObject);
260
+ });
261
+ });
262
+
263
+ describe("dataViewToBase64", () => {
264
+ it("should convert a DataView to a base64 string", () => {
265
+ const encoder = new TextEncoder();
266
+ const bytes = encoder.encode("Hello, World!");
267
+ const dataView = new DataView(bytes.buffer);
268
+ const base64 = dataViewToBase64(dataView);
269
+
270
+ // Decode and verify
271
+ const decoded = atob(base64);
272
+ expect(decoded).toBe("Hello, World!");
273
+ });
274
+
275
+ it("should handle empty DataView", () => {
276
+ const dataView = new DataView(new ArrayBuffer(0));
277
+ const base64 = dataViewToBase64(dataView);
278
+ expect(base64).toBe("");
279
+ });
280
+
281
+ it("should handle DataView with offset and length", () => {
282
+ const encoder = new TextEncoder();
283
+ const bytes = encoder.encode("Hello, World!");
284
+ // Create a DataView that only looks at "World!"
285
+ const dataView = new DataView(bytes.buffer, 7, 6);
286
+ const base64 = dataViewToBase64(dataView);
287
+
288
+ const decoded = atob(base64);
289
+ expect(decoded).toBe("World!");
290
+ });
291
+
292
+ it("should handle binary data", () => {
293
+ const bytes = new Uint8Array([0, 1, 2, 255, 254, 253]);
294
+ const dataView = new DataView(bytes.buffer);
295
+ const base64 = dataViewToBase64(dataView);
296
+
297
+ // Verify round-trip
298
+ const decoded = atob(base64);
299
+ const decodedBytes = new Uint8Array(decoded.length);
300
+ for (let i = 0; i < decoded.length; i++) {
301
+ decodedBytes[i] = decoded.charCodeAt(i);
302
+ }
303
+ expect(Array.from(decodedBytes)).toEqual([0, 1, 2, 255, 254, 253]);
304
+ });
305
+ });
306
+
307
+ describe("serializeBuffersToBase64", () => {
308
+ it("should return empty arrays when no DataViews present", () => {
309
+ const input = { a: 1, b: "text", c: { d: true } };
310
+ const result = serializeBuffersToBase64(input);
311
+
312
+ expect(result).toEqual({
313
+ state: input,
314
+ buffers: [],
315
+ bufferPaths: [],
316
+ });
317
+ });
318
+
319
+ it("should serialize DataViews at top level", () => {
320
+ const encoder = new TextEncoder();
321
+ const dataView = new DataView(encoder.encode("test").buffer);
322
+ const input = { data: dataView, other: 123 };
323
+
324
+ const result = serializeBuffersToBase64(input);
325
+
326
+ expect(result.buffers).toHaveLength(1);
327
+ expect(result.bufferPaths).toEqual([["data"]]);
328
+ expect(typeof result.state.data).toBe("string");
329
+ expect(result.state.other).toBe(123);
330
+ });
331
+
332
+ it("should serialize nested DataViews", () => {
333
+ const encoder = new TextEncoder();
334
+ const dataView1 = new DataView(encoder.encode("first").buffer);
335
+ const dataView2 = new DataView(encoder.encode("second").buffer);
336
+ const input = {
337
+ nested: {
338
+ buffer1: dataView1,
339
+ deeper: {
340
+ buffer2: dataView2,
341
+ },
342
+ },
343
+ regular: "value",
344
+ };
345
+
346
+ const result = serializeBuffersToBase64(input);
347
+
348
+ expect(result.buffers).toHaveLength(2);
349
+ expect(result.bufferPaths).toEqual([
350
+ ["nested", "buffer1"],
351
+ ["nested", "deeper", "buffer2"],
352
+ ]);
353
+ expect(typeof result.state.nested.buffer1).toBe("string");
354
+ expect(typeof result.state.nested.deeper.buffer2).toBe("string");
355
+ });
356
+
357
+ it("should serialize DataViews in arrays", () => {
358
+ const encoder = new TextEncoder();
359
+ const dataView1 = new DataView(encoder.encode("one").buffer);
360
+ const dataView2 = new DataView(encoder.encode("two").buffer);
361
+ const input = {
362
+ items: [dataView1, "middle", dataView2],
363
+ };
364
+
365
+ const result = serializeBuffersToBase64(input);
366
+
367
+ expect(result.buffers).toHaveLength(2);
368
+ expect(result.bufferPaths).toEqual([
369
+ ["items", 0],
370
+ ["items", 2],
371
+ ]);
372
+ expect(result.state.items[1]).toBe("middle");
373
+ });
374
+
375
+ it("should handle mixed nested structures", () => {
376
+ const encoder = new TextEncoder();
377
+ const dataView = new DataView(encoder.encode("data").buffer);
378
+ const input = {
379
+ array: [{ nested: dataView }, [dataView]],
380
+ };
381
+
382
+ const result = serializeBuffersToBase64(input);
383
+
384
+ expect(result.buffers).toHaveLength(2);
385
+ expect(result.bufferPaths).toContainEqual(["array", 0, "nested"]);
386
+ expect(result.bufferPaths).toContainEqual(["array", 1, 0]);
387
+ });
388
+ });
389
+
390
+ describe("isWireFormat", () => {
391
+ it("should return true for valid wire format", () => {
392
+ const wireFormat: WireFormat = {
393
+ state: { a: 1 },
394
+ bufferPaths: [],
395
+ buffers: [],
396
+ };
397
+ expect(isWireFormat(wireFormat)).toBe(true);
398
+ });
399
+
400
+ it("should return true for wire format with data", () => {
401
+ const wireFormat: WireFormat = {
402
+ state: { value: "SGVsbG8=" },
403
+ bufferPaths: [["value"]],
404
+ buffers: ["SGVsbG8=" as Base64String],
405
+ };
406
+ expect(isWireFormat(wireFormat)).toBe(true);
407
+ });
408
+
409
+ it("should return false for null", () => {
410
+ expect(isWireFormat(null)).toBe(false);
411
+ });
412
+
413
+ it("should return false for non-objects", () => {
414
+ expect(isWireFormat("string")).toBe(false);
415
+ expect(isWireFormat(123)).toBe(false);
416
+ expect(isWireFormat(true)).toBe(false);
417
+ expect(isWireFormat(undefined)).toBe(false);
418
+ });
419
+
420
+ it("should return false when missing state", () => {
421
+ expect(isWireFormat({ bufferPaths: [], buffers: [] })).toBe(false);
422
+ });
423
+
424
+ it("should return false when missing bufferPaths", () => {
425
+ expect(isWireFormat({ state: {}, buffers: [] })).toBe(false);
426
+ });
427
+
428
+ it("should return false when missing buffers", () => {
429
+ expect(isWireFormat({ state: {}, bufferPaths: [] })).toBe(false);
430
+ });
431
+
432
+ it("should return false for plain objects", () => {
433
+ expect(isWireFormat({ a: 1, b: 2 })).toBe(false);
434
+ });
435
+ });
436
+
437
+ describe("decodeFromWire from WireFormat", () => {
438
+ it("should return state unchanged when no buffer paths", () => {
439
+ const wire: WireFormat = {
440
+ state: { a: 1, b: "text" },
441
+ bufferPaths: [],
442
+ buffers: [],
443
+ };
444
+ const result = decodeFromWire(wire);
445
+ expect(result).toEqual({ a: 1, b: "text" });
446
+ });
447
+
448
+ it("should decode single buffer at top level", () => {
449
+ const encoder = new TextEncoder();
450
+ const originalData = encoder.encode("Hello");
451
+ const base64 = btoa(String.fromCharCode(...originalData)) as Base64String;
452
+
453
+ const wire: WireFormat = {
454
+ state: { data: base64, other: 123 },
455
+ bufferPaths: [["data"]],
456
+ buffers: [base64],
457
+ };
458
+
459
+ const result = decodeFromWire(wire);
460
+
461
+ expect(result.data).toBeInstanceOf(DataView);
462
+ expect(result.other).toBe(123);
463
+
464
+ // Verify the DataView contains correct data
465
+ const bytes = new Uint8Array((result.data as DataView).buffer);
466
+ const decoded = new TextDecoder().decode(bytes);
467
+ expect(decoded).toBe("Hello");
468
+ });
469
+
470
+ it("should decode nested buffers", () => {
471
+ const encoder = new TextEncoder();
472
+ const data1 = encoder.encode("first");
473
+ const data2 = encoder.encode("second");
474
+ const base641 = btoa(String.fromCharCode(...data1)) as Base64String;
475
+ const base642 = btoa(String.fromCharCode(...data2)) as Base64String;
476
+
477
+ const wire: WireFormat = {
478
+ state: {
479
+ nested: {
480
+ buf1: base641,
481
+ deeper: {
482
+ buf2: base642,
483
+ },
484
+ },
485
+ },
486
+ bufferPaths: [
487
+ ["nested", "buf1"],
488
+ ["nested", "deeper", "buf2"],
489
+ ],
490
+ buffers: [base641, base642],
491
+ };
492
+
493
+ const result = decodeFromWire(wire) as {
494
+ nested: { buf1: unknown; deeper: { buf2: unknown } };
495
+ };
496
+
497
+ expect(result.nested.buf1).toBeInstanceOf(DataView);
498
+ expect(result.nested.deeper.buf2).toBeInstanceOf(DataView);
499
+ });
500
+
501
+ it("should decode buffers in arrays", () => {
502
+ const encoder = new TextEncoder();
503
+ const data = encoder.encode("test");
504
+ const base64 = btoa(String.fromCharCode(...data)) as Base64String;
505
+
506
+ const wire: WireFormat = {
507
+ state: {
508
+ items: [base64, "middle", base64],
509
+ },
510
+ bufferPaths: [
511
+ ["items", 0],
512
+ ["items", 2],
513
+ ],
514
+ buffers: [base64, base64],
515
+ };
516
+
517
+ const result = decodeFromWire(wire) as { items: unknown[] };
518
+
519
+ expect(result.items[0]).toBeInstanceOf(DataView);
520
+ expect(result.items[1]).toBe("middle");
521
+ expect(result.items[2]).toBeInstanceOf(DataView);
522
+ });
523
+
524
+ it("should handle round-trip serialization", () => {
525
+ const encoder = new TextEncoder();
526
+ const dataView1 = new DataView(encoder.encode("data1").buffer);
527
+ const dataView2 = new DataView(encoder.encode("data2").buffer);
528
+
529
+ const original = {
530
+ buffer1: dataView1,
531
+ nested: {
532
+ buffer2: dataView2,
533
+ },
534
+ regular: "value",
535
+ };
536
+
537
+ // Serialize
538
+ const serialized = serializeBuffersToBase64(original);
539
+
540
+ // Deserialize
541
+ const deserialized = decodeFromWire(serialized);
542
+
543
+ // Verify structure is preserved
544
+ expect(deserialized.buffer1).toBeInstanceOf(DataView);
545
+ expect(deserialized.nested.buffer2).toBeInstanceOf(DataView);
546
+ expect(deserialized.regular).toBe("value");
547
+
548
+ // Verify data integrity
549
+ const bytes1 = new Uint8Array((deserialized.buffer1 as DataView).buffer);
550
+ const bytes2 = new Uint8Array(
551
+ (deserialized.nested.buffer2 as DataView).buffer,
552
+ );
553
+ expect(new TextDecoder().decode(bytes1)).toBe("data1");
554
+ expect(new TextDecoder().decode(bytes2)).toBe("data2");
555
+ });
556
+ });
@@ -113,7 +113,7 @@ it("can convert json to tsv with fr-FR locale", () => {
113
113
  const locale = "fr-FR";
114
114
 
115
115
  // Handles floats with fr-FR locale (uses , as decimal separator)
116
- expect(jsonToTSV([{ a: 3.14, b: 2.12345 }], locale)).toEqual(
116
+ expect(jsonToTSV([{ a: 3.14, b: 2.123_45 }], locale)).toEqual(
117
117
  "a\tb\n3,14\t2,12345",
118
118
  );
119
119
  });