Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatContentMarkdownRenderer.ts
4780 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $ } from '../../../../../base/browser/dom.js';6import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../../base/browser/markdownRenderer.js';7import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';8import { IMarkdownString } from '../../../../../base/common/htmlContent.js';9import { DisposableStore } from '../../../../../base/common/lifecycle.js';10import { IMarkdownRenderer, IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';11import { ILanguageService } from '../../../../../editor/common/languages/language.js';12import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';13import { IHoverService } from '../../../../../platform/hover/browser/hover.js';14import { IOpenerService } from '../../../../../platform/opener/common/opener.js';15import product from '../../../../../platform/product/common/product.js';1617export const allowedChatMarkdownHtmlTags = Object.freeze([18'b',19'blockquote',20'br',21'code',22'del',23'em',24'h1',25'h2',26'h3',27'h4',28'h5',29'h6',30'hr',31'i',32'ins',33'li',34'ol',35'p',36'pre',37's',38'strong',39'sub',40'sup',41'table',42'tbody',43'td',44'th',45'thead',46'tr',47'ul',48'a',49'img',5051// TODO@roblourens when we sanitize attributes in markdown source, we can ban these elements at that step. microsoft/vscode-copilot#509152// Not in the official list, but used for codicons and other vscode markdown extensions53'span',54'div',5556'input', // Allowed for rendering checkboxes. Other types of inputs are removed and the inputs are always disabled57]);5859/**60* This wraps the MarkdownRenderer and applies sanitizer options needed for chat content.61*/62export class ChatContentMarkdownRenderer implements IMarkdownRenderer {63constructor(64@ILanguageService languageService: ILanguageService,65@IOpenerService openerService: IOpenerService,66@IConfigurationService configurationService: IConfigurationService,67@IHoverService private readonly hoverService: IHoverService,68@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,69) { }7071render(markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IRenderedMarkdown {72options = {73...options,74sanitizerConfig: {75replaceWithPlaintext: true,76allowedTags: {77override: allowedChatMarkdownHtmlTags,78},79...options?.sanitizerConfig,80allowedLinkSchemes: { augment: [product.urlProtocol] },81remoteImageIsAllowed: (_uri) => false,82}83};8485const mdWithBody: IMarkdownString = (markdown && markdown.supportHtml) ?86{87...markdown,8889// dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this.90// The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown.91value: `<body>\n\n${markdown.value}</body>`,92}93: markdown;94const result = this.markdownRendererService.render(mdWithBody, options, outElement);9596// In some cases, the renderer can return top level text nodes but our CSS expects97// all text to be in a <p> for margin to be applied properly.98// So just normalize it.99result.element.normalize();100for (const child of result.element.childNodes) {101if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {102child.replaceWith($('p', undefined, child.textContent));103}104}105return this.attachCustomHover(result);106}107108private attachCustomHover(result: IRenderedMarkdown): IRenderedMarkdown {109const store = new DisposableStore();110// eslint-disable-next-line no-restricted-syntax111result.element.querySelectorAll('a').forEach((element) => {112if (element.title) {113const title = element.title;114element.title = '';115store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title));116}117});118119return {120element: result.element,121dispose: () => {122result.dispose();123store.dispose();124}125};126}127}128129130