@marimo-team/islands 0.19.6-dev0 → 0.19.6-dev2
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 +20 -28
- package/package.json +2 -2
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +3 -4
- package/src/components/editor/actions/name-cell-input.tsx +2 -4
- package/src/utils/__tests__/data-views.test.ts +0 -45
- package/src/utils/data-views.ts +3 -19
- package/src/utils/json/__tests__/base64.test.ts +145 -69
- package/src/utils/json/base64.ts +59 -38
package/dist/main.js
CHANGED
|
@@ -56758,31 +56758,29 @@ Database schema: ${c}`), (_a3 = r2.aiFix) == null ? void 0 : _a3.setAiCompletion
|
|
|
56758
56758
|
function base64ToDataURL(e, r) {
|
|
56759
56759
|
return `data:${r};base64,${e}`;
|
|
56760
56760
|
}
|
|
56761
|
-
function typedAtob(e) {
|
|
56762
|
-
return window.atob(e);
|
|
56763
|
-
}
|
|
56764
|
-
function typedBtoa(e) {
|
|
56765
|
-
return window.btoa(e);
|
|
56766
|
-
}
|
|
56767
56761
|
function isDataURLString(e) {
|
|
56768
56762
|
return e.startsWith("data:") && e.includes(";base64,");
|
|
56769
56763
|
}
|
|
56770
56764
|
function extractBase64FromDataURL(e) {
|
|
56771
56765
|
return e.split(",")[1];
|
|
56772
56766
|
}
|
|
56773
|
-
function
|
|
56774
|
-
|
|
56767
|
+
function base64ToUint8Array(e) {
|
|
56768
|
+
let r = window.atob(e);
|
|
56769
|
+
return Uint8Array.from(r, (e2) => e2.charCodeAt(0));
|
|
56775
56770
|
}
|
|
56776
|
-
function
|
|
56777
|
-
let r =
|
|
56778
|
-
|
|
56779
|
-
|
|
56771
|
+
function base64ToDataView(e) {
|
|
56772
|
+
let r = base64ToUint8Array(e);
|
|
56773
|
+
return new DataView(r.buffer);
|
|
56774
|
+
}
|
|
56775
|
+
function uint8ArrayToBase64(e) {
|
|
56776
|
+
let r = Array.from(e, (e2) => String.fromCharCode(e2));
|
|
56777
|
+
return window.btoa(r.join(""));
|
|
56778
|
+
}
|
|
56779
|
+
function dataViewToBase64(e) {
|
|
56780
|
+
return uint8ArrayToBase64(new Uint8Array(e.buffer, e.byteOffset, e.byteLength));
|
|
56780
56781
|
}
|
|
56781
56782
|
function safeExtractSetUIElementMessageBuffers(e) {
|
|
56782
|
-
return (e.buffers ?? []).map(
|
|
56783
|
-
let r = byteStringToBinary(typedAtob(e2));
|
|
56784
|
-
return new DataView(r.buffer);
|
|
56785
|
-
});
|
|
56783
|
+
return (e.buffers ?? []).map(base64ToDataView);
|
|
56786
56784
|
}
|
|
56787
56785
|
function getLegacyNumericSpec(e, r, c) {
|
|
56788
56786
|
return {
|
|
@@ -57011,14 +57009,14 @@ Database schema: ${c}`), (_a3 = r2.aiFix) == null ? void 0 : _a3.setAiCompletion
|
|
|
57011
57009
|
}, c = "source_0";
|
|
57012
57010
|
else if (isDataURLString(e)) {
|
|
57013
57011
|
c = "data_0";
|
|
57014
|
-
let d =
|
|
57015
|
-
r =
|
|
57016
|
-
values:
|
|
57012
|
+
let d = extractBase64FromDataURL(e), f = window.atob(d);
|
|
57013
|
+
r = f.startsWith(ARROW_MAGIC_NUMBER) ? {
|
|
57014
|
+
values: base64ToUint8Array(d),
|
|
57017
57015
|
format: {
|
|
57018
57016
|
type: "arrow"
|
|
57019
57017
|
}
|
|
57020
57018
|
} : {
|
|
57021
|
-
values: parseCsvData(
|
|
57019
|
+
values: parseCsvData(f)
|
|
57022
57020
|
};
|
|
57023
57021
|
} else r = {
|
|
57024
57022
|
values: parseCsvData(e)
|
|
@@ -63239,9 +63237,6 @@ ${E}`,
|
|
|
63239
63237
|
};
|
|
63240
63238
|
}
|
|
63241
63239
|
};
|
|
63242
|
-
function dataViewToBase64(e) {
|
|
63243
|
-
return typedBtoa(binaryToByteString(new Uint8Array(e.buffer, e.byteOffset, e.byteLength)));
|
|
63244
|
-
}
|
|
63245
63240
|
function findDataViewPaths(e, r = []) {
|
|
63246
63241
|
let c = [];
|
|
63247
63242
|
if (e instanceof DataView) c.push(r);
|
|
@@ -63290,10 +63285,7 @@ ${E}`,
|
|
|
63290
63285
|
Logger.warn("[anywidget] Could not find buffer at path", r2);
|
|
63291
63286
|
continue;
|
|
63292
63287
|
}
|
|
63293
|
-
|
|
63294
|
-
let e3 = byteStringToBinary(typedAtob(c2));
|
|
63295
|
-
set_default(f, r2, new DataView(e3.buffer));
|
|
63296
|
-
} else set_default(f, r2, c2);
|
|
63288
|
+
typeof c2 == "string" ? set_default(f, r2, base64ToDataView(c2)) : set_default(f, r2, c2);
|
|
63297
63289
|
}
|
|
63298
63290
|
return f;
|
|
63299
63291
|
}
|
|
@@ -101103,7 +101095,7 @@ Defaulting to \`null\`.`;
|
|
|
101103
101095
|
return Logger.warn("Failed to get version from mount config"), null;
|
|
101104
101096
|
}
|
|
101105
101097
|
}
|
|
101106
|
-
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.6-
|
|
101098
|
+
const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.19.6-dev2"), showCodeInRunModeAtom = atom(true);
|
|
101107
101099
|
atom(null);
|
|
101108
101100
|
var VIRTUAL_FILE_REGEX = /\/@file\/([^\s"&'/]+)\.([\dA-Za-z]+)/g, VirtualFileTracker = class e {
|
|
101109
101101
|
constructor() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marimo-team/islands",
|
|
3
|
-
"version": "0.19.6-
|
|
3
|
+
"version": "0.19.6-dev2",
|
|
4
4
|
"main": "dist/main.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
"stylelint-config-standard": "^36.0.1",
|
|
259
259
|
"tailwindcss": "^4.1.18",
|
|
260
260
|
"vega-typings": "^2.1.0",
|
|
261
|
-
"vite": "npm:rolldown-vite@7.3.
|
|
261
|
+
"vite": "npm:rolldown-vite@7.3.1",
|
|
262
262
|
"vite-plugin-top-level-await": "^1.6.0",
|
|
263
263
|
"vite-plugin-wasm": "^3.5.0",
|
|
264
264
|
"vitest": "^3.2.4"
|
|
@@ -12,10 +12,9 @@ import type { TopLevelFacetedUnitSpec } from "@/plugins/impl/data-explorer/queri
|
|
|
12
12
|
import { arrow } from "@/plugins/impl/vega/formats";
|
|
13
13
|
import { parseCsvData } from "@/plugins/impl/vega/loader";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
base64ToUint8Array,
|
|
16
16
|
extractBase64FromDataURL,
|
|
17
17
|
isDataURLString,
|
|
18
|
-
typedAtob,
|
|
19
18
|
} from "@/utils/json/base64";
|
|
20
19
|
|
|
21
20
|
export function getLegacyNumericSpec(
|
|
@@ -281,12 +280,12 @@ export function getDataSpecAndSourceName<T>(data: string | T[]): {
|
|
|
281
280
|
} else if (isDataURLString(data)) {
|
|
282
281
|
sourceName = "data_0";
|
|
283
282
|
const base64 = extractBase64FromDataURL(data);
|
|
284
|
-
const decoded =
|
|
283
|
+
const decoded = window.atob(base64);
|
|
285
284
|
|
|
286
285
|
// eslint-disable-next-line unicorn/prefer-ternary
|
|
287
286
|
if (decoded.startsWith(ARROW_MAGIC_NUMBER)) {
|
|
288
287
|
dataSpec = {
|
|
289
|
-
values:
|
|
288
|
+
values: base64ToUint8Array(base64),
|
|
290
289
|
// @ts-expect-error vega-typings does not include arrow format
|
|
291
290
|
format: { type: "arrow" },
|
|
292
291
|
};
|
|
@@ -98,10 +98,8 @@ export const NameCellContentEditable: React.FC<{
|
|
|
98
98
|
e.stopPropagation();
|
|
99
99
|
|
|
100
100
|
// On Enter, blur the input to commit the change
|
|
101
|
-
if (e.key === "Enter") {
|
|
102
|
-
|
|
103
|
-
e.target.blur();
|
|
104
|
-
}
|
|
101
|
+
if (e.key === "Enter" && e.target instanceof HTMLElement) {
|
|
102
|
+
e.target.blur();
|
|
105
103
|
}
|
|
106
104
|
}}
|
|
107
105
|
>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
|
-
dataViewToBase64,
|
|
5
4
|
decodeFromWire,
|
|
6
5
|
isWireFormat,
|
|
7
6
|
serializeBuffersToBase64,
|
|
@@ -260,50 +259,6 @@ describe("Immutability Tests", () => {
|
|
|
260
259
|
});
|
|
261
260
|
});
|
|
262
261
|
|
|
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([...decodedBytes]).toEqual([0, 1, 2, 255, 254, 253]);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
262
|
describe("serializeBuffersToBase64", () => {
|
|
308
263
|
it("should return empty arrays when no DataViews present", () => {
|
|
309
264
|
const input = { a: 1, b: "text", c: { d: true } };
|
package/src/utils/data-views.ts
CHANGED
|
@@ -3,26 +3,11 @@ import { get, set } from "lodash-es";
|
|
|
3
3
|
import { invariant } from "./invariant";
|
|
4
4
|
import {
|
|
5
5
|
type Base64String,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
typedAtob,
|
|
9
|
-
typedBtoa,
|
|
6
|
+
base64ToDataView,
|
|
7
|
+
dataViewToBase64,
|
|
10
8
|
} from "./json/base64";
|
|
11
9
|
import { Logger } from "./Logger";
|
|
12
10
|
|
|
13
|
-
/**
|
|
14
|
-
* Convert a DataView to a base64 string.
|
|
15
|
-
*/
|
|
16
|
-
export function dataViewToBase64(dataView: DataView): Base64String {
|
|
17
|
-
const bytes = new Uint8Array(
|
|
18
|
-
dataView.buffer,
|
|
19
|
-
dataView.byteOffset,
|
|
20
|
-
dataView.byteLength,
|
|
21
|
-
);
|
|
22
|
-
const byteString = binaryToByteString(bytes);
|
|
23
|
-
return typedBtoa(byteString);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
11
|
/**
|
|
27
12
|
* Recursively find all DataViews in an object and return their paths.
|
|
28
13
|
*
|
|
@@ -148,8 +133,7 @@ export function decodeFromWire<T extends Record<string, unknown>>(input: {
|
|
|
148
133
|
|
|
149
134
|
// Handle both base64 strings (from wire format) and DataViews (direct usage)
|
|
150
135
|
if (typeof buffer === "string") {
|
|
151
|
-
|
|
152
|
-
set(out, bufferPath, new DataView(bytes.buffer));
|
|
136
|
+
set(out, bufferPath, base64ToDataView(buffer));
|
|
153
137
|
} else {
|
|
154
138
|
set(out, bufferPath, buffer);
|
|
155
139
|
}
|
|
@@ -2,99 +2,175 @@
|
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
type Base64String,
|
|
5
|
-
type ByteString,
|
|
6
5
|
base64ToDataURL,
|
|
7
|
-
|
|
6
|
+
base64ToDataView,
|
|
7
|
+
base64ToUint8Array,
|
|
8
8
|
type DataURLString,
|
|
9
|
-
|
|
9
|
+
dataViewToBase64,
|
|
10
|
+
deserializeJson,
|
|
10
11
|
extractBase64FromDataURL,
|
|
11
12
|
isDataURLString,
|
|
12
13
|
type JsonString,
|
|
13
|
-
|
|
14
|
-
typedBtoa,
|
|
14
|
+
uint8ArrayToBase64,
|
|
15
15
|
} from "../base64";
|
|
16
16
|
|
|
17
|
-
describe("
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
describe("base64", () => {
|
|
18
|
+
describe("deserializeJson", () => {
|
|
19
|
+
test("deserializes JSON string to object", () => {
|
|
20
|
+
const json = '{"name":"marimo","type":"notebook"}' as JsonString<{
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
}>;
|
|
24
|
+
const result = deserializeJson(json);
|
|
25
|
+
expect(result).toEqual({ name: "marimo", type: "notebook" });
|
|
26
|
+
});
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
test("serializeJsonToBase64 should correctly encode JSON to Base64", () => {
|
|
29
|
-
const base64Encoded = serializeJsonToBase64(testData);
|
|
30
|
-
expect(base64Encoded).toBe(expectedBase64);
|
|
28
|
+
test("deserializes JSON array", () => {
|
|
29
|
+
const json = "[1,2,3]" as JsonString<number[]>;
|
|
30
|
+
const result = deserializeJson(json);
|
|
31
|
+
expect(result).toEqual([1, 2, 3]);
|
|
32
|
+
});
|
|
31
33
|
});
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
describe("base64ToDataURL", () => {
|
|
36
|
+
test("creates proper data URL with mime type", () => {
|
|
37
|
+
const base64 = "SGVsbG8=" as Base64String;
|
|
38
|
+
const result = base64ToDataURL(base64, "text/plain");
|
|
39
|
+
expect(result).toBe("data:text/plain;base64,SGVsbG8=");
|
|
40
|
+
});
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
test("handles image mime types", () => {
|
|
43
|
+
const base64 = "iVBORw0KGgo=" as Base64String;
|
|
44
|
+
const result = base64ToDataURL(base64, "image/png");
|
|
45
|
+
expect(result).toBe("data:image/png;base64,iVBORw0KGgo=");
|
|
46
|
+
});
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
describe("isDataURLString", () => {
|
|
50
|
+
test.each([
|
|
51
|
+
["data:text/plain;base64,SGVsbG8=", true],
|
|
52
|
+
["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA", true],
|
|
53
|
+
["not-a-data-url", false],
|
|
54
|
+
["base64,SGVsbG8=", false],
|
|
55
|
+
["data:text/plain,hello", false],
|
|
56
|
+
["data:text/plain;charset=utf-8,hello", false],
|
|
57
|
+
])("isDataURLString(%s) returns %s", (input, expected) => {
|
|
58
|
+
expect(isDataURLString(input)).toBe(expected);
|
|
59
|
+
});
|
|
48
60
|
});
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
describe("extractBase64FromDataURL", () => {
|
|
63
|
+
test.each([
|
|
64
|
+
["data:text/plain;base64,SGVsbG8=", "SGVsbG8="],
|
|
65
|
+
[
|
|
66
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA",
|
|
67
|
+
"iVBORw0KGgoAAAANSUhEUgA",
|
|
68
|
+
],
|
|
69
|
+
["data:application/json;base64,", ""],
|
|
70
|
+
["data:text/html;charset=utf-8;base64,PGh0bWw+", "PGh0bWw+"],
|
|
71
|
+
])("extracts base64 from %s", (dataUrl, expected) => {
|
|
72
|
+
expect(extractBase64FromDataURL(dataUrl as DataURLString)).toBe(expected);
|
|
73
|
+
});
|
|
55
74
|
});
|
|
56
75
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
describe("base64ToUint8Array", () => {
|
|
77
|
+
test("converts base64 to Uint8Array", () => {
|
|
78
|
+
const base64 = "QUJD" as Base64String; // "ABC"
|
|
79
|
+
const result = base64ToUint8Array(base64);
|
|
80
|
+
expect(result).toEqual(new Uint8Array([65, 66, 67]));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("handles empty base64 string", () => {
|
|
84
|
+
const base64 = "" as Base64String;
|
|
85
|
+
const result = base64ToUint8Array(base64);
|
|
86
|
+
expect(result).toEqual(new Uint8Array([]));
|
|
87
|
+
});
|
|
64
88
|
});
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
describe("base64ToDataView", () => {
|
|
91
|
+
test("converts base64 to DataView", () => {
|
|
92
|
+
const base64 = "QUJD" as Base64String; // "ABC"
|
|
93
|
+
const result = base64ToDataView(base64);
|
|
94
|
+
expect(result.byteLength).toBe(3);
|
|
95
|
+
expect(result.getUint8(0)).toBe(65);
|
|
96
|
+
expect(result.getUint8(1)).toBe(66);
|
|
97
|
+
expect(result.getUint8(2)).toBe(67);
|
|
98
|
+
});
|
|
69
99
|
});
|
|
70
100
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
describe("uint8ArrayToBase64", () => {
|
|
102
|
+
test("converts Uint8Array to base64", () => {
|
|
103
|
+
const array = new Uint8Array([65, 66, 67]);
|
|
104
|
+
const result = uint8ArrayToBase64(array);
|
|
105
|
+
expect(result).toBe("QUJD");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("handles empty Uint8Array", () => {
|
|
109
|
+
const array = new Uint8Array([]);
|
|
110
|
+
const result = uint8ArrayToBase64(array);
|
|
111
|
+
expect(result).toBe("");
|
|
112
|
+
});
|
|
81
113
|
});
|
|
82
114
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
describe("dataViewToBase64", () => {
|
|
116
|
+
test("should convert a DataView to a base64 string", () => {
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
const bytes = encoder.encode("Hello, World!");
|
|
119
|
+
const dataView = new DataView(bytes.buffer);
|
|
120
|
+
const base64 = dataViewToBase64(dataView);
|
|
121
|
+
|
|
122
|
+
// Decode and verify
|
|
123
|
+
const decoded = atob(base64);
|
|
124
|
+
expect(decoded).toBe("Hello, World!");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("should handle empty DataView", () => {
|
|
128
|
+
const dataView = new DataView(new ArrayBuffer(0));
|
|
129
|
+
const base64 = dataViewToBase64(dataView);
|
|
130
|
+
expect(base64).toBe("");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("should handle DataView with offset and length", () => {
|
|
134
|
+
const encoder = new TextEncoder();
|
|
135
|
+
const bytes = encoder.encode("Hello, World!");
|
|
136
|
+
// Create a DataView that only looks at "World!"
|
|
137
|
+
const dataView = new DataView(bytes.buffer, 7, 6);
|
|
138
|
+
const base64 = dataViewToBase64(dataView);
|
|
139
|
+
|
|
140
|
+
const decoded = atob(base64);
|
|
141
|
+
expect(decoded).toBe("World!");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("should handle binary data", () => {
|
|
145
|
+
const bytes = new Uint8Array([0, 1, 2, 255, 254, 253]);
|
|
146
|
+
const dataView = new DataView(bytes.buffer);
|
|
147
|
+
const base64 = dataViewToBase64(dataView);
|
|
148
|
+
|
|
149
|
+
// Verify round-trip
|
|
150
|
+
const decoded = atob(base64);
|
|
151
|
+
const decodedBytes = new Uint8Array(decoded.length);
|
|
152
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
153
|
+
decodedBytes[i] = decoded.charCodeAt(i);
|
|
154
|
+
}
|
|
155
|
+
expect([...decodedBytes]).toEqual([0, 1, 2, 255, 254, 253]);
|
|
156
|
+
});
|
|
87
157
|
});
|
|
88
158
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
159
|
+
describe("round-trip conversions", () => {
|
|
160
|
+
test("Uint8Array to base64 and back", () => {
|
|
161
|
+
const original = new Uint8Array([1, 2, 3, 255, 128, 0]);
|
|
162
|
+
const base64 = uint8ArrayToBase64(original);
|
|
163
|
+
const result = base64ToUint8Array(base64);
|
|
164
|
+
expect(result).toEqual(original);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("DataView to base64 and back", () => {
|
|
168
|
+
const buffer = new ArrayBuffer(4);
|
|
169
|
+
const original = new DataView(buffer);
|
|
170
|
+
original.setUint32(0, 0x12_34_56_78);
|
|
171
|
+
const base64 = dataViewToBase64(original);
|
|
172
|
+
const result = base64ToDataView(base64);
|
|
173
|
+
expect(result.getUint32(0)).toBe(0x12_34_56_78);
|
|
174
|
+
});
|
|
93
175
|
});
|
|
94
176
|
});
|
|
95
|
-
|
|
96
|
-
// Serialization: JSON to Base64
|
|
97
|
-
function serializeJsonToBase64<T>(jsonObject: T) {
|
|
98
|
-
const jsonString = JSON.stringify(jsonObject);
|
|
99
|
-
return btoa(encodeURIComponent(jsonString)) as Base64String<JsonString<T>>;
|
|
100
|
-
}
|
package/src/utils/json/base64.ts
CHANGED
|
@@ -3,64 +3,88 @@
|
|
|
3
3
|
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
4
4
|
import type { TypedString } from "../typed";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* A JSON string of a given type.
|
|
8
|
+
*/
|
|
6
9
|
export type JsonString<T = unknown> = TypedString<"Json"> & {
|
|
7
10
|
_of_: T;
|
|
8
11
|
};
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export type ByteString<T = unknown> = TypedString<"ByteString"> & {
|
|
15
|
-
_of_: T;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type DataURLString = `data:${string};base64,${Base64String<string>}`;
|
|
13
|
+
/**
|
|
14
|
+
* A base64-encoded string.
|
|
15
|
+
*/
|
|
16
|
+
export type Base64String = TypedString<"Base64">;
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
18
|
+
/**
|
|
19
|
+
* A data URL string.
|
|
20
|
+
*/
|
|
21
|
+
export type DataURLString = `data:${string};base64,${Base64String}`;
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Typed JSON deserialization.
|
|
25
|
+
*/
|
|
27
26
|
export function deserializeJson<T>(jsonString: JsonString<T>): T {
|
|
28
27
|
return JSON.parse(jsonString) as T;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Convert a base64 string to a data URL string.
|
|
32
|
+
*/
|
|
33
|
+
export function base64ToDataURL(
|
|
34
|
+
base64: Base64String,
|
|
33
35
|
mimeType: string,
|
|
34
36
|
): DataURLString {
|
|
35
|
-
return `data:${mimeType};base64,${base64}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function typedAtob<T>(base64: Base64String<T>): ByteString<T> {
|
|
39
|
-
return window.atob(base64) as ByteString<T>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function typedBtoa<T>(bytes: ByteString<T>): Base64String<T> {
|
|
43
|
-
return window.btoa(bytes) as Base64String<T>;
|
|
37
|
+
return `data:${mimeType};base64,${base64}`;
|
|
44
38
|
}
|
|
45
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check if a string is a data URL string.
|
|
42
|
+
*/
|
|
46
43
|
export function isDataURLString(str: string): str is DataURLString {
|
|
47
44
|
return str.startsWith("data:") && str.includes(";base64,");
|
|
48
45
|
}
|
|
49
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Extract the base64 string from a data URL string.
|
|
49
|
+
*/
|
|
50
50
|
export function extractBase64FromDataURL(str: DataURLString): Base64String {
|
|
51
51
|
return str.split(",")[1] as Base64String;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Convert a base64 string to a Uint8Array.
|
|
56
|
+
*/
|
|
57
|
+
export function base64ToUint8Array(bytes: Base64String): Uint8Array {
|
|
58
|
+
const binary = window.atob(bytes);
|
|
59
|
+
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert a base64 string to a DataView.
|
|
64
|
+
*/
|
|
65
|
+
export function base64ToDataView(bytes: Base64String): DataView {
|
|
66
|
+
const uint8Array = base64ToUint8Array(bytes);
|
|
67
|
+
return new DataView(uint8Array.buffer);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert a Uint8Array to a base64 string.
|
|
72
|
+
*/
|
|
73
|
+
export function uint8ArrayToBase64(binary: Uint8Array): Base64String {
|
|
74
|
+
const chars = Array.from(binary, (byte) => String.fromCharCode(byte));
|
|
75
|
+
return window.btoa(chars.join("")) as Base64String;
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Convert a DataView to a base64 string.
|
|
80
|
+
*/
|
|
81
|
+
export function dataViewToBase64(dataView: DataView): Base64String {
|
|
82
|
+
const uint8Array = new Uint8Array(
|
|
83
|
+
dataView.buffer,
|
|
84
|
+
dataView.byteOffset,
|
|
85
|
+
dataView.byteLength,
|
|
86
|
+
);
|
|
87
|
+
return uint8ArrayToBase64(uint8Array);
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
export function safeExtractSetUIElementMessageBuffers(
|
|
@@ -68,8 +92,5 @@ export function safeExtractSetUIElementMessageBuffers(
|
|
|
68
92
|
): readonly DataView[] {
|
|
69
93
|
// @ts-expect-error - TypeScript doesn't know that these strings are actually base64 strings
|
|
70
94
|
const strs: Base64String[] = notification.buffers ?? [];
|
|
71
|
-
return strs.map(
|
|
72
|
-
const bytes = byteStringToBinary(typedAtob(str));
|
|
73
|
-
return new DataView(bytes.buffer);
|
|
74
|
-
});
|
|
95
|
+
return strs.map(base64ToDataView);
|
|
75
96
|
}
|