mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-17 01:25:36 -05:00
Support output renderers via Angular contributions (#6146)
- Added a registry for output components - Refactored existing renderers to plug in via Angular - Added Markdown renderer using new Angular contribution point - Added support to notebook module to dynamically load new components
This commit is contained in:
@@ -6,6 +6,10 @@
|
||||
-->
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column">
|
||||
<div style="flex: 0 0 auto; user-select: none;">
|
||||
<div #output class="output-userselect" ></div>
|
||||
<div #output class="output-userselect">
|
||||
<ng-template component-host>
|
||||
</ng-template>
|
||||
<pre *ngIf="hasError" class="p-Widget jp-RenderedText">{{errorText}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 = <IMimeComponentRegistry>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 = `<pre>Javascript Error: ${error.message}</pre>`;
|
||||
// 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 = <HTMLElement>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<IMimeComponent>;
|
||||
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 = <HTMLElement>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>('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 = <HTMLElement>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() {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
);
|
||||
|
||||
/* *************** 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
|
||||
});
|
||||
|
||||
@@ -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<IMimeComponentRegistry>(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,
|
||||
|
||||
@@ -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<any> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<string>;
|
||||
|
||||
/**
|
||||
* The file types for which the factory should be the default.
|
||||
*/
|
||||
readonly defaultFor?: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* 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<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string>;
|
||||
|
||||
/**
|
||||
* The extensions of the file type (e.g. `".txt"`). Can be a compound
|
||||
* extension (e.g. `".table.json`).
|
||||
*/
|
||||
readonly extensions: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* 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<IDocumentWidgetFactoryOptions>;
|
||||
|
||||
/**
|
||||
* The optional file type associated with the extension.
|
||||
*/
|
||||
readonly fileTypes?: ReadonlyArray<IFileType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<IExtension>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<IRenderMime.IRendererFactory> = [
|
||||
htmlRendererFactory,
|
||||
// markdownRendererFactory,
|
||||
// latexRendererFactory,
|
||||
svgRendererFactory,
|
||||
imageRendererFactory,
|
||||
@@ -103,3 +92,4 @@ export const standardRendererFactories: ReadonlyArray<IRenderMime.IRendererFacto
|
||||
textRendererFactory,
|
||||
dataResourceRendererFactory
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
|
||||
<div class="icon in-progress" *ngIf="loading === true"></div>
|
||||
<div #output link-handler [isTrusted]="isTrusted" class="notebook-preview" style="flex: 1 1 auto">
|
||||
</div>
|
||||
</div>
|
||||
@@ -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<string>('notebook.showPreview', this._cellModel.notebookModel.notebookUri, content).then((htmlcontent) => {
|
||||
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this._cellModel);
|
||||
htmlcontent = this.sanitizeContent(htmlcontent);
|
||||
let outputElement = <HTMLElement>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();
|
||||
}
|
||||
}
|
||||
195
src/sql/workbench/parts/notebook/outputs/mimeRegistry.ts
Normal file
195
src/sql/workbench/parts/notebook/outputs/mimeRegistry.ts
Normal file
@@ -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<string>;
|
||||
|
||||
/**
|
||||
* 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<IMimeComponent>;
|
||||
}
|
||||
|
||||
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<IMimeComponent>;
|
||||
getAllCtors(): Array<Type<IMimeComponent>>;
|
||||
getAllMimeTypes(): Array<string>;
|
||||
}
|
||||
|
||||
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<IMimeComponent> {
|
||||
let componentDescriptor = this._componentDefinitions[mimeType];
|
||||
return componentDescriptor ? componentDescriptor.ctor : undefined;
|
||||
}
|
||||
|
||||
public getAllCtors(): Array<Type<IMimeComponent>> {
|
||||
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<string> {
|
||||
return Object.keys(this._componentDefinitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* The ordered list of mimeTypes.
|
||||
*/
|
||||
get mimeTypes(): ReadonlyArray<string> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -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 = `<pre>Javascript Error: ${error.message}</pre>`;
|
||||
// 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(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user