Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { $ } from '../../../../base/browser/dom.js';
7
import { MarkdownRenderOptions } from '../../../../base/browser/markdownRenderer.js';
8
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
10
import { DisposableStore } from '../../../../base/common/lifecycle.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
13
import { ILanguageService } from '../../../../editor/common/languages/language.js';
14
import { ICommandService } from '../../../../platform/commands/common/commands.js';
15
import { IFileService } from '../../../../platform/files/common/files.js';
16
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
17
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
18
import product from '../../../../platform/product/common/product.js';
19
import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../files/browser/fileConstants.js';
20
21
export const allowedChatMarkdownHtmlTags = Object.freeze([
22
'b',
23
'blockquote',
24
'br',
25
'code',
26
'em',
27
'h1',
28
'h2',
29
'h3',
30
'h4',
31
'h5',
32
'h6',
33
'hr',
34
'i',
35
'li',
36
'ol',
37
'p',
38
'pre',
39
'strong',
40
'sub',
41
'sup',
42
'table',
43
'tbody',
44
'td',
45
'th',
46
'thead',
47
'tr',
48
'ul',
49
'a',
50
'img',
51
52
// TODO@roblourens when we sanitize attributes in markdown source, we can ban these elements at that step. microsoft/vscode-copilot#5091
53
// Not in the official list, but used for codicons and other vscode markdown extensions
54
'span',
55
'div',
56
57
'input', // Allowed for rendering checkboxes. Other types of inputs are removed and the inputs are always disabled
58
]);
59
60
/**
61
* This wraps the MarkdownRenderer and applies sanitizer options needed for Chat.
62
*/
63
export class ChatMarkdownRenderer extends MarkdownRenderer {
64
constructor(
65
options: IMarkdownRendererOptions | undefined,
66
@ILanguageService languageService: ILanguageService,
67
@IOpenerService openerService: IOpenerService,
68
@IHoverService private readonly hoverService: IHoverService,
69
@IFileService private readonly fileService: IFileService,
70
@ICommandService private readonly commandService: ICommandService,
71
) {
72
super(options ?? {}, languageService, openerService);
73
}
74
75
override render(markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IMarkdownRenderResult {
76
options = {
77
...options,
78
sanitizerConfig: {
79
replaceWithPlaintext: true,
80
allowedTags: {
81
override: allowedChatMarkdownHtmlTags,
82
},
83
...options?.sanitizerConfig,
84
allowedLinkSchemes: { augment: [product.urlProtocol] },
85
remoteImageIsAllowed: (_uri) => false,
86
}
87
};
88
89
const mdWithBody: IMarkdownString = (markdown && markdown.supportHtml) ?
90
{
91
...markdown,
92
93
// dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this.
94
// The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown.
95
value: `<body>\n\n${markdown.value}</body>`,
96
}
97
: markdown;
98
const result = super.render(mdWithBody, options, outElement);
99
100
// In some cases, the renderer can return top level text nodes but our CSS expects
101
// all text to be in a <p> for margin to be applied properly.
102
// So just normalize it.
103
result.element.normalize();
104
for (const child of result.element.childNodes) {
105
if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {
106
child.replaceWith($('p', undefined, child.textContent));
107
}
108
}
109
return this.attachCustomHover(result);
110
}
111
112
private attachCustomHover(result: IMarkdownRenderResult): IMarkdownRenderResult {
113
const store = new DisposableStore();
114
result.element.querySelectorAll('a').forEach((element) => {
115
if (element.title) {
116
const title = element.title;
117
element.title = '';
118
store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title));
119
}
120
});
121
122
return {
123
element: result.element,
124
dispose: () => {
125
result.dispose();
126
store.dispose();
127
}
128
};
129
}
130
131
protected override async openMarkdownLink(link: string, markdown: IMarkdownString) {
132
try {
133
const uri = URI.parse(link);
134
if ((await this.fileService.stat(uri)).isDirectory) {
135
return this.commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, uri);
136
}
137
} catch {
138
// noop
139
}
140
141
return super.openMarkdownLink(link, markdown);
142
}
143
}
144
145