import { memo, useEffect } from "react" import { useRemark } from "react-remark" import rehypeHighlight, { Options } from "rehype-highlight" import styled from "styled-components" import { visit } from "unist-util-visit" import { useExtensionState } from "../../context/ExtensionStateContext" import { CODE_BLOCK_BG_COLOR } from "./CodeBlock" interface MarkdownBlockProps { markdown?: string } /** * Custom remark plugin that converts plain URLs in text into clickable links * * The original bug: We were converting text nodes into paragraph nodes, * which broke the markdown structure because text nodes should remain as text nodes * within their parent elements (like paragraphs, list items, etc.). * This caused the entire content to disappear because the structure became invalid. */ const remarkUrlToLink = () => { return (tree: any) => { // Visit all "text" nodes in the markdown AST (Abstract Syntax Tree) visit(tree, "text", (node: any, index, parent) => { const urlRegex = /https?:\/\/[^\s<>)"]+/g const matches = node.value.match(urlRegex) if (!matches) return const parts = node.value.split(urlRegex) const children: any[] = [] parts.forEach((part: string, i: number) => { if (part) children.push({ type: "text", value: part }) if (matches[i]) { children.push({ type: "link", url: matches[i], children: [{ type: "text", value: matches[i] }], }) } }) // Fix: Instead of converting the node to a paragraph (which broke things), // we replace the original text node with our new nodes in the parent's children array. // This preserves the document structure while adding our links. if (parent) { parent.children.splice(index, 1, ...children) } }) } } const StyledMarkdown = styled.div` pre { background-color: ${CODE_BLOCK_BG_COLOR}; border-radius: 3px; margin: 13px 0; padding: 10px; max-width: 100%; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; } pre > code { white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; width: 100%; display: inline-block; .hljs-deletion { background-color: var(--vscode-diffEditor-removedTextBackground); display: inline-block; width: 100%; } .hljs-addition { background-color: var(--vscode-diffEditor-insertedTextBackground); display: inline-block; width: 100%; } } code { span.line:empty { display: none; } white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; border-radius: 3px; background-color: ${CODE_BLOCK_BG_COLOR}; font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px)); font-family: var(--vscode-editor-font-family); } code:not(pre > code) { font-family: var(--vscode-editor-font-family, monospace); color: var(--vscode-textPreformat-foreground, #f78383); background-color: var(--vscode-textCodeBlock-background, #1e1e1e); padding: 0px 2px; border-radius: 3px; border: 1px solid var(--vscode-textSeparator-foreground, #424242); white-space: pre-line; word-break: break-word; overflow-wrap: anywhere; } font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-size: var(--vscode-font-size, 13px); p, li, ol, ul { line-height: 1.4; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; } ol, ul { padding-left: 2.5em; margin-left: 0; } p { white-space: pre-wrap; } a { text-decoration: none; } a { &:hover { text-decoration: underline; } } ` const StyledPre = styled.pre<{ theme: any }>` & .hljs { color: var(--vscode-editor-foreground, #fff); } ${(props) => Object.keys(props.theme) .map((key, index) => { return ` & ${key} { color: ${props.theme[key]}; } ` }) .join("")} ` const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => { const { theme } = useExtensionState() const [reactContent, setMarkdown] = useRemark({ remarkPlugins: [ remarkUrlToLink, () => { return (tree) => { visit(tree, "code", (node: any) => { if (!node.lang) { node.lang = "javascript" } else if (node.lang.includes(".")) { node.lang = node.lang.split(".").slice(-1)[0] } }) } }, ], rehypePlugins: [ rehypeHighlight as any, { // languages: {}, } as Options, ], rehypeReactOptions: { components: { pre: ({ node, ...preProps }: any) => , }, }, }) useEffect(() => { setMarkdown(markdown || "") }, [markdown, setMarkdown, theme]) return (
{reactContent}
) }) export default MarkdownBlock