/* 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 (