Skip to content

Commit

Permalink
Use insane with notebook markdown content (#131134)
Browse files Browse the repository at this point in the history
Runs insane against markdown content. Also requires hooking up a way for renderers to detect if the workspace is trusted or not
  • Loading branch information
mjbvz authored Aug 18, 2021
1 parent db37686 commit 3866c35
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 17 deletions.
48 changes: 45 additions & 3 deletions extensions/markdown-language-features/notebook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,48 @@
*--------------------------------------------------------------------------------------------*/

const MarkdownIt = require('markdown-it');
const insane = require('insane');
import type { InsaneOptions } from 'insane';

function _extInsaneOptions(opts: InsaneOptions, allowedAttributesForAll: string[]): InsaneOptions {
const allowedAttributes: Record<string, string[]> = opts.allowedAttributes ?? {};
if (opts.allowedTags) {
for (const tag of opts.allowedTags) {
let array = allowedAttributes[tag];
if (!array) {
array = allowedAttributesForAll;
} else {
array = array.concat(allowedAttributesForAll);
}
allowedAttributes[tag] = array;
}
}

return { ...opts, allowedAttributes };
}

export function activate() {
const insaneOptions: InsaneOptions = _extInsaneOptions({
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'],
allowedAttributes: {
'a': ['href', 'x-dispatch'],
'button': ['data-href', 'x-dispatch'],
'img': ['src'],
'input': ['type', 'placeholder', 'checked', 'required'],
'label': ['for'],
'select': ['required'],
'span': ['data-command', 'role'],
'textarea': ['name', 'placeholder', 'required'],
},
allowedSchemes: ['http', 'https']
}, [
'align',
'class',
'id',
'style',
'aria-hidden',
]);

export function activate(ctx: { workspace: { isTrusted: boolean } }) {
let markdownIt = new MarkdownIt({
html: true
});
Expand Down Expand Up @@ -172,8 +212,10 @@ export function activate() {
} else {
previewNode.classList.remove('emptyMarkdownCell');

const rendered = markdownIt.render(text);
previewNode.innerHTML = rendered;
const unsanitizedRenderedMarkdown = markdownIt.render(text);
previewNode.innerHTML = ctx.workspace.isTrusted
? unsanitizedRenderedMarkdown
: insane(unsanitizedRenderedMarkdown, insaneOptions);
}
},
extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {
Expand Down
19 changes: 19 additions & 0 deletions extensions/markdown-language-features/notebook/insane.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

declare module 'insane' {
export interface InsaneOptions {
readonly allowedSchemes?: readonly string[],
readonly allowedTags?: readonly string[],
readonly allowedAttributes?: { readonly [key: string]: string[] },
readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean,
}

export function insane(
html: string,
options?: InsaneOptions,
strict?: boolean,
): string;
}
1 change: 1 addition & 0 deletions extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@
},
"dependencies": {
"highlight.js": "^10.4.1",
"insane": "^2.6.2",
"markdown-it": "^12.0.3",
"markdown-it-front-matter": "^0.2.1",
"vscode-extension-telemetry": "0.2.6",
Expand Down
18 changes: 18 additions & 0 deletions extensions/markdown-language-features/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,34 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==

[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/assignment/-/assignment-2.0.0.tgz#ffd17b21bf5d6b22e777b989681a815456a3dd3e"
integrity sha1-/9F7Ib9dayLnd7mJaBqBVFaj3T4=

entities@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==

[email protected]:
version "0.5.0"
resolved "https://registry.yarnpkg.com/he/-/he-0.5.0.tgz#2c05ffaef90b68e860f3fd2b54ef580989277ee2"
integrity sha1-LAX/rvkLaOhg8/0rVO9YCYknfuI=

highlight.js@*, highlight.js@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==

insane@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/insane/-/insane-2.6.2.tgz#c2ab68bb3e006ab451560d1b446917329c0a8120"
integrity sha1-wqtouz4AarRRVg0bRGkXMpwKgSA=
dependencies:
assignment "2.0.0"
he "0.5.0"

linkify-it@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { asWebviewUri } from 'vs/workbench/api/common/shared/webview';
import { CellEditState, ICellOutputViewModel, ICommonCellInfo, ICommonNotebookEditor, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IGenericCellViewModel, IInsetRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads';
Expand Down Expand Up @@ -102,6 +103,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
) {
super();

Expand All @@ -127,6 +129,13 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
return Promise.resolve(true);
};
}

this._register(workspaceTrustManagementService.onDidChangeTrust(e => {
this._sendMessageToWebview({
type: 'updateWorkspaceTrust',
isTrusted: e,
});
}));
}

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

private generateContent(baseUrl: string) {
const renderersData = this.getRendererData();
const preloadScript = preloadsScriptStr(
this.options,
{ dragAndDropEnabled: this.options.dragAndDropEnabled },
renderersData,
this.workspaceTrustManagementService.isWorkspaceTrusted());

return html`
<html lang="en">
<head>
Expand Down Expand Up @@ -302,7 +317,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
</head>
<body style="overflow: hidden;">
<div id='container' class="widgetarea" style="position: absolute;width:100%;top: 0px"></div>
<script type="module">${preloadsScriptStr(this.options, { dragAndDropEnabled: this.options.dragAndDropEnabled }, renderersData)}</script>
<script type="module">${preloadScript}</script>
</body>
</html>`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ export interface INotebookOptionsMessage {
readonly options: PreloadOptions;
}

export interface INotebookUpdateWorkspaceTrust {
readonly type: 'updateWorkspaceTrust';
readonly isTrusted: boolean;
}

export type FromWebviewMessage = WebviewIntialized |
IDimensionMessage |
IMouseEnterMessage |
Expand Down Expand Up @@ -373,6 +378,7 @@ export type ToWebviewMessage = IClearMessage |
IUpdateSelectedMarkupCellsMessage |
IInitializeMarkupCells |
INotebookStylesMessage |
INotebookOptionsMessage;
INotebookOptionsMessage |
INotebookUpdateWorkspaceTrust;

export type AnyMessage = FromWebviewMessage | ToWebviewMessage;
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,18 @@ export interface PreloadOptions {
dragAndDropEnabled: boolean;
}

interface PreloadContext {
readonly style: PreloadStyles;
readonly options: PreloadOptions;
readonly rendererData: readonly RendererMetadata[];
readonly isWorkspaceTrusted: boolean;
}

declare function __import(path: string): Promise<any>;

async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, rendererData: readonly RendererMetadata[]) {
let currentOptions = options;
async function webviewPreloads(ctx: PreloadContext) {
let currentOptions = ctx.options;
let isWorkspaceTrusted = ctx.isWorkspaceTrusted;

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

interface ScriptModule {
Expand Down Expand Up @@ -207,7 +216,7 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
if (entry.target.id === observedElementInfo.id && entry.contentRect) {
if (observedElementInfo.output) {
if (entry.contentRect.height !== 0) {
entry.target.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`;
entry.target.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`;
} else {
entry.target.style.padding = `0px`;
}
Expand Down Expand Up @@ -549,12 +558,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
if (offsetHeight !== 0 && cps.padding === '0px') {
// we set padding to zero if the output height is zero (then we can have a zero-height output DOM node)
// thus we need to ensure the padding is accounted when updating the init height of the output
dimensionUpdater.updateHeight(outputId, offsetHeight + style.outputNodePadding * 2, {
dimensionUpdater.updateHeight(outputId, offsetHeight + ctx.style.outputNodePadding * 2, {
isOutput: true,
init: true,
});

outputNode.style.padding = `${style.outputNodePadding}px 0 ${style.outputNodePadding}px 0`;
outputNode.style.padding = `${ctx.style.outputNodePadding}px 0 ${ctx.style.outputNodePadding}px 0`;
} else {
dimensionUpdater.updateHeight(outputId, outputNode.offsetHeight, {
isOutput: true,
Expand Down Expand Up @@ -657,6 +666,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
currentOptions = event.data.options;
viewModel.toggleDragDropEnabled(currentOptions.dragAndDropEnabled);
break;

case 'updateWorkspaceTrust': {
isWorkspaceTrusted = event.data.isTrusted;
viewModel.rerenderMarkupCells();
break;
}
}
});

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

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

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

constructor() {
for (const renderer of rendererData) {
for (const renderer of ctx.rendererData) {
this._renderers.set(renderer.id, new Renderer(renderer, async (extensionId) => {
const ext = this._renderers.get(extensionId);
if (!ext) {
Expand Down Expand Up @@ -936,6 +954,12 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
cell?.unhide();
}

public rerenderMarkupCells() {
for (const cell of this._markupCells.values()) {
cell.rerender();
}
}

private getExpectedMarkupCell(id: string): MarkupCell | undefined {
const cell = this._markupCells.get(id);
if (!cell) {
Expand Down Expand Up @@ -1166,6 +1190,10 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re
}
}

public rerender() {
this.updateContentAndRender(this._content);
}

public hide() {
this.element.style.visibility = 'hidden';
}
Expand Down Expand Up @@ -1409,14 +1437,18 @@ export interface RendererMetadata {
readonly messaging: boolean;
}

export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[]) {
// TS will try compiling `import()` in webviePreloads, so use an helper function instead
export function preloadsScriptStr(styleValues: PreloadStyles, options: PreloadOptions, renderers: readonly RendererMetadata[], isWorkspaceTrusted: boolean) {
const ctx: PreloadContext = {
style: styleValues,
options,
rendererData: renderers,
isWorkspaceTrusted
};
// TS will try compiling `import()` in webviewPreloads, so use an helper function instead
// of using `import(...)` directly
return `
const __import = (x) => import(x);
(${webviewPreloads})(
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(styleValues))}")),
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(options))}")),
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(renderers))}"))
JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}"))
)\n//# sourceURL=notebookWebviewPreloads.js\n`;
}

0 comments on commit 3866c35

Please sign in to comment.