Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/notebook/index.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 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
return originalRender.apply(this, arguments as any);
384
};
385
}
386
387
function addLinkRenderer(md: MarkdownIt): void {
388
const original = md.renderer.rules.link_open;
389
390
md.renderer.rules.link_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
391
const token = tokens[idx];
392
const href = token.attrGet('href');
393
if (typeof href === 'string' && href.startsWith('#')) {
394
token.attrSet('href', '#' + slugify(href.slice(1)));
395
}
396
if (original) {
397
return original(tokens, idx, options, env, self);
398
} else {
399
return self.renderToken(tokens, idx, options);
400
}
401
};
402
}
403
404
function slugify(text: string): string {
405
const slugifiedHeading = encodeURI(
406
text.trim()
407
.toLowerCase()
408
.replace(/\s+/g, '-') // Replace whitespace with -
409
// allow-any-unicode-next-line
410
.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
411
.replace(/^\-+/, '') // Remove leading -
412
.replace(/\-+$/, '') // Remove trailing -
413
);
414
return slugifiedHeading;
415
}
416
417