// v1.14.x-html-artifact-panes: extracted from MessageBubble.tsx so both the
// in-chat bubble renderer and the MarkdownArtifactPane share the same Shiki +
// remark-gfm + path-linkifier pipeline. Behavior preserved byte-for-byte from
// the original MessageBubble.MarkdownBody helper (and its linkify helpers).
import { memo, Children, cloneElement, isValidElement } from 'react';
import type { ReactElement, ReactNode } from 'react';
import Markdown from 'react-markdown';
import type { Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { CodeBlock } from './CodeBlock';
import { linkifyPaths } from '@/lib/linkify-paths';
function linkifyChildren(children: ReactNode, keyPrefix = 'l'): ReactNode {
const arr = Children.toArray(children);
return arr.map((child, i) => {
if (typeof child === 'string') {
return (
{linkifyPaths(child, `${keyPrefix}-${i}`)}
);
}
if (isValidElement(child)) {
const el = child as ReactElement<{ children?: ReactNode }>;
if (el.type === 'code' || el.type === CodeBlock) return child;
const grandchildren = el.props.children;
if (grandchildren === undefined) return child;
return cloneElement(el, {
key: el.key ?? `linkified-${i}`,
children: linkifyChildren(grandchildren, `${keyPrefix}-${i}`),
});
}
return child;
});
}
const codeRenderer = (props: { children?: unknown; className?: string }) => {
const { children, className, ...rest } = props;
const text = String(children ?? '').replace(/\n$/, '');
const langMatch = /language-([\w-]+)/.exec(className ?? '');
const isBlock = !!langMatch || text.includes('\n');
if (isBlock) {
return
{children as React.ReactNode}
);
};
// Hoisted to module level — closes over nothing render-scoped, so a stable
// reference avoids react-markdown reconstructing its vdom on every token delta.
const MARKDOWN_COMPONENTS: Components = {
pre: ({ children }) => <>{children}>,
code: codeRenderer,
a: ({ children, href }) => (
{children}
),
ul: ({ children }) => (
{linkifyChildren(children)}
), h1: ({ children }) =>{children}), table: ({ children }) => (