/* Copyright 2026 Marimo. All rights reserved. */
import { useState } from "react";
import type { OutputMessage } from "@/core/kernel/messages";
import { isUrl } from "@/utils/urls";
import { OutputRenderer } from "../../Output";
import { HtmlOutput } from "../HtmlOutput";
import { ImageOutput } from "../ImageOutput";
import { JsonOutput } from "../JsonOutput";
import { VideoOutput } from "../VideoOutput";
import {
decodePythonKey,
formatTuplePayload,
formatSetPayload,
formatFrozensetPayload,
splitLeaf,
} from "./formatting";
export const COLLAPSED_TEXT_LENGTH = 100;
// Keep long text and quotes wrapping together.
/* oxlint-disable jsx-a11y/prefer-tag-over-role */
const CollapsibleTextOutput: React.FC<{ text: string }> = ({ text }) => {
const [collapsed, setCollapsed] = useState(true);
if (text.length <= COLLAPSED_TEXT_LENGTH) {
return {text};
}
return (
setCollapsed(!collapsed)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setCollapsed(!collapsed);
}
}}
>
{collapsed ? `${text.slice(0, COLLAPSED_TEXT_LENGTH)}...` : text}
);
};
/* oxlint-enable jsx-a11y/prefer-tag-over-role */
type LeafRenderer = (
data: string,
mimeType: OutputMessage["mimetype"],
) => React.ReactNode;
const LEAF_RENDERERS: Record = {
"image/": (v) => ,
"video/": (v) => ,
"text/html:": (v) => (
),
"text/markdown:": (v) => (
),
"text/plain+float:": (v) => {v},
"text/plain+bigint:": (v) => {v},
"text/plain+set:": (v) => {formatSetPayload(v)},
"text/plain+frozenset:": (v) => {formatFrozensetPayload(v)},
"text/plain+tuple:": (v) => {formatTuplePayload(v)},
"text/plain:": (v) => ,
"application/json:": (v) => ,
"application/": (v, mime) => (
(
{mime}:{v}
)}
/>
),
};
function tryRenderMimeLeaf(value: string): React.ReactNode | null {
for (const prefix of Object.keys(LEAF_RENDERERS)) {
if (value.startsWith(prefix)) {
const [data, mime] = splitLeaf(value);
return mime
? LEAF_RENDERERS[prefix](data, mime as OutputMessage["mimetype"])
: null;
}
}
return null;
}
interface KeyRenderResult {
keyName?: string | number;
}
export function renderKeyName(
props: Record,
{ keyName }: KeyRenderResult,
): React.ReactNode {
const decoded = decodePythonKey(keyName);
if (!decoded) {
return undefined;
}
return {decoded.text};
}
export function renderKeyQuote(
_props: Record,
{ keyName }: KeyRenderResult,
): React.ReactNode {
// The viewer treats null/undefined as a request for its default quotes.
return decodePythonKey(keyName)?.quoted === false ? : undefined;
}
export interface RenderResult {
type: string;
value?: unknown;
}
export function renderStringValue(
_props: React.HTMLAttributes,
result: RenderResult,
mode: "python" | "json",
): React.ReactNode {
if (result.type !== "value" || typeof result.value !== "string") {
return undefined;
}
const { value } = result;
if (mode !== "python") {
return renderPlainString(value);
}
const mimeResult = tryRenderMimeLeaf(value);
if (mimeResult !== null) {
return {mimeResult};
}
if (isUrl(value)) {
return (
{value}
);
}
return renderPlainString(value);
}
function renderPlainString(value: string) {
if (value.length <= COLLAPSED_TEXT_LENGTH) {
return undefined;
}
return (
"
"
);
}
export function renderArrow(
props: React.HTMLAttributes,
{ keyName }: KeyRenderResult,
): React.ReactNode {
// alpha.43's data-expanded is inverted and omits initial collapse rules.
// NestedOpen's transform reflects the effective state.
const expanded = props.style?.transform === "rotate(0deg)";
return (
);
}
function renderLiteral(text: string) {
return (_props: Record, { type }: RenderResult) =>
type === "value" ? {text} : undefined;
}
export const PYTHON_TRUE_RENDER = renderLiteral("True");
export const PYTHON_FALSE_RENDER = renderLiteral("False");
export const PYTHON_NULL_RENDER = renderLiteral("None");
export const JSON_NULL_RENDER = renderLiteral("null");