Path: blob/main/extensions/markdown-language-features/notebook/index.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import DOMPurify from 'dompurify';6import MarkdownIt from 'markdown-it';7import type * as MarkdownItToken from 'markdown-it/lib/token';8import type { ActivationFunction } from 'vscode-notebook-renderer';910const allowedHtmlTags = Object.freeze(['a',11'abbr',12'b',13'bdo',14'blockquote',15'br',16'caption',17'cite',18'code',19'col',20'colgroup',21'dd',22'del',23'details',24'dfn',25'div',26'dl',27'dt',28'em',29'figcaption',30'figure',31'h1',32'h2',33'h3',34'h4',35'h5',36'h6',37'hr',38'i',39'img',40'ins',41'kbd',42'label',43'li',44'mark',45'ol',46'p',47'pre',48'q',49'rp',50'rt',51'ruby',52'samp',53'small',54'small',55'source',56'span',57'strike',58'strong',59'sub',60'summary',61'sup',62'table',63'tbody',64'td',65'tfoot',66'th',67'thead',68'time',69'tr',70'tt',71'u',72'ul',73'var',74'video',75'wbr',76]);7778const allowedSvgTags = Object.freeze([79'svg',80'a',81'altglyph',82'altglyphdef',83'altglyphitem',84'animatecolor',85'animatemotion',86'animatetransform',87'circle',88'clippath',89'defs',90'desc',91'ellipse',92'filter',93'font',94'g',95'glyph',96'glyphref',97'hkern',98'image',99'line',100'lineargradient',101'marker',102'mask',103'metadata',104'mpath',105'path',106'pattern',107'polygon',108'polyline',109'radialgradient',110'rect',111'stop',112'style',113'switch',114'symbol',115'text',116'textpath',117'title',118'tref',119'tspan',120'view',121'vkern',122]);123124const sanitizerOptions: DOMPurify.Config = {125ALLOWED_TAGS: [126...allowedHtmlTags,127...allowedSvgTags,128],129};130131export const activate: ActivationFunction<void> = (ctx) => {132const markdownIt: MarkdownIt = new MarkdownIt({133html: true,134linkify: true,135highlight: (str: string, lang?: string) => {136if (lang) {137return `<div class="vscode-code-block" data-vscode-code-block-lang="${markdownIt.utils.escapeHtml(lang)}">${markdownIt.utils.escapeHtml(str)}</div>`;138}139return markdownIt.utils.escapeHtml(str);140}141});142markdownIt.linkify.set({ fuzzyLink: false });143144addNamedHeaderRendering(markdownIt);145addLinkRenderer(markdownIt);146147const style = document.createElement('style');148style.textContent = `149.emptyMarkdownCell::before {150content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}";151font-style: italic;152opacity: 0.6;153}154155img {156max-width: 100%;157max-height: 100%;158}159160a {161text-decoration: none;162}163164a:hover {165text-decoration: underline;166}167168a:focus,169input:focus,170select:focus,171textarea:focus {172outline: 1px solid -webkit-focus-ring-color;173outline-offset: -1px;174}175176hr {177border: 0;178height: 2px;179border-bottom: 2px solid;180}181182h2, h3, h4, h5, h6 {183font-weight: normal;184}185186h1 {187font-size: 2.3em;188}189190h2 {191font-size: 2em;192}193194h3 {195font-size: 1.7em;196}197198h3 {199font-size: 1.5em;200}201202h4 {203font-size: 1.3em;204}205206h5 {207font-size: 1.2em;208}209210h1,211h2,212h3 {213font-weight: normal;214}215216div {217width: 100%;218}219220/* Adjust margin of first item in markdown cell */221*:first-child {222margin-top: 0px;223}224225/* h1 tags don't need top margin */226h1:first-child {227margin-top: 0;228}229230/* Removes bottom margin when only one item exists in markdown cell */231#preview > *:only-child,232#preview > *:last-child {233margin-bottom: 0;234padding-bottom: 0;235}236237/* makes all markdown cells consistent */238div {239min-height: var(--notebook-markdown-min-height);240}241242table {243border-collapse: collapse;244border-spacing: 0;245}246247table th,248table td {249border: 1px solid;250}251252table > thead > tr > th {253text-align: left;254border-bottom: 1px solid;255}256257table > thead > tr > th,258table > thead > tr > td,259table > tbody > tr > th,260table > tbody > tr > td {261padding: 5px 10px;262}263264table > tbody > tr + tr > td {265border-top: 1px solid;266}267268blockquote {269margin: 0 7px 0 5px;270padding: 0 16px 0 10px;271border-left-width: 5px;272border-left-style: solid;273}274275code {276font-size: 1em;277font-family: var(--vscode-editor-font-family);278}279280pre code {281line-height: 1.357em;282white-space: pre-wrap;283padding: 0;284}285286li p {287margin-bottom: 0.7em;288}289290ul,291ol {292margin-bottom: 0.7em;293}294`;295const template = document.createElement('template');296template.classList.add('markdown-style');297template.content.appendChild(style);298document.head.appendChild(template);299300return {301renderOutputItem: (outputInfo, element) => {302let previewNode: HTMLElement;303if (!element.shadowRoot) {304const previewRoot = element.attachShadow({ mode: 'open' });305306// Insert styles into markdown preview shadow dom so that they are applied.307// First add default webview style308const defaultStyles = document.getElementById('_defaultStyles') as HTMLStyleElement;309previewRoot.appendChild(defaultStyles.cloneNode(true));310311// And then contributed styles312for (const element of document.getElementsByClassName('markdown-style')) {313if (element instanceof HTMLTemplateElement) {314previewRoot.appendChild(element.content.cloneNode(true));315} else {316previewRoot.appendChild(element.cloneNode(true));317}318}319320previewNode = document.createElement('div');321previewNode.id = 'preview';322previewRoot.appendChild(previewNode);323} else {324previewNode = element.shadowRoot.getElementById('preview')!;325}326327const text = outputInfo.text();328if (text.trim().length === 0) {329previewNode.innerText = '';330previewNode.classList.add('emptyMarkdownCell');331} else {332previewNode.classList.remove('emptyMarkdownCell');333const markdownText = outputInfo.mime.startsWith('text/x-') ? `\`\`\`${outputInfo.mime.substr(7)}\n${text}\n\`\`\``334: (outputInfo.mime.startsWith('application/') ? `\`\`\`${outputInfo.mime.substr(12)}\n${text}\n\`\`\`` : text);335const unsanitizedRenderedMarkdown = markdownIt.render(markdownText, {336outputItem: outputInfo,337});338previewNode.innerHTML = (ctx.workspace.isTrusted339? unsanitizedRenderedMarkdown340: DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions)) as string;341}342},343extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {344try {345f(markdownIt);346} catch (err) {347console.error('Error extending markdown-it', err);348}349}350};351};352353354function addNamedHeaderRendering(md: InstanceType<typeof MarkdownIt>): void {355const slugCounter = new Map<string, number>();356357const originalHeaderOpen = md.renderer.rules.heading_open;358md.renderer.rules.heading_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {359const title = tokens[idx + 1].children!.reduce<string>((acc, t) => acc + t.content, '');360let slug = slugify(title);361362if (slugCounter.has(slug)) {363const count = slugCounter.get(slug)!;364slugCounter.set(slug, count + 1);365slug = slugify(slug + '-' + (count + 1));366} else {367slugCounter.set(slug, 0);368}369370tokens[idx].attrSet('id', slug);371372if (originalHeaderOpen) {373return originalHeaderOpen(tokens, idx, options, env, self);374} else {375return self.renderToken(tokens, idx, options);376}377};378379const originalRender = md.render;380md.render = function () {381slugCounter.clear();382return originalRender.apply(this, arguments as any);383};384}385386function addLinkRenderer(md: MarkdownIt): void {387const original = md.renderer.rules.link_open;388389md.renderer.rules.link_open = (tokens: MarkdownItToken[], idx: number, options, env, self) => {390const token = tokens[idx];391const href = token.attrGet('href');392if (typeof href === 'string' && href.startsWith('#')) {393token.attrSet('href', '#' + slugify(href.slice(1)));394}395if (original) {396return original(tokens, idx, options, env, self);397} else {398return self.renderToken(tokens, idx, options);399}400};401}402403function slugify(text: string): string {404const slugifiedHeading = encodeURI(405text.trim()406.toLowerCase()407.replace(/\s+/g, '-') // Replace whitespace with -408// allow-any-unicode-next-line409.replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators410.replace(/^\-+/, '') // Remove leading -411.replace(/\-+$/, '') // Remove trailing -412);413return slugifiedHeading;414}415416417