mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-12 04:38:11 -06:00
For #121256 This change adds the current `ouputItem` to the notebook markdown renderer's environment Renders that extend our markdown renderer can use this to access output item metadata for example
286 lines
7.3 KiB
TypeScript
286 lines
7.3 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as DOMPurify from 'dompurify';
|
|
import MarkdownIt from 'markdown-it';
|
|
import type * as MarkdownItToken from 'markdown-it/lib/token';
|
|
import type { ActivationFunction } from 'vscode-notebook-renderer';
|
|
|
|
const sanitizerOptions: DOMPurify.Config = {
|
|
ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'],
|
|
};
|
|
|
|
export const activate: ActivationFunction<void> = (ctx) => {
|
|
const markdownIt: MarkdownIt = new MarkdownIt({
|
|
html: true,
|
|
linkify: true,
|
|
highlight: (str: string, lang?: string) => {
|
|
if (lang) {
|
|
return `<code class="vscode-code-block" data-vscode-code-block-lang="${markdownIt.utils.escapeHtml(lang)}">${markdownIt.utils.escapeHtml(str)}</code>`;
|
|
}
|
|
return `<code>${markdownIt.utils.escapeHtml(str)}</code>`;
|
|
}
|
|
});
|
|
markdownIt.linkify.set({ fuzzyLink: false });
|
|
|
|
addNamedHeaderRendering(markdownIt);
|
|
addLinkRenderer(markdownIt);
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.emptyMarkdownCell::before {
|
|
content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}";
|
|
font-style: italic;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
}
|
|
|
|
a {
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
a:focus,
|
|
input:focus,
|
|
select:focus,
|
|
textarea:focus {
|
|
outline: 1px solid -webkit-focus-ring-color;
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
hr {
|
|
border: 0;
|
|
height: 2px;
|
|
border-bottom: 2px solid;
|
|
}
|
|
|
|
h2, h3, h4, h5, h6 {
|
|
font-weight: normal;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.3em;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 2em;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.7em;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
h4 {
|
|
font-size: 1.3em;
|
|
}
|
|
|
|
h5 {
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
font-weight: normal;
|
|
}
|
|
|
|
div {
|
|
width: 100%;
|
|
}
|
|
|
|
/* Adjust margin of first item in markdown cell */
|
|
*:first-child {
|
|
margin-top: 0px;
|
|
}
|
|
|
|
/* h1 tags don't need top margin */
|
|
h1:first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Removes bottom margin when only one item exists in markdown cell */
|
|
#preview > *:only-child,
|
|
#preview > *:last-child {
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
/* makes all markdown cells consistent */
|
|
div {
|
|
min-height: var(--notebook-markdown-min-height);
|
|
}
|
|
|
|
table {
|
|
border-collapse: collapse;
|
|
border-spacing: 0;
|
|
}
|
|
|
|
table th,
|
|
table td {
|
|
border: 1px solid;
|
|
}
|
|
|
|
table > thead > tr > th {
|
|
text-align: left;
|
|
border-bottom: 1px solid;
|
|
}
|
|
|
|
table > thead > tr > th,
|
|
table > thead > tr > td,
|
|
table > tbody > tr > th,
|
|
table > tbody > tr > td {
|
|
padding: 5px 10px;
|
|
}
|
|
|
|
table > tbody > tr + tr > td {
|
|
border-top: 1px solid;
|
|
}
|
|
|
|
blockquote {
|
|
margin: 0 7px 0 5px;
|
|
padding: 0 16px 0 10px;
|
|
border-left-width: 5px;
|
|
border-left-style: solid;
|
|
}
|
|
|
|
code {
|
|
font-size: 1em;
|
|
}
|
|
|
|
pre code {
|
|
font-family: var(--vscode-editor-font-family);
|
|
|
|
line-height: 1.357em;
|
|
white-space: pre-wrap;
|
|
}
|
|
`;
|
|
const template = document.createElement('template');
|
|
template.classList.add('markdown-style');
|
|
template.content.appendChild(style);
|
|
document.head.appendChild(template);
|
|
|
|
return {
|
|
renderOutputItem: (outputInfo, element) => {
|
|
let previewNode: HTMLElement;
|
|
if (!element.shadowRoot) {
|
|
const previewRoot = element.attachShadow({ mode: 'open' });
|
|
|
|
// Insert styles into markdown preview shadow dom so that they are applied.
|
|
// First add default webview style
|
|
const defaultStyles = document.getElementById('_defaultStyles') as HTMLStyleElement;
|
|
previewRoot.appendChild(defaultStyles.cloneNode(true));
|
|
|
|
// And then contributed styles
|
|
for (const element of document.getElementsByClassName('markdown-style')) {
|
|
if (element instanceof HTMLTemplateElement) {
|
|
previewRoot.appendChild(element.content.cloneNode(true));
|
|
} else {
|
|
previewRoot.appendChild(element.cloneNode(true));
|
|
}
|
|
}
|
|
|
|
previewNode = document.createElement('div');
|
|
previewNode.id = 'preview';
|
|
previewRoot.appendChild(previewNode);
|
|
} else {
|
|
previewNode = element.shadowRoot.getElementById('preview')!;
|
|
}
|
|
|
|
const text = outputInfo.text();
|
|
if (text.trim().length === 0) {
|
|
previewNode.innerText = '';
|
|
previewNode.classList.add('emptyMarkdownCell');
|
|
} else {
|
|
previewNode.classList.remove('emptyMarkdownCell');
|
|
const markdownText = outputInfo.mime.startsWith('text/x-') ? `\`\`\`${outputInfo.mime.substr(7)}\n${text}\n\`\`\``
|
|
: (outputInfo.mime.startsWith('application/') ? `\`\`\`${outputInfo.mime.substr(12)}\n${text}\n\`\`\`` : text);
|
|
const unsanitizedRenderedMarkdown = markdownIt.render(markdownText, {
|
|
outputItem: outputInfo,
|
|
});
|
|
previewNode.innerHTML = (ctx.workspace.isTrusted
|
|
? unsanitizedRenderedMarkdown
|
|
: DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions)) as string;
|
|
}
|
|
},
|
|
extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {
|
|
f(markdownIt);
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
function addNamedHeaderRendering(md: InstanceType<typeof MarkdownIt>): void {
|
|
const slugCounter = new Map<string, number>();
|
|
|
|
const originalHeaderOpen = md.renderer.rules.heading_open;
|
|
md.renderer.rules.heading_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
|
|
const title = tokens[idx + 1].children!.reduce<string>((acc, t) => acc + t.content, '');
|
|
let slug = slugify(title);
|
|
|
|
if (slugCounter.has(slug)) {
|
|
const count = slugCounter.get(slug)!;
|
|
slugCounter.set(slug, count + 1);
|
|
slug = slugify(slug + '-' + (count + 1));
|
|
} else {
|
|
slugCounter.set(slug, 0);
|
|
}
|
|
|
|
tokens[idx].attrSet('id', slug);
|
|
|
|
if (originalHeaderOpen) {
|
|
return originalHeaderOpen(tokens, idx, options, env, self);
|
|
} else {
|
|
return self.renderToken(tokens, idx, options);
|
|
}
|
|
};
|
|
|
|
const originalRender = md.render;
|
|
md.render = function () {
|
|
slugCounter.clear();
|
|
return originalRender.apply(this, arguments as any);
|
|
};
|
|
}
|
|
|
|
function addLinkRenderer(md: MarkdownIt): void {
|
|
const original = md.renderer.rules.link_open;
|
|
|
|
md.renderer.rules.link_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {
|
|
const token = tokens[idx];
|
|
const href = token.attrGet('href');
|
|
if (typeof href === 'string' && href.startsWith('#')) {
|
|
token.attrSet('href', '#' + slugify(href.slice(1)));
|
|
}
|
|
if (original) {
|
|
return original(tokens, idx, options, env, self);
|
|
} else {
|
|
return self.renderToken(tokens, idx, options);
|
|
}
|
|
};
|
|
}
|
|
|
|
function slugify(text: string): string {
|
|
const slugifiedHeading = encodeURI(
|
|
text.trim()
|
|
.toLowerCase()
|
|
.replace(/\s+/g, '-') // Replace whitespace with -
|
|
// allow-any-unicode-next-line
|
|
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
|
|
.replace(/^\-+/, '') // Remove leading -
|
|
.replace(/\-+$/, '') // Remove trailing -
|
|
);
|
|
return slugifiedHeading;
|
|
}
|