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