/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import * as vscode from 'vscode'; import * as path from 'path'; import { MarkdownEngine } from './markdownEngine'; import * as nls from 'vscode-nls'; import { Logger } from './logger'; import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security'; const localize = nls.loadMessageBundle(); const previewStrings = { cspAlertMessageText: localize('preview.securityMessage.text', 'Some content has been disabled in this document'), cspAlertMessageTitle: localize('preview.securityMessage.title', '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: localize('preview.securityMessage.label', 'Content Disabled Security Warning') }; export function isMarkdownFile(document: vscode.TextDocument) { return document.languageId === 'markdown' && document.uri.scheme !== 'markdown'; // prevent processing of own documents } export function getMarkdownUri(uri: vscode.Uri) { if (uri.scheme === 'markdown') { return uri; } return uri.with({ scheme: 'markdown', path: uri.path + '.rendered', query: uri.toString() }); } class MarkdownPreviewConfig { public static getConfigForResource(resource: vscode.Uri) { return new MarkdownPreviewConfig(resource); } public readonly scrollBeyondLastLine: boolean; public readonly wordWrap: boolean; public readonly previewFrontMatter: string; public readonly lineBreaks: boolean; public readonly doubleClickToSwitchToEditor: boolean; public readonly scrollEditorWithPreview: boolean; public readonly scrollPreviewWithEditorSelection: boolean; public readonly markEditorSelection: boolean; public readonly lineHeight: number; public readonly fontSize: number; public readonly fontFamily: string | undefined; public readonly styles: string[]; private constructor(resource: vscode.Uri) { const editorConfig = vscode.workspace.getConfiguration('editor', resource); const markdownConfig = vscode.workspace.getConfiguration('markdown', resource); const markdownEditorConfig = vscode.workspace.getConfiguration('[markdown]'); this.scrollBeyondLastLine = editorConfig.get('scrollBeyondLastLine', false); this.wordWrap = editorConfig.get('wordWrap', 'off') !== 'off'; if (markdownEditorConfig && markdownEditorConfig['editor.wordWrap']) { this.wordWrap = markdownEditorConfig['editor.wordWrap'] !== 'off'; } this.previewFrontMatter = markdownConfig.get('previewFrontMatter', 'hide'); this.scrollPreviewWithEditorSelection = !!markdownConfig.get('preview.scrollPreviewWithEditorSelection', true); this.scrollEditorWithPreview = !!markdownConfig.get('preview.scrollEditorWithPreview', true); this.lineBreaks = !!markdownConfig.get('preview.breaks', false); this.doubleClickToSwitchToEditor = !!markdownConfig.get('preview.doubleClickToSwitchToEditor', true); this.markEditorSelection = !!markdownConfig.get('preview.markEditorSelection', true); this.fontFamily = markdownConfig.get('preview.fontFamily', undefined); this.fontSize = Math.max(8, +markdownConfig.get('preview.fontSize', NaN)); this.lineHeight = Math.max(0.6, +markdownConfig.get('preview.lineHeight', NaN)); this.styles = markdownConfig.get('styles', []); } public isEqualTo(otherConfig: MarkdownPreviewConfig) { for (let key in this) { if (this.hasOwnProperty(key) && key !== 'styles') { if (this[key] !== otherConfig[key]) { return false; } } } // Check styles if (this.styles.length !== otherConfig.styles.length) { return false; } for (let i = 0; i < this.styles.length; ++i) { if (this.styles[i] !== otherConfig.styles[i]) { return false; } } return true; } [key: string]: any; } class PreviewConfigManager { private previewConfigurationsForWorkspaces = new Map(); public loadAndCacheConfiguration( resource: vscode.Uri ) { const config = MarkdownPreviewConfig.getConfigForResource(resource); this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config); return config; } public shouldUpdateConfiguration( resource: vscode.Uri ): boolean { const key = this.getKey(resource); const currentConfig = this.previewConfigurationsForWorkspaces.get(key); const newConfig = MarkdownPreviewConfig.getConfigForResource(resource); return (!currentConfig || !currentConfig.isEqualTo(newConfig)); } private getKey( resource: vscode.Uri ): string { const folder = vscode.workspace.getWorkspaceFolder(resource); if (!folder) { return ''; } return folder.uri.toString(); } } export class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { private _onDidChange = new vscode.EventEmitter(); private _waiting: boolean = false; private previewConfigurations = new PreviewConfigManager(); private extraStyles: Array = []; private extraScripts: Array = []; constructor( private engine: MarkdownEngine, private context: vscode.ExtensionContext, private cspArbiter: ContentSecurityPolicyArbiter, private logger: Logger ) { } public addScript(resource: vscode.Uri): void { this.extraScripts.push(resource); } public addStyle(resource: vscode.Uri): void { this.extraStyles.push(resource); } private getMediaPath(mediaFile: string): string { return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))).toString(); } private fixHref(resource: vscode.Uri, href: string): string { if (!href) { return href; } // Use href if it is already an URL const hrefUri = vscode.Uri.parse(href); if (['file', 'http', 'https'].indexOf(hrefUri.scheme) >= 0) { return hrefUri.toString(); } // Use href as file URI if it is absolute if (path.isAbsolute(href)) { return vscode.Uri.file(href).toString(); } // use a workspace relative path if there is a workspace let root = vscode.workspace.getWorkspaceFolder(resource); if (root) { return vscode.Uri.file(path.join(root.uri.fsPath, href)).toString(); } // otherwise look relative to the markdown file return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href)).toString(); } private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfig): string { if (config.styles && Array.isArray(config.styles)) { return config.styles.map(style => { return ``; }).join('\n'); } return ''; } private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfig): string { return ``; } private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfig): string { const baseStyles = [ this.getMediaPath('markdown.css'), this.getMediaPath('tomorrow.css') ].concat(this.extraStyles.map(resource => resource.toString())); return `${baseStyles.map(href => ``).join('\n')} ${this.getSettingsOverrideStyles(nonce, config)} ${this.computeCustomStyleSheetIncludes(resource, config)}`; } private getScripts(nonce: string): string { const scripts = [this.getMediaPath('main.js')].concat(this.extraScripts.map(resource => resource.toString())); return scripts .map(source => ``) .join('\n'); } public async provideTextDocumentContent(uri: vscode.Uri): Promise { const sourceUri = vscode.Uri.parse(uri.query); let initialLine: number | undefined = undefined; const editor = vscode.window.activeTextEditor; if (editor && editor.document.uri.toString() === sourceUri.toString()) { initialLine = editor.selection.active.line; } const document = await vscode.workspace.openTextDocument(sourceUri); const config = this.previewConfigurations.loadAndCacheConfiguration(sourceUri); const initialData = { previewUri: uri.toString(), source: sourceUri.toString(), line: initialLine, scrollPreviewWithEditorSelection: config.scrollPreviewWithEditorSelection, scrollEditorWithPreview: config.scrollEditorWithPreview, doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor }; this.logger.log('provideTextDocumentContent', initialData); // Content Security Policy const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCspForResource(sourceUri, nonce); const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', document.getText()); return ` ${csp} ${this.getStyles(sourceUri, nonce, config)} ${body}
${this.getScripts(nonce)} `; } public updateConfiguration() { // update all generated md documents for (const document of vscode.workspace.textDocuments) { if (document.uri.scheme === 'markdown') { const sourceUri = vscode.Uri.parse(document.uri.query); if (this.previewConfigurations.shouldUpdateConfiguration(sourceUri)) { this.update(document.uri); } } } } get onDidChange(): vscode.Event { return this._onDidChange.event; } public update(uri: vscode.Uri) { if (!this._waiting) { this._waiting = true; setTimeout(() => { this._waiting = false; this._onDidChange.fire(uri); }, 300); } } private getCspForResource(resource: vscode.Uri, nonce: string): string { switch (this.cspArbiter.getSecurityLevelForResource(resource)) { case MarkdownPreviewSecurityLevel.AllowInsecureContent: return ``; case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent: return ''; case MarkdownPreviewSecurityLevel.Strict: default: return ``; } } }