/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import * as uri from 'vscode-uri'; import { ILogger } from '../logging'; import { MarkdownItEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { escapeAttribute, getNonce } from '../util/dom'; import { WebviewResourceProvider } from '../util/resources'; import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig'; import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security'; /** * Strings used inside the markdown preview. * * Stored here and then injected in the preview so that they * can be localized using our normal localization process. */ const previewStrings = { cspAlertMessageText: vscode.l10n.t("Some content has been disabled in this document"), cspAlertMessageTitle: 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"), cspAlertMessageLabel: vscode.l10n.t("Content Disabled Security Warning") }; export interface MarkdownContentProviderOutput { html: string; containingImages: Set; } export interface ImageInfo { readonly id: string; readonly width: number; readonly height: number; } export class MdDocumentRenderer { constructor( private readonly _engine: MarkdownItEngine, private readonly _context: vscode.ExtensionContext, private readonly _cspArbiter: ContentSecurityPolicyArbiter, private readonly _contributionProvider: MarkdownContributionProvider, private readonly _logger: ILogger ) { this.iconPath = { dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'), light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'), }; } public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri }; public async renderDocument( markdownDocument: vscode.TextDocument, resourceProvider: WebviewResourceProvider, previewConfigurations: MarkdownPreviewConfigurationManager, initialLine: number | undefined, selectedLine: number | undefined, state: any | undefined, imageInfo: readonly ImageInfo[], token: vscode.CancellationToken ): Promise { const sourceUri = markdownDocument.uri; const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { source: sourceUri.toString(), fragment: state?.fragment || markdownDocument.uri.fragment || undefined, line: initialLine, selectedLine, scrollPreviewWithEditor: config.scrollPreviewWithEditor, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor, disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(), webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(), }; this._logger.verbose('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData); // Content Security Policy const nonce = getNonce(); const csp = this._getCsp(resourceProvider, sourceUri, nonce); const body = await this.renderBody(markdownDocument, resourceProvider); if (token.isCancellationRequested) { return { html: '', containingImages: new Set() }; } const html = ` ${csp} ${this._getStyles(resourceProvider, sourceUri, config, imageInfo)} ${body.html} ${this._getScripts(resourceProvider, nonce)} `; return { html, containingImages: body.containingImages, }; } public async renderBody( markdownDocument: vscode.TextDocument, resourceProvider: WebviewResourceProvider, ): Promise { const rendered = await this._engine.render(markdownDocument, resourceProvider); const html = `
${rendered.html}
`; return { html, containingImages: rendered.containingImages }; } public renderFileNotFoundDocument(resource: vscode.Uri): string { const resourcePath = uri.Utils.basename(resource); const body = vscode.l10n.t('{0} cannot be found', resourcePath); return ` ${body} `; } private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string { const webviewResource = resourceProvider.asWebviewUri( vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile)); return webviewResource.toString(); } private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string { if (!href) { return href; } if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) { return href; } // Assume it must be a local file if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) { return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString(); } // Use a workspace relative path if there is a workspace const root = vscode.workspace.getWorkspaceFolder(resource); if (root) { return resourceProvider.asWebviewUri(vscode.Uri.joinPath(root.uri, href)).toString(); } // Otherwise look relative to the markdown file return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString(); } private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { if (!Array.isArray(config.styles)) { return ''; } const out: string[] = []; for (const style of config.styles) { out.push(``); } return out.join('\n'); } private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { return [ config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '', isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`, isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`, ].join(' '); } private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string { if (!imageInfo.length) { return ''; } let ret = '\n'; return ret; } private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string { const baseStyles: string[] = []; for (const resource of this._contributionProvider.contributions.previewStyles) { baseStyles.push(``); } return `${baseStyles.join('\n')} ${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)} ${this._getImageStabilizerStyles(imageInfo)}`; } private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string { const out: string[] = []; for (const resource of this._contributionProvider.contributions.previewScripts) { out.push(``); } return out.join('\n'); } private _getCsp( provider: WebviewResourceProvider, resource: vscode.Uri, nonce: string ): string { const rule = provider.cspSource; switch (this._cspArbiter.getSecurityLevelForResource(resource)) { case MarkdownPreviewSecurityLevel.AllowInsecureContent: return ``; case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent: return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: return ''; case MarkdownPreviewSecurityLevel.Strict: default: return ``; } } }