Skip to content

Commit 3866c35

Browse files
authored
Use insane with notebook markdown content (#131134)
Runs insane against markdown content. Also requires hooking up a way for renderers to detect if the workspace is trusted or not
1 parent db37686 commit 3866c35

File tree

7 files changed

+150
-17
lines changed

7 files changed

+150
-17
lines changed

extensions/markdown-language-features/notebook/index.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,48 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
const MarkdownIt = require('markdown-it');
7+
const insane = require('insane');
8+
import type { InsaneOptions } from 'insane';
9+
10+
function _extInsaneOptions(opts: InsaneOptions, allowedAttributesForAll: string[]): InsaneOptions {
11+
const allowedAttributes: Record<string, string[]> = opts.allowedAttributes ?? {};
12+
if (opts.allowedTags) {
13+
for (const tag of opts.allowedTags) {
14+
let array = allowedAttributes[tag];
15+
if (!array) {
16+
array = allowedAttributesForAll;
17+
} else {
18+
array = array.concat(allowedAttributesForAll);
19+
}
20+
allowedAttributes[tag] = array;
21+
}
22+
}
23+
24+
return { ...opts, allowedAttributes };
25+
}
726

8-
export function activate() {
27+
const insaneOptions: InsaneOptions = _extInsaneOptions({
28+
allowedTags: ['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'],
29+
allowedAttributes: {
30+
'a': ['href', 'x-dispatch'],
31+
'button': ['data-href', 'x-dispatch'],
32+
'img': ['src'],
33+
'input': ['type', 'placeholder', 'checked', 'required'],
34+
'label': ['for'],
35+
'select': ['required'],
36+
'span': ['data-command', 'role'],
37+
'textarea': ['name', 'placeholder', 'required'],
38+
},
39+
allowedSchemes: ['http', 'https']
40+
}, [
41+
'align',
42+
'class',
43+
'id',
44+
'style',
45+
'aria-hidden',
46+
]);
47+
48+
export function activate(ctx: { workspace: { isTrusted: boolean } }) {
949
let markdownIt = new MarkdownIt({
1050
html: true
1151
});
@@ -172,8 +212,10 @@ export function activate() {
172212
} else {
173213
previewNode.classList.remove('emptyMarkdownCell');
174214

175-
const rendered = markdownIt.render(text);
176-
previewNode.innerHTML = rendered;
215+
const unsanitizedRenderedMarkdown = markdownIt.render(text);
216+
previewNode.innerHTML = ctx.workspace.isTrusted
217+
? unsanitizedRenderedMarkdown
218+
: insane(unsanitizedRenderedMarkdown, insaneOptions);
177219
}
178220
},
179221
extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
declare module 'insane' {
7+
export interface InsaneOptions {
8+
readonly allowedSchemes?: readonly string[],
9+
readonly allowedTags?: readonly string[],
10+
readonly allowedAttributes?: { readonly [key: string]: string[] },
11+
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
12+
}
13+
14+
export function insane(
15+
html: string,
16+
options?: InsaneOptions,
17+
strict?: boolean,
18+
): string;
19+
}

extensions/markdown-language-features/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@
353353
},
354354
"dependencies": {
355355
"highlight.js": "^10.4.1",
356+
"insane": "^2.6.2",
356357
"markdown-it": "^12.0.3",
357358
"markdown-it-front-matter": "^0.2.1",
358359
"vscode-extension-telemetry": "0.2.6",

extensions/markdown-language-features/yarn.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,34 @@ argparse@^2.0.1:
3636
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
3737
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
3838

39+
40+
version "2.0.0"
41+
resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e"
42+
integrity sha1-/9F7Ib9dayLnd7mJaBqBVFaj3T4=
43+
3944
entities@~2.1.0:
4045
version "2.1.0"
4146
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
4247
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
4348

49+
50+
version "0.5.0"
51+
resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2"
52+
integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI=
53+
4454
highlight.js@*, highlight.js@^10.4.1:
4555
version "10.4.1"
4656
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
4757
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
4858

59+
insane@^2.6.2:
60+
version "2.6.2"
61+
resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.2.tgz#c2ab68bb3e006ab451560d1b446917329c0a8120"
62+
integrity sha1-wqtouz4AarRRVg0bRGkXMpwKgSA=
63+
dependencies:
64+
assignment "2.0.0"
65+
he "0.5.0"
66+
4967
linkify-it@^3.0.1:
5068
version "3.0.2"
5169
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"

src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { IFileService } from 'vs/platform/files/common/files';
2424
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
2525
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2626
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
27+
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
2728
import { asWebviewUri } from 'vs/workbench/api/common/shared/webview';
2829
import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
2930
import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads';
@@ -102,6 +103,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
102103
@IMenuService private readonly menuService: IMenuService,
103104
@IContextKeyService private readonly contextKeyService: IContextKeyService,
104105
@ITelemetryService private readonly telemetryService: ITelemetryService,
106+
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
105107
) {
106108
super();
107109

@@ -127,6 +129,13 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
127129
return Promise.resolve(true);
128130
};
129131
}
132+
133+
this._register(workspaceTrustManagementService.onDidChangeTrust(e => {
134+
this._sendMessageToWebview({
135+
type: 'updateWorkspaceTrust',
136+
isTrusted: e,
137+
});
138+
}));
130139
}
131140

132141
updateOptions(options: {
@@ -182,6 +191,12 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
182191

183192
private generateContent(baseUrl: string) {
184193
const renderersData = this.getRendererData();
194+
const preloadScript = preloadsScriptStr(
195+
this.options,
196+
{ dragAndDropEnabled: this.options.dragAndDropEnabled },
197+
renderersData,
198+
this.workspaceTrustManagementService.isWorkspaceTrusted());
199+
185200
return html`
186201
<html lang="en">
187202
<head>
@@ -302,7 +317,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
302317
</head>
303318
<body style="overflow: hidden;">
304319
<div id='container' class="widgetarea" style="position: absolute;width:100%;top: 0px"></div>
305-
<script type="module">${preloadsScriptStr(this.options, { dragAndDropEnabled: this.options.dragAndDropEnabled }, renderersData)}</script>
320+
<script type="module">${preloadScript}</script>
306321
</body>
307322
</html>`;
308323
}

src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,11 @@ export interface INotebookOptionsMessage {
326326
readonly options: PreloadOptions;
327327
}
328328

329+
export interface INotebookUpdateWorkspaceTrust {
330+
readonly type: 'updateWorkspaceTrust';
331+
readonly isTrusted: boolean;
332+
}
333+
329334
export type FromWebviewMessage = WebviewIntialized |
330335
IDimensionMessage |
331336
IMouseEnterMessage |
@@ -373,6 +378,7 @@ export type ToWebviewMessage = IClearMessage |
373378
IUpdateSelectedMarkupCellsMessage |
374379
IInitializeMarkupCells |
375380
INotebookStylesMessage |
376-
INotebookOptionsMessage;
381+
INotebookOptionsMessage |
382+
INotebookUpdateWorkspaceTrust;
377383

378384
export type AnyMessage = FromWebviewMessage | ToWebviewMessage;

src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,18 @@ export interface PreloadOptions {
4343
dragAndDropEnabled: boolean;
4444
}
4545

46+
interface PreloadContext {
47+
readonly style: PreloadStyles;
48+
readonly options: PreloadOptions;
49+
readonly rendererData: readonly RendererMetadata[];
50+
readonly isWorkspaceTrusted: boolean;
51+
}
52+
4653
declare function __import(path: string): Promise<any>;
4754

48-
async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, rendererData: readonly RendererMetadata[]) {
49-
let currentOptions = options;
55+
async function webviewPreloads(ctx: PreloadContext) {
56+
let currentOptions = ctx.options;
57+
let isWorkspaceTrusted = ctx.isWorkspaceTrusted;
5058

5159
const acquireVsCodeApi = globalThis.acquireVsCodeApi;
5260
const vscode = acquireVsCodeApi();
@@ -133,6 +141,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
133141
getRenderer(id: string): Promise<any | undefined>;
134142
postMessage?(message: unknown): void;
135143
onDidReceiveMessage?: Event<unknown>;
144+
readonly workspace: { readonly isTrusted: boolean };
136145
}
137146

138147
interface ScriptModule {
@@ -207,7 +216,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
207216
if (entry.target.id === observedElementInfo.id && entry.contentRect) {
208217
if (observedElementInfo.output) {
209218
if (entry.contentRect.height !== 0) {
210-
entry.target.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`;
219+
entry.target.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`;
211220
} else {
212221
entry.target.style.padding = `0px`;
213222
}
@@ -549,12 +558,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
549558
if (offsetHeight !== 0 && cps.padding === '0px') {
550559
// we set padding to zero if the output height is zero (then we can have a zero-height output DOM node)
551560
// thus we need to ensure the padding is accounted when updating the init height of the output
552-
dimensionUpdater.updateHeight(outputId, offsetHeight + style.outputNodePadding * 2, {
561+
dimensionUpdater.updateHeight(outputId, offsetHeight + ctx.style.outputNodePadding * 2, {
553562
isOutput: true,
554563
init: true,
555564
});
556565

557-
outputNode.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`;
566+
outputNode.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`;
558567
} else {
559568
dimensionUpdater.updateHeight(outputId, outputNode.offsetHeight, {
560569
isOutput: true,
@@ -657,6 +666,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
657666
currentOptions = event.data.options;
658667
viewModel.toggleDragDropEnabled(currentOptions.dragAndDropEnabled);
659668
break;
669+
670+
case 'updateWorkspaceTrust': {
671+
isWorkspaceTrusted = event.data.isTrusted;
672+
viewModel.rerenderMarkupCells();
673+
break;
674+
}
660675
}
661676
});
662677

@@ -700,6 +715,9 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
700715
// TODO: This is async so that we can return a promise to the API in the future.
701716
// Currently the API is always resolved before we call `createRendererContext`.
702717
getRenderer: async (id: string) => renderers.getRenderer(id)?.api,
718+
workspace: {
719+
get isTrusted() { return isWorkspaceTrusted; }
720+
}
703721
};
704722

705723
if (messaging) {
@@ -722,7 +740,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
722740

723741
// Squash any errors extends errors. They won't prevent the renderer
724742
// itself from working, so just log them.
725-
await Promise.all(rendererData
743+
await Promise.all(ctx.rendererData
726744
.filter(d => d.extends === this.data.id)
727745
.map(d => this.loadExtension(d.id).catch(console.error)),
728746
);
@@ -807,7 +825,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
807825
private readonly _renderers = new Map</* id */ string, Renderer>();
808826

809827
constructor() {
810-
for (const renderer of rendererData) {
828+
for (const renderer of ctx.rendererData) {
811829
this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => {
812830
const ext = this._renderers.get(extensionId);
813831
if (!ext) {
@@ -936,6 +954,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
936954
cell?.unhide();
937955
}
938956

957+
public rerenderMarkupCells() {
958+
for (const cell of this._markupCells.values()) {
959+
cell.rerender();
960+
}
961+
}
962+
939963
private getExpectedMarkupCell(id: string): MarkupCell | undefined {
940964
const cell = this._markupCells.get(id);
941965
if (!cell) {
@@ -1166,6 +1190,10 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
11661190
}
11671191
}
11681192

1193+
public rerender() {
1194+
this.updateContentAndRender(this._content);
1195+
}
1196+
11691197
public hide() {
11701198
this.element.style.visibility = 'hidden';
11711199
}
@@ -1409,14 +1437,18 @@ export interface RendererMetadata {
14091437
readonly messaging: boolean;
14101438
}
14111439

1412-
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[]) {
1413-
// TS will try compiling `import()` in webviePreloads, so use an helper function instead
1440+
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[], isWorkspaceTrusted: boolean) {
1441+
const ctx: PreloadContext = {
1442+
style: styleValues,
1443+
options,
1444+
rendererData: renderers,
1445+
isWorkspaceTrusted
1446+
};
1447+
// TS will try compiling `import()` in webviewPreloads, so use an helper function instead
14141448
// of using `import(...)` directly
14151449
return `
14161450
const __import = (x) => import(x);
14171451
(${webviewPreloads})(
1418-
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(styleValues))}")),
1419-
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(options))}")),
1420-
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(renderers))}"))
1452+
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}"))
14211453
)\n//# sourceURL=notebookWebviewPreloads.js\n`;
14221454
}

0 commit comments

Comments
 (0)