import React from 'react'; import { CopyButton } from '@renderer/components/common/CopyButton'; import { PROSE_BODY } from '@renderer/constants/cssVariables'; import { FileLink, isRelativeUrl } from './viewers/FileLink'; import { extractTextFromReactNode } from './markdownCopyUtils'; import { highlightSearchInChildren, type SearchContext } from './searchHighlightUtils'; import type { Components } from 'react-markdown'; interface MarkdownComponentOptions { copyCodeBlocks?: boolean; } /** * Create inline markdown components for rendering prose content. * When searchCtx is provided, search term highlighting is applied * to text nodes while preserving full markdown rendering. */ export function createMarkdownComponents( searchCtx: SearchContext | null, options: MarkdownComponentOptions = {} ): Components { const { copyCodeBlocks = false } = options; const hl = (children: React.ReactNode): React.ReactNode => searchCtx ? highlightSearchInChildren(children, searchCtx) : children; return { // Headings - Bold text with generous spacing to break up content h1: ({ children }) => (

{hl(children)}

), h2: ({ children }) => (

{hl(children)}

), h3: ({ children }) => (

{hl(children)}

), h4: ({ children }) => (

{hl(children)}

), h5: ({ children }) => (
{hl(children)}
), h6: ({ children }) => (
{hl(children)}
), // Paragraphs p: ({ children }) => (

{hl(children)}

), // Links — inline element, no hl(); parent block element's hl() descends here a: ({ href, children }) => { if (href && isRelativeUrl(href)) { return {children}; } return ( {children} ); }, // Strong/Bold — inline element, no hl() strong: ({ children }) => ( {children} ), // Emphasis/Italic — inline element, no hl() em: ({ children }) => ( {children} ), // Strikethrough — inline element, no hl() del: ({ children }) => ( {children} ), // Inline code vs block code (block is highlighted by rehype-highlight; preserve hljs class) code: ({ className, children }) => { const hasLanguageClass = className?.includes('language-'); const content = typeof children === 'string' ? children : ''; const isMultiLine = content.includes('\n'); const isBlock = (hasLanguageClass ?? false) || isMultiLine; if (isBlock) { return ( {hl(children)} ); } // Inline code — no hl(); parent block element's hl() descends here return ( {children} ); }, // Code blocks pre: ({ children }) => { const codeText = copyCodeBlocks ? extractTextFromReactNode(children).trim() : ''; return (
          {codeText ?  : null}
          {children}
        
); }, // Blockquotes blockquote: ({ children }) => (
{hl(children)}
), // Lists ul: ({ children }) => ( ), ol: ({ children }) => (
    {children}
), li: ({ children }) => (
  • {hl(children)}
  • ), // Tables table: ({ children }) => (
    {children}
    ), thead: ({ children }) => ( {children} ), th: ({ children }) => ( {hl(children)} ), td: ({ children }) => ( {hl(children)} ), // Horizontal rule hr: () =>
    , }; } /** Default markdown components without search highlighting (used by CompactBoundary) */ export const markdownComponents: Components = createMarkdownComponents(null); /** Markdown components for message-style content with both whole-message and code-block copy */ export const markdownComponentsWithCodeCopy: Components = createMarkdownComponents(null, { copyCodeBlocks: true, });