Skip to content

Commit 07eee2b

Browse files
authored
Merge pull request microsoft#256773 from mjbvz/repulsive-ape
More clean up for markdown renderer interfaces
2 parents b85658c + 10b0fa7 commit 07eee2b

File tree

7 files changed

+55
-50
lines changed

7 files changed

+55
-50
lines changed

src/vs/base/browser/domSanitize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ function hookDomPurifyHrefAndSrcSanitizer(allowedLinkProtocols: readonly string[
153153
return toDisposable(() => dompurify.removeHook('afterSanitizeAttributes'));
154154
}
155155

156-
export interface SanitizeOptions {
156+
export interface DomSanitizerConfig {
157157
/**
158158
* Configured the allowed html tags.
159159
*/
@@ -209,7 +209,7 @@ const defaultDomPurifyConfig = Object.freeze({
209209
*
210210
* @returns A sanitized string of html.
211211
*/
212-
export function sanitizeHtml(untrusted: string, config?: SanitizeOptions): TrustedHTML {
212+
export function sanitizeHtml(untrusted: string, config?: DomSanitizerConfig): TrustedHTML {
213213
const store = new DisposableStore();
214214
try {
215215
const resolvedConfig: dompurify.Config = { ...defaultDomPurifyConfig };
@@ -258,6 +258,6 @@ export function sanitizeHtml(untrusted: string, config?: SanitizeOptions): Trust
258258
/**
259259
* Sanitizes the given `value` and reset the given `node` with it.
260260
*/
261-
export function safeSetInnerHtml(node: HTMLElement, untrusted: string, config?: SanitizeOptions): void {
261+
export function safeSetInnerHtml(node: HTMLElement, untrusted: string, config?: DomSanitizerConfig): void {
262262
node.innerHTML = sanitizeHtml(untrusted, config) as any;
263263
}

src/vs/base/browser/markdownRenderer.ts

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,40 @@ import { StandardKeyboardEvent } from './keyboardEvent.js';
2626
import { StandardMouseEvent } from './mouseEvent.js';
2727
import { renderLabelWithIcons } from './ui/iconLabel/iconLabels.js';
2828

29-
export interface MarkedOptions extends Readonly<Omit<marked.MarkedOptions, 'extensions' | 'baseUrl'>> {
30-
readonly markedExtensions?: marked.MarkedExtension[];
31-
}
32-
29+
/**
30+
* Options for the rendering of markdown with {@link renderMarkdown}.
31+
*/
3332
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
3433
readonly codeBlockRenderer?: (languageId: string, value: string) => Promise<HTMLElement>;
3534
readonly codeBlockRendererSync?: (languageId: string, value: string, raw?: string) => HTMLElement;
3635
readonly asyncRenderCallback?: () => void;
36+
3737
readonly fillInIncompleteTokens?: boolean;
38-
readonly remoteImageIsAllowed?: (uri: URI) => boolean;
3938

40-
readonly sanitizerOptions?: ISanitizerOptions;
39+
readonly sanitizerConfig?: MarkdownSanitizerConfig;
4140

42-
readonly markedOptions?: MarkedOptions;
41+
readonly markedOptions?: MarkdownRendererMarkedOptions;
42+
readonly markedExtensions?: marked.MarkedExtension[];
4343
}
4444

45-
export interface ISanitizerOptions {
45+
/**
46+
* Subset of options passed to `Marked` for rendering markdown.
47+
*/
48+
export interface MarkdownRendererMarkedOptions {
49+
readonly gfm?: boolean;
50+
readonly breaks?: boolean;
51+
}
52+
53+
export interface MarkdownSanitizerConfig {
4654
readonly replaceWithPlaintext?: boolean;
4755
readonly allowedTags?: {
4856
readonly override: readonly string[];
4957
};
5058
readonly customAttrSanitizer?: (attrName: string, attrValue: string) => boolean | string;
51-
readonly allowedProductProtocols?: readonly string[];
59+
readonly allowedLinkSchemes?: {
60+
readonly augment: readonly string[];
61+
};
62+
readonly remoteImageIsAllowed?: (uri: URI) => boolean;
5263
}
5364

5465
const defaultMarkedRenderers = Object.freeze({
@@ -110,14 +121,14 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
110121
const disposables = new DisposableStore();
111122
let isDisposed = false;
112123

113-
const markedInstance = new marked.Marked(...(options.markedOptions?.markedExtensions ?? []));
124+
const markedInstance = new marked.Marked(...(options.markedExtensions ?? []));
114125
const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown);
115126
const value = preprocessMarkdownString(markdown);
116127

117128
let renderedMarkdown: string;
118129
if (options.fillInIncompleteTokens) {
119130
// The defaults are applied by parse but not lexer()/parser(), and they need to be present
120-
const opts: MarkedOptions = {
131+
const opts: marked.MarkedOptions = {
121132
...markedInstance.defaults,
122133
...options.markedOptions,
123134
renderer
@@ -136,12 +147,12 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
136147
}
137148

138149
const htmlParser = new DOMParser();
139-
const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, renderedMarkdown) as unknown as string, 'text/html');
150+
const markdownHtmlDoc = htmlParser.parseFromString(sanitizeRenderedMarkdown(renderedMarkdown, markdown.isTrusted ?? false, options.sanitizerConfig) as unknown as string, 'text/html');
140151

141152
rewriteRenderedLinks(markdown, options, markdownHtmlDoc.body);
142153

143154
const element = target ?? document.createElement('div');
144-
element.innerHTML = sanitizeRenderedMarkdown({ isTrusted: markdown.isTrusted, ...options.sanitizerOptions }, markdownHtmlDoc.body.innerHTML) as unknown as string;
155+
element.innerHTML = sanitizeRenderedMarkdown(markdownHtmlDoc.body.innerHTML, markdown.isTrusted ?? false, options.sanitizerConfig) as unknown as string;
145156

146157
if (codeBlocks.length > 0) {
147158
Promise.all(codeBlocks).then((tuples) => {
@@ -222,9 +233,9 @@ function rewriteRenderedLinks(markdown: IMarkdownString, options: MarkdownRender
222233

223234
el.setAttribute('src', massageHref(markdown, href, true));
224235

225-
if (options.remoteImageIsAllowed) {
236+
if (options.sanitizerConfig?.remoteImageIsAllowed) {
226237
const uri = URI.parse(href);
227-
if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.remoteImageIsAllowed(uri)) {
238+
if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.sanitizerConfig.remoteImageIsAllowed(uri)) {
228239
el.replaceWith(DOM.$('', undefined, el.outerHTML));
229240
}
230241
}
@@ -280,7 +291,7 @@ function createMarkdownRenderer(marked: marked.Marked, options: MarkdownRenderOp
280291
// Note: we always pass the output through dompurify after this so that we don't rely on
281292
// marked for real sanitization.
282293
renderer.html = ({ text }) => {
283-
if (options.sanitizerOptions?.replaceWithPlaintext) {
294+
if (options.sanitizerConfig?.replaceWithPlaintext) {
284295
return escape(text);
285296
}
286297

@@ -401,17 +412,15 @@ function resolveWithBaseUri(baseUri: URI, href: string): string {
401412
}
402413
}
403414

404-
interface IInternalSanitizerOptions extends ISanitizerOptions {
405-
readonly isTrusted?: boolean | MarkdownStringTrustedOptions;
406-
}
407415

408416
const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
409417

410418
function sanitizeRenderedMarkdown(
411-
options: IInternalSanitizerOptions,
412419
renderedMarkdown: string,
420+
isTrusted: boolean | MarkdownStringTrustedOptions,
421+
options: MarkdownSanitizerConfig = {},
413422
): TrustedHTML {
414-
const sanitizerConfig = getSanitizerOptions(options);
423+
const sanitizerConfig = getSanitizerOptions(isTrusted, options);
415424
return domSanitize.sanitizeHtml(renderedMarkdown, sanitizerConfig);
416425
}
417426

@@ -448,7 +457,7 @@ export const allowedMarkdownHtmlAttributes = [
448457
'class',
449458
];
450459

451-
function getSanitizerOptions(options: IInternalSanitizerOptions): domSanitize.SanitizeOptions {
460+
function getSanitizerOptions(isTrusted: boolean | MarkdownStringTrustedOptions, options: MarkdownSanitizerConfig): domSanitize.DomSanitizerConfig {
452461
const allowedLinkSchemes = [
453462
Schemas.http,
454463
Schemas.https,
@@ -460,12 +469,12 @@ function getSanitizerOptions(options: IInternalSanitizerOptions): domSanitize.Sa
460469
Schemas.vscodeNotebookCell
461470
];
462471

463-
if (options.isTrusted) {
472+
if (isTrusted) {
464473
allowedLinkSchemes.push(Schemas.command);
465474
}
466475

467-
if (options.allowedProductProtocols) {
468-
allowedLinkSchemes.push(...options.allowedProductProtocols);
476+
if (options.allowedLinkSchemes?.augment) {
477+
allowedLinkSchemes.push(...options.allowedLinkSchemes.augment);
469478
}
470479

471480
return {
@@ -606,7 +615,7 @@ export function renderAsPlaintext(str: IMarkdownString | string, options?: {
606615
}
607616

608617
const html = marked.parse(value, { async: false, renderer: options?.includeCodeBlocksFences ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value });
609-
return sanitizeRenderedMarkdown({ isTrusted: false }, html)
618+
return sanitizeRenderedMarkdown(html, /* isTrusted */ false, {})
610619
.toString()
611620
.replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m)
612621
.trim();

src/vs/base/browser/ui/button/button.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { localize } from '../../../../nls.js';
2424
import type { IManagedHover } from '../hover/hover.js';
2525
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
2626
import { IActionProvider } from '../dropdown/dropdown.js';
27-
import { safeSetInnerHtml, SanitizeOptions } from '../../domSanitize.js';
27+
import { safeSetInnerHtml, DomSanitizerConfig } from '../../domSanitize.js';
2828

2929
export interface IButtonOptions extends Partial<IButtonStyles> {
3030
readonly title?: boolean | string;
@@ -79,7 +79,7 @@ export interface IButtonWithDescription extends IButton {
7979
}
8080

8181
// Only allow a very limited set of inline html tags
82-
const buttonSanitizerOptions = Object.freeze<SanitizeOptions>({
82+
const buttonSanitizerConfig = Object.freeze<DomSanitizerConfig>({
8383
allowedTags: {
8484
override: ['b', 'i', 'u', 'code', 'span'],
8585
},
@@ -253,7 +253,7 @@ export class Button extends Disposable implements IButton {
253253
// Don't include outer `<p>`
254254
const root = rendered.element.querySelector('p')?.innerHTML;
255255
if (root) {
256-
safeSetInnerHtml(labelElement, root, buttonSanitizerOptions);
256+
safeSetInnerHtml(labelElement, root, buttonSanitizerConfig);
257257
} else {
258258
reset(labelElement);
259259
}
@@ -654,7 +654,7 @@ export class ButtonWithIcon extends Button {
654654

655655
const root = rendered.element.querySelector('p')?.innerHTML;
656656
if (root) {
657-
safeSetInnerHtml(this._mdlabelElement, root, buttonSanitizerOptions);
657+
safeSetInnerHtml(this._mdlabelElement, root, buttonSanitizerConfig);
658658
} else {
659659
reset(this._mdlabelElement);
660660
}

src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from '../../../../../base/browser/dom.js';
7-
import { allowedMarkdownHtmlAttributes, MarkedOptions } from '../../../../../base/browser/markdownRenderer.js';
7+
import { allowedMarkdownHtmlAttributes, MarkdownRendererMarkedOptions } from '../../../../../base/browser/markdownRenderer.js';
88
import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js';
99
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
1010
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
@@ -118,16 +118,13 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
118118
: [];
119119

120120
// Don't set to 'false' for responses, respect defaults
121-
const markedOpts: MarkedOptions = isRequestVM(element) ? {
121+
const markedOpts: MarkdownRendererMarkedOptions = isRequestVM(element) ? {
122122
gfm: true,
123123
breaks: true,
124-
markedExtensions,
125-
} : {
126-
markedExtensions,
127-
};
124+
} : {};
128125

129126
const result = this._register(renderer.render(markdown.content, {
130-
sanitizerOptions: MarkedKatexSupport.getSanitizerOptions({
127+
sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({
131128
allowedTags: allowedChatMarkdownHtmlTags,
132129
allowedAttributes: allowedMarkdownHtmlAttributes,
133130
}),
@@ -240,6 +237,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
240237
},
241238
asyncRenderCallback: () => this._onDidChangeHeight.fire(),
242239
markedOptions: markedOpts,
240+
markedExtensions,
243241
}, this.domNode));
244242

245243
const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer);

src/vs/workbench/contrib/chat/browser/chatMarkdownRenderer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ export class ChatMarkdownRenderer extends MarkdownRenderer {
7373
override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, outElement?: HTMLElement): IMarkdownRenderResult {
7474
options = {
7575
...options,
76-
remoteImageIsAllowed: (_uri) => false,
77-
sanitizerOptions: {
76+
sanitizerConfig: {
7877
replaceWithPlaintext: true,
7978
allowedTags: {
8079
override: allowedChatMarkdownHtmlTags,
8180
},
82-
...options?.sanitizerOptions,
83-
allowedProductProtocols: [product.urlProtocol]
81+
...options?.sanitizerConfig,
82+
allowedLinkSchemes: { augment: [product.urlProtocol] },
83+
remoteImageIsAllowed: (_uri) => false,
8484
}
8585
};
8686

src/vs/workbench/contrib/markdown/browser/markedKatexSupport.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { importAMDNodeModule, resolveAmdNodeModulePath } from '../../../../amdX.js';
7-
import { ISanitizerOptions } from '../../../../base/browser/markdownRenderer.js';
7+
import { MarkdownSanitizerConfig } from '../../../../base/browser/markdownRenderer.js';
88
import { CodeWindow } from '../../../../base/browser/window.js';
99
import { Lazy } from '../../../../base/common/lazy.js';
1010
import type * as marked from '../../../../base/common/marked/marked.js';
@@ -14,7 +14,7 @@ export class MarkedKatexSupport {
1414
public static getSanitizerOptions(baseConfig: {
1515
readonly allowedTags: readonly string[];
1616
readonly allowedAttributes: readonly string[];
17-
}): ISanitizerOptions {
17+
}): MarkdownSanitizerConfig {
1818
return {
1919
allowedTags: {
2020
override: [

src/vs/workbench/contrib/markdown/test/browser/markdownKatexSupport.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ suite('Markdown Katex Support Test', () => {
1717
async function renderMarkdownWithKatex(str: string) {
1818
const katex = await MarkedKatexSupport.loadExtension(getWindow(document), {});
1919
const rendered = store.add(renderMarkdown(new MarkdownString(str), {
20-
sanitizerOptions: MarkedKatexSupport.getSanitizerOptions({
20+
sanitizerConfig: MarkedKatexSupport.getSanitizerOptions({
2121
allowedTags: basicMarkupHtmlTags,
2222
allowedAttributes: defaultAllowedAttrs,
2323
}),
24-
markedOptions: {
25-
markedExtensions: [katex],
26-
}
24+
markedExtensions: [katex],
2725
}));
2826
return rendered;
2927
}

0 commit comments

Comments
 (0)