Skip to content

Commit 93b8f6d

Browse files
xyOz-devdaniel-lxs
andauthored
Update MarkdownBlock.tsx (#4841)
* Update MarkdownBlock.tsx * fix: correct URL handling in MarkdownBlock and add tests for trailing punctuation * Fix URL punctuation rendering * fix: improve URL handling in MarkdownBlock and update tests for trailing punctuation --------- Co-authored-by: Daniel Riccio <[email protected]>
1 parent 9da598c commit 93b8f6d

File tree

2 files changed

+102
-45
lines changed

2 files changed

+102
-45
lines changed

webview-ui/src/components/common/MarkdownBlock.tsx

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,49 @@ const remarkUrlToLink = () => {
3030
const urlRegex = /https?:\/\/[^\s<>)"]+/g
3131
const matches = node.value.match(urlRegex)
3232

33-
if (!matches) {
33+
if (!matches || !parent) {
3434
return
3535
}
3636

3737
const parts = node.value.split(urlRegex)
3838
const children: any[] = []
39+
const cleanedMatches = matches.map((url: string) => url.replace(/[.,;:!?'"]+$/, ""))
3940

4041
parts.forEach((part: string, i: number) => {
4142
if (part) {
4243
children.push({ type: "text", value: part })
4344
}
4445

45-
if (matches[i]) {
46-
children.push({ type: "link", url: matches[i], children: [{ type: "text", value: matches[i] }] })
46+
if (cleanedMatches[i]) {
47+
const originalUrl = matches[i]
48+
const cleanedUrl = cleanedMatches[i]
49+
const removedPunctuation = originalUrl.substring(cleanedUrl.length)
50+
51+
// Create a proper link node with all required properties
52+
children.push({
53+
type: "link",
54+
url: cleanedUrl,
55+
title: null,
56+
children: [{ type: "text", value: cleanedUrl }],
57+
data: {
58+
hProperties: {
59+
href: cleanedUrl,
60+
},
61+
},
62+
})
63+
64+
if (removedPunctuation) {
65+
children.push({ type: "text", value: removedPunctuation })
66+
}
4767
}
4868
})
4969

50-
// Fix: Instead of converting the node to a paragraph (which broke things),
51-
// we replace the original text node with our new nodes in the parent's children array.
70+
// Replace the original text node with our new nodes in the parent's children array.
5271
// This preserves the document structure while adding our links.
53-
if (parent) {
54-
parent.children.splice(index, 1, ...children)
55-
}
72+
parent.children.splice(index!, 1, ...children)
73+
74+
// Return SKIP to prevent visiting the newly created nodes
75+
return ["skip", index! + children.length]
5676
})
5777
}
5878
}
@@ -169,44 +189,42 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
169189
rehypePlugins: [rehypeKatex as any],
170190
rehypeReactOptions: {
171191
components: {
172-
a: ({ href, children }: any) => {
192+
a: ({ href, children, ...props }: any) => {
193+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
194+
// Only process file:// protocol or local file paths
195+
const isLocalPath = href.startsWith("file://") || href.startsWith("/") || !href.includes("://")
196+
197+
if (!isLocalPath) {
198+
return
199+
}
200+
201+
e.preventDefault()
202+
203+
// Handle absolute vs project-relative paths
204+
let filePath = href.replace("file://", "")
205+
206+
// Extract line number if present
207+
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
208+
let values = undefined
209+
if (match) {
210+
filePath = match[1]
211+
values = { line: parseInt(match[2]) }
212+
}
213+
214+
// Add ./ prefix if needed
215+
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
216+
filePath = "./" + filePath
217+
}
218+
219+
vscode.postMessage({
220+
type: "openFile",
221+
text: filePath,
222+
values,
223+
})
224+
}
225+
173226
return (
174-
<a
175-
href={href}
176-
title={href}
177-
onClick={(e) => {
178-
// Only process file:// protocol or local file paths
179-
const isLocalPath =
180-
href.startsWith("file://") || href.startsWith("/") || !href.includes("://")
181-
182-
if (!isLocalPath) {
183-
return
184-
}
185-
186-
e.preventDefault()
187-
188-
// Handle absolute vs project-relative paths
189-
let filePath = href.replace("file://", "")
190-
191-
// Extract line number if present
192-
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
193-
let values = undefined
194-
if (match) {
195-
filePath = match[1]
196-
values = { line: parseInt(match[2]) }
197-
}
198-
199-
// Add ./ prefix if needed
200-
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
201-
filePath = "./" + filePath
202-
}
203-
204-
vscode.postMessage({
205-
type: "openFile",
206-
text: filePath,
207-
values,
208-
})
209-
}}>
227+
<a {...props} href={href} onClick={handleClick}>
210228
{children}
211229
</a>
212230
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from "react"
2+
import { render, screen } from "@testing-library/react"
3+
import MarkdownBlock from "../MarkdownBlock"
4+
import { vi } from "vitest"
5+
6+
vi.mock("@src/utils/vscode", () => ({
7+
vscode: {
8+
postMessage: vi.fn(),
9+
},
10+
}))
11+
12+
vi.mock("@src/context/ExtensionStateContext", () => ({
13+
useExtensionState: () => ({
14+
theme: "dark",
15+
}),
16+
}))
17+
18+
describe("MarkdownBlock", () => {
19+
it("should correctly handle URLs with trailing punctuation", async () => {
20+
const markdown = "Check out this link: https://example.com."
21+
const { container } = render(<MarkdownBlock markdown={markdown} />)
22+
23+
// Wait for the content to be processed
24+
await screen.findByText(/Check out this link/, { exact: false })
25+
26+
// Check for nested links - this should not happen
27+
const nestedLinks = container.querySelectorAll("a a")
28+
expect(nestedLinks.length).toBe(0)
29+
30+
// Should have exactly one link
31+
const linkElement = screen.getByRole("link")
32+
expect(linkElement).toHaveAttribute("href", "https://example.com")
33+
expect(linkElement.textContent).toBe("https://example.com")
34+
35+
// Check that the period is outside the link
36+
const paragraph = container.querySelector("p")
37+
expect(paragraph?.textContent).toBe("Check out this link: https://example.com.")
38+
})
39+
})

0 commit comments

Comments
 (0)