Path: blob/main/src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts
3296 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 { 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 { URI } from '../../../../base/common/uri.js';11import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';12import { ILanguageService } from '../../../../editor/common/languages/language.js';13import { ICommandService } from '../../../../platform/commands/common/commands.js';14import { IFileService } from '../../../../platform/files/common/files.js';15import { IHoverService } from '../../../../platform/hover/browser/hover.js';16import { IOpenerService } from '../../../../platform/opener/common/opener.js';17import product from '../../../../platform/product/common/product.js';18import { REVEAL_IN_EXPLORER_COMMAND_ID } from '../../files/browser/fileConstants.js';1920export const allowedChatMarkdownHtmlTags = Object.freeze([21'b',22'blockquote',23'br',24'code',25'em',26'h1',27'h2',28'h3',29'h4',30'h5',31'h6',32'hr',33'i',34'li',35'ol',36'p',37'pre',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.61*/62export class ChatMarkdownRenderer extends MarkdownRenderer {63constructor(64options: IMarkdownRendererOptions | undefined,65@ILanguageService languageService: ILanguageService,66@IOpenerService openerService: IOpenerService,67@IHoverService private readonly hoverService: IHoverService,68@IFileService private readonly fileService: IFileService,69@ICommandService private readonly commandService: ICommandService,70) {71super(options ?? {}, languageService, openerService);72}7374override render(markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IMarkdownRenderResult {75options = {76...options,77sanitizerConfig: {78replaceWithPlaintext: true,79allowedTags: {80override: allowedChatMarkdownHtmlTags,81},82...options?.sanitizerConfig,83allowedLinkSchemes: { augment: [product.urlProtocol] },84remoteImageIsAllowed: (_uri) => false,85}86};8788const mdWithBody: IMarkdownString = (markdown && markdown.supportHtml) ?89{90...markdown,9192// dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this.93// The \n\n prevents marked.js from parsing the body contents as just text in an 'html' token, instead of actual markdown.94value: `<body>\n\n${markdown.value}</body>`,95}96: markdown;97const result = super.render(mdWithBody, options, outElement);9899// In some cases, the renderer can return top level text nodes but our CSS expects100// all text to be in a <p> for margin to be applied properly.101// So just normalize it.102result.element.normalize();103for (const child of result.element.childNodes) {104if (child.nodeType === Node.TEXT_NODE && child.textContent?.trim()) {105child.replaceWith($('p', undefined, child.textContent));106}107}108return this.attachCustomHover(result);109}110111private attachCustomHover(result: IMarkdownRenderResult): IMarkdownRenderResult {112const store = new DisposableStore();113result.element.querySelectorAll('a').forEach((element) => {114if (element.title) {115const title = element.title;116element.title = '';117store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title));118}119});120121return {122element: result.element,123dispose: () => {124result.dispose();125store.dispose();126}127};128}129130protected override async openMarkdownLink(link: string, markdown: IMarkdownString) {131try {132const uri = URI.parse(link);133if ((await this.fileService.stat(uri)).isDirectory) {134return this.commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, uri);135}136} catch {137// noop138}139140return super.openMarkdownLink(link, markdown);141}142}143144145