Skip to content

Commit 78d0dbd

Browse files
committed
fix: improve URL handling in MarkdownBlock and update tests for trailing punctuation
1 parent bd2eb06 commit 78d0dbd

File tree

2 files changed

+66
-51
lines changed

2 files changed

+66
-51
lines changed

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

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const remarkUrlToLink = () => {
2828
const urlRegex = /https?:\/\/[^\s<>)"]+/g
2929
const matches = node.value.match(urlRegex)
3030

31-
if (!matches) {
31+
if (!matches || !parent) {
3232
return
3333
}
3434

@@ -45,25 +45,32 @@ const remarkUrlToLink = () => {
4545
const originalUrl = matches[i]
4646
const cleanedUrl = cleanedMatches[i]
4747
const removedPunctuation = originalUrl.substring(cleanedUrl.length)
48-
48+
49+
// Create a proper link node with all required properties
4950
children.push({
5051
type: "link",
5152
url: cleanedUrl,
53+
title: null,
5254
children: [{ type: "text", value: cleanedUrl }],
55+
data: {
56+
hProperties: {
57+
href: cleanedUrl,
58+
},
59+
},
5360
})
54-
61+
5562
if (removedPunctuation) {
5663
children.push({ type: "text", value: removedPunctuation })
5764
}
5865
}
5966
})
6067

61-
// Fix: Instead of converting the node to a paragraph (which broke things),
62-
// we replace the original text node with our new nodes in the parent's children array.
68+
// Replace the original text node with our new nodes in the parent's children array.
6369
// This preserves the document structure while adding our links.
64-
if (parent) {
65-
parent.children.splice(index, 1, ...children)
66-
}
70+
parent.children.splice(index!, 1, ...children)
71+
72+
// Return SKIP to prevent visiting the newly created nodes
73+
return ["skip", index! + children.length]
6774
})
6875
}
6976
}
@@ -154,44 +161,42 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
154161
rehypePlugins: [],
155162
rehypeReactOptions: {
156163
components: {
157-
a: ({ href, children }: any) => {
164+
a: ({ href, children, ...props }: any) => {
165+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
166+
// Only process file:// protocol or local file paths
167+
const isLocalPath = href.startsWith("file://") || href.startsWith("/") || !href.includes("://")
168+
169+
if (!isLocalPath) {
170+
return
171+
}
172+
173+
e.preventDefault()
174+
175+
// Handle absolute vs project-relative paths
176+
let filePath = href.replace("file://", "")
177+
178+
// Extract line number if present
179+
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
180+
let values = undefined
181+
if (match) {
182+
filePath = match[1]
183+
values = { line: parseInt(match[2]) }
184+
}
185+
186+
// Add ./ prefix if needed
187+
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
188+
filePath = "./" + filePath
189+
}
190+
191+
vscode.postMessage({
192+
type: "openFile",
193+
text: filePath,
194+
values,
195+
})
196+
}
197+
158198
return (
159-
<a
160-
href={href}
161-
title={href}
162-
onClick={(e) => {
163-
// Only process file:// protocol or local file paths
164-
const isLocalPath =
165-
href.startsWith("file://") || href.startsWith("/") || !href.includes("://")
166-
167-
if (!isLocalPath) {
168-
return
169-
}
170-
171-
e.preventDefault()
172-
173-
// Handle absolute vs project-relative paths
174-
let filePath = href.replace("file://", "")
175-
176-
// Extract line number if present
177-
const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
178-
let values = undefined
179-
if (match) {
180-
filePath = match[1]
181-
values = { line: parseInt(match[2]) }
182-
}
183-
184-
// Add ./ prefix if needed
185-
if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
186-
filePath = "./" + filePath
187-
}
188-
189-
vscode.postMessage({
190-
type: "openFile",
191-
text: filePath,
192-
values,
193-
})
194-
}}>
199+
<a {...props} href={href} onClick={handleClick}>
195200
{children}
196201
</a>
197202
)

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,22 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
1818
describe("MarkdownBlock", () => {
1919
it("should correctly handle URLs with trailing punctuation", async () => {
2020
const markdown = "Check out this link: https://example.com."
21-
render(<MarkdownBlock markdown={markdown} />)
21+
const { container } = render(<MarkdownBlock markdown={markdown} />)
2222

23-
await (async () => {
24-
const linkElement = screen.getByRole("link")
25-
expect(linkElement).toHaveAttribute("href", "https://example.com")
26-
expect(linkElement.textContent).toBe("https://example.com.")
27-
})
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.")
2838
})
2939
})

0 commit comments

Comments
 (0)