Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/notebook/index.ts
5251 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 DOMPurify from 'dompurify';
7
import MarkdownIt from 'markdown-it';
8
import type * as MarkdownItToken from 'markdown-it/lib/token';
9
import type { ActivationFunction } from 'vscode-notebook-renderer';
10
11
const allowedHtmlTags = Object.freeze(['a',
12
'abbr',
13
'b',
14
'bdo',
15
'blockquote',
16
'br',
17
'caption',
18
'cite',
19
'code',
20
'col',
21
'colgroup',
22
'dd',
23
'del',
24
'details',
25
'dfn',
26
'div',
27
'dl',
28
'dt',
29
'em',
30
'figcaption',
31
'figure',
32
'h1',
33
'h2',
34
'h3',
35
'h4',
36
'h5',
37
'h6',
38
'hr',
39
'i',
40
'img',
41
'ins',
42
'kbd',
43
'label',
44
'li',
45
'mark',
46
'ol',
47
'p',
48
'pre',
49
'q',
50
'rp',
51
'rt',
52
'ruby',
53
'samp',
54
'small',
55
'small',
56
'source',
57
'span',
58
'strike',
59
'strong',
60
'sub',
61
'summary',
62
'sup',
63
'table',
64
'tbody',
65
'td',
66
'tfoot',
67
'th',
68
'thead',
69
'time',
70
'tr',
71
'tt',
72
'u',
73
'ul',
74
'var',
75
'video',
76
'wbr',
77
]);
78
79
const allowedSvgTags = Object.freeze([
80
'svg',
81
'a',
82
'altglyph',
83
'altglyphdef',
84
'altglyphitem',
85
'animatecolor',
86
'animatemotion',
87
'animatetransform',
88
'circle',
89
'clippath',
90
'defs',
91
'desc',
92
'ellipse',
93
'filter',
94
'font',
95
'g',
96
'glyph',
97
'glyphref',
98
'hkern',
99
'image',
100
'line',
101
'lineargradient',
102
'marker',
103
'mask',
104
'metadata',
105
'mpath',
106
'path',
107
'pattern',
108
'polygon',
109
'polyline',
110
'radialgradient',
111
'rect',
112
'stop',
113
'style',
114
'switch',
115
'symbol',
116
'text',
117
'textpath',
118
'title',
119
'tref',
120
'tspan',
121
'view',
122
'vkern',
123
]);
124
125
const sanitizerOptions: DOMPurify.Config = {
126
ALLOWED_TAGS: [
127
...allowedHtmlTags,
128
...allowedSvgTags,
129
],
130
};
131
132
export const activate: ActivationFunction<void> = (ctx) => {
133
const markdownIt: MarkdownIt = new MarkdownIt({
134
html: true,
135
linkify: true,
136
highlight: (str: string, lang?: string) => {
137
if (lang) {
138
return `<div class="vscode-code-block" data-vscode-code-block-lang="${markdownIt.utils.escapeHtml(lang)}">${markdownIt.utils.escapeHtml(str)}</div>`;
139
}
140
return markdownIt.utils.escapeHtml(str);
141
}
142
});
143
markdownIt.linkify.set({ fuzzyLink: false });
144
145
addNamedHeaderRendering(markdownIt);
146
addLinkRenderer(markdownIt);
147
148
const style = document.createElement('style');
149
style.textContent = `
150
.emptyMarkdownCell::before {
151
content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}";
152
font-style: italic;
153
opacity: 0.6;
154
}
155
156
img {
157
max-width: 100%;
158
max-height: 100%;
159
}
160
161
a {
162
text-decoration: none;
163
}
164
165
a:hover {
166
text-decoration: underline;
167
}
168
169
a:focus,
170
input:focus,
171
select:focus,
172
textarea:focus {
173
outline: 1px solid -webkit-focus-ring-color;
174
outline-offset: -1px;
175
}
176
177
hr {
178
border: 0;
179
height: 2px;
180
border-bottom: 2px solid;
181
}
182
183
h2, h3, h4, h5, h6 {
184
font-weight: normal;
185
}
186
187
h1 {
188
font-size: 2.3em;
189
}
190
191
h2 {
192
font-size: 2em;
193
}
194
195
h3 {
196
font-size: 1.7em;
197
}
198
199
h3 {
200
font-size: 1.5em;
201
}
202
203
h4 {
204
font-size: 1.3em;
205
}
206
207
h5 {
208
font-size: 1.2em;
209
}
210
211
h1,
212
h2,
213
h3 {
214
font-weight: normal;
215
}
216
217
div {
218
width: 100%;
219
}
220
221
/* Adjust margin of first item in markdown cell */
222
*:first-child {
223
margin-top: 0px;
224
}
225
226
/* h1 tags don't need top margin */
227
h1:first-child {
228
margin-top: 0;
229
}
230
231
/* Removes bottom margin when only one item exists in markdown cell */
232
#preview > *:only-child,
233
#preview > *:last-child {
234
margin-bottom: 0;
235
padding-bottom: 0;
236
}
237
238
/* makes all markdown cells consistent */
239
div {
240
min-height: var(--notebook-markdown-min-height);
241
}
242
243
table {
244
border-collapse: collapse;
245
border-spacing: 0;
246
}
247
248
table th,
249
table td {
250
border: 1px solid;
251
}
252
253
table > thead > tr > th {
254
text-align: left;
255
border-bottom: 1px solid;
256
}
257
258
table > thead > tr > th,
259
table > thead > tr > td,
260
table > tbody > tr > th,
261
table > tbody > tr > td {
262
padding: 5px 10px;
263
}
264
265
table > tbody > tr + tr > td {
266
border-top: 1px solid;
267
}
268
269
blockquote {
270
margin: 0 7px 0 5px;
271
padding: 0 16px 0 10px;
272
border-left-width: 5px;
273
border-left-style: solid;
274
}
275
276
code {
277
font-size: 1em;
278
font-family: var(--vscode-editor-font-family);
279
}
280
281
pre code {
282
line-height: 1.357em;
283
white-space: pre-wrap;
284
padding: 0;
285
}
286
287
li p {
288
margin-bottom: 0.7em;
289
}
290
291
ul,
292
ol {
293
margin-bottom: 0.7em;
294
}
295
`;
296
const template = document.createElement('template');
297
template.classList.add('markdown-style');
298
template.content.appendChild(style);
299
document.head.appendChild(template);
300
301
return {
302
renderOutputItem: (outputInfo, element) => {
303
let previewNode: HTMLElement;
304
if (!element.shadowRoot) {
305
const previewRoot = element.attachShadow({ mode: 'open' });
306
307
// Insert styles into markdown preview shadow dom so that they are applied.
308
// First add default webview style
309
const defaultStyles = document.getElementById('_defaultStyles') as HTMLStyleElement;
310
previewRoot.appendChild(defaultStyles.cloneNode(true));
311
312
// And then contributed styles
313
for (const element of document.getElementsByClassName('markdown-style')) {
314
if (element instanceof HTMLTemplateElement) {
315
previewRoot.appendChild(element.content.cloneNode(true));
316
} else {
317
previewRoot.appendChild(element.cloneNode(true));
318
}
319
}
320
321
previewNode = document.createElement('div');
322
previewNode.id = 'preview';
323
previewRoot.appendChild(previewNode);
324
} else {
325
previewNode = element.shadowRoot.getElementById('preview')!;
326
}
327
328
const text = outputInfo.text();
329
if (text.trim().length === 0) {
330
previewNode.innerText = '';
331
previewNode.classList.add('emptyMarkdownCell');
332
} else {
333
previewNode.classList.remove('emptyMarkdownCell');
334
const markdownText = outputInfo.mime.startsWith('text/x-') ? `\`\`\`${outputInfo.mime.substr(7)}\n${text}\n\`\`\``
335
: (outputInfo.mime.startsWith('application/') ? `\`\`\`${outputInfo.mime.substr(12)}\n${text}\n\`\`\`` : text);
336
const unsanitizedRenderedMarkdown = markdownIt.render(markdownText, {
337
outputItem: outputInfo,
338
});
339
previewNode.innerHTML = (ctx.workspace.isTrusted
340
? unsanitizedRenderedMarkdown
341
: DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions)) as string;
342
}
343
},
344
extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {
345
try {
346
f(markdownIt);
347
} catch (err) {
348
console.error('Error extending markdown-it', err);
349
}
350
}
351
};
352
};
353
354
355
function addNamedHeaderRendering(md: InstanceType<typeof MarkdownIt>): void {
356
const slugCounter = new Map<string, number>();
357
358
const originalHeaderOpen = md.renderer.rules.heading_open;
359
md.renderer.rules.heading_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
360
const title = tokens[idx + 1].children!.reduce<string>((acc, t) => acc + t.content, '');
361
let slug = slugify(title);
362
363
if (slugCounter.has(slug)) {
364
const count = slugCounter.get(slug)!;
365
slugCounter.set(slug, count + 1);
366
slug = slugify(slug + '-' + (count + 1));
367
} else {
368
slugCounter.set(slug, 0);
369
}
370
371
tokens[idx].attrSet('id', slug);
372
373
if (originalHeaderOpen) {
374
return originalHeaderOpen(tokens, idx, options, env, self);
375
} else {
376
return self.renderToken(tokens, idx, options);
377
}
378
};
379
380
const originalRender = md.render;
381
md.render = function () {
382
slugCounter.clear();
383
// eslint-disable-next-line local/code-no-any-casts
384
return originalRender.apply(this, arguments as any);
385
};
386
}
387
388
function addLinkRenderer(md: MarkdownIt): void {
389
const original = md.renderer.rules.link_open;
390
391
md.renderer.rules.link_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
392
const token = tokens[idx];
393
const href = token.attrGet('href');
394
if (typeof href === 'string' && href.startsWith('#')) {
395
token.attrSet('href', '#' + slugify(href.slice(1)));
396
}
397
if (original) {
398
return original(tokens, idx, options, env, self);
399
} else {
400
return self.renderToken(tokens, idx, options);
401
}
402
};
403
}
404
405
function slugify(text: string): string {
406
const slugifiedHeading = encodeURI(
407
text.trim()
408
.toLowerCase()
409
.replace(/\s+/g, '-') // Replace whitespace with -
410
// allow-any-unicode-next-line
411
.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
412
.replace(/^\-+/, '') // Remove leading -
413
.replace(/\-+$/, '') // Remove trailing -
414
);
415
return slugifiedHeading;
416
}
417
418