|
|
|
|
@@ -3,18 +3,17 @@
|
|
|
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
|
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
|
|
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import { MarkdownEngine } from '../markdownEngine';
|
|
|
|
|
|
|
|
|
|
import * as vscode from 'vscode';
|
|
|
|
|
import * as nls from 'vscode-nls';
|
|
|
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
|
|
|
|
|
|
import { Logger } from '../logger';
|
|
|
|
|
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
|
|
|
|
import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig';
|
|
|
|
|
import { MarkdownEngine } from '../markdownEngine';
|
|
|
|
|
import { MarkdownContributionProvider } from '../markdownExtensions';
|
|
|
|
|
import { toResoruceUri } from '../util/resources';
|
|
|
|
|
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
|
|
|
|
import { WebviewResourceProvider } from '../util/resources';
|
|
|
|
|
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
|
|
|
|
|
|
|
|
|
|
const localize = nls.loadMessageBundle();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Strings used inside the markdown preview.
|
|
|
|
|
@@ -36,8 +35,8 @@ const previewStrings = {
|
|
|
|
|
'Content Disabled Security Warning')
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function escapeAttribute(value: string): string {
|
|
|
|
|
return value.replace(/"/g, '"');
|
|
|
|
|
function escapeAttribute(value: string | vscode.Uri): string {
|
|
|
|
|
return value.toString().replace(/"/g, '"');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class MarkdownContentProvider {
|
|
|
|
|
@@ -51,7 +50,7 @@ export class MarkdownContentProvider {
|
|
|
|
|
|
|
|
|
|
public async provideTextDocumentContent(
|
|
|
|
|
markdownDocument: vscode.TextDocument,
|
|
|
|
|
webviewResourceRoot: string,
|
|
|
|
|
resourceProvider: WebviewResourceProvider,
|
|
|
|
|
previewConfigurations: MarkdownPreviewConfigurationManager,
|
|
|
|
|
initialLine: number | undefined = undefined,
|
|
|
|
|
state?: any
|
|
|
|
|
@@ -66,18 +65,18 @@ export class MarkdownContentProvider {
|
|
|
|
|
scrollEditorWithPreview: config.scrollEditorWithPreview,
|
|
|
|
|
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
|
|
|
|
|
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings(),
|
|
|
|
|
webviewResourceRoot: webviewResourceRoot,
|
|
|
|
|
webviewResourceRoot: resourceProvider.toWebviewResource(markdownDocument.uri).toString(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.logger.log('provideTextDocumentContent', initialData);
|
|
|
|
|
|
|
|
|
|
// Content Security Policy
|
|
|
|
|
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
|
|
|
|
|
const csp = this.getCspForResource(webviewResourceRoot, sourceUri, nonce);
|
|
|
|
|
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
|
|
|
|
|
|
|
|
|
|
const body = await this.engine.render(markdownDocument);
|
|
|
|
|
return `<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
|
|
|
|
|
<head>
|
|
|
|
|
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
|
|
|
|
${csp}
|
|
|
|
|
@@ -85,14 +84,14 @@ export class MarkdownContentProvider {
|
|
|
|
|
data-settings="${escapeAttribute(JSON.stringify(initialData))}"
|
|
|
|
|
data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
|
|
|
|
|
data-state="${escapeAttribute(JSON.stringify(state || {}))}">
|
|
|
|
|
<script src="${this.extensionResourcePath(webviewResourceRoot, 'pre.js')}" nonce="${nonce}"></script>
|
|
|
|
|
${this.getStyles(webviewResourceRoot, sourceUri, nonce, config, state)}
|
|
|
|
|
<base href="${toResoruceUri(webviewResourceRoot, markdownDocument.uri)}">
|
|
|
|
|
<script src="${this.extensionResourcePath(resourceProvider, 'pre.js')}" nonce="${nonce}"></script>
|
|
|
|
|
${this.getStyles(resourceProvider, sourceUri, config, state)}
|
|
|
|
|
<base href="${resourceProvider.toWebviewResource(markdownDocument.uri)}">
|
|
|
|
|
</head>
|
|
|
|
|
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
|
|
|
|
${body}
|
|
|
|
|
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
|
|
|
|
|
${this.getScripts(webviewResourceRoot, nonce)}
|
|
|
|
|
${this.getScripts(resourceProvider, nonce)}
|
|
|
|
|
</body>
|
|
|
|
|
</html>`;
|
|
|
|
|
}
|
|
|
|
|
@@ -110,12 +109,13 @@ export class MarkdownContentProvider {
|
|
|
|
|
</html>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extensionResourcePath(webviewResourceRoot: string, mediaFile: string): string {
|
|
|
|
|
return toResoruceUri(webviewResourceRoot, vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))))
|
|
|
|
|
.toString();
|
|
|
|
|
private extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
|
|
|
|
|
const webviewResource = resourceProvider.toWebviewResource(
|
|
|
|
|
vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile))));
|
|
|
|
|
return webviewResource.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fixHref(webviewResourceRoot: string, resource: vscode.Uri, href: string): string {
|
|
|
|
|
private fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
|
|
|
|
|
if (!href) {
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
@@ -126,36 +126,36 @@ export class MarkdownContentProvider {
|
|
|
|
|
|
|
|
|
|
// Assume it must be a local file
|
|
|
|
|
if (path.isAbsolute(href)) {
|
|
|
|
|
return toResoruceUri(webviewResourceRoot, vscode.Uri.file(href)).toString();
|
|
|
|
|
return resourceProvider.toWebviewResource(vscode.Uri.file(href)).toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Use a workspace relative path if there is a workspace
|
|
|
|
|
const root = vscode.workspace.getWorkspaceFolder(resource);
|
|
|
|
|
if (root) {
|
|
|
|
|
return toResoruceUri(webviewResourceRoot, vscode.Uri.file(path.join(root.uri.fsPath, href))).toString();
|
|
|
|
|
return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(root.uri.fsPath, href))).toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise look relative to the markdown file
|
|
|
|
|
return toResoruceUri(webviewResourceRoot, vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString();
|
|
|
|
|
return resourceProvider.toWebviewResource(vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))).toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private computeCustomStyleSheetIncludes(webviewResourceRoot: string, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
|
|
|
|
|
if (Array.isArray(config.styles)) {
|
|
|
|
|
return config.styles.map(style => {
|
|
|
|
|
return `<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this.fixHref(webviewResourceRoot, resource, style))}" type="text/css" media="screen">`;
|
|
|
|
|
}).join('\n');
|
|
|
|
|
private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
|
|
|
|
|
if (!Array.isArray(config.styles)) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
const out: string[] = [];
|
|
|
|
|
for (const style of config.styles) {
|
|
|
|
|
out.push(`<link rel="stylesheet" class="code-user-style" data-source="${escapeAttribute(style)}" href="${escapeAttribute(this.fixHref(resourceProvider, resource, style))}" type="text/css" media="screen">`);
|
|
|
|
|
}
|
|
|
|
|
return out.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfiguration): string {
|
|
|
|
|
return `<style nonce="${nonce}">
|
|
|
|
|
html, body {
|
|
|
|
|
${config.fontFamily ? `font-family: ${config.fontFamily};` : ''}
|
|
|
|
|
${isNaN(config.fontSize) ? '' : `font-size: ${config.fontSize}px;`}
|
|
|
|
|
${isNaN(config.lineHeight) ? '' : `line-height: ${config.lineHeight};`}
|
|
|
|
|
}
|
|
|
|
|
</style>`;
|
|
|
|
|
private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
|
|
|
|
|
return [
|
|
|
|
|
config.fontFamily ? `--vscode-markdown-font-family: ${config.fontFamily};` : '',
|
|
|
|
|
isNaN(config.fontSize) ? '' : `--vscode-markdown-font-size: ${config.fontSize}px;`,
|
|
|
|
|
isNaN(config.lineHeight) ? '' : `--vscode-markdown-line-height: ${config.lineHeight};`,
|
|
|
|
|
].join(' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getImageStabilizerStyles(state?: any) {
|
|
|
|
|
@@ -173,41 +173,47 @@ export class MarkdownContentProvider {
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getStyles(webviewResourceRoot: string, resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration, state?: any): string {
|
|
|
|
|
const baseStyles = this.contributionProvider.contributions.previewStyles
|
|
|
|
|
.map(resource => `<link rel="stylesheet" type="text/css" href="${escapeAttribute(toResoruceUri(webviewResourceRoot, resource).toString())}">`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
private getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, state?: any): string {
|
|
|
|
|
const baseStyles: string[] = [];
|
|
|
|
|
for (const resource of this.contributionProvider.contributions.previewStyles) {
|
|
|
|
|
baseStyles.push(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.toWebviewResource(resource))}">`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${baseStyles}
|
|
|
|
|
${this.getSettingsOverrideStyles(nonce, config)}
|
|
|
|
|
${this.computeCustomStyleSheetIncludes(webviewResourceRoot, resource, config)}
|
|
|
|
|
return `${baseStyles.join('\n')}
|
|
|
|
|
${this.computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
|
|
|
|
|
${this.getImageStabilizerStyles(state)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getScripts(resourceRoot: string, nonce: string): string {
|
|
|
|
|
return this.contributionProvider.contributions.previewScripts
|
|
|
|
|
.map(resource => `<script async src="${escapeAttribute(toResoruceUri(resourceRoot, resource).toString())}" nonce="${nonce}" charset="UTF-8"></script>`)
|
|
|
|
|
.join('\n');
|
|
|
|
|
private getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
|
|
|
|
|
const out: string[] = [];
|
|
|
|
|
for (const resource of this.contributionProvider.contributions.previewScripts) {
|
|
|
|
|
out.push(`<script async
|
|
|
|
|
src="${escapeAttribute(resourceProvider.toWebviewResource(resource))}"
|
|
|
|
|
nonce="${nonce}"
|
|
|
|
|
charset="UTF-8"></script>`);
|
|
|
|
|
}
|
|
|
|
|
return out.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getCspForResource(
|
|
|
|
|
webviewResourceRoot: string,
|
|
|
|
|
private getCsp(
|
|
|
|
|
provider: WebviewResourceProvider,
|
|
|
|
|
resource: vscode.Uri,
|
|
|
|
|
nonce: string
|
|
|
|
|
): string {
|
|
|
|
|
const rule = provider.cspRule;
|
|
|
|
|
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
|
|
|
|
|
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${webviewResourceRoot} http: https: data:; media-src 'self' ${webviewResourceRoot} http: https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${webviewResourceRoot} 'unsafe-inline' http: https: data:; font-src 'self' ${webviewResourceRoot} http: https: data:;">`;
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="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:;">`;
|
|
|
|
|
|
|
|
|
|
case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*; media-src 'self' ${webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src 'self' ${webviewResourceRoot} 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src 'self' ${webviewResourceRoot} https: data: http://localhost:* http://127.0.0.1:*;">`;
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="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:*;">`;
|
|
|
|
|
|
|
|
|
|
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
|
|
|
|
|
return '';
|
|
|
|
|
|
|
|
|
|
case MarkdownPreviewSecurityLevel.Strict:
|
|
|
|
|
default:
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' ${webviewResourceRoot} https: data:; media-src 'self' ${webviewResourceRoot} https: data:; script-src 'nonce-${nonce}'; style-src 'self' ${webviewResourceRoot} 'unsafe-inline' https: data:; font-src 'self' ${webviewResourceRoot} https: data:;">`;
|
|
|
|
|
return `<meta http-equiv="Content-Security-Policy" content="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:;">`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|