diff --git a/src/sql/platform/dashboard/common/modelComponentRegistry.ts b/src/sql/platform/dashboard/common/modelComponentRegistry.ts
index 0f43f96c9a..d3ad66f8aa 100644
--- a/src/sql/platform/dashboard/common/modelComponentRegistry.ts
+++ b/src/sql/platform/dashboard/common/modelComponentRegistry.ts
@@ -6,8 +6,6 @@ import { Type } from '@angular/core';
import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes';
import * as platform from 'vs/platform/registry/common/platform';
-import { IJSONSchema } from 'vs/base/common/jsonSchema';
-import * as nls from 'vs/nls';
import { IComponent } from 'sql/workbench/electron-browser/modelComponents/interfaces';
export type ComponentIdentifier = string;
diff --git a/src/sql/workbench/parts/notebook/cellViews/output.component.html b/src/sql/workbench/parts/notebook/cellViews/output.component.html
index de4a1a3416..8946a84479 100644
--- a/src/sql/workbench/parts/notebook/cellViews/output.component.html
+++ b/src/sql/workbench/parts/notebook/cellViews/output.component.html
@@ -6,6 +6,10 @@
-->
diff --git a/src/sql/workbench/parts/notebook/cellViews/output.component.ts b/src/sql/workbench/parts/notebook/cellViews/output.component.ts
index 636d4b7977..1568e653ed 100644
--- a/src/sql/workbench/parts/notebook/cellViews/output.component.ts
+++ b/src/sql/workbench/parts/notebook/cellViews/output.component.ts
@@ -5,52 +5,70 @@
import 'vs/css!./code';
import 'vs/css!./media/output';
-import { OnInit, Component, Input, Inject, ElementRef, ViewChild, SimpleChange } from '@angular/core';
+import { OnInit, Component, Input, Inject, ElementRef, ViewChild, SimpleChange, AfterViewInit, forwardRef, ChangeDetectorRef, ComponentRef, ComponentFactoryResolver } from '@angular/core';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { Event } from 'vs/base/common/event';
import { nb } from 'azdata';
import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
-import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
-import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
import * as outputProcessor from 'sql/workbench/parts/notebook/outputs/common/outputProcessor';
-import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
-import { IThemeService } from 'vs/platform/theme/common/themeService';
+import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import * as DOM from 'vs/base/browser/dom';
+import { ComponentHostDirective } from 'sql/workbench/parts/dashboard/common/componentHost.directive';
+import { Extensions, IMimeComponent, IMimeComponentRegistry } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
+import * as colors from 'vs/platform/theme/common/colorRegistry';
+import * as themeColors from 'vs/workbench/common/theme';
+import { Registry } from 'vs/platform/registry/common/platform';
+import { localize } from 'vs/nls';
+import * as types from 'vs/base/common/types';
+import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
export const OUTPUT_SELECTOR: string = 'output-component';
const USER_SELECT_CLASS = 'actionselect';
+const componentRegistry = Registry.as(Extensions.MimeComponentContribution);
+
@Component({
selector: OUTPUT_SELECTOR,
templateUrl: decodeURI(require.toUrl('./output.component.html'))
})
-export class OutputComponent extends AngularDisposable implements OnInit {
+export class OutputComponent extends AngularDisposable implements OnInit, AfterViewInit {
@ViewChild('output', { read: ElementRef }) private outputElement: ElementRef;
+ @ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective;
@Input() cellOutput: nb.ICellOutput;
@Input() cellModel: ICellModel;
+
private _trusted: boolean;
private _initialized: boolean = false;
private _activeCellId: string;
- registry: RenderMimeRegistry;
-
+ private _componentInstance: IMimeComponent;
+ public errorText: string;
constructor(
- @Inject(INotebookService) private _notebookService: INotebookService,
- @Inject(IThemeService) private _themeService: IThemeService
+ @Inject(IThemeService) private _themeService: IThemeService,
+ @Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef,
+ @Inject(forwardRef(() => ElementRef)) private _ref: ElementRef,
+ @Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver
) {
super();
- this.registry = this._notebookService.getMimeRegistry();
}
ngOnInit() {
- this.renderOutput();
+ this._register(this._themeService.onThemeChange(event => this.updateTheme(event)));
+ this.layout();
this._initialized = true;
this._register(Event.debounce(this.cellModel.notebookModel.layoutChanged, (l, e) => e, 50, /*leading=*/false)
- (() => this.renderOutput()));
+ (() => this.layout()));
+ }
+
+ ngAfterViewInit() {
+ this.updateTheme(this._themeService.getTheme());
+ if (this.componentHost) {
+ this.loadComponent();
+ }
+ this._changeref.detectChanges();
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
-
for (let propName in changes) {
if (propName === 'activeCellId') {
this.toggleUserSelect(this.isActive());
@@ -60,24 +78,31 @@ export class OutputComponent extends AngularDisposable implements OnInit {
}
private toggleUserSelect(userSelect: boolean): void {
- if (!this.outputElement) {
+ if (!this.nativeOutputElement) {
return;
}
if (userSelect) {
- DOM.addClass(this.outputElement.nativeElement, USER_SELECT_CLASS);
+ DOM.addClass(this.nativeOutputElement, USER_SELECT_CLASS);
} else {
- DOM.removeClass(this.outputElement.nativeElement, USER_SELECT_CLASS);
+ DOM.removeClass(this.nativeOutputElement, USER_SELECT_CLASS);
}
}
- private renderOutput(): void {
- let options = outputProcessor.getBundleOptions({ value: this.cellOutput, trusted: this.trustedMode });
- options.themeService = this._themeService;
- // TODO handle safe/unsafe mapping
- this.createRenderedMimetype(options, this.outputElement.nativeElement);
+ private get nativeOutputElement() {
+ return this.outputElement ? this.outputElement.nativeElement : undefined;
}
public layout(): void {
+ if (this.componentInstance && this.componentInstance.layout) {
+ this.componentInstance.layout();
+ }
+ }
+
+ private get componentInstance(): IMimeComponent {
+ if (!this._componentInstance) {
+ this.loadComponent();
+ }
+ return this._componentInstance;
}
get trustedMode(): boolean {
@@ -87,7 +112,7 @@ export class OutputComponent extends AngularDisposable implements OnInit {
@Input() set trustedMode(value: boolean) {
this._trusted = value;
if (this._initialized) {
- this.renderOutput();
+ this.layout();
}
}
@@ -99,36 +124,67 @@ export class OutputComponent extends AngularDisposable implements OnInit {
return this._activeCellId;
}
- protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void {
- let mimeType = this.registry.preferredMimeType(
- options.data,
- options.trusted ? 'any' : 'ensure'
- );
- if (mimeType) {
- let output = this.registry.createRenderer(mimeType);
- output.node = node;
- let model = new MimeModel(options);
- output.renderModel(model).catch(error => {
- // Manually append error message to output
- output.node.innerHTML = `Javascript Error: ${error.message}`;
- // Remove mime-type-specific CSS classes
- output.node.className = 'p-Widget jp-RenderedText';
- output.node.setAttribute(
- 'data-mime-type',
- 'application/vnd.jupyter.stderr'
- );
- });
- //this.setState({ node: node });
- } else {
- // TODO Localize
- node.innerHTML =
- `No ${options.trusted ? '' : '(safe) '}renderer could be ` +
- 'found for output. It has the following MIME types: ' +
- Object.keys(options.data).join(', ');
- //this.setState({ node: node });
- }
- }
protected isActive() {
return this.cellModel && this.cellModel.id === this.activeCellId;
}
+
+ public hasError(): boolean {
+ return !types.isUndefinedOrNull(this.errorText);
+ }
+ private updateTheme(theme: ITheme): void {
+ let el = this._ref.nativeElement;
+ let backgroundColor = theme.getColor(colors.editorBackground, true);
+ let foregroundColor = theme.getColor(themeColors.SIDE_BAR_FOREGROUND, true);
+
+ if (backgroundColor) {
+ el.style.backgroundColor = backgroundColor.toString();
+ }
+ if (foregroundColor) {
+ el.style.color = foregroundColor.toString();
+ }
+ }
+
+ private loadComponent(): void {
+ let options = outputProcessor.getBundleOptions({ value: this.cellOutput, trusted: this.trustedMode });
+ options.themeService = this._themeService;
+ let mimeType = componentRegistry.getPreferredMimeType(
+ options.data,
+ options.trusted ? 'any' : 'ensure'
+ );
+ this.errorText = undefined;
+ if (!mimeType) {
+ this.errorText = localize('noMimeTypeFound', "No {0}renderer could be found for output. It has the following MIME types: {1}",
+ options.trusted ? '' : localize('safe', 'safe '),
+ Object.keys(options.data).join(', '));
+ return;
+ }
+ let selector = componentRegistry.getCtorFromMimeType(mimeType);
+ if (!selector) {
+ this.errorText = localize('noSelectorFound', "No component could be found for selector {0}", mimeType);
+ return;
+ }
+
+ let componentFactory = this._componentFactoryResolver.resolveComponentFactory(selector);
+
+ let viewContainerRef = this.componentHost.viewContainerRef;
+ viewContainerRef.clear();
+
+ let componentRef: ComponentRef;
+ try {
+ componentRef = viewContainerRef.createComponent(componentFactory, 0);
+ this._componentInstance = componentRef.instance;
+ this._componentInstance.mimeType = mimeType;
+ this._componentInstance.cellModel = this.cellModel;
+ this._componentInstance.bundleOptions = options;
+ this._changeref.detectChanges();
+ let el = componentRef.location.nativeElement;
+
+ // set widget styles to conform to its box
+ el.style.overflow = 'hidden';
+ el.style.position = 'relative';
+ } catch (e) {
+ this.errorText = localize('componentRenderError', "Error rendering component: {0}", getErrorMessage(e));
+ return;
+ }
+ }
}
diff --git a/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts b/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts
index de0f4d4839..c34b0999bd 100644
--- a/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts
+++ b/src/sql/workbench/parts/notebook/cellViews/textCell.component.ts
@@ -6,7 +6,7 @@ import 'vs/css!./textCell';
import 'vs/css!./media/markdown';
import 'vs/css!./media/highlight';
-import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener, AfterContentInit } from '@angular/core';
+import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener } from '@angular/core';
import * as path from 'path';
import { localize } from 'vs/nls';
@@ -24,6 +24,7 @@ import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces'
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/outputs/sanitizer';
import { NotebookModel } from 'sql/workbench/parts/notebook/models/notebookModel';
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/cellToggleMoreActions';
+import { convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/notebookUtils';
export const TEXT_SELECTOR: string = 'text-cell-component';
const USER_SELECT_CLASS = 'actionselect';
@@ -153,7 +154,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
}
this._commandService.executeCommand('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
- htmlcontent = this.convertVscodeResourceToFileInSubDirectories(htmlcontent);
+ htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this.cellModel);
htmlcontent = this.sanitizeContent(htmlcontent);
let outputElement = this.output.nativeElement;
outputElement.innerHTML = htmlcontent;
@@ -169,24 +170,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
}
return content;
}
- // Only replace vscode-resource with file when in the same (or a sub) directory
- // This matches Jupyter Notebook viewer behavior
- private convertVscodeResourceToFileInSubDirectories(htmlContent: string): string {
- let htmlContentCopy = htmlContent;
- while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
- let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
- let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
- let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
- // If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
- if (!path.relative(path.dirname(this.cellModel.notebookModel.notebookUri.fsPath), filePath).includes('..')) {
- // ok to change from vscode-resource: to file:
- htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
- }
- htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
- }
- return htmlContent;
- }
-
// Todo: implement layout
public layout() {
diff --git a/src/sql/workbench/parts/notebook/notebook.contribution.ts b/src/sql/workbench/parts/notebook/notebook.contribution.ts
index 076857504e..2aad27327e 100644
--- a/src/sql/workbench/parts/notebook/notebook.contribution.ts
+++ b/src/sql/workbench/parts/notebook/notebook.contribution.ts
@@ -13,6 +13,9 @@ import { NotebookEditor } from 'sql/workbench/parts/notebook/notebookEditor';
import { NewNotebookAction } from 'sql/workbench/parts/notebook/notebookActions';
import { KeyMod } from 'vs/editor/common/standalone/standaloneBase';
import { KeyCode } from 'vs/base/common/keyCodes';
+import { registerComponentType } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
+import { MimeRendererComponent as MimeRendererComponent } from 'sql/workbench/parts/notebook/outputs/mimeRenderer.component';
+import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/outputs/markdownOutput.component';
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
@@ -36,4 +39,107 @@ actionRegistry.registerWorkbenchAction(
),
NewNotebookAction.LABEL
-);
\ No newline at end of file
+);
+
+/* *************** Output components *************** */
+// Note: most existing types use the same component to render. In order to
+// preserve correct rank order, we register it once for each different rank of
+// MIME types.
+
+/**
+ * A mime renderer component for raw html.
+ */
+registerComponentType({
+ mimeTypes: ['text/html'],
+ rank: 50,
+ safe: true,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for images.
+ */
+registerComponentType({
+ mimeTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
+ rank: 90,
+ safe: true,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for svg.
+ */
+registerComponentType({
+ mimeTypes: ['image/svg+xml'],
+ rank: 80,
+ safe: false,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for plain and jupyter console text data.
+ */
+registerComponentType({
+ mimeTypes: [
+ 'text/plain',
+ 'application/vnd.jupyter.stdout',
+ 'application/vnd.jupyter.stderr'
+ ],
+ rank: 120,
+ safe: true,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A placeholder component for deprecated rendered JavaScript.
+ */
+registerComponentType({
+ mimeTypes: ['text/javascript', 'application/javascript'],
+ rank: 110,
+ safe: false,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for grid data.
+ * This will be replaced by a dedicated component in the future
+ */
+registerComponentType({
+ mimeTypes: [
+ 'application/vnd.dataresource+json',
+ 'application/vnd.dataresource'
+ ],
+ rank: 40,
+ safe: true,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for LaTeX.
+ * This will be replaced by a dedicated component in the future
+ */
+registerComponentType({
+ mimeTypes: ['text/latex'],
+ rank: 70,
+ safe: true,
+ ctor: MimeRendererComponent,
+ selector: MimeRendererComponent.SELECTOR
+});
+
+/**
+ * A mime renderer component for Markdown.
+ * This will be replaced by a dedicated component in the future
+ */
+registerComponentType({
+ mimeTypes: ['text/markdown'],
+ rank: 60,
+ safe: true,
+ ctor: MarkdownOutputComponent,
+ selector: MarkdownOutputComponent.SELECTOR
+});
diff --git a/src/sql/workbench/parts/notebook/notebook.module.ts b/src/sql/workbench/parts/notebook/notebook.module.ts
index 71e07b5c3e..071dca73bf 100644
--- a/src/sql/workbench/parts/notebook/notebook.module.ts
+++ b/src/sql/workbench/parts/notebook/notebook.module.ts
@@ -25,9 +25,13 @@ import LoadingSpinner from 'sql/workbench/electron-browser/modelComponents/loadi
import { Checkbox } from 'sql/base/electron-browser/ui/checkbox/checkbox.component';
import { SelectBox } from 'sql/platform/ui/electron-browser/selectBox/selectBox.component';
import { InputBox } from 'sql/base/electron-browser/ui/inputBox/inputBox.component';
+import { IMimeComponentRegistry, Extensions } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
+import { Registry } from 'vs/platform/registry/common/platform';
import { LinkHandlerDirective } from 'sql/workbench/parts/notebook/cellViews/linkHandler.directive';
export const NotebookModule = (params, selector: string, instantiationService: IInstantiationService): any => {
+ let outputComponents = Registry.as(Extensions.MimeComponentContribution).getAllCtors();
+
@NgModule({
declarations: [
Checkbox,
@@ -44,9 +48,13 @@ export const NotebookModule = (params, selector: string, instantiationService: I
OutputAreaComponent,
OutputComponent,
StdInComponent,
- LinkHandlerDirective
+ LinkHandlerDirective,
+ ...outputComponents
+ ],
+ entryComponents: [
+ NotebookComponent,
+ ...outputComponents
],
- entryComponents: [NotebookComponent],
imports: [
FormsModule,
CommonModule,
diff --git a/src/sql/workbench/parts/notebook/notebookUtils.ts b/src/sql/workbench/parts/notebook/notebookUtils.ts
index 6b3f04e20a..c66843c4c9 100644
--- a/src/sql/workbench/parts/notebook/notebookUtils.ts
+++ b/src/sql/workbench/parts/notebook/notebookUtils.ts
@@ -11,6 +11,7 @@ import { localize } from 'vs/nls';
import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile';
import { IOutputChannel } from 'vs/workbench/contrib/output/common/output';
+import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
/**
@@ -121,3 +122,23 @@ export async function asyncForEach(array: any, callback: any): Promise {
await callback(array[index], index, array);
}
}
+
+/**
+ * Only replace vscode-resource with file when in the same (or a sub) directory
+ * This matches Jupyter Notebook viewer behavior
+ */
+export function convertVscodeResourceToFileInSubDirectories(htmlContent: string, cellModel: ICellModel): string {
+ let htmlContentCopy = htmlContent;
+ while (htmlContentCopy.search('(?<=img src=\"vscode-resource:)') > 0) {
+ let pathStartIndex = htmlContentCopy.search('(?<=img src=\"vscode-resource:)');
+ let pathEndIndex = htmlContentCopy.indexOf('\" ', pathStartIndex);
+ let filePath = htmlContentCopy.substring(pathStartIndex, pathEndIndex);
+ // If the asset is in the same folder or a subfolder, replace 'vscode-resource:' with 'file:', so the image is visible
+ if (!path.relative(path.dirname(cellModel.notebookModel.notebookUri.fsPath), filePath).includes('..')) {
+ // ok to change from vscode-resource: to file:
+ htmlContent = htmlContent.replace('vscode-resource:' + filePath, 'file:' + filePath);
+ }
+ htmlContentCopy = htmlContentCopy.slice(pathEndIndex);
+ }
+ return htmlContent;
+}
diff --git a/src/sql/workbench/parts/notebook/outputs/common/renderMimeInterfaces.ts b/src/sql/workbench/parts/notebook/outputs/common/renderMimeInterfaces.ts
index db4e0aafd9..e47396b3ea 100644
--- a/src/sql/workbench/parts/notebook/outputs/common/renderMimeInterfaces.ts
+++ b/src/sql/workbench/parts/notebook/outputs/common/renderMimeInterfaces.ts
@@ -60,151 +60,6 @@ export namespace IRenderMime {
metadata?: ReadonlyJSONObject;
}
- /**
- * The options used to initialize a document widget factory.
- *
- * This interface is intended to be used by mime renderer extensions
- * to define a document opener that uses its renderer factory.
- */
- export interface IDocumentWidgetFactoryOptions {
- /**
- * The name of the widget to display in dialogs.
- */
- readonly name: string;
-
- /**
- * The name of the document model type.
- */
- readonly modelName?: string;
-
- /**
- * The primary file type of the widget.
- */
- readonly primaryFileType: string;
-
- /**
- * The file types the widget can view.
- */
- readonly fileTypes: ReadonlyArray;
-
- /**
- * The file types for which the factory should be the default.
- */
- readonly defaultFor?: ReadonlyArray;
-
- /**
- * The file types for which the factory should be the default for rendering,
- * if that is different than the default factory (which may be for editing)
- * If undefined, then it will fall back on the default file type.
- */
- readonly defaultRendered?: ReadonlyArray;
- }
-
- /**
- * A file type to associate with the renderer.
- */
- export interface IFileType {
- /**
- * The name of the file type.
- */
- readonly name: string;
-
- /**
- * The mime types associated the file type.
- */
- readonly mimeTypes: ReadonlyArray;
-
- /**
- * The extensions of the file type (e.g. `".txt"`). Can be a compound
- * extension (e.g. `".table.json`).
- */
- readonly extensions: ReadonlyArray;
-
- /**
- * An optional display name for the file type.
- */
- readonly displayName?: string;
-
- /**
- * An optional pattern for a file name (e.g. `^Dockerfile$`).
- */
- readonly pattern?: string;
-
- /**
- * The icon class name for the file type.
- */
- readonly iconClass?: string;
-
- /**
- * The icon label for the file type.
- */
- readonly iconLabel?: string;
-
- /**
- * The file format for the file type ('text', 'base64', or 'json').
- */
- readonly fileFormat?: string;
- }
-
- /**
- * An interface for using a RenderMime.IRenderer for output and read-only documents.
- */
- export interface IExtension {
- /**
- * The ID of the extension.
- *
- * #### Notes
- * The convention for extension IDs in JupyterLab is the full NPM package
- * name followed by a colon and a unique string token, e.g.
- * `'@jupyterlab/apputils-extension:settings'` or `'foo-extension:bar'`.
- */
- readonly id: string;
-
- /**
- * A renderer factory to be registered to render the MIME type.
- */
- readonly rendererFactory: IRendererFactory;
-
- /**
- * The rank passed to `RenderMime.addFactory`. If not given,
- * defaults to the `defaultRank` of the factory.
- */
- readonly rank?: number;
-
- /**
- * The timeout after user activity to re-render the data.
- */
- readonly renderTimeout?: number;
-
- /**
- * Preferred data type from the model. Defaults to `string`.
- */
- readonly dataType?: 'string' | 'json';
-
- /**
- * The options used to open a document with the renderer factory.
- */
- readonly documentWidgetFactoryOptions?:
- | IDocumentWidgetFactoryOptions
- | ReadonlyArray;
-
- /**
- * The optional file type associated with the extension.
- */
- readonly fileTypes?: ReadonlyArray;
- }
-
- /**
- * The interface for a module that exports an extension or extensions as
- * the default value.
- */
- export interface IExtensionModule {
- /**
- * The default export.
- */
- readonly default: IExtension | ReadonlyArray;
- }
-
/**
* A widget which displays the contents of a mime model.
*/
@@ -279,17 +134,17 @@ export namespace IRenderMime {
/**
* An optional url resolver.
*/
- resolver: IResolver | null;
+ resolver?: IResolver | null;
/**
* An optional link handler.
*/
- linkHandler: ILinkHandler | null;
+ linkHandler?: ILinkHandler | null;
/**
* The LaTeX typesetter.
*/
- latexTypesetter: ILatexTypesetter | null;
+ latexTypesetter?: ILatexTypesetter | null;
}
/**
diff --git a/src/sql/workbench/parts/notebook/outputs/factories.ts b/src/sql/workbench/parts/notebook/outputs/factories.ts
index 6853a4c172..5346cd19ec 100644
--- a/src/sql/workbench/parts/notebook/outputs/factories.ts
+++ b/src/sql/workbench/parts/notebook/outputs/factories.ts
@@ -36,16 +36,6 @@ export const imageRendererFactory: IRenderMime.IRendererFactory = {
// createRenderer: options => new widgets.RenderedLatex(options)
// };
-// /**
-// * A mime renderer factory for Markdown.
-// */
-// export const markdownRendererFactory: IRenderMime.IRendererFactory = {
-// safe: true,
-// mimeTypes: ['text/markdown'],
-// defaultRank: 60,
-// createRenderer: options => new widgets.RenderedMarkdown(options)
-// };
-
/**
* A mime renderer factory for svg.
*/
@@ -95,7 +85,6 @@ export const dataResourceRendererFactory: IRenderMime.IRendererFactory = {
*/
export const standardRendererFactories: ReadonlyArray = [
htmlRendererFactory,
- // markdownRendererFactory,
// latexRendererFactory,
svgRendererFactory,
imageRendererFactory,
@@ -103,3 +92,4 @@ export const standardRendererFactories: ReadonlyArray
+
+
+
+
diff --git a/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts
new file mode 100644
index 0000000000..9fa41adc49
--- /dev/null
+++ b/src/sql/workbench/parts/notebook/outputs/markdownOutput.component.ts
@@ -0,0 +1,129 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import 'vs/css!../cellViews/textCell';
+import 'vs/css!../cellViews/media/markdown';
+import 'vs/css!../cellViews/media/highlight';
+
+import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild } from '@angular/core';
+import { ICommandService } from 'vs/platform/commands/common/commands';
+import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/outputs/sanitizer';
+import { AngularDisposable } from 'sql/base/node/lifecycle';
+import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
+import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
+import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
+import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
+import { convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/notebookUtils';
+
+@Component({
+ selector: MarkdownOutputComponent.SELECTOR,
+ templateUrl: decodeURI(require.toUrl('./markdownOutput.component.html'))
+})
+export class MarkdownOutputComponent extends AngularDisposable implements IMimeComponent, OnInit {
+ public static readonly SELECTOR: string = 'markdown-output';
+
+ @ViewChild('output', { read: ElementRef }) private output: ElementRef;
+
+ private _sanitizer: ISanitizer;
+ private _lastTrustedMode: boolean;
+
+ private _bundleOptions: MimeModel.IOptions;
+ private _initialized: boolean = false;
+ public loading: boolean = false;
+ private _cellModel: ICellModel;
+
+ constructor(
+ @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
+ @Inject(ICommandService) private _commandService: ICommandService,
+ @Inject(INotebookService) private _notebookService: INotebookService
+
+ ) {
+ super();
+ this._sanitizer = this._notebookService.getMimeRegistry().sanitizer;
+ }
+
+ @Input() set bundleOptions(value: MimeModel.IOptions) {
+ this._bundleOptions = value;
+ if (this._initialized) {
+ this.updatePreview();
+ }
+ }
+
+ @Input() mimeType: string;
+
+ get cellModel(): ICellModel {
+ return this._cellModel;
+ }
+
+ @Input() set cellModel(value: ICellModel) {
+ this._cellModel = value;
+ }
+
+ public get isTrusted(): boolean {
+ return this._bundleOptions && this._bundleOptions.trusted;
+ }
+
+ //Gets sanitizer from ISanitizer interface
+ private get sanitizer(): ISanitizer {
+ if (this._sanitizer) {
+ return this._sanitizer;
+ }
+ return this._sanitizer = defaultSanitizer;
+ }
+
+ private setLoading(isLoading: boolean): void {
+ this.loading = isLoading;
+ this._changeRef.detectChanges();
+ }
+
+ ngOnInit() {
+ this.updatePreview();
+ }
+
+ /**
+ * Updates the preview of markdown component with latest changes
+ * If content is empty and in non-edit mode, default it to 'Double-click to edit'
+ * Sanitizes the data to be shown in markdown cell
+ */
+ private updatePreview() {
+ if (!this._bundleOptions || !this._cellModel) {
+ return;
+ }
+ let trustedChanged = this._bundleOptions && this._lastTrustedMode !== this.isTrusted;
+ if (trustedChanged || !this._initialized) {
+ this._lastTrustedMode = this.isTrusted;
+ let content = this._bundleOptions.data['text/markdown'];
+ if (!content) {
+
+ } else {
+ this._commandService.executeCommand('notebook.showPreview', this._cellModel.notebookModel.notebookUri, content).then((htmlcontent) => {
+ htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this._cellModel);
+ htmlcontent = this.sanitizeContent(htmlcontent);
+ let outputElement = this.output.nativeElement;
+ outputElement.innerHTML = htmlcontent;
+ this.setLoading(false);
+ });
+ }
+ this._initialized = true;
+ }
+ }
+
+ //Sanitizes the content based on trusted mode of Cell Model
+ private sanitizeContent(content: string): string {
+ if (this.isTrusted) {
+ content = this.sanitizer.sanitize(content);
+ }
+ return content;
+ }
+
+
+ public layout() {
+ // Do we need to update on layout changed?
+ }
+
+ public handleContentChanged(): void {
+ this.updatePreview();
+ }
+}
\ No newline at end of file
diff --git a/src/sql/workbench/parts/notebook/outputs/mimeRegistry.ts b/src/sql/workbench/parts/notebook/outputs/mimeRegistry.ts
new file mode 100644
index 0000000000..79396fc9fe
--- /dev/null
+++ b/src/sql/workbench/parts/notebook/outputs/mimeRegistry.ts
@@ -0,0 +1,195 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+import { Type } from '@angular/core';
+
+import * as platform from 'vs/platform/registry/common/platform';
+import { ReadonlyJSONObject } from 'sql/workbench/parts/notebook/models/jsonext';
+import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
+import * as types from 'vs/base/common/types';
+import { IRenderMime } from 'sql/workbench/parts/notebook/outputs/common/renderMimeInterfaces';
+import { ICellModel } from 'sql/workbench/parts/notebook/models/modelInterfaces';
+
+export type FactoryIdentifier = string;
+
+export const Extensions = {
+ MimeComponentContribution: 'notebook.contributions.mimecomponents'
+};
+
+export interface IMimeComponent {
+ bundleOptions: MimeModel.IOptions;
+ mimeType: string;
+ cellModel?: ICellModel;
+ layout(): void;
+}
+
+export interface IMimeComponentDefinition {
+ /**
+ * Whether the component is a "safe" component.
+ *
+ * #### Notes
+ * A "safe" component produces renderer widgets which can render
+ * untrusted model data in a usable way. *All* renderers must
+ * handle untrusted data safely, but some may simply failover
+ * with a "Run cell to view output" message. A "safe" renderer
+ * is an indication that its sanitized output will be useful.
+ */
+ readonly safe: boolean;
+
+ /**
+ * The mime types handled by this component.
+ */
+ readonly mimeTypes: ReadonlyArray;
+
+ /**
+ * The angular selector for this component
+ */
+ readonly selector: string;
+ /**
+ * The default rank of the factory. If not given, defaults to 100.
+ */
+ readonly rank?: number;
+
+ readonly ctor: Type;
+}
+
+export type SafetyLevel = 'ensure' | 'prefer' | 'any';
+type RankPair = { readonly id: number; readonly rank: number };
+
+/**
+ * A type alias for a mapping of mime type -> rank pair.
+ */
+type RankMap = { [key: string]: RankPair };
+
+/**
+ * A type alias for a mapping of mime type -> ordered factories.
+ */
+export type ComponentMap = { [key: string]: IMimeComponentDefinition };
+
+export interface IMimeComponentRegistry {
+
+ /**
+ * Add a MIME component to the registry.
+ *
+ * @param componentDefinition - The definition of this component including
+ * the constructor to initialize it, supported `mimeTypes`, and `rank` order
+ * of preference vs. other mime types.
+ * If no `rank` is given, it will default to 100.
+ *
+ * #### Notes
+ * The renderer will replace an existing renderer for the given
+ * mimeType.
+ */
+ registerComponentType(componentDefinition: IMimeComponentDefinition): void;
+
+ /**
+ * Find the preferred mime type for a mime bundle.
+ *
+ * @param bundle - The bundle of mime data.
+ *
+ * @param safe - How to consider safe/unsafe factories. If 'ensure',
+ * it will only consider safe factories. If 'any', any factory will be
+ * considered. If 'prefer', unsafe factories will be considered, but
+ * only after the safe options have been exhausted.
+ *
+ * @returns The preferred mime type from the available factories,
+ * or `undefined` if the mime type cannot be rendered.
+ */
+ getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel): string;
+ getCtorFromMimeType(mimeType: string): Type;
+ getAllCtors(): Array>;
+ getAllMimeTypes(): Array;
+}
+
+class MimeComponentRegistry implements IMimeComponentRegistry {
+ private _id = 0;
+ private _ranks: RankMap = {};
+ private _types: string[] | null = null;
+ private _componentDefinitions: ComponentMap = {};
+
+ registerComponentType(componentDefinition: IMimeComponentDefinition): void {
+ let rank = !types.isUndefinedOrNull(componentDefinition.rank) ? componentDefinition.rank : 100;
+ for (let mt of componentDefinition.mimeTypes) {
+ this._componentDefinitions[mt] = componentDefinition;
+ this._ranks[mt] = { rank, id: this._id++ };
+ }
+ this._types = null;
+ }
+
+ public getPreferredMimeType(bundle: ReadonlyJSONObject, safe: SafetyLevel = 'ensure'): string | undefined {
+ // Try to find a safe factory first, if preferred.
+ if (safe === 'ensure' || safe === 'prefer') {
+ for (let mt of this.mimeTypes) {
+ if (mt in bundle && this._componentDefinitions[mt].safe) {
+ return mt;
+ }
+ }
+ }
+
+ if (safe !== 'ensure') {
+ // Otherwise, search for the best factory among all factories.
+ for (let mt of this.mimeTypes) {
+ if (mt in bundle) {
+ return mt;
+ }
+ }
+ }
+
+ // Otherwise, no matching mime type exists.
+ return undefined;
+ }
+
+ public getCtorFromMimeType(mimeType: string): Type {
+ let componentDescriptor = this._componentDefinitions[mimeType];
+ return componentDescriptor ? componentDescriptor.ctor : undefined;
+ }
+
+ public getAllCtors(): Array> {
+ let addedCtors = [];
+ let ctors = Object.values(this._componentDefinitions)
+ .map((c: IMimeComponentDefinition) => c.ctor)
+ .filter(ctor => {
+ let shouldAdd = !addedCtors.find((ctor2) => ctor === ctor2);
+ if (shouldAdd) {
+ addedCtors.push(ctor);
+ }
+ return shouldAdd;
+ });
+ return ctors;
+ }
+
+ public getAllMimeTypes(): Array {
+ return Object.keys(this._componentDefinitions);
+ }
+
+ /**
+ * The ordered list of mimeTypes.
+ */
+ get mimeTypes(): ReadonlyArray {
+ return this._types || (this._types = sortedTypes(this._ranks));
+ }
+
+}
+
+const componentRegistry = new MimeComponentRegistry();
+platform.Registry.add(Extensions.MimeComponentContribution, componentRegistry);
+
+export function registerComponentType(componentDefinition: IMimeComponentDefinition): void {
+ componentRegistry.registerComponentType(componentDefinition);
+}
+
+
+/**
+ * Get the mime types in the map, ordered by rank.
+ */
+function sortedTypes(map: RankMap): string[] {
+ return Object.keys(map).sort((a, b) => {
+ let p1 = map[a];
+ let p2 = map[b];
+ if (p1.rank !== p2.rank) {
+ return p1.rank - p2.rank;
+ }
+ return p1.id - p2.id;
+ });
+}
\ No newline at end of file
diff --git a/src/sql/workbench/parts/notebook/outputs/mimeRenderer.component.ts b/src/sql/workbench/parts/notebook/outputs/mimeRenderer.component.ts
new file mode 100644
index 0000000000..9677c758f5
--- /dev/null
+++ b/src/sql/workbench/parts/notebook/outputs/mimeRenderer.component.ts
@@ -0,0 +1,78 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistry';
+import { AngularDisposable } from 'sql/base/node/lifecycle';
+import { ElementRef, forwardRef, Inject, Component, OnInit, Input } from '@angular/core';
+import { MimeModel } from 'sql/workbench/parts/notebook/outputs/common/mimemodel';
+import { INotebookService } from 'sql/workbench/services/notebook/common/notebookService';
+import { RenderMimeRegistry } from 'sql/workbench/parts/notebook/outputs/registry';
+import { localize } from 'vs/nls';
+
+@Component({
+ selector: MimeRendererComponent.SELECTOR,
+ template: ``
+})
+export class MimeRendererComponent extends AngularDisposable implements IMimeComponent, OnInit {
+ public static readonly SELECTOR = 'mime-output';
+ private _bundleOptions: MimeModel.IOptions;
+ private registry: RenderMimeRegistry;
+ private _initialized: boolean = false;
+
+ constructor(
+ @Inject(forwardRef(() => ElementRef)) private el: ElementRef,
+ @Inject(INotebookService) private _notebookService: INotebookService,
+ ) {
+ super();
+ this.registry = this._notebookService.getMimeRegistry();
+ }
+
+ @Input() set bundleOptions(value: MimeModel.IOptions) {
+ this._bundleOptions = value;
+ if (this._initialized) {
+ this.renderOutput();
+ }
+ }
+
+ @Input() mimeType: string;
+
+ ngOnInit(): void {
+ this.renderOutput();
+ this._initialized = true;
+ }
+
+ layout(): void {
+ // Re-layout the output when layout is requested
+ this.renderOutput();
+ }
+
+ private renderOutput(): void {
+ // TODO handle safe/unsafe mapping
+ this.createRenderedMimetype(this._bundleOptions, this.el.nativeElement);
+ }
+
+ protected createRenderedMimetype(options: MimeModel.IOptions, node: HTMLElement): void {
+ if (this.mimeType) {
+ let renderer = this.registry.createRenderer(this.mimeType);
+ renderer.node = node;
+ let model = new MimeModel(options);
+ renderer.renderModel(model).catch(error => {
+ // Manually append error message to output
+ renderer.node.innerHTML = `Javascript Error: ${error.message}`;
+ // Remove mime-type-specific CSS classes
+ renderer.node.className = 'p-Widget jp-RenderedText';
+ renderer.node.setAttribute(
+ 'data-mime-type',
+ 'application/vnd.jupyter.stderr'
+ );
+ });
+ } else {
+ node.innerHTML = localize('noRendererFound',
+ "No {0} renderer could be found for output. It has the following MIME types: {1}",
+ options.trusted ? '' : localize('safe', "(safe) "),
+ Object.keys(options.data).join(', '));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/sql/workbench/parts/notebook/outputs/widgets.ts b/src/sql/workbench/parts/notebook/outputs/widgets.ts
index 3e622783c0..f7a08ebc81 100644
--- a/src/sql/workbench/parts/notebook/outputs/widgets.ts
+++ b/src/sql/workbench/parts/notebook/outputs/widgets.ts
@@ -7,7 +7,6 @@ import * as renderers from './renderers';
import { IRenderMime } from './common/renderMimeInterfaces';
import { ReadonlyJSONObject } from '../models/jsonext';
import * as tableRenderers from 'sql/workbench/parts/notebook/outputs/tableRenderers';
-import { IThemeService } from 'vs/platform/theme/common/themeService';
/**
* A common base class for mime renderers.
@@ -376,4 +375,4 @@ export class RenderedDataResource extends RenderedCommon {
themeService: model.themeService
});
}
-}
\ No newline at end of file
+}