Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/src/preview/documentRenderer.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as vscode from 'vscode';
7
import * as uri from 'vscode-uri';
8
import { ILogger } from '../logging';
9
import { MarkdownItEngine } from '../markdownEngine';
10
import { MarkdownContributionProvider } from '../markdownExtensions';
11
import { escapeAttribute, getNonce } from '../util/dom';
12
import { WebviewResourceProvider } from '../util/resources';
13
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
14
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
15
16
17
/**
18
* Strings used inside the markdown preview.
19
*
20
* Stored here and then injected in the preview so that they
21
* can be localized using our normal localization process.
22
*/
23
const previewStrings = {
24
cspAlertMessageText: vscode.l10n.t("Some content has been disabled in this document"),
25
26
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"),
27
28
cspAlertMessageLabel: vscode.l10n.t("Content Disabled Security Warning")
29
};
30
31
export interface MarkdownContentProviderOutput {
32
html: string;
33
containingImages: Set<string>;
34
}
35
36
export interface ImageInfo {
37
readonly id: string;
38
readonly width: number;
39
readonly height: number;
40
}
41
42
export class MdDocumentRenderer {
43
constructor(
44
private readonly _engine: MarkdownItEngine,
45
private readonly _context: vscode.ExtensionContext,
46
private readonly _cspArbiter: ContentSecurityPolicyArbiter,
47
private readonly _contributionProvider: MarkdownContributionProvider,
48
private readonly _logger: ILogger
49
) {
50
this.iconPath = {
51
dark: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-dark.svg'),
52
light: vscode.Uri.joinPath(this._context.extensionUri, 'media', 'preview-light.svg'),
53
};
54
}
55
56
public readonly iconPath: { light: vscode.Uri; dark: vscode.Uri };
57
58
public async renderDocument(
59
markdownDocument: vscode.TextDocument,
60
resourceProvider: WebviewResourceProvider,
61
previewConfigurations: MarkdownPreviewConfigurationManager,
62
initialLine: number | undefined,
63
selectedLine: number | undefined,
64
state: any | undefined,
65
imageInfo: readonly ImageInfo[],
66
token: vscode.CancellationToken
67
): Promise<MarkdownContentProviderOutput> {
68
const sourceUri = markdownDocument.uri;
69
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
70
const initialData = {
71
source: sourceUri.toString(),
72
fragment: state?.fragment || markdownDocument.uri.fragment || undefined,
73
line: initialLine,
74
selectedLine,
75
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
76
scrollEditorWithPreview: config.scrollEditorWithPreview,
77
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
78
disableSecurityWarnings: this._cspArbiter.shouldDisableSecurityWarnings(),
79
webviewResourceRoot: resourceProvider.asWebviewUri(markdownDocument.uri).toString(),
80
};
81
82
this._logger.trace('DocumentRenderer', `provideTextDocumentContent - ${markdownDocument.uri}`, initialData);
83
84
// Content Security Policy
85
const nonce = getNonce();
86
const csp = this._getCsp(resourceProvider, sourceUri, nonce);
87
88
const body = await this.renderBody(markdownDocument, resourceProvider);
89
if (token.isCancellationRequested) {
90
return { html: '', containingImages: new Set() };
91
}
92
93
const 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"
99
data-settings="${escapeAttribute(JSON.stringify(initialData))}"
100
data-strings="${escapeAttribute(JSON.stringify(previewStrings))}"
101
data-state="${escapeAttribute(JSON.stringify(state || {}))}"
102
data-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>`;
111
return {
112
html,
113
containingImages: body.containingImages,
114
};
115
}
116
117
public async renderBody(
118
markdownDocument: vscode.TextDocument,
119
resourceProvider: WebviewResourceProvider,
120
): Promise<MarkdownContentProviderOutput> {
121
const rendered = await this._engine.render(markdownDocument, resourceProvider);
122
const html = `<div class="markdown-body" dir="auto">${rendered.html}<div class="code-line" data-line="${markdownDocument.lineCount}"></div></div>`;
123
return {
124
html,
125
containingImages: rendered.containingImages
126
};
127
}
128
129
public renderFileNotFoundDocument(resource: vscode.Uri): string {
130
const resourcePath = uri.Utils.basename(resource);
131
const body = vscode.l10n.t('{0} cannot be found', resourcePath);
132
return `<!DOCTYPE html>
133
<html>
134
<body class="vscode-body">
135
${body}
136
</body>
137
</html>`;
138
}
139
140
private _extensionResourcePath(resourceProvider: WebviewResourceProvider, mediaFile: string): string {
141
const webviewResource = resourceProvider.asWebviewUri(
142
vscode.Uri.joinPath(this._context.extensionUri, 'media', mediaFile));
143
return webviewResource.toString();
144
}
145
146
private _fixHref(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, href: string): string {
147
if (!href) {
148
return href;
149
}
150
151
if (href.startsWith('http:') || href.startsWith('https:') || href.startsWith('file:')) {
152
return href;
153
}
154
155
// Assume it must be a local file
156
if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) {
157
return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString();
158
}
159
160
// Use a workspace relative path if there is a workspace
161
const root = vscode.workspace.getWorkspaceFolder(resource);
162
if (root) {
163
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(root.uri, href)).toString();
164
}
165
166
// Otherwise look relative to the markdown file
167
return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString();
168
}
169
170
private _computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
171
if (!Array.isArray(config.styles)) {
172
return '';
173
}
174
const out: string[] = [];
175
for (const style of config.styles) {
176
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">`);
177
}
178
return out.join('\n');
179
}
180
181
private _getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string {
182
return [
183
config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '',
184
isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`,
185
isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`,
186
].join(' ');
187
}
188
189
private _getImageStabilizerStyles(imageInfo: readonly ImageInfo[]): string {
190
if (!imageInfo.length) {
191
return '';
192
}
193
194
let ret = '<style>\n';
195
for (const imgInfo of imageInfo) {
196
ret += `#${imgInfo.id}.loading {
197
height: ${imgInfo.height}px;
198
width: ${imgInfo.width}px;
199
}\n`;
200
}
201
ret += '</style>\n';
202
203
return ret;
204
}
205
206
private _getStyles(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration, imageInfo: readonly ImageInfo[]): string {
207
const baseStyles: string[] = [];
208
for (const resource of this._contributionProvider.contributions.previewStyles) {
209
baseStyles.push(`<link rel="stylesheet" type="text/css" href="${escapeAttribute(resourceProvider.asWebviewUri(resource))}">`);
210
}
211
212
return `${baseStyles.join('\n')}
213
${this._computeCustomStyleSheetIncludes(resourceProvider, resource, config)}
214
${this._getImageStabilizerStyles(imageInfo)}`;
215
}
216
217
private _getScripts(resourceProvider: WebviewResourceProvider, nonce: string): string {
218
const out: string[] = [];
219
for (const resource of this._contributionProvider.contributions.previewScripts) {
220
out.push(`<script async
221
src="${escapeAttribute(resourceProvider.asWebviewUri(resource))}"
222
nonce="${nonce}"
223
charset="UTF-8"></script>`);
224
}
225
return out.join('\n');
226
}
227
228
private _getCsp(
229
provider: WebviewResourceProvider,
230
resource: vscode.Uri,
231
nonce: string
232
): string {
233
const rule = provider.cspSource.split(';')[0];
234
switch (this._cspArbiter.getSecurityLevelForResource(resource)) {
235
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
236
return `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:;`;
237
238
case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
239
return `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:*;`;
240
241
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
242
return ``;
243
244
case MarkdownPreviewSecurityLevel.Strict:
245
default:
246
return `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
}
250
251