Path: blob/main/extensions/markdown-language-features/src/preview/documentRenderer.ts
5240 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 * as vscode from 'vscode';6import * as uri from 'vscode-uri';7import { ILogger } from '../logging';8import { MarkdownItEngine } from '../markdownEngine';9import { MarkdownContributionProvider } from '../markdownExtensions';10import { escapeAttribute } from '../util/dom';11import { WebviewResourceProvider } from '../util/resources';12import { generateUuid } from '../util/uuid';13import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';14import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';151617/**18* Strings used inside the markdown preview.19*20* Stored here and then injected in the preview so that they21* can be localized using our normal localization process.22*/23const previewStrings = {24cspAlertMessageText: vscode.l10n.t("Some content has been disabled in this document"),2526cspAlertMessageTitle: vscode.l10n.t("Potentially unsafe or insecure content has been disabled in the Markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts"),2728cspAlertMessageLabel: vscode.l10n.t("Content Disabled Security Warning")29};3031export interface MarkdownContentProviderOutput {32html: string;33containingImages: Set<string>;34}3536export interface ImageInfo {37readonly id: string;38readonly width: number;39readonly height: number;40}4142export class MdDocumentRenderer {43constructor(44private readonly _engine: MarkdownItEngine,45private readonly _context: vscode.ExtensionContext,46private readonly _cspArbiter: ContentSecurityPolicyArbiter,47private readonly _contributionProvider: MarkdownContributionProvider,48private readonly _logger: ILogger49) {50this.iconPath = {51dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'),52light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'),53};54}5556public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri };5758public async renderDocument(59markdownDocument: vscode.TextDocument,60resourceProvider: WebviewResourceProvider,61previewConfigurations: MarkdownPreviewConfigurationManager,62initialLine: number | undefined,63selectedLine: number | undefined,64state: any | undefined,65imageInfo: readonly ImageInfo[],66token: vscode.CancellationToken67): Promise<MarkdownContentProviderOutput> {68const sourceUri = markdownDocument.uri;69const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);70const initialData = {71source: sourceUri.toString(),72fragment: state?.fragment || markdownDocument.uri.fragment || undefined,73line: initialLine,74selectedLine,75scrollPreviewWithEditor: config.scrollPreviewWithEditor,76scrollEditorWithPreview: config.scrollEditorWithPreview,77doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,78disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(),79webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),80};8182this._logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);8384// Content Security Policy85const nonce = generateUuid();86const csp = this._getCsp(resourceProvider, sourceUri, nonce);8788const body = await this.renderBody(markdownDocument, resourceProvider);89if (token.isCancellationRequested) {90return { html: '', containingImages: new Set() };91}9293const html = `<!DOCTYPE html>94<html style="${escapeAttribute(this._getSettingsOverrideStyles(config))}">95<head>96<meta http-equiv="Content-type" content="text/html;charset=UTF-8">97<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(csp)}">98<meta id="vscode-markdown-preview-data"99data-settings="${escapeAttribute(JSON.stringify(initialData))}"100data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"101data-state="${escapeAttribute(JSON.stringify(state || {}))}"102data-initial-md-content="${escapeAttribute(body.html)}">103<script src="${this._extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>104${this._getStyles(resourceProvider, sourceUri, config, imageInfo)}105<base href="${resourceProvider.asWebviewUri(markdownDocument.uri)}">106</head>107<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">108${this._getScripts(resourceProvider, nonce)}109</body>110</html>`;111return {112html,113containingImages: body.containingImages,114};115}116117public async renderBody(118markdownDocument: vscode.TextDocument,119resourceProvider: WebviewResourceProvider,120): Promise<MarkdownContentProviderOutput> {121const rendered = await this._engine.render(markdownDocument, resourceProvider);122const html = `<div class="markdown-body" dir="auto">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`;123return {124html,125containingImages: rendered.containingImages126};127}128129public renderFileNotFoundDocument(resource: vscode.Uri): string {130const resourcePath = uri.Utils.basename(resource);131const body = vscode.l10n.t('{0} cannot be found', resourcePath);132return `<!DOCTYPE html>133<html>134<body class="vscode-body">135${body}136</body>137</html>`;138}139140private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {141const webviewResource = resourceProvider.asWebviewUri(142vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile));143return webviewResource.toString();144}145146private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {147if (!href) {148return href;149}150151if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) {152return href;153}154155// Assume it must be a local file156if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) {157return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString();158}159160// Use a workspace relative path if there is a workspace161const root = vscode.workspace.getWorkspaceFolder(resource);162if (root) {163return resourceProvider.asWebviewUri(vscode.Uri.joinPath(root.uri, href)).toString();164}165166// Otherwise look relative to the markdown file167return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();168}169170private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {171if (!Array.isArray(config.styles)) {172return '';173}174const out: string[] = [];175for (const style of config.styles) {176out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this._fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);177}178return out.join('\n');179}180181private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {182return [183config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '',184isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`,185isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`,186].join(' ');187}188189private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string {190if (!imageInfo.length) {191return '';192}193194let ret = '<style>\n';195for (const imgInfo of imageInfo) {196ret += `#${imgInfo.id}.loading {197height: ${imgInfo.height}px;198width: ${imgInfo.width}px;199}\n`;200}201ret += '</style>\n';202203return ret;204}205206private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string {207const baseStyles: string[] = [];208for (const resource of this._contributionProvider.contributions.previewStyles) {209baseStyles.push(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.asWebviewUri(resource))}">`);210}211212return `${baseStyles.join('\n')}213${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)}214${this._getImageStabilizerStyles(imageInfo)}`;215}216217private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {218const out: string[] = [];219for (const resource of this._contributionProvider.contributions.previewScripts) {220out.push(`<script async221src="${escapeAttribute(resourceProvider.asWebviewUri(resource))}"222nonce="${nonce}"223charset="UTF-8"></script>`);224}225return out.join('\n');226}227228private _getCsp(229provider: WebviewResourceProvider,230resource: vscode.Uri,231nonce: string232): string {233const rule = provider.cspSource.split(';')[0];234switch (this._cspArbiter.getSecurityLevelForResource(resource)) {235case MarkdownPreviewSecurityLevel.AllowInsecureContent:236return `default-src 'none'; img-src 'self' ${rule} http: https: data:; media-src 'self' ${rule} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' http: https: data:; font-src 'self' ${rule} http: https: data:;`;237238case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:239return `default-src 'none'; img-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' ${rule} https: data: http://localhost:* http://127.0.0.1:*;`;240241case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:242return ``;243244case MarkdownPreviewSecurityLevel.Strict:245default:246return `default-src 'none'; img-src 'self' ${rule} https: data:; media-src 'self' ${rule} https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${rule} 'unsafe-inline' https: data:; font-src 'self' ${rule} https: data:;`;247}248}249}250251252