'use client'; import { CodeSnippet } from '@/app/components/codeSnippet'; import { SearchQueryParams } from '@/lib/types'; import { cn, createPathWithQueryParams } from '@/lib/utils'; import type { Element, Root } from "hast"; import { Schema as SanitizeSchema } from 'hast-util-sanitize'; import { CopyIcon, SearchIcon } from 'lucide-react'; import type { Heading, Nodes } from "mdast"; import { findAndReplace } from 'mdast-util-find-and-replace'; import { useRouter } from 'next/navigation'; import React, { useCallback, useMemo, forwardRef, memo } from 'react'; import Markdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import remarkGfm from 'remark-gfm'; import type { PluggableList, Plugin } from "unified"; import { visit } from 'unist-util-visit'; import { CodeBlock } from './codeBlock'; import { FILE_REFERENCE_REGEX } from '@/features/chat/constants'; import { createFileReference } from '@/features/chat/utils'; import { SINGLE_TENANT_ORG_DOMAIN } from '@/lib/constants'; import isEqual from "fast-deep-equal/react"; export const REFERENCE_PAYLOAD_ATTRIBUTE = 'data-reference-payload'; const annotateCodeBlocks: Plugin<[], Root> = () => { return (tree: Root) => { visit(tree, 'element', (node, _index, parent) => { if (node.tagName !== 'code' || !parent || !('tagName' in parent)) { return; } if (parent.tagName === 'pre') { node.properties.isBlock = true; parent.properties.isBlock = true; } else { node.properties.isBlock = false; } }) } } // @see: https://unifiedjs.com/learn/guide/create-a-remark-plugin/ function remarkReferencesPlugin() { return function (tree: Nodes) { findAndReplace(tree, [ FILE_REFERENCE_REGEX, (_, repo: string, fileName: string, startLine?: string, endLine?: string) => { // Create display text let displayText = fileName.split('/').pop() ?? fileName; const fileReference = createFileReference({ repo: repo, path: fileName, startLine, endLine, }); if (fileReference.range) { displayText += `:${fileReference.range.startLine}-${fileReference.range.endLine}`; } return { type: 'html', // @note: if you add additional attributes to this span, make sure to update the rehypeSanitize plugin to allow them. // // @note: we attach the reference id to the DOM element as a class name since there may be multiple reference elements // with the same id (i.e., referencing the same file & range). value: `${displayText}` } } ]) } } const remarkTocExtractor = () => { return function (tree: Nodes) { visit(tree, 'heading', (node: Heading) => { const textContent = node.children .filter((child) => child.type === 'text') .map((child) => child.value) .join(''); const id = textContent.toLowerCase().replace(/[^\w\s]/g, '').replace(/\s+/g, '-'); // Add id to the heading node for linking node.data = node.data || {}; node.data.hProperties = node.data.hProperties || {}; node.data.hProperties.id = id; }); }; } interface MarkdownRendererProps { content: string; className?: string; } const MarkdownRendererComponent = forwardRef(({ content, className }, ref) => { const router = useRouter(); const remarkPlugins = useMemo((): PluggableList => { return [ remarkGfm, remarkReferencesPlugin, remarkTocExtractor, ]; }, []); const rehypePlugins = useMemo((): PluggableList => { return [ rehypeRaw, [ rehypeSanitize, { ...defaultSchema, attributes: { ...defaultSchema.attributes, span: [...(defaultSchema.attributes?.span ?? []), 'role', 'className', 'data*'], }, strip: [], } satisfies SanitizeSchema, ], annotateCodeBlocks, ]; }, []); const renderPre = useCallback(({ children, node, ...rest }: React.JSX.IntrinsicElements['pre'] & { node?: Element }) => { if (node?.properties && node.properties.isBlock === true) { return children; } return (
                {children}
            
) }, []); const renderCode = useCallback(({ className, children, node, ...rest }: React.JSX.IntrinsicElements['code'] & { node?: Element }) => { const text = children?.toString().trimEnd() ?? ''; if (node?.properties && node.properties.isBlock === true) { const match = /language-(\w+)/.exec(className || ''); const language = match ? match[1] : undefined; return ( ) } return ( {children} {/* Invisible bridge to prevent hover gap */} ) }, [router]); return (
*:first-child]:mt-0", className)} > {content}
); }); MarkdownRendererComponent.displayName = 'MarkdownRenderer'; export const MarkdownRenderer = memo(MarkdownRendererComponent, isEqual);