Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts
4780 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 { sanitizeHtml } from '../../../../base/browser/domSanitize.js';
7
import { allowedMarkdownHtmlAttributes, allowedMarkdownHtmlTags } from '../../../../base/browser/markdownRenderer.js';
8
import { raceCancellationError } from '../../../../base/common/async.js';
9
import { CancellationToken } from '../../../../base/common/cancellation.js';
10
import * as marked from '../../../../base/common/marked/marked.js';
11
import { Schemas } from '../../../../base/common/network.js';
12
import { escape } from '../../../../base/common/strings.js';
13
import { ILanguageService } from '../../../../editor/common/languages/language.js';
14
import { tokenizeToString } from '../../../../editor/common/languages/textToHtmlTokenizer.js';
15
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
16
import { markedGfmHeadingIdPlugin } from './markedGfmHeadingIdPlugin.js';
17
18
export const DEFAULT_MARKDOWN_STYLES = `
19
body {
20
padding: 10px 20px;
21
line-height: 22px;
22
max-width: 882px;
23
margin: 0 auto;
24
}
25
26
body *:last-child {
27
margin-bottom: 0;
28
}
29
30
img {
31
max-width: 100%;
32
max-height: 100%;
33
}
34
35
a {
36
text-decoration: var(--text-link-decoration);
37
}
38
39
a:hover {
40
text-decoration: underline;
41
}
42
43
a:focus,
44
input:focus,
45
select:focus,
46
textarea:focus {
47
outline: 1px solid -webkit-focus-ring-color;
48
outline-offset: -1px;
49
}
50
51
hr {
52
border: 0;
53
height: 2px;
54
border-bottom: 2px solid;
55
}
56
57
h1 {
58
padding-bottom: 0.3em;
59
line-height: 1.2;
60
border-bottom-width: 1px;
61
border-bottom-style: solid;
62
}
63
64
h1, h2, h3 {
65
font-weight: normal;
66
}
67
68
table {
69
border-collapse: collapse;
70
}
71
72
th {
73
text-align: left;
74
border-bottom: 1px solid;
75
}
76
77
th,
78
td {
79
padding: 5px 10px;
80
}
81
82
table > tbody > tr + tr > td {
83
border-top-width: 1px;
84
border-top-style: solid;
85
}
86
87
blockquote {
88
margin: 0 7px 0 5px;
89
padding: 0 16px 0 10px;
90
border-left-width: 5px;
91
border-left-style: solid;
92
}
93
94
code {
95
font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
96
}
97
98
pre {
99
padding: 16px;
100
border-radius: 3px;
101
overflow: auto;
102
}
103
104
pre code {
105
font-family: var(--vscode-editor-font-family);
106
font-weight: var(--vscode-editor-font-weight);
107
font-size: var(--vscode-editor-font-size);
108
line-height: 1.5;
109
color: var(--vscode-editor-foreground);
110
tab-size: 4;
111
}
112
113
.monaco-tokenized-source {
114
white-space: pre;
115
}
116
117
/** Theming */
118
119
.pre {
120
background-color: var(--vscode-textCodeBlock-background);
121
}
122
123
.vscode-high-contrast h1 {
124
border-color: rgb(0, 0, 0);
125
}
126
127
.vscode-light th {
128
border-color: rgba(0, 0, 0, 0.69);
129
}
130
131
.vscode-dark th {
132
border-color: rgba(255, 255, 255, 0.69);
133
}
134
135
.vscode-light h1,
136
.vscode-light hr,
137
.vscode-light td {
138
border-color: rgba(0, 0, 0, 0.18);
139
}
140
141
.vscode-dark h1,
142
.vscode-dark hr,
143
.vscode-dark td {
144
border-color: rgba(255, 255, 255, 0.18);
145
}
146
147
@media (forced-colors: active) and (prefers-color-scheme: light){
148
body {
149
forced-color-adjust: none;
150
}
151
}
152
153
@media (forced-colors: active) and (prefers-color-scheme: dark){
154
body {
155
forced-color-adjust: none;
156
}
157
}
158
`;
159
160
const defaultAllowedLinkProtocols = Object.freeze([
161
Schemas.http,
162
Schemas.https,
163
]);
164
165
function sanitize(documentContent: string, sanitizerConfig: MarkdownDocumentSanitizerConfig | undefined): TrustedHTML {
166
return sanitizeHtml(documentContent, {
167
allowedLinkProtocols: {
168
override: sanitizerConfig?.allowedLinkProtocols?.override ?? defaultAllowedLinkProtocols,
169
},
170
allowRelativeLinkPaths: sanitizerConfig?.allowRelativeLinkPaths,
171
allowedMediaProtocols: sanitizerConfig?.allowedMediaProtocols,
172
allowRelativeMediaPaths: sanitizerConfig?.allowRelativeMediaPaths,
173
allowedTags: {
174
override: allowedMarkdownHtmlTags,
175
augment: sanitizerConfig?.allowedTags?.augment
176
},
177
allowedAttributes: {
178
override: [
179
...allowedMarkdownHtmlAttributes,
180
'name',
181
'id',
182
'class',
183
'role',
184
'tabindex',
185
'placeholder',
186
],
187
augment: sanitizerConfig?.allowedAttributes?.augment ?? [],
188
}
189
});
190
}
191
192
interface MarkdownDocumentSanitizerConfig {
193
readonly allowedLinkProtocols?: {
194
readonly override: readonly string[] | '*';
195
};
196
readonly allowRelativeLinkPaths?: boolean;
197
198
readonly allowedMediaProtocols?: {
199
readonly override: readonly string[] | '*';
200
};
201
readonly allowRelativeMediaPaths?: boolean;
202
203
readonly allowedTags?: {
204
readonly augment: readonly string[];
205
};
206
207
readonly allowedAttributes?: {
208
readonly augment: readonly string[];
209
};
210
}
211
212
interface IRenderMarkdownDocumentOptions {
213
readonly sanitizerConfig?: MarkdownDocumentSanitizerConfig;
214
readonly markedExtensions?: readonly marked.MarkedExtension[];
215
}
216
217
/**
218
* Renders a string of markdown for use in an external document context.
219
*
220
* Uses VS Code's syntax highlighting code blocks. Also does not attach all the hooks and customization that normal
221
* markdown renderer.
222
*/
223
export async function renderMarkdownDocument(
224
text: string,
225
extensionService: IExtensionService,
226
languageService: ILanguageService,
227
options?: IRenderMarkdownDocumentOptions,
228
token: CancellationToken = CancellationToken.None,
229
): Promise<TrustedHTML> {
230
const m = new marked.Marked(
231
MarkedHighlight.markedHighlight({
232
async: true,
233
async highlight(code: string, lang: string): Promise<string> {
234
if (typeof lang !== 'string') {
235
return escape(code);
236
}
237
238
await extensionService.whenInstalledExtensionsRegistered();
239
if (token?.isCancellationRequested) {
240
return '';
241
}
242
243
const languageId = languageService.getLanguageIdByLanguageName(lang) ?? languageService.getLanguageIdByLanguageName(lang.split(/\s+|:|,|(?!^)\{|\?]/, 1)[0]);
244
return tokenizeToString(languageService, code, languageId);
245
}
246
}),
247
markedGfmHeadingIdPlugin(),
248
...(options?.markedExtensions ?? []),
249
);
250
251
const raw = await raceCancellationError(m.parse(text, { async: true }), token ?? CancellationToken.None);
252
return sanitize(raw, options?.sanitizerConfig);
253
}
254
255
namespace MarkedHighlight {
256
// Copied from https://github.com/markedjs/marked-highlight/blob/main/src/index.js
257
258
export function markedHighlight(options: marked.MarkedOptions & { highlight: (code: string, lang: string) => string | Promise<string> }): marked.MarkedExtension {
259
if (typeof options === 'function') {
260
options = {
261
highlight: options,
262
};
263
}
264
265
if (!options || typeof options.highlight !== 'function') {
266
throw new Error('Must provide highlight function');
267
}
268
269
return {
270
async: !!options.async,
271
walkTokens(token: marked.Token): Promise<void> | void {
272
if (token.type !== 'code') {
273
return;
274
}
275
276
if (options.async) {
277
return Promise.resolve(options.highlight(token.text, token.lang)).then(updateToken(token));
278
}
279
280
const code = options.highlight(token.text, token.lang);
281
if (code instanceof Promise) {
282
throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');
283
}
284
updateToken(token)(code);
285
},
286
renderer: {
287
code({ text, lang, escaped }: marked.Tokens.Code) {
288
const classAttr = lang
289
? ` class="language-${escape(lang)}"`
290
: '';
291
text = text.replace(/\n$/, '');
292
return `<pre><code${classAttr}>${escaped ? text : escape(text, true)}\n</code></pre>`;
293
},
294
},
295
};
296
}
297
298
function updateToken(token: any) {
299
return (code: string) => {
300
if (typeof code === 'string' && code !== token.text) {
301
token.escaped = true;
302
token.text = code;
303
}
304
};
305
}
306
307
// copied from marked helpers
308
const escapeTest = /[&<>"']/;
309
const escapeReplace = new RegExp(escapeTest.source, 'g');
310
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
311
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
312
const escapeReplacement: Record<string, string> = {
313
'&': '&amp;',
314
'<': '&lt;',
315
'>': '&gt;',
316
'"': '&quot;',
317
[`'`]: '&#39;',
318
};
319
const getEscapeReplacement = (ch: string) => escapeReplacement[ch];
320
function escape(html: string, encode?: boolean) {
321
if (encode) {
322
if (escapeTest.test(html)) {
323
return html.replace(escapeReplace, getEscapeReplacement);
324
}
325
} else {
326
if (escapeTestNoEncode.test(html)) {
327
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
328
}
329
}
330
331
return html;
332
}
333
}
334
335