/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useMemo } from "react";
import type { Components } from "react-markdown";
import ReactMarkdown from "react-markdown";
import type { ReactMarkdownProps } from "react-markdown/lib/ast-to-react";
import type { PluggableList } from "react-markdown/lib/react-markdown";
import remarkDirective from "remark-directive";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { visit } from "unist-util-visit";
import { Checkbox, Chip } from "@sparkle/components";
import { BlockquoteBlock } from "@sparkle/components/markdown/BlockquoteBlock";
import { CodeBlockWithExtendedSupport } from "@sparkle/components/markdown/CodeBlockWithExtendedSupport";
import { LiBlock, OlBlock, UlBlock } from "@sparkle/components/markdown/List";
import { MarkdownContentContext } from "@sparkle/components/markdown/MarkdownContentContext";
import { ParagraphBlock } from "@sparkle/components/markdown/ParagraphBlock";
import { PreBlock } from "@sparkle/components/markdown/PreBlock";
import { safeRehypeKatex } from "@sparkle/components/markdown/safeRehypeKatex";
import {
TableBlock,
TableBodyBlock,
TableDataBlock,
TableHeadBlock,
TableHeaderBlock,
} from "@sparkle/components/markdown/TableBlock";
import {
preserveLineBreaks,
sanitizeContent,
} from "@sparkle/components/markdown/utils";
import { cn } from "@sparkle/lib/utils";
export const markdownHeaderClasses = {
h1: "s-heading-2xl",
h2: "s-heading-xl",
h3: "s-heading-lg",
h4: "s-text-base s-font-semibold",
h5: "s-text-sm s-font-semibold",
h6: "s-text-sm s-font-regular s-italic",
};
const sizes = {
p: "s-text-base s-leading-7",
...markdownHeaderClasses,
};
function showUnsupportedDirective() {
return (tree: any) => {
visit(tree, ["textDirective"], (node) => {
if (node.type === "textDirective") {
// it's not a valid directive, so we'll leave it as plain text
node.type = "text";
node.value = `:${node.name}${node.children ? node.children.map((c: any) => c.value).join("") : ""}`;
}
});
};
}
export function Markdown({
content,
isStreaming = false,
textColor = "s-text-foreground dark:s-text-foreground-night",
forcedTextSize,
isLastMessage = false,
compactSpacing = false,
additionalMarkdownComponents,
additionalMarkdownPlugins,
canCopyQuotes = true,
}: {
content: string;
isStreaming?: boolean;
textColor?: string;
isLastMessage?: boolean;
compactSpacing?: boolean; // When true, removes vertical padding from paragraph blocks for tighter spacing
forcedTextSize?: string;
additionalMarkdownComponents?: Components;
additionalMarkdownPlugins?: PluggableList;
canCopyQuotes?: boolean;
}) {
const processedContent = useMemo(() => {
let sanitized = sanitizeContent(content);
if (compactSpacing) {
sanitized = preserveLineBreaks(sanitized);
}
return sanitized;
}, [content, compactSpacing]);
// Note on re-renderings. A lot of effort has been put into preventing rerendering across markdown
// AST parsing rounds (happening at each token being streamed).
//
// When adding a new directive and associated component that depends on external data (eg
// workspace or message), you can use the customRenderer.visualization pattern. It is essential
// for the customRenderer argument to be memoized to avoid re-renderings through the
// markdownComponents memoization dependency on `customRenderer`.
//
// Make sure to spend some time understanding the re-rendering or lack thereof through the parser
// rounds.
//
// Minimal test whenever editing this code: ensure that code block content of a streaming message
// can be selected without blinking.
// Memoize markdown components to avoid unnecessary re-renders that disrupt text selection
const markdownComponents: Components = useMemo(() => {
return {
pre: ({ children }) =>