mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
213 lines
4.8 KiB
TypeScript
213 lines
4.8 KiB
TypeScript
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) => <StyledPre {...preProps} theme={theme} />,
|
|
},
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
setMarkdown(markdown || "")
|
|
}, [markdown, setMarkdown, theme])
|
|
|
|
return (
|
|
<div style={{}}>
|
|
<StyledMarkdown>{reactContent}</StyledMarkdown>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
export default MarkdownBlock
|