Better notebook markup renderer api (#121882)

* Better notebook markup renderer api

For #121256

- Use js modules for notebook output renderers
- Rename apis from `markdown` to `markup`
- Use imports and exports for apis instead of globals for apis
- Use esbuild instead of webpack so we can emit js modules
- Clearly split top level markup renderes from renderers that extend other renderers

* Use constant instead of comment for replacement
This commit is contained in:
Matt Bierner 2021-04-26 16:30:34 -07:00 committed by GitHub
parent 495d162a4e
commit 4cb27d2ec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 295 additions and 201 deletions

View file

@ -8,6 +8,7 @@ require('events').EventEmitter.defaultMaxListeners = 100;
const gulp = require('gulp');
const path = require('path');
const child_process = require('child_process');
const nodeUtil = require('util');
const es = require('event-stream');
const filter = require('gulp-filter');
@ -203,30 +204,38 @@ gulp.task(compileExtensionsBuildLegacyTask);
// Additional projects to webpack. These typically build code for webviews
const webpackMediaConfigFiles = [
'markdown-language-features/webpack.config.js',
'markdown-language-features/webpack.notebook.js',
'notebook-markdown-extensions/webpack.notebook.js',
'simple-browser/webpack.config.js',
];
const compileExtensionMediaTask = task.define('compile-extension-media', () => webpackExtensionMedia(false));
// Additional projects to run esbuild on. These typically build code for webviews
const esbuildMediaScripts = [
'markdown-language-features/esbuild.js',
'notebook-markdown-extensions/esbuild.js',
];
const compileExtensionMediaTask = task.define('compile-extension-media', () => buildExtensionMedia(false));
gulp.task(compileExtensionMediaTask);
exports.compileExtensionMediaTask = compileExtensionMediaTask;
const watchExtensionMedia = task.define('watch-extension-media', () => webpackExtensionMedia(true));
const watchExtensionMedia = task.define('watch-extension-media', () => buildExtensionMedia(true));
gulp.task(watchExtensionMedia);
exports.watchExtensionMedia = watchExtensionMedia;
function webpackExtensionMedia(isWatch, outputRoot) {
const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => buildExtensionMedia(false, '.build/extensions'));
gulp.task(compileExtensionMediaBuildTask);
async function buildExtensionMedia(isWatch, outputRoot) {
const webpackConfigLocations = webpackMediaConfigFiles.map(p => {
return {
configPath: path.join(extensionsPath, p),
outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined
};
});
return webpackExtensions('packaging extension media', isWatch, webpackConfigLocations);
return Promise.all([
webpackExtensions('webpacking extension media', isWatch, webpackConfigLocations),
esbuildExtensions('esbuilding extension media', isWatch, outputRoot, esbuildMediaScripts.map(p => path.join(extensionsPath, p))),
]);
}
const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => webpackExtensionMedia(false, '.build/extensions'));
gulp.task(compileExtensionMediaBuildTask);
//#endregion
@ -336,4 +345,47 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) {
});
}
/**
* @param {string} taskName
* @param {boolean} isWatch
* @param {string | undefined} outputRoot
* @param {string} esbuildScripts
*/
async function esbuildExtensions(taskName, isWatch, outputRoot, esbuildScripts) {
function reporter(/** @type {string} */ stdError, /** @type {string} */script) {
const matches = (stdError || '').match(/\> (.+): error: (.+)?/g);
fancyLog(`Finished ${ansiColors.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`);
for (const match of matches || []) {
fancyLog.error(match);
}
}
const scripts = esbuildScripts.map(script => {
return new Promise((resolve, reject) => {
const args = [script];
if (isWatch) {
args.push('--watch');
}
if (outputRoot) {
args.push('--outputRoot', outputRoot);
}
const proc = child_process.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => {
if (error) {
return reject(error);
}
reporter(stderr, script);
if (stderr) {
return reject();
}
return resolve();
});
proc.stdout.on('data', (data) => {
fancyLog(`${ansiColors.green(taskName)}: ${data.toString('utf8')}`);
});
});
});
return Promise.all(scripts);
}

View file

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const esbuild = require('esbuild');
const args = process.argv.slice(2);
const isWatch = args.indexOf('--watch') >= 0;
let outputRoot = __dirname;
const outputRootIndex = args.indexOf('--outputRoot');
if (outputRootIndex >= 0) {
outputRoot = args[outputRootIndex + 1];
}
const outDir = path.join(outputRoot, 'notebook-out');
esbuild.build({
entryPoints: [
path.join(__dirname, 'notebook', 'index.ts'),
],
bundle: true,
minify: true,
sourcemap: false,
format: 'esm',
outdir: outDir,
platform: 'browser',
target: ['es2020'],
incremental: isWatch,
}).catch(() => process.exit(1));

View file

@ -3,31 +3,37 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as MarkdownIt from 'markdown-it';
const MarkdownIt = require('markdown-it');
declare const acquireNotebookRendererApi: any;
type extendMarkdownItFnType = (
(f: (md: MarkdownIt.MarkdownIt) => void) => void
);
(function () {
const markdownIt = new MarkdownIt({
export async function activate(ctx: {
dependencies: ReadonlyArray<{ entrypoint: string }>
}) {
let markdownIt = new MarkdownIt({
html: true
});
(globalThis as any).extendMarkdownIt = ((f: (md: MarkdownIt.MarkdownIt) => void) => {
f(markdownIt);
}) as extendMarkdownItFnType;
const notebook = acquireNotebookRendererApi('notebookCoreTestRenderer');
notebook.onDidCreateMarkdown(({ element, content }: any) => {
const rendered = markdownIt.render(content);
element.innerHTML = rendered;
// Insert styles into markdown preview shadow dom so that they are applied
for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) {
element.appendChild(markdownStyleNode.cloneNode(true));
// Should we load the deps before this point?
// Also could we await inside `renderMarkup`?
await Promise.all(ctx.dependencies.map(async (dep) => {
try {
const api = await import(dep.entrypoint);
if (api?.extendMarkdownIt) {
markdownIt = api.extendMarkdownIt(markdownIt);
}
} catch (e) {
console.error('Could not load markdown entryPoint', e);
}
});
}());
}));
return {
renderMarkup: (context: { element: HTMLElement, content: string }) => {
const rendered = markdownIt.render(context.content);
context.element.innerHTML = rendered;
// Insert styles into markdown preview shadow dom so that they are applied
for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) {
context.element.appendChild(markdownStyleNode.cloneNode(true));
}
}
};
}

View file

@ -3,6 +3,7 @@
"compilerOptions": {
"outDir": "./dist/",
"jsx": "react",
"module": "es2020",
"lib": [
"es2018",
"DOM",

View file

@ -40,11 +40,14 @@
}
},
"contributes": {
"notebookMarkdownRenderer": [
"notebookMarkupRenderers": [
{
"id": "markdownItRenderer",
"displayName": "Markdown it renderer",
"entrypoint": "./notebook-out/index.js"
"entrypoint": "./notebook-out/index.js",
"mimeTypes": [
"text/markdown"
]
}
],
"commands": [
@ -343,7 +346,7 @@
"vscode:prepublish": "npm run build-ext && npm run build-preview",
"build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json",
"build-preview": "npx webpack-cli --mode production",
"build-notebook": "npx webpack-cli --config webpack.notebook --mode production",
"build-notebook": "node ./esbuild",
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
},

View file

@ -1,28 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
module.exports = {
mode: 'production',
entry: {
index: path.join(__dirname, 'notebook', 'index.ts')
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'notebook-out')
}
};

View file

@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const fse = require('fs-extra');
const esbuild = require('esbuild');
const args = process.argv.slice(2);
const isWatch = args.indexOf('--watch') >= 0;
let outputRoot = __dirname;
const outputRootIndex = args.indexOf('--outputRoot');
if (outputRootIndex >= 0) {
outputRoot = args[outputRootIndex + 1];
}
const outDir = path.join(outputRoot, 'notebook-out');
esbuild.build({
entryPoints: [
path.join(__dirname, 'notebook', 'katex.ts'),
path.join(__dirname, 'notebook', 'emoji.ts')
],
bundle: true,
minify: true,
sourcemap: false,
format: 'esm',
outdir: outDir,
platform: 'browser',
target: ['es2020'],
incremental: isWatch,
}).catch(() => process.exit(1));
fse.copySync(
path.join(__dirname, 'node_modules/katex/dist/katex.min.css'),
path.join(outDir, 'katex.min.css'));
fse.copySync(
path.join(__dirname, 'node_modules/katex/dist/fonts'),
path.join(outDir, 'fonts/'));

View file

@ -4,17 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import type * as markdownIt from 'markdown-it';
declare const extendMarkdownIt: undefined | (
(f: (md: markdownIt.MarkdownIt) => void) => void
);
(function () {
if (typeof extendMarkdownIt !== 'undefined') {
const emoji = require('markdown-it-emoji');
extendMarkdownIt((md: markdownIt.MarkdownIt) => {
md.use(emoji);
});
}
}());
const emoji = require('markdown-it-emoji');
export function extendMarkdownIt(md: markdownIt.MarkdownIt) {
return md.use(emoji);
}

View file

@ -4,11 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import type * as markdownIt from 'markdown-it';
declare const extendMarkdownIt: undefined | (
(f: (md: markdownIt.MarkdownIt) => void) => void
);
const styleHref = (document.currentScript as any).src.replace(/katex.js$/, 'katex.min.css');
const styleHref = import.meta.url.replace(/katex.js$/, 'katex.min.css');
const link = document.createElement('link');
link.rel = 'stylesheet';
@ -17,12 +13,8 @@ link.href = styleHref;
document.head.append(link);
(function () {
const katex = require('@iktakahiro/markdown-it-katex');
if (typeof extendMarkdownIt !== 'undefined') {
const katex = require('@iktakahiro/markdown-it-katex');
extendMarkdownIt((md: markdownIt.MarkdownIt) => {
md.use(katex);
});
}
}());
export function extendMarkdownIt(md: markdownIt.MarkdownIt) {
return md.use(katex);
}

View file

@ -3,6 +3,7 @@
"compilerOptions": {
"outDir": "./dist/",
"jsx": "react",
"module": "es2020",
"lib": [
"es2018",
"DOM",

View file

@ -18,23 +18,25 @@
"virtualWorkspaces": false
},
"contributes": {
"notebookMarkdownRenderer": [
"notebookMarkupRenderers": [
{
"id": "markdownItRenderer-katex",
"displayName": "Markdown it katex renderer",
"entrypoint": "./notebook-out/katex.js"
"entrypoint": "./notebook-out/katex.js",
"dependsOn": "markdownItRenderer"
},
{
"id": "markdownItRenderer-emoji",
"displayName": "Markdown it emoji renderer",
"entrypoint": "./notebook-out/emoji.js"
"entrypoint": "./notebook-out/emoji.js",
"dependsOn": "markdownItRenderer"
}
]
},
"scripts": {
"compile": "npm run build-notebook",
"watch": "npm run build-notebook",
"build-notebook": "npx webpack-cli --config webpack.notebook.js --mode production"
"build-notebook": "node ./esbuild"
},
"devDependencies": {
"@iktakahiro/markdown-it-katex": "https://github.com/mjbvz/markdown-it-katex.git",

View file

@ -1,50 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
context: path.resolve(__dirname),
mode: 'production',
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
entry: {
katex: './notebook/katex.ts',
emoji: './notebook/emoji.ts',
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'notebook-out')
},
plugins: [
// @ts-ignore
new CopyPlugin({
patterns: [
{
from: './node_modules/katex/dist/katex.min.css',
to: 'katex.min.css'
},
{
from: './node_modules/katex/dist/fonts',
to: 'fonts/'
},
],
}),
]
};

View file

@ -10,6 +10,7 @@
"postinstall": "node ./postinstall"
},
"devDependencies": {
"esbuild": "^0.11.12",
"vscode-grammar-updater": "^1.0.3"
}
}

View file

@ -14,6 +14,11 @@ cson-parser@^1.3.3:
dependencies:
coffee-script "^1.10.0"
esbuild@^0.11.12:
version "0.11.12"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.12.tgz#8cbe15bcb44212624c3e77c896a835f74dc71c3c"
integrity sha512-c8cso/1RwVj+fbDvLtUgSG4ZJQ0y9Zdrl6Ot/GAjyy4pdMCHaFnDMts5gqFnWRPLajWtEnI+3hlET4R9fVoZng==
fast-plist@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8"

View file

@ -42,16 +42,20 @@ export interface INotebookRendererContribution {
readonly [NotebookRendererContribution.optionalDependencies]: readonly string[];
}
enum NotebookMarkdownRendererContribution {
enum NotebookMarkupRendererContribution {
id = 'id',
displayName = 'displayName',
entrypoint = 'entrypoint',
dependsOn = 'dependsOn',
mimeTypes = 'mimeTypes',
}
export interface INotebookMarkdownRendererContribution {
readonly [NotebookMarkdownRendererContribution.id]?: string;
readonly [NotebookMarkdownRendererContribution.displayName]: string;
readonly [NotebookMarkdownRendererContribution.entrypoint]: string;
export interface INotebookMarkupRendererContribution {
readonly [NotebookMarkupRendererContribution.id]?: string;
readonly [NotebookMarkupRendererContribution.displayName]: string;
readonly [NotebookMarkupRendererContribution.entrypoint]: string;
readonly [NotebookMarkupRendererContribution.dependsOn]: string | undefined;
readonly [NotebookMarkupRendererContribution.mimeTypes]: string[] | undefined;
}
const notebookProviderContribution: IJSONSchema = {
@ -160,30 +164,39 @@ const notebookRendererContribution: IJSONSchema = {
}
}
};
const notebookMarkdownRendererContribution: IJSONSchema = {
const notebookMarkupRendererContribution: IJSONSchema = {
description: nls.localize('contributes.notebook.markdownRenderer', 'Contributes a renderer for markdown cells in notebooks.'),
type: 'array',
defaultSnippets: [{ body: [{ id: '', displayName: '', entrypoint: '' }] }],
items: {
type: 'object',
required: [
NotebookMarkdownRendererContribution.id,
NotebookMarkdownRendererContribution.displayName,
NotebookMarkdownRendererContribution.entrypoint,
NotebookMarkupRendererContribution.id,
NotebookMarkupRendererContribution.displayName,
NotebookMarkupRendererContribution.entrypoint,
],
properties: {
[NotebookMarkdownRendererContribution.id]: {
[NotebookMarkupRendererContribution.id]: {
type: 'string',
description: nls.localize('contributes.notebook.markdownRenderer.id', 'Unique identifier of the notebook markdown renderer.'),
},
[NotebookMarkdownRendererContribution.displayName]: {
[NotebookMarkupRendererContribution.displayName]: {
type: 'string',
description: nls.localize('contributes.notebook.markdownRenderer.displayName', 'Human readable name of the notebook markdown renderer.'),
},
[NotebookMarkdownRendererContribution.entrypoint]: {
[NotebookMarkupRendererContribution.entrypoint]: {
type: 'string',
description: nls.localize('contributes.notebook.markdownRenderer.entrypoint', 'File to load in the webview to render the extension.'),
},
[NotebookMarkupRendererContribution.mimeTypes]: {
type: 'array',
items: { type: 'string' },
description: nls.localize('contributes.notebook.markdownRenderer.mimeTypes', 'The mime type that the renderer handles.'),
},
[NotebookMarkupRendererContribution.dependsOn]: {
type: 'string',
description: nls.localize('contributes.notebook.markdownRenderer.dependsOn', 'If specified, this renderer augments another renderer instead of providing full rendering.'),
},
}
}
};
@ -200,8 +213,8 @@ export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensi
jsonSchema: notebookRendererContribution
});
export const notebookMarkdownRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint<INotebookMarkdownRendererContribution[]>(
export const notebookMarkupRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint<INotebookMarkupRendererContribution[]>(
{
extensionPoint: 'notebookMarkdownRenderer',
jsonSchema: notebookMarkdownRendererContribution
extensionPoint: 'notebookMarkupRenderers',
jsonSchema: notebookMarkupRendererContribution
});

View file

@ -20,12 +20,12 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Memento } from 'vs/workbench/common/memento';
import { INotebookEditorContribution, notebookMarkdownRendererExtensionPoint, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint';
import { INotebookEditorContribution, notebookMarkupRendererExtensionPoint, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint';
import { NotebookEditorOptions, updateEditorTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookMarkdownRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookMarkdownRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookMarkdownRenderer';
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellUri, DisplayOrderKey, INotebookExclusiveDocumentFilter, INotebookMarkupRendererInfo, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, mimeTypeIsAlwaysSecure, mimeTypeSupportedByCore, NotebookDataDto, NotebookEditorPriority, NotebookRendererMatch, NotebookTextDiffEditorPreview, RENDERER_NOT_AVAILABLE, sortMimeTypes, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookMarkupRendererInfo as NotebookMarkupRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookMarkdownRenderer';
import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer';
import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { ComplexNotebookProviderInfo, INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService';
@ -272,7 +272,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd
private readonly _notebookProviders = new Map<string, ComplexNotebookProviderInfo | SimpleNotebookProviderInfo>();
private readonly _notebookProviderInfoStore: NotebookProviderInfoStore;
private readonly _notebookRenderersInfoStore = this._instantiationService.createInstance(NotebookOutputRendererInfoStore);
private readonly _markdownRenderersInfos = new Set<INotebookMarkdownRendererInfo>();
private readonly _markdownRenderersInfos = new Set<INotebookMarkupRendererInfo>();
private readonly _models = new ResourceMap<ModelData>();
private readonly _onDidCreateNotebookDocument = this._register(new Emitter<NotebookTextModel>());
@ -333,8 +333,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd
}
}
});
notebookMarkdownRendererExtensionPoint.setHandler((renderers) => {
notebookMarkupRendererExtensionPoint.setHandler((renderers) => {
this._markdownRenderersInfos.clear();
for (const extension of renderers) {
@ -355,11 +354,13 @@ export class NotebookService extends Disposable implements INotebookService, IEd
continue;
}
this._markdownRenderersInfos.add(new NotebookMarkdownRendererInfo({
this._markdownRenderersInfos.add(new NotebookMarkupRendererInfo({
id,
extension: extension.description,
entrypoint: notebookContribution.entrypoint,
displayName: notebookContribution.displayName,
mimeTypes: notebookContribution.mimeTypes,
dependsOn: notebookContribution.dependsOn,
}));
}
}
@ -518,7 +519,7 @@ export class NotebookService extends Disposable implements INotebookService, IEd
this._notebookRenderersInfoStore.setPreferred(mimeType, rendererId);
}
getMarkdownRendererInfo(): INotebookMarkdownRendererInfo[] {
getMarkupRendererInfo(): INotebookMarkupRendererInfo[] {
return Array.from(this._markdownRenderersInfos);
}

View file

@ -449,7 +449,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
this.element.style.position = 'absolute';
}
private generateContent(coreDependencies: string, baseUrl: string) {
const markdownRenderersSrc = this.getMarkdownRendererScripts();
const markupRenderer = this.getMarkdownRenderer();
const outputWidth = `calc(100% - ${this.options.leftMargin + this.options.rightMargin + this.options.runGutter}px)`;
const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`;
return html`
@ -718,31 +718,42 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Disposable {
</script>
${coreDependencies}
<div id='container' class="widgetarea" style="position: absolute;width:100%;top: 0px"></div>
<script>${preloadsScriptStr({
<script type="module">${preloadsScriptStr({
outputNodePadding: this.options.outputNodePadding,
outputNodeLeftPadding: this.options.outputNodeLeftPadding,
}, {
entrypoint: markupRenderer[0].entrypoint,
dependencies: markupRenderer[0].dependencies,
})}</script>
${markdownRenderersSrc}
</body>
</html>`;
}
private getMarkdownRendererScripts() {
const markdownRenderers = this.notebookService.getMarkdownRendererInfo();
private getMarkdownRenderer(): Array<{ entrypoint: string, dependencies: Array<{ entrypoint: string }> }> {
const allRenderers = this.notebookService.getMarkupRendererInfo();
return markdownRenderers
.sort((a, b) => {
// prefer built-in extension
if (a.extensionIsBuiltin) {
return b.extensionIsBuiltin ? 0 : -1;
const topLevelMarkdownRenderers = allRenderers
.filter(renderer => !renderer.dependsOn)
.filter(renderer => renderer.mimeTypes?.includes('text/markdown'));
const subRenderers = new Map<string, Array<{ entrypoint: string }>>();
for (const renderer of allRenderers) {
if (renderer.dependsOn) {
if (!subRenderers.has(renderer.dependsOn)) {
subRenderers.set(renderer.dependsOn, []);
}
return b.extensionIsBuiltin ? 1 : -1;
})
.map(renderer => {
return asWebviewUri(this.environmentService, this.id, renderer.entrypoint);
})
.map(src => `<script src="${src}"></script>`)
.join('\n');
const entryPoint = asWebviewUri(this.environmentService, this.id, renderer.entrypoint);
subRenderers.get(renderer.dependsOn)!.push({ entrypoint: entryPoint.toString(true) });
}
}
return topLevelMarkdownRenderers.map((renderer) => {
const src = asWebviewUri(this.environmentService, this.id, renderer.entrypoint);
return {
entrypoint: src.toString(),
dependencies: subRenderers.get(renderer.id) || [],
};
});
}
postRendererMessage(rendererId: string, message: any) {
@ -1139,7 +1150,7 @@ var requirejs = (function() {
this.localResourceRootsCache = [
...this.notebookService.getNotebookProviderResourceRoots(),
...this.notebookService.getMarkdownRendererInfo().map(x => dirname(x.entrypoint)),
...this.notebookService.getMarkupRendererInfo().map(x => dirname(x.entrypoint)),
...workspaceFolders,
rootPath,
];

View file

@ -29,6 +29,10 @@ declare class ResizeObserver {
declare const __outputNodePadding__: number;
declare const __outputNodeLeftPadding__: number;
declare const markdownRenderer: {
renderMarkup: (context: { element: HTMLElement, content: string }) => void,
};
type Listener<T> = { fn: (evt: T) => void; thisArg: unknown; };
interface EmitterLike<T> {
@ -409,18 +413,12 @@ function webviewPreloads() {
metadata: unknown;
}
interface ICreateMarkdownInfo {
readonly content: string;
readonly element: HTMLElement;
}
interface IDestroyCellInfo {
outputId: string;
}
const onWillDestroyOutput = createEmitter<[string | undefined /* namespace */, IDestroyCellInfo | undefined /* cell uri */]>();
const onDidCreateOutput = createEmitter<[string | undefined /* namespace */, ICreateCellInfo]>();
const onDidCreateMarkdown = createEmitter<[string | undefined /* namespace */, ICreateMarkdownInfo]>();
const onDidReceiveMessage = createEmitter<[string, unknown]>();
const matchesNs = (namespace: string, query: string | undefined) => namespace === '*' || query === namespace || query === 'undefined';
@ -447,7 +445,6 @@ function webviewPreloads() {
onDidReceiveMessage: mapEmitter(onDidReceiveMessage, ([ns, data]) => ns === namespace ? data : dontEmit),
onWillDestroyOutput: mapEmitter(onWillDestroyOutput, ([ns, data]) => matchesNs(namespace, ns) ? data : dontEmit),
onDidCreateOutput: mapEmitter(onDidCreateOutput, ([ns, data]) => matchesNs(namespace, ns) ? data : dontEmit),
onDidCreateMarkdown: mapEmitter(onDidCreateMarkdown, ([ns, data]) => data),
};
};
@ -805,6 +802,8 @@ function webviewPreloads() {
}
});
void markdownRenderer;
vscode.postMessage({
__vscode_notebook_message: true,
type: 'initialized'
@ -913,10 +912,10 @@ function webviewPreloads() {
previewContainerNode.innerText = '';
} else {
previewContainerNode.classList.remove('emptyMarkdownCell');
onDidCreateMarkdown.fire([undefined /* data.apiNamespace */, {
markdownRenderer.renderMarkup({
element: previewNode,
content: content
}]);
});
if (!hasPostedRenderedMathTelemetry) {
const hasRenderedMath = previewNode.querySelector('.katex');
@ -1015,11 +1014,24 @@ function webviewPreloads() {
}();
}
export function preloadsScriptStr(values: {
export function preloadsScriptStr(styleValues: {
outputNodePadding: number;
outputNodeLeftPadding: number;
}, markdownRenderer: {
entrypoint: string,
dependencies: Array<{ entrypoint: string }>,
}) {
return `(${webviewPreloads})()`
.replace(/__outputNodePadding__/g, `${values.outputNodePadding}`)
.replace(/__outputNodeLeftPadding__/g, `${values.outputNodeLeftPadding}`);
const markdownCtx = {
dependencies: markdownRenderer.dependencies,
};
return `${webviewPreloads}`
.slice(0, -1)
.replace(/function webviewPreloads\(\) \{/g, '')
.replace(/__outputNodePadding__/g, `${styleValues.outputNodePadding}`)
.replace(/__outputNodeLeftPadding__/g, `${styleValues.outputNodeLeftPadding}`)
.replace(/void markdownRenderer;/g, `
import * as markdownRendererModule from "${markdownRenderer.entrypoint}";
const markdownRenderer = await markdownRendererModule.activate(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(markdownCtx))}")))
`);
}

View file

@ -141,11 +141,14 @@ export interface INotebookRendererInfo {
matches(mimeType: string, kernelProvides: ReadonlyArray<string>): NotebookRendererMatch;
}
export interface INotebookMarkdownRendererInfo {
export interface INotebookMarkupRendererInfo {
readonly id: string;
readonly entrypoint: URI;
readonly extensionLocation: URI;
readonly extensionId: ExtensionIdentifier;
readonly extensionIsBuiltin: boolean;
readonly dependsOn: string | undefined;
readonly mimeTypes: readonly string[] | undefined;
}
export interface NotebookCellOutputMetadata {

View file

@ -6,9 +6,9 @@
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { INotebookMarkdownRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookMarkupRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
export class NotebookMarkdownRendererInfo implements INotebookMarkdownRendererInfo {
export class NotebookMarkupRendererInfo implements INotebookMarkupRendererInfo {
readonly id: string;
readonly entrypoint: URI;
@ -16,12 +16,16 @@ export class NotebookMarkdownRendererInfo implements INotebookMarkdownRendererIn
readonly extensionLocation: URI;
readonly extensionId: ExtensionIdentifier;
readonly extensionIsBuiltin: boolean;
readonly dependsOn: string | undefined;
readonly mimeTypes: readonly string[] | undefined;
constructor(descriptor: {
readonly id: string;
readonly displayName: string;
readonly entrypoint: string;
readonly extension: IExtensionDescription;
readonly mimeTypes: readonly string[] | undefined,
readonly dependsOn: string | undefined,
}) {
this.id = descriptor.id;
this.extensionId = descriptor.extension.identifier;
@ -29,5 +33,7 @@ export class NotebookMarkdownRendererInfo implements INotebookMarkdownRendererIn
this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint);
this.displayName = descriptor.displayName;
this.extensionIsBuiltin = descriptor.extension.isBuiltin;
this.dependsOn = descriptor.dependsOn;
this.mimeTypes = descriptor.mimeTypes;
}
}

View file

@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri';
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol';
import { Event } from 'vs/base/common/event';
import { INotebookRendererInfo, NotebookDataDto, TransientOptions, INotebookExclusiveDocumentFilter, IOrderedMimeType, IOutputDto, INotebookMarkdownRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { INotebookRendererInfo, NotebookDataDto, TransientOptions, INotebookExclusiveDocumentFilter, IOrderedMimeType, IOutputDto, INotebookMarkupRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
import { CancellationToken } from 'vs/base/common/cancellation';
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
@ -71,7 +71,7 @@ export interface INotebookService {
getMimeTypeInfo(textModel: NotebookTextModel, kernelProvides: readonly string[] | undefined, output: IOutputDto): readonly IOrderedMimeType[];
getRendererInfo(id: string): INotebookRendererInfo | undefined;
getMarkdownRendererInfo(): INotebookMarkdownRendererInfo[];
getMarkupRendererInfo(): INotebookMarkupRendererInfo[];
/** Updates the preferred renderer for the given mimetype in the workspace. */
updateMimePreferredRenderer(mimeType: string, rendererId: string): void;