import type { Element } from 'hast';
import React from 'react';
import Markdown, { defaultUrlTransform } from 'react-markdown';
import rehypeKatex from 'rehype-katex';
import { defListHastHandlers, remarkDefinitionList } from 'remark-definition-list';
import remarkDirective from 'remark-directive';
import remarkGfm from 'remark-gfm';
import remarkAlert from 'remark-github-blockquote-alert';
import remarkMath from 'remark-math';
import remarkSupersub from 'remark-supersub';
import { SKIP, visit } from 'unist-util-visit';
import { CodeBlockHandlerProvider } from './CodeBlockContext';
import {
createDefaultCodeBlockHandlers,
ExpandCodeBlockHandler,
isExpandLanguage,
} from './codeBlockHandlers';
import { useCodeBlockRendererRegistry } from './CodeBlockRendering';
import { preprocessMathDelimiters } from './preprocessMathDelimiters';
import { MarkdownFigure } from './MarkdownFigure';
import { MarkdownImage } from './MarkdownImage';
import { MarkdownLink } from './MarkdownLink';
import { normalizeCustomSchemeLinks } from './normalizeCustomSchemeLinks';
import { normalizeDirectives } from './normalizeDirectives';
import { remarkDirectiveHandler } from './remarkDirectiveHandler';
// Custom URL schemes that we handle in our components
const ALLOWED_CUSTOM_SCHEMES = [
'artifact:',
'image:',
'store:',
'document:',
'document://',
'collection:',
];
/**
* Custom URL transform that allows our custom schemes while using
* the default transform for standard URLs (which sanitizes unsafe schemes).
*/
function customUrlTransform(url: string): string {
if (ALLOWED_CUSTOM_SCHEMES.some(scheme => url.startsWith(scheme))) {
return url;
}
return defaultUrlTransform(url);
}
/**
* Remark plugin to remove HTML comments from markdown
*/
function remarkRemoveComments() {
return (tree: any) => {
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
if (node.value && //.test(node.value)) {
if (parent && typeof index === 'number' && parent.children) {
parent.children.splice(index, 1);
return [SKIP, index];
}
}
});
};
}
export interface MarkdownRendererProps {
children: string;
components?: any;
remarkPlugins?: any[];
removeComments?: boolean;
/**
* Optional workflow run id used to resolve shorthand artifact paths (e.g. artifact:out/result.csv)
*/
artifactRunId?: string;
/** Additional className for the markdown wrapper */
className?: string;
/** Additional className for code blocks */
codeClassName?: string;
/** Additional className for inline code */
inlineCodeClassName?: string;
/** Additional className for links */
linkClassName?: string;
/** Additional className for images */
imageClassName?: string;
/** Callback when user selects a proposal option */
onProposalSelect?: (optionId: string) => void;
/** Callback when user submits free-form response to proposal */
onProposalSubmit?: (response: string) => void;
}
// Create default handlers once, outside component
const defaultCodeBlockHandlers = createDefaultCodeBlockHandlers();
export function MarkdownRenderer({
children,
components,
remarkPlugins = [],
removeComments = true,
artifactRunId,
className,
codeClassName,
inlineCodeClassName,
linkClassName,
imageClassName,
onProposalSelect,
onProposalSubmit,
}: MarkdownRendererProps) {
const codeBlockRegistry = useCodeBlockRendererRegistry();
const normalizedMarkdown = React.useMemo(
() => normalizeDirectives(normalizeCustomSchemeLinks(preprocessMathDelimiters(children))),
[children]
);
// Remark plugins (markdown parsing)
// Order matters: GFM first, then directive (must precede handler),
// then definition-list/supersub, then math, then user plugins.
const remarkPluginsArray = React.useMemo(() => {
const result: any[] = [
remarkGfm,
remarkDirective,
remarkDirectiveHandler,
remarkAlert,
remarkDefinitionList,
remarkSupersub,
remarkMath,
...remarkPlugins,
];
if (removeComments) {
result.push(remarkRemoveComments);
}
return result;
}, [remarkPlugins, removeComments]);
// Rehype plugins (HTML processing, including KaTeX for math)
const rehypePluginsArray = React.useMemo(() => [rehypeKatex], []);
// Remark-rehype bridge options (custom AST handlers for definition lists)
const remarkRehypeOptions = React.useMemo(() => ({
handlers: {
...defListHastHandlers,
},
}), []);
const componentsWithOverrides = React.useMemo(() => {
const baseComponents = components || {};
const ExistingCode = baseComponents.code;
const ExistingLink = baseComponents.a;
const ExistingImg = baseComponents.img;
const CodeComponent = ({
node,
className: codeClassName_,
children: codeChildren,
...props
}: {
node?: Element;
className?: string;
children?: React.ReactNode;
}) => {
const match = /language-([\w-]+)/.exec(codeClassName_ || '');
const isInline = !match;
const language = match ? match[1] : '';
// Check registry for custom renderer (includes default handlers)
if (!isInline && language) {
// First check user-provided registry
if (codeBlockRegistry) {
const CustomComponent = codeBlockRegistry.getComponent(language);
if (CustomComponent) {
const code = String(codeChildren || '').trim();
return
{codeChildren}
);
};
const LinkComponent = (props: {
node?: Element;
href?: string;
children?: React.ReactNode;
}) => {
const { node, href, children: linkChildren, ...rest } = props as any;
return (