/* Copyright 2026 Marimo. All rights reserved. */ import { EditorView } from "@codemirror/view"; import { useAtomValue } from "jotai"; import { BetweenHorizontalStartIcon } from "lucide-react"; import { memo, Suspense, useState } from "react"; import { Streamdown, type StreamdownProps } from "streamdown"; import { Button, type ButtonProps } from "@/components/ui/button"; import { maybeAddMarimoImport } from "@/core/cells/add-missing-import"; import { useCellActions } from "@/core/cells/cells"; import { useLastFocusedCellId } from "@/core/cells/focus"; import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown"; import { SQLLanguageAdapter } from "@/core/codemirror/language/languages/sql/sql"; import { autoInstantiateAtom } from "@/core/config/config"; import { useAsyncData } from "@/hooks/useAsyncData"; import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror"; import { useTheme } from "@/theme/useTheme"; import { copyToClipboard } from "@/utils/copy"; import "./markdown-renderer.css"; const extensions = [EditorView.lineWrapping]; interface CodeBlockProps { code: string; language: string | undefined; } function maybeTransform( language: string | undefined, code: string, ): { language: string; code: string; } { // Default to python if (!language) { return { language: "python", code }; } // Already in the right language if (language === "python") { return { language, code }; } // Convert to python if (language === "sql") { return { language: "python", code: SQLLanguageAdapter.fromQuery(code) }; } // Convert to python if (language === "markdown") { return { language: "python", code: MarkdownLanguageAdapter.fromMarkdown(code), }; } // Run shell commands if (language === "shell" || language === "bash") { return { language: "python", code: `import subprocess\nsubprocess.run("${code}")`, }; } // Store as a string return { language: "python", code: `_${language} = """\n${code}\n"""`, }; } const InsertCodeBlockButton = ({ code, language }: CodeBlockProps) => { const { createNewCell } = useCellActions(); const lastFocusedCellId = useLastFocusedCellId(); const autoInstantiate = useAtomValue(autoInstantiateAtom); const handleInsertCode = () => { const result = maybeTransform(language, code); if (language === "sql") { maybeAddMarimoImport({ autoInstantiate, createNewCell, fromCellId: lastFocusedCellId, }); } createNewCell({ code: result.code, before: false, cellId: lastFocusedCellId ?? "__end__", }); }; return ( ); }; const CodeBlock = ({ code, language }: CodeBlockProps) => { const { theme } = useTheme(); const [value, setValue] = useState(code); if (value !== code) { setValue(code); } const handleCopyCode = async () => { await copyToClipboard(value); }; return (
Copy
); }; const CopyButton: React.FC = ({ onClick, ...props }) => { const [copied, setCopied] = useState(false); return ( ); }; type Components = StreamdownProps["components"]; const COMPONENTS: Components = { code: ({ children, className }) => { const language = className?.replace("language-", ""); if (language && typeof children === "string") { const code = children.trim(); return (
{language}
); } return {children}; }, }; // Lazy-load the math plugin to keep katex (~264 KB) out of the critical path. // The first render works without math; once loaded, it re-renders with math support. let mathPluginCache: StreamdownProps["plugins"] | undefined; const useMathPlugin = () => { const { data: plugins } = useAsyncData(async () => { if (mathPluginCache) { return mathPluginCache; } const mod = await import("@streamdown/math"); mathPluginCache = { math: mod.math }; return mathPluginCache; }, []); return plugins; }; export const MarkdownRenderer = memo(({ content }: { content: string }) => { const plugins = useMathPlugin(); return ( {content} ); }); MarkdownRenderer.displayName = "MarkdownRenderer";