Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatContentMarkdownRenderer.ts
4780 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 { IRenderedMarkdown, 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 { IMarkdownRenderer, IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
12
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
13
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
14
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
15
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
16
import product from '../../../../../platform/product/common/product.js';
17
18
export const allowedChatMarkdownHtmlTags = Object.freeze([
19
'b',
20
'blockquote',
21
'br',
22
'code',
23
'del',
24
'em',
25
'h1',
26
'h2',
27
'h3',
28
'h4',
29
'h5',
30
'h6',
31
'hr',
32
'i',
33
'ins',
34
'li',
35
'ol',
36
'p',
37
'pre',
38
's',
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 content.
62
*/
63
export class ChatContentMarkdownRenderer implements IMarkdownRenderer {
64
constructor(
65
@ILanguageService languageService: ILanguageService,
66
@IOpenerService openerService: IOpenerService,
67
@IConfigurationService configurationService: IConfigurationService,
68
@IHoverService private readonly hoverService: IHoverService,
69
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
70
) { }
71
72
render(markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IRenderedMarkdown {
73
options = {
74
...options,
75
sanitizerConfig: {
76
replaceWithPlaintext: true,
77
allowedTags: {
78
override: allowedChatMarkdownHtmlTags,
79
},
80
...options?.sanitizerConfig,
81
allowedLinkSchemes: { augment: [product.urlProtocol] },
82
remoteImageIsAllowed: (_uri) => false,
83
}
84
};
85
86
const mdWithBody: IMarkdownString = (markdown && markdown.supportHtml) ?
87
{
88
...markdown,
89
90
// dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this.
91
// The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown.
92
value: `<body>\n\n${markdown.value}</body>`,
93
}
94
: markdown;
95
const result = this.markdownRendererService.render(mdWithBody, options, outElement);
96
97
// In some cases, the renderer can return top level text nodes but our CSS expects
98
// all text to be in a <p> for margin to be applied properly.
99
// So just normalize it.
100
result.element.normalize();
101
for (const child of result.element.childNodes) {
102
if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {
103
child.replaceWith($('p', undefined, child.textContent));
104
}
105
}
106
return this.attachCustomHover(result);
107
}
108
109
private attachCustomHover(result: IRenderedMarkdown): IRenderedMarkdown {
110
const store = new DisposableStore();
111
// eslint-disable-next-line no-restricted-syntax
112
result.element.querySelectorAll('a').forEach((element) => {
113
if (element.title) {
114
const title = element.title;
115
element.title = '';
116
store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title));
117
}
118
});
119
120
return {
121
element: result.element,
122
dispose: () => {
123
result.dispose();
124
store.dispose();
125
}
126
};
127
}
128
}
129
130