mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-25 09:35:37 -05:00
Notebooks: Support for In-Proc Markdown Renderer (#6164)
* NB improve startup using built-in markdown render This is a sample branch showing perf improvements if we load content using built-in markdown rendering - Has issues where images aren't correctly rendered due to sanitization, need to copy renderer code and update settings - Moves content load up before anythign to do with providers since we can render without knowing about these things # Conflicts: # src/sql/workbench/parts/notebook/cellViews/textCell.component.ts * Re-enable logging of each cell's rendering time * Fix test issue * Kernel loading working with new markdown renderer # Conflicts: # src/sql/workbench/parts/notebook/cellViews/textCell.component.ts * Fixed tests, cleaned up code * markdownOutput component integration * PR Comments * PR feedback 2 * PR feedback again
This commit is contained in:
@@ -24,11 +24,17 @@ 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';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/outputs/notebookMarkdown';
|
||||
import { convertVscodeResourceToFileInSubDirectories, useInProcMarkdown } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
|
||||
export const TEXT_SELECTOR: string = 'text-cell-component';
|
||||
const USER_SELECT_CLASS = 'actionselect';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: TEXT_SELECTOR,
|
||||
templateUrl: decodeURI(require.toUrl('./textCell.component.html'))
|
||||
@@ -73,17 +79,28 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
public readonly onDidClickLink = this._onDidClickLink.event;
|
||||
private _cellToggleMoreActions: CellToggleMoreActions;
|
||||
private _hover: boolean;
|
||||
private markdownRenderer: NotebookMarkdownRenderer;
|
||||
private markdownResult: IMarkdownRenderResult;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => CommonServiceInterface)) private _bootstrapService: CommonServiceInterface,
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService,
|
||||
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
|
||||
@Inject(ICommandService) private _commandService: ICommandService
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(IOpenerService) private readonly openerService: IOpenerService,
|
||||
@Inject(IConfigurationService) private configurationService: IConfigurationService,
|
||||
|
||||
) {
|
||||
super();
|
||||
this.isEditMode = true;
|
||||
this._cellToggleMoreActions = this._instantiationService.createInstance(CellToggleMoreActions);
|
||||
this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
|
||||
this._register(toDisposable(() => {
|
||||
if (this.markdownResult) {
|
||||
this.markdownResult.dispose();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
//Gets sanitizer from ISanitizer interface
|
||||
@@ -142,7 +159,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
* 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() {
|
||||
private updatePreview(): void {
|
||||
let trustedChanged = this.cellModel && this._lastTrustedMode !== this.cellModel.trustedMode;
|
||||
let contentChanged = this._content !== this.cellModel.source || this.cellModel.source.length === 0;
|
||||
if (trustedChanged || contentChanged) {
|
||||
@@ -153,13 +170,24 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
|
||||
this._content = this.cellModel.source;
|
||||
}
|
||||
|
||||
this._commandService.executeCommand<string>('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
|
||||
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this.cellModel);
|
||||
htmlcontent = this.sanitizeContent(htmlcontent);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = htmlcontent;
|
||||
if (useInProcMarkdown(this.configurationService)) {
|
||||
this.markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
|
||||
this.markdownResult = this.markdownRenderer.render({
|
||||
isTrusted: this.cellModel.trustedMode,
|
||||
value: this._content
|
||||
});
|
||||
this.setLoading(false);
|
||||
});
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = this.markdownResult.element.innerHTML;
|
||||
} else {
|
||||
this._commandService.executeCommand<string>('notebook.showPreview', this.cellModel.notebookModel.notebookUri, this._content).then((htmlcontent) => {
|
||||
htmlcontent = convertVscodeResourceToFileInSubDirectories(htmlcontent, this.cellModel);
|
||||
htmlcontent = this.sanitizeContent(htmlcontent);
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = htmlcontent;
|
||||
this.setLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -405,6 +405,7 @@ export interface INotebookModel {
|
||||
|
||||
serializationStateChanged(changeType: NotebookChangeType): void;
|
||||
|
||||
standardKernels: IStandardKernelWithProvider[];
|
||||
}
|
||||
|
||||
export interface NotebookContentChange {
|
||||
@@ -502,7 +503,6 @@ export interface INotebookModelOptions {
|
||||
contentManager: IContentManager;
|
||||
notebookManagers: INotebookManager[];
|
||||
providerId: string;
|
||||
standardKernels: IStandardKernelWithProvider[];
|
||||
defaultKernel: nb.IKernelSpec;
|
||||
cellMagicMapper: ICellMagicMapper;
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
private _clientSessionListeners: IDisposable[] = [];
|
||||
private _connectionUrisToDispose: string[] = [];
|
||||
private _textCellsLoading: number = 0;
|
||||
private _standardKernels: notebookUtils.IStandardKernelWithProvider[];
|
||||
|
||||
public requestConnectionHandler: () => Promise<boolean>;
|
||||
|
||||
@@ -92,14 +93,6 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
this._trustedMode = false;
|
||||
this._providerId = _notebookOptions.providerId;
|
||||
this._onProviderIdChanged.fire(this._providerId);
|
||||
this._notebookOptions.standardKernels.forEach(kernel => {
|
||||
let displayName = kernel.displayName;
|
||||
if (!displayName) {
|
||||
displayName = kernel.name;
|
||||
}
|
||||
this._kernelDisplayNameToConnectionProviderIds.set(displayName, kernel.connectionProviderIds);
|
||||
this._kernelDisplayNameToNotebookProviderIds.set(displayName, kernel.notebookProvider);
|
||||
});
|
||||
if (this._notebookOptions.layoutChanged) {
|
||||
this._notebookOptions.layoutChanged(() => this._layoutChanged.fire());
|
||||
}
|
||||
@@ -274,6 +267,15 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
return this._onValidConnectionSelected.event;
|
||||
}
|
||||
|
||||
public get standardKernels(): notebookUtils.IStandardKernelWithProvider[] {
|
||||
return this._standardKernels;
|
||||
}
|
||||
|
||||
public set standardKernels(kernels) {
|
||||
this._standardKernels = kernels;
|
||||
this.setKernelDisplayNameMapsWithStandardKernels();
|
||||
}
|
||||
|
||||
public getApplicableConnectionProviderIds(kernelDisplayName: string): string[] {
|
||||
let ids = [];
|
||||
if (kernelDisplayName) {
|
||||
@@ -282,7 +284,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
return !ids ? [] : ids;
|
||||
}
|
||||
|
||||
public async requestModelLoad(isTrusted: boolean = false): Promise<void> {
|
||||
public async loadContents(isTrusted: boolean = false): Promise<void> {
|
||||
try {
|
||||
this._trustedMode = isTrusted;
|
||||
let contents = null;
|
||||
@@ -294,7 +296,7 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
// if cells already exist, create them with language info (if it is saved)
|
||||
this._cells = [];
|
||||
if (contents) {
|
||||
this._defaultLanguageInfo = this.getDefaultLanguageInfo(contents);
|
||||
this._defaultLanguageInfo = contents && contents.metadata && contents.metadata.language_info;
|
||||
this._savedKernelInfo = this.getSavedKernelInfo(contents);
|
||||
if (contents.cells && contents.cells.length > 0) {
|
||||
this._cells = contents.cells.map(c => {
|
||||
@@ -304,6 +306,13 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this._inErrorState = true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
public async requestModelLoad(): Promise<void> {
|
||||
try {
|
||||
this.setDefaultKernelAndProviderId();
|
||||
this.trySetLanguageFromLangInfo();
|
||||
} catch (error) {
|
||||
@@ -421,8 +430,8 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
}
|
||||
|
||||
public async startSession(manager: INotebookManager, displayName?: string, setErrorStateOnFail?: boolean): Promise<void> {
|
||||
if (displayName) {
|
||||
let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName);
|
||||
if (displayName && this._standardKernels) {
|
||||
let standardKernel = this._standardKernels.find(kernel => kernel.displayName === displayName);
|
||||
this._defaultKernel = displayName ? { name: standardKernel.name, display_name: standardKernel.displayName } : this._defaultKernel;
|
||||
}
|
||||
if (this._defaultKernel) {
|
||||
@@ -506,30 +515,35 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
this._defaultKernel = notebookConstants.sqlKernelSpec;
|
||||
this._providerId = SQL_NOTEBOOK_PROVIDER;
|
||||
}
|
||||
// update default language
|
||||
this._defaultLanguageInfo = {
|
||||
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
|
||||
version: ''
|
||||
};
|
||||
if (!this._defaultLanguageInfo || this._defaultLanguageInfo.name) {
|
||||
// update default language
|
||||
this._defaultLanguageInfo = {
|
||||
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
|
||||
version: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private isValidConnection(profile: IConnectionProfile | connection.Connection) {
|
||||
let standardKernels = this._notebookOptions.standardKernels.find(kernel => this._defaultKernel && kernel.displayName === this._defaultKernel.display_name);
|
||||
let connectionProviderIds = standardKernels ? standardKernels.connectionProviderIds : undefined;
|
||||
return profile && connectionProviderIds && connectionProviderIds.find(provider => provider === profile.providerName) !== undefined;
|
||||
if (this._standardKernels) {
|
||||
let standardKernels = this._standardKernels.find(kernel => this._defaultKernel && kernel.displayName === this._defaultKernel.display_name);
|
||||
let connectionProviderIds = standardKernels ? standardKernels.connectionProviderIds : undefined;
|
||||
return profile && connectionProviderIds && connectionProviderIds.find(provider => provider === profile.providerName) !== undefined;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public getStandardKernelFromName(name: string): notebookUtils.IStandardKernelWithProvider {
|
||||
if (name) {
|
||||
let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.name.toLowerCase() === name.toLowerCase());
|
||||
if (name && this._standardKernels) {
|
||||
let kernel = this._standardKernels.find(kernel => kernel.name.toLowerCase() === name.toLowerCase());
|
||||
return kernel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getStandardKernelFromDisplayName(displayName: string): notebookUtils.IStandardKernelWithProvider {
|
||||
if (displayName) {
|
||||
let kernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName.toLowerCase() === displayName.toLowerCase());
|
||||
if (displayName && this._standardKernels) {
|
||||
let kernel = this._standardKernels.find(kernel => kernel.displayName.toLowerCase() === displayName.toLowerCase());
|
||||
return kernel;
|
||||
}
|
||||
return undefined;
|
||||
@@ -713,15 +727,6 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
}
|
||||
}
|
||||
|
||||
// Get default language if saved in notebook file
|
||||
// Otherwise, default to python
|
||||
private getDefaultLanguageInfo(notebook: nb.INotebookContents): nb.ILanguageInfo {
|
||||
return (notebook && notebook.metadata && notebook.metadata.language_info) ? notebook.metadata.language_info : {
|
||||
name: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'sql' : 'python',
|
||||
version: '',
|
||||
mimetype: this._providerId === SQL_NOTEBOOK_PROVIDER ? 'x-sql' : 'x-python'
|
||||
};
|
||||
}
|
||||
|
||||
// Get default kernel info if saved in notebook file
|
||||
private getSavedKernelInfo(notebook: nb.INotebookContents): nb.IKernelInfo {
|
||||
@@ -746,11 +751,12 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
if (this._savedKernelInfo.display_name !== displayName) {
|
||||
this._savedKernelInfo.display_name = displayName;
|
||||
}
|
||||
|
||||
let standardKernel = this._notebookOptions.standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName));
|
||||
if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) {
|
||||
this._savedKernelInfo.name = standardKernel.name;
|
||||
this._savedKernelInfo.display_name = standardKernel.displayName;
|
||||
if (this._standardKernels) {
|
||||
let standardKernel = this._standardKernels.find(kernel => kernel.displayName === displayName || displayName.startsWith(kernel.displayName));
|
||||
if (standardKernel && this._savedKernelInfo.name && this._savedKernelInfo.name !== standardKernel.name) {
|
||||
this._savedKernelInfo.name = standardKernel.name;
|
||||
this._savedKernelInfo.display_name = standardKernel.displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -951,6 +957,23 @@ export class NotebookModel extends Disposable implements INotebookModel {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set maps with values to have a way to determine the connection
|
||||
* provider and notebook provider ids from a kernel display name
|
||||
*/
|
||||
private setKernelDisplayNameMapsWithStandardKernels(): void {
|
||||
if (this._standardKernels) {
|
||||
this._standardKernels.forEach(kernel => {
|
||||
let displayName = kernel.displayName;
|
||||
if (!displayName) {
|
||||
displayName = kernel.name;
|
||||
}
|
||||
this._kernelDisplayNameToConnectionProviderIds.set(displayName, kernel.connectionProviderIds);
|
||||
this._kernelDisplayNameToNotebookProviderIds.set(displayName, kernel.notebookProvider);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the model to JSON.
|
||||
*/
|
||||
|
||||
@@ -69,7 +69,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
protected isLoading: boolean;
|
||||
private notebookManagers: INotebookManager[] = [];
|
||||
private _modelReadyDeferred = new Deferred<NotebookModel>();
|
||||
private _modelRegisteredDeferred = new Deferred<NotebookModel>();
|
||||
private profile: IConnectionProfile;
|
||||
private _trustedAction: TrustedAction;
|
||||
private _runAllCellsAction: RunAllCellsAction;
|
||||
@@ -146,10 +145,6 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
return this._model && this._model.activeCell ? this._model.activeCell.id : '';
|
||||
}
|
||||
|
||||
public get modelRegistered(): Promise<NotebookModel> {
|
||||
return this._modelRegisteredDeferred.promise;
|
||||
}
|
||||
|
||||
public get cells(): ICellModel[] {
|
||||
return this._model ? this._model.cells : [];
|
||||
}
|
||||
@@ -222,6 +217,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
|
||||
private async doLoad(): Promise<void> {
|
||||
try {
|
||||
await this.createModelAndLoadContents();
|
||||
await this.setNotebookManager();
|
||||
await this.loadModel();
|
||||
this._modelReadyDeferred.resolve(this._model);
|
||||
@@ -268,7 +264,17 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
}
|
||||
|
||||
private async loadModel(): Promise<void> {
|
||||
// Wait on provider information to be available before loading kernel and other information
|
||||
await this.awaitNonDefaultProvider();
|
||||
await this._model.requestModelLoad();
|
||||
this.detectChanges();
|
||||
await this._model.startSession(this._model.notebookManager, undefined, true);
|
||||
this.setContextKeyServiceWithProviderId(this._model.providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
private async createModelAndLoadContents(): Promise<void> {
|
||||
let model = new NotebookModel({
|
||||
factory: this.modelFactory,
|
||||
notebookUri: this._notebookParams.notebookUri,
|
||||
@@ -276,27 +282,22 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
notificationService: this.notificationService,
|
||||
notebookManagers: this.notebookManagers,
|
||||
contentManager: this._notebookParams.input.contentManager,
|
||||
standardKernels: this._notebookParams.input.standardKernels,
|
||||
cellMagicMapper: new CellMagicMapper(this.notebookService.languageMagics),
|
||||
providerId: 'sql', // this is tricky; really should also depend on the connection profile
|
||||
providerId: 'sql',
|
||||
defaultKernel: this._notebookParams.input.defaultKernel,
|
||||
layoutChanged: this._notebookParams.input.layoutChanged,
|
||||
capabilitiesService: this.capabilitiesService,
|
||||
editorLoadedTimestamp: this._notebookParams.input.editorOpenedTimestamp
|
||||
}, this.profile, this.logService, this.notificationService, this.telemetryService);
|
||||
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
|
||||
let trusted = await this.notebookService.isNotebookTrustCached(this._notebookParams.notebookUri, this.isDirty());
|
||||
await model.requestModelLoad(trusted);
|
||||
model.onError((errInfo: INotification) => this.handleModelError(errInfo));
|
||||
model.contentChanged((change) => this.handleContentChanged(change));
|
||||
model.onProviderIdChange((provider) => this.handleProviderIdChanged(provider));
|
||||
model.kernelChanged((kernelArgs) => this.handleKernelChanged(kernelArgs));
|
||||
this._model = this._register(model);
|
||||
this.updateToolbarComponents(this._model.trustedMode);
|
||||
this._modelRegisteredDeferred.resolve(this._model);
|
||||
await this._model.loadContents(trusted);
|
||||
this.setLoading(false);
|
||||
await model.startSession(this.model.notebookManager, undefined, true);
|
||||
this.setContextKeyServiceWithProviderId(model.providerId);
|
||||
this.fillInActionsForCurrentContext();
|
||||
this.updateToolbarComponents(this._model.trustedMode);
|
||||
this.detectChanges();
|
||||
}
|
||||
|
||||
@@ -311,6 +312,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe
|
||||
private async awaitNonDefaultProvider(): Promise<void> {
|
||||
// Wait on registration for now. Long-term would be good to cache and refresh
|
||||
await this.notebookService.registrationComplete;
|
||||
this.model.standardKernels = this._notebookParams.input.standardKernels;
|
||||
// Refresh the provider if we had been using default
|
||||
let providerInfo = await this._notebookParams.providerInfo;
|
||||
|
||||
|
||||
@@ -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 { IConfigurationRegistry, Extensions as ConfigExtensions } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { localize } from 'vs/nls';
|
||||
import product from 'vs/platform/product/node/product';
|
||||
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';
|
||||
@@ -40,6 +43,19 @@ actionRegistry.registerWorkbenchAction(
|
||||
),
|
||||
NewNotebookAction.LABEL
|
||||
);
|
||||
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
'id': 'notebook',
|
||||
'title': 'Notebook',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'notebook.useInProcMarkdown': {
|
||||
'type': 'boolean',
|
||||
'default': product.quality === 'stable' ? false : true,
|
||||
'description': localize('notebook.inProcMarkdown', 'Use in-process markdown viewer to render text cells more quickly (Experimental).')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* *************** Output components *************** */
|
||||
// Note: most existing types use the same component to render. In order to
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DEFAULT_NOTEBOOK_PROVIDER, DEFAULT_NOTEBOOK_FILETYPE, INotebookService
|
||||
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';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
|
||||
/**
|
||||
@@ -142,3 +143,7 @@ export function convertVscodeResourceToFileInSubDirectories(htmlContent: string,
|
||||
}
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
export function useInProcMarkdown(configurationService: IConfigurationService): boolean {
|
||||
return configurationService.getValue('notebook.useInProcMarkdown');
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ import { IMimeComponent } from 'sql/workbench/parts/notebook/outputs/mimeRegistr
|
||||
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';
|
||||
import { convertVscodeResourceToFileInSubDirectories, useInProcMarkdown } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/outputs/notebookMarkdown';
|
||||
|
||||
@Component({
|
||||
selector: MarkdownOutputComponent.SELECTOR,
|
||||
@@ -33,15 +36,19 @@ export class MarkdownOutputComponent extends AngularDisposable implements IMimeC
|
||||
private _initialized: boolean = false;
|
||||
public loading: boolean = false;
|
||||
private _cellModel: ICellModel;
|
||||
private _markdownRenderer: NotebookMarkdownRenderer;
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
|
||||
@Inject(ICommandService) private _commandService: ICommandService,
|
||||
@Inject(INotebookService) private _notebookService: INotebookService
|
||||
@Inject(INotebookService) private _notebookService: INotebookService,
|
||||
@Inject(IConfigurationService) private _configurationService: IConfigurationService,
|
||||
@Inject(IInstantiationService) private _instantiationService: IInstantiationService
|
||||
|
||||
) {
|
||||
super();
|
||||
this._sanitizer = this._notebookService.getMimeRegistry().sanitizer;
|
||||
this._markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
|
||||
}
|
||||
|
||||
@Input() set bundleOptions(value: MimeModel.IOptions) {
|
||||
@@ -95,16 +102,26 @@ export class MarkdownOutputComponent extends AngularDisposable implements IMimeC
|
||||
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);
|
||||
if (useInProcMarkdown(this._configurationService)) {
|
||||
this._markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
|
||||
let markdownResult = this._markdownRenderer.render({
|
||||
isTrusted: this.cellModel.trustedMode,
|
||||
value: content.toString()
|
||||
});
|
||||
let outputElement = <HTMLElement>this.output.nativeElement;
|
||||
outputElement.innerHTML = markdownResult.element.innerHTML;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
240
src/sql/workbench/parts/notebook/outputs/notebookMarkdown.ts
Normal file
240
src/sql/workbench/parts/notebook/outputs/notebookMarkdown.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as path from 'path';
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import { RenderOptions } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { IMarkdownString, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
import marked = require('vs/base/common/marked/marked');
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Based off of HtmlContentRenderer
|
||||
export class NotebookMarkdownRenderer {
|
||||
private _notebookURI: URI;
|
||||
private _baseUrls: string[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
render(markdown: IMarkdownString): IMarkdownRenderResult {
|
||||
const element: HTMLElement = markdown ? this.renderMarkdown(markdown, undefined) : document.createElement('span');
|
||||
return {
|
||||
element,
|
||||
dispose: () => dispose()
|
||||
};
|
||||
}
|
||||
|
||||
createElement(options: RenderOptions): HTMLElement {
|
||||
const tagName = options.inline ? 'span' : 'div';
|
||||
const element = document.createElement(tagName);
|
||||
if (options.className) {
|
||||
element.className = options.className;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
parse(text: string): any {
|
||||
let data = JSON.parse(text);
|
||||
data = revive(data, 0);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create html nodes for the given content element.
|
||||
* Adapted from htmlContentRenderer. Ensures that the markdown renderer
|
||||
* gets passed in the correct baseUrl for the notebook's saved location,
|
||||
* respects the trusted state of a notebook, and allows command links to
|
||||
* be clickable.
|
||||
*/
|
||||
renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement {
|
||||
const element = this.createElement(options);
|
||||
|
||||
// signal to code-block render that the element has been created
|
||||
let signalInnerHTML: () => void;
|
||||
const withInnerHTML = new Promise(c => signalInnerHTML = c);
|
||||
|
||||
let notebookFolder = path.dirname(this._notebookURI.path) + '/';
|
||||
if (!this._baseUrls.includes(notebookFolder)) {
|
||||
this._baseUrls.push(notebookFolder);
|
||||
}
|
||||
const renderer = new marked.Renderer({ baseUrl: notebookFolder });
|
||||
renderer.image = (href: string, title: string, text: string) => {
|
||||
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
|
||||
let dimensions: string[] = [];
|
||||
if (href) {
|
||||
const splitted = href.split('|').map(s => s.trim());
|
||||
href = splitted[0];
|
||||
const parameters = splitted[1];
|
||||
if (parameters) {
|
||||
const heightFromParams = /height=(\d+)/.exec(parameters);
|
||||
const widthFromParams = /width=(\d+)/.exec(parameters);
|
||||
const height = heightFromParams ? heightFromParams[1] : '';
|
||||
const width = widthFromParams ? widthFromParams[1] : '';
|
||||
const widthIsFinite = isFinite(parseInt(width));
|
||||
const heightIsFinite = isFinite(parseInt(height));
|
||||
if (widthIsFinite) {
|
||||
dimensions.push(`width="${width}"`);
|
||||
}
|
||||
if (heightIsFinite) {
|
||||
dimensions.push(`height="${height}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
let attributes: string[] = [];
|
||||
if (href) {
|
||||
attributes.push(`src="${href}"`);
|
||||
}
|
||||
if (text) {
|
||||
attributes.push(`alt="${text}"`);
|
||||
}
|
||||
if (title) {
|
||||
attributes.push(`title="${title}"`);
|
||||
}
|
||||
if (dimensions.length) {
|
||||
attributes = attributes.concat(dimensions);
|
||||
}
|
||||
return '<img ' + attributes.join(' ') + '>';
|
||||
};
|
||||
renderer.link = (href: string, title: string, text: string): string => {
|
||||
href = this.cleanUrl(!markdown.isTrusted, notebookFolder, href);
|
||||
if (href === null) {
|
||||
return text;
|
||||
}
|
||||
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
|
||||
if (href === text) { // raw link case
|
||||
text = removeMarkdownEscapes(text);
|
||||
}
|
||||
title = removeMarkdownEscapes(title);
|
||||
href = removeMarkdownEscapes(href);
|
||||
if (
|
||||
!href
|
||||
|| !markdown.isTrusted
|
||||
|| href.match(/^data:|javascript:/i)
|
||||
|| href.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)
|
||||
) {
|
||||
// drop the link
|
||||
return text;
|
||||
|
||||
} else {
|
||||
// HTML Encode href
|
||||
href = href.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return `<a href=${href} data-href="${href}" title="${title || href}">${text}</a>`;
|
||||
}
|
||||
};
|
||||
renderer.paragraph = (text): string => {
|
||||
return `<p>${text}</p>`;
|
||||
};
|
||||
|
||||
if (options.codeBlockRenderer) {
|
||||
renderer.code = (code, lang) => {
|
||||
const value = options.codeBlockRenderer!(lang, code);
|
||||
// when code-block rendering is async we return sync
|
||||
// but update the node with the real result later.
|
||||
const id = defaultGenerator.nextId();
|
||||
|
||||
const promise = value.then(strValue => {
|
||||
withInnerHTML.then(e => {
|
||||
const span = element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
span.innerHTML = strValue;
|
||||
}
|
||||
}).catch(err => {
|
||||
// ignore
|
||||
});
|
||||
});
|
||||
|
||||
if (options.codeBlockRenderCallback) {
|
||||
promise.then(options.codeBlockRenderCallback);
|
||||
}
|
||||
|
||||
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
|
||||
};
|
||||
}
|
||||
|
||||
const markedOptions: marked.MarkedOptions = {
|
||||
sanitize: !markdown.isTrusted,
|
||||
renderer,
|
||||
baseUrl: notebookFolder
|
||||
};
|
||||
|
||||
element.innerHTML = marked.parse(markdown.value, markedOptions);
|
||||
signalInnerHTML!();
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
// This following methods have been adapted from marked.js
|
||||
// Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/)
|
||||
cleanUrl(sanitize: boolean, base: string, href: string) {
|
||||
if (sanitize) {
|
||||
let prot: string;
|
||||
try {
|
||||
prot = decodeURIComponent(unescape(href))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (URI.parse(href)) {
|
||||
return href;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
let originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
||||
if (base && !originIndependentUrl.test(href) && !fs.existsSync(href)) {
|
||||
href = this.resolveUrl(base, href);
|
||||
}
|
||||
try {
|
||||
href = encodeURI(href).replace(/%25/g, '%');
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
}
|
||||
|
||||
resolveUrl(base: string, href: string) {
|
||||
if (!this._baseUrls[' ' + base]) {
|
||||
// we can ignore everything in base after the last slash of its path component,
|
||||
// but we might need to add _that_
|
||||
// https://tools.ietf.org/html/rfc3986#section-3
|
||||
if (/^[^:]+:\/*[^/]*$/.test(base)) {
|
||||
this._baseUrls[' ' + base] = base + '/';
|
||||
} else {
|
||||
// Remove trailing 'c's. /c*$/ is vulnerable to REDOS.
|
||||
this._baseUrls[' ' + base] = base.replace(/c*$/, '');
|
||||
}
|
||||
}
|
||||
base = this._baseUrls[' ' + base];
|
||||
|
||||
if (href.slice(0, 2) === '//') {
|
||||
return base.replace(/:[\s\S]*/, ':') + href;
|
||||
} else if (href.charAt(0) === '/') {
|
||||
return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href;
|
||||
} else {
|
||||
return base + href;
|
||||
}
|
||||
}
|
||||
|
||||
// end marked.js adaptation
|
||||
|
||||
setNotebookURI(val: URI) {
|
||||
this._notebookURI = val;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user