Path: blob/main/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts
4780 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 { sanitizeHtml } from '../../../../base/browser/domSanitize.js';6import { allowedMarkdownHtmlAttributes, allowedMarkdownHtmlTags } from '../../../../base/browser/markdownRenderer.js';7import { raceCancellationError } from '../../../../base/common/async.js';8import { CancellationToken } from '../../../../base/common/cancellation.js';9import * as marked from '../../../../base/common/marked/marked.js';10import { Schemas } from '../../../../base/common/network.js';11import { escape } from '../../../../base/common/strings.js';12import { ILanguageService } from '../../../../editor/common/languages/language.js';13import { tokenizeToString } from '../../../../editor/common/languages/textToHtmlTokenizer.js';14import { IExtensionService } from '../../../services/extensions/common/extensions.js';15import { markedGfmHeadingIdPlugin } from './markedGfmHeadingIdPlugin.js';1617export const DEFAULT_MARKDOWN_STYLES = `18body {19padding: 10px 20px;20line-height: 22px;21max-width: 882px;22margin: 0 auto;23}2425body *:last-child {26margin-bottom: 0;27}2829img {30max-width: 100%;31max-height: 100%;32}3334a {35text-decoration: var(--text-link-decoration);36}3738a:hover {39text-decoration: underline;40}4142a:focus,43input:focus,44select:focus,45textarea:focus {46outline: 1px solid -webkit-focus-ring-color;47outline-offset: -1px;48}4950hr {51border: 0;52height: 2px;53border-bottom: 2px solid;54}5556h1 {57padding-bottom: 0.3em;58line-height: 1.2;59border-bottom-width: 1px;60border-bottom-style: solid;61}6263h1, h2, h3 {64font-weight: normal;65}6667table {68border-collapse: collapse;69}7071th {72text-align: left;73border-bottom: 1px solid;74}7576th,77td {78padding: 5px 10px;79}8081table > tbody > tr + tr > td {82border-top-width: 1px;83border-top-style: solid;84}8586blockquote {87margin: 0 7px 0 5px;88padding: 0 16px 0 10px;89border-left-width: 5px;90border-left-style: solid;91}9293code {94font-family: "SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;95}9697pre {98padding: 16px;99border-radius: 3px;100overflow: auto;101}102103pre code {104font-family: var(--vscode-editor-font-family);105font-weight: var(--vscode-editor-font-weight);106font-size: var(--vscode-editor-font-size);107line-height: 1.5;108color: var(--vscode-editor-foreground);109tab-size: 4;110}111112.monaco-tokenized-source {113white-space: pre;114}115116/** Theming */117118.pre {119background-color: var(--vscode-textCodeBlock-background);120}121122.vscode-high-contrast h1 {123border-color: rgb(0, 0, 0);124}125126.vscode-light th {127border-color: rgba(0, 0, 0, 0.69);128}129130.vscode-dark th {131border-color: rgba(255, 255, 255, 0.69);132}133134.vscode-light h1,135.vscode-light hr,136.vscode-light td {137border-color: rgba(0, 0, 0, 0.18);138}139140.vscode-dark h1,141.vscode-dark hr,142.vscode-dark td {143border-color: rgba(255, 255, 255, 0.18);144}145146@media (forced-colors: active) and (prefers-color-scheme: light){147body {148forced-color-adjust: none;149}150}151152@media (forced-colors: active) and (prefers-color-scheme: dark){153body {154forced-color-adjust: none;155}156}157`;158159const defaultAllowedLinkProtocols = Object.freeze([160Schemas.http,161Schemas.https,162]);163164function sanitize(documentContent: string, sanitizerConfig: MarkdownDocumentSanitizerConfig | undefined): TrustedHTML {165return sanitizeHtml(documentContent, {166allowedLinkProtocols: {167override: sanitizerConfig?.allowedLinkProtocols?.override ?? defaultAllowedLinkProtocols,168},169allowRelativeLinkPaths: sanitizerConfig?.allowRelativeLinkPaths,170allowedMediaProtocols: sanitizerConfig?.allowedMediaProtocols,171allowRelativeMediaPaths: sanitizerConfig?.allowRelativeMediaPaths,172allowedTags: {173override: allowedMarkdownHtmlTags,174augment: sanitizerConfig?.allowedTags?.augment175},176allowedAttributes: {177override: [178...allowedMarkdownHtmlAttributes,179'name',180'id',181'class',182'role',183'tabindex',184'placeholder',185],186augment: sanitizerConfig?.allowedAttributes?.augment ?? [],187}188});189}190191interface MarkdownDocumentSanitizerConfig {192readonly allowedLinkProtocols?: {193readonly override: readonly string[] | '*';194};195readonly allowRelativeLinkPaths?: boolean;196197readonly allowedMediaProtocols?: {198readonly override: readonly string[] | '*';199};200readonly allowRelativeMediaPaths?: boolean;201202readonly allowedTags?: {203readonly augment: readonly string[];204};205206readonly allowedAttributes?: {207readonly augment: readonly string[];208};209}210211interface IRenderMarkdownDocumentOptions {212readonly sanitizerConfig?: MarkdownDocumentSanitizerConfig;213readonly markedExtensions?: readonly marked.MarkedExtension[];214}215216/**217* Renders a string of markdown for use in an external document context.218*219* Uses VS Code's syntax highlighting code blocks. Also does not attach all the hooks and customization that normal220* markdown renderer.221*/222export async function renderMarkdownDocument(223text: string,224extensionService: IExtensionService,225languageService: ILanguageService,226options?: IRenderMarkdownDocumentOptions,227token: CancellationToken = CancellationToken.None,228): Promise<TrustedHTML> {229const m = new marked.Marked(230MarkedHighlight.markedHighlight({231async: true,232async highlight(code: string, lang: string): Promise<string> {233if (typeof lang !== 'string') {234return escape(code);235}236237await extensionService.whenInstalledExtensionsRegistered();238if (token?.isCancellationRequested) {239return '';240}241242const languageId = languageService.getLanguageIdByLanguageName(lang) ?? languageService.getLanguageIdByLanguageName(lang.split(/\s+|:|,|(?!^)\{|\?]/, 1)[0]);243return tokenizeToString(languageService, code, languageId);244}245}),246markedGfmHeadingIdPlugin(),247...(options?.markedExtensions ?? []),248);249250const raw = await raceCancellationError(m.parse(text, { async: true }), token ?? CancellationToken.None);251return sanitize(raw, options?.sanitizerConfig);252}253254namespace MarkedHighlight {255// Copied from https://github.com/markedjs/marked-highlight/blob/main/src/index.js256257export function markedHighlight(options: marked.MarkedOptions & { highlight: (code: string, lang: string) => string | Promise<string> }): marked.MarkedExtension {258if (typeof options === 'function') {259options = {260highlight: options,261};262}263264if (!options || typeof options.highlight !== 'function') {265throw new Error('Must provide highlight function');266}267268return {269async: !!options.async,270walkTokens(token: marked.Token): Promise<void> | void {271if (token.type !== 'code') {272return;273}274275if (options.async) {276return Promise.resolve(options.highlight(token.text, token.lang)).then(updateToken(token));277}278279const code = options.highlight(token.text, token.lang);280if (code instanceof Promise) {281throw 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.');282}283updateToken(token)(code);284},285renderer: {286code({ text, lang, escaped }: marked.Tokens.Code) {287const classAttr = lang288? ` class="language-${escape(lang)}"`289: '';290text = text.replace(/\n$/, '');291return `<pre><code${classAttr}>${escaped ? text : escape(text, true)}\n</code></pre>`;292},293},294};295}296297function updateToken(token: any) {298return (code: string) => {299if (typeof code === 'string' && code !== token.text) {300token.escaped = true;301token.text = code;302}303};304}305306// copied from marked helpers307const escapeTest = /[&<>"']/;308const escapeReplace = new RegExp(escapeTest.source, 'g');309const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;310const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');311const escapeReplacement: Record<string, string> = {312'&': '&',313'<': '<',314'>': '>',315'"': '"',316[`'`]: ''',317};318const getEscapeReplacement = (ch: string) => escapeReplacement[ch];319function escape(html: string, encode?: boolean) {320if (encode) {321if (escapeTest.test(html)) {322return html.replace(escapeReplace, getEscapeReplacement);323}324} else {325if (escapeTestNoEncode.test(html)) {326return html.replace(escapeReplaceNoEncode, getEscapeReplacement);327}328}329330return html;331}332}333334335