@marimo-team/islands 0.23.3-dev45 → 0.23.3-dev48
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 +3 -3
- package/dist/{reveal-component-Dl4bgjB2.js → reveal-component-CFuofbBD.js} +1 -1
- package/dist/{slide-form-Lvti-hPv.js → slide-form-DgMI37ES.js} +427 -425
- package/package.json +1 -1
- package/src/components/editor/output/JsonOutput.tsx +42 -12
- package/src/components/editor/output/__tests__/json-output.test.ts +38 -0
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import { memo, useState } from "react";
|
|
|
18
18
|
import type { OutputMessage } from "@/core/kernel/messages";
|
|
19
19
|
import { cn } from "@/utils/cn";
|
|
20
20
|
import { copyToClipboard } from "@/utils/copy";
|
|
21
|
+
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
21
22
|
import { isUrl } from "@/utils/urls";
|
|
22
23
|
import { useTheme } from "../../../theme/useTheme";
|
|
23
24
|
import { logNever } from "../../../utils/assertNever";
|
|
@@ -386,14 +387,41 @@ function renderLeaf(leaf: string, render: LeafRenderer): React.ReactNode {
|
|
|
386
387
|
// See `_key_formatter` in marimo/_output/formatters/structures.py.
|
|
387
388
|
const KEY_ENCODED_PREFIX = "text/plain+";
|
|
388
389
|
|
|
390
|
+
// Format elements for a Python collection literal. Non-finite floats
|
|
391
|
+
// (NaN / Infinity / -Infinity) parse as JS `number` via
|
|
392
|
+
// `jsonParseWithSpecialChar`; `JSON.stringify` on those returns `null`,
|
|
393
|
+
// so render them as the same `float(...)` literals we use for scalar
|
|
394
|
+
// float keys (see `decodeKeyForCopy`).
|
|
395
|
+
function formatCollectionItems(items: unknown[]): string {
|
|
396
|
+
return items
|
|
397
|
+
.map((x) => {
|
|
398
|
+
if (typeof x === "number" && !Number.isFinite(x)) {
|
|
399
|
+
if (Number.isNaN(x)) {
|
|
400
|
+
return "float('nan')";
|
|
401
|
+
}
|
|
402
|
+
return x > 0 ? "float('inf')" : "-float('inf')";
|
|
403
|
+
}
|
|
404
|
+
return JSON.stringify(x);
|
|
405
|
+
})
|
|
406
|
+
.join(", ");
|
|
407
|
+
}
|
|
408
|
+
|
|
389
409
|
// Format a JSON-list payload as a Python tuple literal. 1-element tuples
|
|
390
410
|
// need a trailing comma — `(1)` is just `1` in Python, `(1,)` is the tuple.
|
|
411
|
+
// Uses `jsonParseWithSpecialChar` so bare `NaN`/`Infinity`/`-Infinity`
|
|
412
|
+
// emitted by Python's json.dumps round-trip cleanly.
|
|
391
413
|
function formatTuplePayload(jsonList: string): string {
|
|
392
|
-
const items =
|
|
393
|
-
|
|
414
|
+
const items = jsonParseWithSpecialChar<unknown[]>(jsonList);
|
|
415
|
+
// `jsonParseWithSpecialChar` returns `{}` when both parse passes fail;
|
|
416
|
+
// fall back to the raw payload so a malformed wire form doesn't crash
|
|
417
|
+
// rendering/copy. Matches the defensive pattern in `formatSetPayload`.
|
|
418
|
+
if (!Array.isArray(items)) {
|
|
419
|
+
return jsonList;
|
|
420
|
+
}
|
|
394
421
|
if (items.length === 0) {
|
|
395
422
|
return "()";
|
|
396
423
|
}
|
|
424
|
+
const inner = formatCollectionItems(items);
|
|
397
425
|
if (items.length === 1) {
|
|
398
426
|
return `(${inner},)`;
|
|
399
427
|
}
|
|
@@ -403,29 +431,31 @@ function formatTuplePayload(jsonList: string): string {
|
|
|
403
431
|
// Format a JSON-list payload as a Python frozenset literal. Empty → `frozenset()`
|
|
404
432
|
// rather than `frozenset({})` (which reads like a dict).
|
|
405
433
|
function formatFrozensetPayload(jsonList: string): string {
|
|
406
|
-
const items =
|
|
434
|
+
const items = jsonParseWithSpecialChar<unknown[]>(jsonList);
|
|
435
|
+
if (!Array.isArray(items)) {
|
|
436
|
+
return jsonList;
|
|
437
|
+
}
|
|
407
438
|
if (items.length === 0) {
|
|
408
439
|
return "frozenset()";
|
|
409
440
|
}
|
|
410
|
-
const inner = items
|
|
441
|
+
const inner = formatCollectionItems(items);
|
|
411
442
|
return `frozenset({${inner}})`;
|
|
412
443
|
}
|
|
413
444
|
|
|
414
445
|
// Format a JSON-list payload as a Python set literal. Empty → `set()`
|
|
415
446
|
// (not `{}`, which is a dict literal in Python).
|
|
416
447
|
function formatSetPayload(jsonList: string): string {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (items.length === 0) {
|
|
420
|
-
return "set()";
|
|
421
|
-
}
|
|
422
|
-
const inner = items.map((x) => JSON.stringify(x)).join(", ");
|
|
423
|
-
return `{${inner}}`;
|
|
424
|
-
} catch {
|
|
448
|
+
const items = jsonParseWithSpecialChar<unknown[]>(jsonList);
|
|
449
|
+
if (!Array.isArray(items)) {
|
|
425
450
|
// Back-compat: older wire form was `text/plain+set:{1, 2, 3}` (Python
|
|
426
451
|
// set-literal string, not JSON). Pass it through as-is rather than crash.
|
|
427
452
|
return jsonList;
|
|
428
453
|
}
|
|
454
|
+
if (items.length === 0) {
|
|
455
|
+
return "set()";
|
|
456
|
+
}
|
|
457
|
+
const inner = formatCollectionItems(items);
|
|
458
|
+
return `{${inner}}`;
|
|
429
459
|
}
|
|
430
460
|
|
|
431
461
|
// Renderers for decoded non-string keys. Visual affordances match Python:
|
|
@@ -417,6 +417,44 @@ describe("getCopyValue with encoded non-string keys", () => {
|
|
|
417
417
|
`);
|
|
418
418
|
});
|
|
419
419
|
|
|
420
|
+
it("parses tuple/frozenset payloads containing bare NaN/Infinity", () => {
|
|
421
|
+
// Python's json.dumps emits bare `NaN`/`Infinity` inside the embedded
|
|
422
|
+
// tuple/frozenset payload strings (JSON spec violation, but ECMA-262-
|
|
423
|
+
// friendly via the fallback in jsonParseWithSpecialChar). The outer
|
|
424
|
+
// JSON stays strict because those tokens live inside a JSON string
|
|
425
|
+
// key/value. Regression for tuple-key payloads that previously broke
|
|
426
|
+
// the frontend's `JSON.parse` and threw.
|
|
427
|
+
const value = {
|
|
428
|
+
"text/plain+tuple:[NaN]": "tn",
|
|
429
|
+
"text/plain+tuple:[Infinity, -Infinity]": "ti",
|
|
430
|
+
k: "text/plain+frozenset:[Infinity, 1]",
|
|
431
|
+
};
|
|
432
|
+
expect(getCopyValue(value)).toMatchInlineSnapshot(`
|
|
433
|
+
"{
|
|
434
|
+
(float('nan'),): "tn",
|
|
435
|
+
(float('inf'), -float('inf')): "ti",
|
|
436
|
+
"k": frozenset({float('inf'), 1})
|
|
437
|
+
}"
|
|
438
|
+
`);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("falls back to the raw payload for malformed tuple/frozenset", () => {
|
|
442
|
+
// `jsonParseWithSpecialChar` returns `{}` on parse failure rather
|
|
443
|
+
// than throwing; without an `Array.isArray` guard, the formatters
|
|
444
|
+
// would crash on `.length`/`.map`. Pass the raw payload through so
|
|
445
|
+
// a malformed wire form doesn't break the whole render.
|
|
446
|
+
const value = {
|
|
447
|
+
"text/plain+tuple:not a json list": "t",
|
|
448
|
+
k: "text/plain+frozenset:also broken",
|
|
449
|
+
};
|
|
450
|
+
expect(getCopyValue(value)).toMatchInlineSnapshot(`
|
|
451
|
+
"{
|
|
452
|
+
not a json list: "t",
|
|
453
|
+
"k": also broken
|
|
454
|
+
}"
|
|
455
|
+
`);
|
|
456
|
+
});
|
|
457
|
+
|
|
420
458
|
it("unescapes string keys that looked encoded", () => {
|
|
421
459
|
const value = {
|
|
422
460
|
"text/plain+str:text/plain+int:2": "hello",
|