Repository: microsoft/vscode (TypeScript)
Date: 2022-05-06 22:49:58+00:00
Task type: Bug Fix

Issue:
Markdown Render VSCode Command Link
Issue Type: <b>Bug</b>

Hello,

Regarding the `MarkdownString`, in Markdown this:

`[Name](link)`

Renders a link. In a VS Code Extension, this is also possible:

`[Run It](command:pkg.command)`

Creating a link to run a command.

One would think creating an actual anchor link like this:

`<a href="command:pkg.command">Run It</a>`

Would work but the click does not execute the command.

VS Code version: Code 1.63.0 (7db1a2b88f7557e0a43fec75b6ba7e50b3e9f77e, 2021-12-07T05:18:59.299Z)
OS version: Linux arm64 5.15.7-1-MANJARO-ARM
Restricted Mode: No

<details>
<summary>System Info</summary>

|Item|Value|
|---|---|
|CPUs|unknown (6 x 1416)|
|GPU Status|2d_canvas: enabled<br>gpu_compositing: enabled<br>multiple_raster_threads: enabled_on<br>oop_rasterization: disabled_off<br>opengl: enabled_on<br>rasterization: disabled_software<br>skia_renderer: enabled_on<br>video_decode: disabled_software<br>vulkan: disabled_off<br>webgl: enabled<br>webgl2: enabled|
|Load (avg)|2, 1, 2|
|Memory (System)|3.77GB (0.02GB free)|
|Process Argv|. --crash-reporter-id 2a85f066-a8e2-4971-b1ff-37959f57a0f0|
|Screen Reader|no|
|VM|0%|
|DESKTOP_SESSION|plasma|
|XDG_CURRENT_DESKTOP|KDE|
|XDG_SESSION_DESKTOP|KDE|
|XDG_SESSION_TYPE|x11|
</details><details><summary>Extensions (4)</summary>

Extension|Author (truncated)|Version
---|---|---
vscode-eslint|dba|2.2.2
gitlens|eam|11.7.0
vscode-drawio|hed|1.6.4
vsliveshare|ms-|1.0.5242


</details>
<!-- generated by issue reporter -->


Issue discussion:
can i take this?
I think they'd be happy if you created a PR.

For completeness: `data-href` works:

`<a data-href="command:pkg.command">Run It</a>`

 but the cursor does not change to a hand icon like if `href` was populated. 
> I think they'd be happy if you created a PR.
> 
> For completeness: `data-href` works:
> 
> `<a data-href="command:pkg.command">Run It</a>`
> 
> but the cursor does not change to a hand icon like if `href` was populated.

Thanks, will take a look
This is what I found. In markdown links are detected at two places:
  - [src/vs/editor/common/languages/linkComputer.ts](https://github.com/microsoft/vscode/blob/main/src/vs/editor/common/languages/linkComputer.ts) detects anything starting with http:// or https:// or file:// 
  - [extensions/markdown-language-features/src/features/documentLinkProvider.ts](https://github.com/microsoft/vscode/blob/main/extensions/markdown-language-features/src/features/documentLinkProvider.ts) checks for `[]()` and returns anything inside () as URI

That's why `[Run It](command:pkg.command)` is detected as a link but `<a href="command:pkg.command">Run It</a>` isn't. 

I couldn't get `<a data-href="command:pkg.command">Run It</a>` to work. I tried holding ctrl while clicking it and tried in preview as well. Can you tell me how to reproduce it?
You'll need to [register a command](https://code.visualstudio.com/api/extension-guides/command#creating-new-commands) for it to take an action. The example in the link is probably the easiest. After registration create a MarkdownString containing the registered command. In regards to the linked example:

`<a data-href="command:myExtension.sayHello">Say Hello</a>`

You should be able to click on it and see the console log. Note that the cursor doesn't change to a hand icon.
@rbrisita Have you found any solution? For me even `data-href` doesn't work. :( I've used a registered and available command.
The way I implemented Markdown and executing a registered command (through package.json) was by adding a [`MarkdownString`](https://code.visualstudio.com/api/references/vscode-api#MarkdownString) (mds) to the tooltip of a [`StatusBarItem`](https://code.visualstudio.com/api/references/vscode-api#StatusBarItem). The structure is very particular because it is an implementation of the [CommonMark Spec ](https://spec.commonmark.org/0.30) and is something like this:

```typescript
const stop = '# [$(debug-stop)](command:my_ext.stop)';
const table = `
<table>
<tbody>
<tr>
<td align="center">

${stop}

</td>
</tr>
</tbody>
</table>
`;
mds.value = table;
statusBar.tooltip = mds;
```

Make sure you are using the correct VS Code version too.

Solution patch:
diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts
--- a/src/vs/base/browser/markdownRenderer.ts
+++ b/src/vs/base/browser/markdownRenderer.ts
@@ -130,30 +130,17 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
 		if (href === text) { // raw link case
 			text = removeMarkdownEscapes(text);
 		}
-		href = _href(href, false);
-		if (markdown.baseUri) {
-			href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
-		}
+
 		title = typeof title === 'string' ? removeMarkdownEscapes(title) : '';
 		href = removeMarkdownEscapes(href);
-		if (
-			!href
-			|| /^data:|javascript:/i.test(href)
-			|| (/^command:/i.test(href) && !markdown.isTrusted)
-			|| /^command:(\/\/\/)?_workbench\.downloadResource/i.test(href)
-		) {
-			// drop the link
-			return text;
-
-		} else {
-			// HTML Encode href
-			href = href.replace(/&/g, '&amp;')
-				.replace(/</g, '&lt;')
-				.replace(/>/g, '&gt;')
-				.replace(/"/g, '&quot;')
-				.replace(/'/g, '&#39;');
-			return `<a href="" data-href="${href}" title="${title || href}">${text}</a>`;
-		}
+
+		// HTML Encode href
+		href = href.replace(/&/g, '&amp;')
+			.replace(/</g, '&lt;')
+			.replace(/>/g, '&gt;')
+			.replace(/"/g, '&quot;')
+			.replace(/'/g, '&#39;');
+		return `<a href="${href}" title="${title || href}">${text}</a>`;
 	};
 	renderer.paragraph = (text): string => {
 		return `<p>${text}</p>`;
@@ -267,6 +254,27 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
 			}
 		});
 
+	markdownHtmlDoc.body.querySelectorAll('a')
+		.forEach(a => {
+			const href = a.getAttribute('href'); // Get the raw 'href' attribute value as text, not the resolved 'href'
+			a.setAttribute('href', ''); // Clear out href. We use the `data-href` for handling clicks instead
+			if (
+				!href
+				|| /^data:|javascript:/i.test(href)
+				|| (/^command:/i.test(href) && !markdown.isTrusted)
+				|| /^command:(\/\/\/)?_workbench\.downloadResource/i.test(href)
+			) {
+				// drop the link
+				a.replaceWith(a.textContent ?? '');
+			} else {
+				let resolvedHref = _href(href, false);
+				if (markdown.baseUri) {
+					resolvedHref = resolveWithBaseUri(URI.from(markdown.baseUri), href);
+				}
+				a.dataset.href = resolvedHref;
+			}
+		});
+
 	element.innerHTML = sanitizeRenderedMarkdown(markdown, markdownHtmlDoc.body.innerHTML) as unknown as string;
 
 	// signal that async code blocks can be now be inserted


Test patch:
diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts
--- a/src/vs/base/test/browser/markdownRenderer.test.ts
+++ b/src/vs/base/test/browser/markdownRenderer.test.ts
@@ -148,7 +148,7 @@ suite('MarkdownRenderer', () => {
 			mds.appendMarkdown(`[$(zap)-link](#link)`);
 
 			let result: HTMLElement = renderMarkdown(mds).element;
-			assert.strictEqual(result.innerHTML, `<p><a href="" data-href="#link" title="#link"><span class="codicon codicon-zap"></span>-link</a></p>`);
+			assert.strictEqual(result.innerHTML, `<p><a data-href="#link" href="" title="#link"><span class="codicon codicon-zap"></span>-link</a></p>`);
 		});
 
 		test('render icon in table', () => {
@@ -168,7 +168,7 @@ suite('MarkdownRenderer', () => {
 </thead>
 <tbody><tr>
 <td><span class="codicon codicon-zap"></span></td>
-<td><a href="" data-href="#link" title="#link"><span class="codicon codicon-zap"></span>-link</a></td>
+<td><a data-href="#link" href="" title="#link"><span class="codicon codicon-zap"></span>-link</a></td>
 </tr>
 </tbody></table>
 `);
@@ -211,6 +211,25 @@ suite('MarkdownRenderer', () => {
 		assert.ok(data.documentUri.toString().startsWith('file:///c%3A/'));
 	});
 
+	test('Should not render command links by default', () => {
+		const md = new MarkdownString(`[command1](command:doFoo) <a href="command:doFoo">command2</a>`, {
+			supportHtml: true
+		});
+
+		const result: HTMLElement = renderMarkdown(md).element;
+		assert.strictEqual(result.innerHTML, `<p>command1 command2</p>`);
+	});
+
+	test('Should render command links in trusted strings', () => {
+		const md = new MarkdownString(`[command1](command:doFoo) <a href="command:doFoo">command2</a>`, {
+			isTrusted: true,
+			supportHtml: true,
+		});
+
+		const result: HTMLElement = renderMarkdown(md).element;
+		assert.strictEqual(result.innerHTML, `<p><a data-href="command:doFoo" href="" title="command:doFoo">command1</a> <a data-href="command:doFoo" href="">command2</a></p>`);
+	});
+
 	suite('PlaintextMarkdownRender', () => {
 
 		test('test code, blockquote, heading, list, listitem, paragraph, table, tablerow, tablecell, strong, em, br, del, text are rendered plaintext', () => {