moves notebooks code to browser (#7313)

This commit is contained in:
Anthony Dresser
2019-09-23 13:32:29 -07:00
committed by GitHub
parent 6f06ab440a
commit 5e3ec6ea39
11 changed files with 20 additions and 28 deletions

View File

@@ -0,0 +1,191 @@
/*
https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs2015.css
*/
/*
* Visual Studio 2015 dark style
* Author: Nicolas LLOBERA <nllobera@gmail.com>
*/
.notebook-preview .hljs-keyword,
.notebook-preview .hljs-literal,
.notebook-preview .hljs-symbol,
.notebook-preview .hljs-name {
color: #569CD6;
}
.notebook-preview .hljs-link {
color: #569CD6;
text-decoration: underline;
}
.notebook-preview .hljs-built_in,
.notebook-preview .hljs-type {
color: #4EC9B0;
}
.notebook-preview .hljs-number,
.notebook-preview .hljs-class {
color: #B8D7A3;
}
.notebook-preview .hljs-string,
.notebook-preview .hljs-meta-string {
color: #D69D85;
}
.notebook-preview .hljs-regexp,
.notebook-preview .hljs-template-tag {
color: #9A5334;
}
.notebook-preview .hljs-subst,
.notebook-preview .hljs-function,
.notebook-preview .hljs-title,
.notebook-preview .hljs-params,
.notebook-preview .hljs-formula {
color: #DCDCDC;
}
.notebook-preview pre code .hljs-subst,
.notebook-preview pre code .hljs-function,
.notebook-preview pre code .hljs-title,
.notebook-preview pre code .hljs-params,
.notebook-preview pre code .hljs-formula {
color: var(--vscode-editor-foreground);
}
.notebook-preview .hljs-comment,
.notebook-preview .hljs-quote {
color: #57A64A;
font-style: italic;
}
.notebook-preview .hljs-doctag {
color: #608B4E;
}
.notebook-preview .hljs-meta,
.notebook-preview .hljs-meta-keyword,
.notebook-preview .hljs-tag {
color: #9B9B9B;
}
.notebook-preview .hljs-variable,
.notebook-preview .hljs-template-variable {
color: #BD63C5;
}
.notebook-preview .hljs-attr,
.notebook-preview .hljs-attribute,
.notebook-preview .hljs-builtin-name {
color: #9CDCFE;
}
.notebook-preview .hljs-section {
color: gold;
}
.notebook-preview .hljs-emphasis {
font-style: italic;
}
.notebook-preview .hljs-strong {
font-weight: bold;
}
/*.hljs-code {
font-family:'Monospace';
}*/
.notebook-preview .hljs-bullet,
.notebook-preview .hljs-selector-tag,
.notebook-preview .hljs-selector-id,
.notebook-preview .hljs-selector-class,
.notebook-preview .hljs-selector-attr,
.notebook-preview .hljs-selector-pseudo {
color: #D7BA7D;
}
.notebook-preview .hljs-addition {
background-color: var(--vscode-diffEditor-insertedTextBackground, rgba(155, 185, 85, 0.2));
color: rgb(155, 185, 85);
display: inline-block;
width: 100%;
}
.notebook-preview .hljs-deletion {
background: var(--vscode-diffEditor-removedTextBackground, rgba(255, 0, 0, 0.2));
color: rgb(255, 0, 0);
display: inline-block;
width: 100%;
}
/*
From https://raw.githubusercontent.com/isagalaev/highlight.js/master/src/styles/vs.css
*/
/*
Visual Studio-like style based on original C# coloring by Jason Diamond <jason@diamond.name>
*/
.notebook-preview .hljs-function,
.notebook-preview .hljs-params {
color: inherit;
}
.notebook-preview .hljs-comment,
.notebook-preview .hljs-quote,
.notebook-preview .hljs-variable {
color: #008000;
}
.notebook-preview .hljs-keyword,
.notebook-preview .hljs-selector-tag,
.notebook-preview .hljs-built_in,
.notebook-preview .hljs-name,
.notebook-preview .hljs-tag {
color: #00f;
}
.notebook-preview .hljs-string,
.notebook-preview .hljs-title,
.notebook-preview .hljs-section,
.notebook-preview .hljs-attribute,
.notebook-preview .hljs-literal,
.notebook-preview .hljs-template-tag,
.notebook-preview .hljs-template-variable,
.notebook-preview .hljs-type {
color: #a31515;
}
.notebook-preview .hljs-selector-attr,
.notebook-preview .hljs-selector-pseudo,
.notebook-preview .hljs-meta {
color: #2b91af;
}
.notebook-preview .hljs-doctag {
color: #808080;
}
.notebook-preview .hljs-attr {
color: #f00;
}
.notebook-preview .hljs-symbol,
.notebook-preview .hljs-bullet,
.notebook-preview .hljs-link {
color: #00b0e8;
}
.notebook-preview .hljs-emphasis {
font-style: italic;
}
.notebook-preview .hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1,231 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.notebook-preview {
font-size: 14px;
line-height: 22px;
word-wrap: break-word;
}
.notebook-preview #code-csp-warning {
position: fixed;
top: 0;
right: 0;
color: white;
margin: 16px;
text-align: center;
font-size: 12px;
font-family: sans-serif;
background-color:#444444;
cursor: pointer;
padding: 6px;
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
}
.notebook-preview #code-csp-warning:hover {
text-decoration: none;
background-color:#007acc;
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
}
.notebook-preview .scrollBeyondLastLine {
margin-bottom: calc(100vh - 22px);
}
.notebook-preview .showEditorSelection .code-line {
position: relative;
}
.notebook-preview .showEditorSelection .code-active-line:before,
.notebook-preview .showEditorSelection .code-line:hover:before {
content: "";
display: block;
position: absolute;
top: 0;
left: -12px;
height: 100%;
}
.notebook-preview .showEditorSelection li.code-active-line:before,
.notebook-preview .showEditorSelection li.code-line:hover:before {
left: -30px;
}
.notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(0, 0, 0, 0.15);
}
.notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(0, 0, 0, 0.40);
}
.notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.vs-dark .notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 255, 255, 0.4);
}
.vs-dark .notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 255, 255, 0.60);
}
.vs-dark .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.hc-black .notebook-preview .showEditorSelection .code-active-line:before {
border-left: 3px solid rgba(255, 160, 0, 0.7);
}
.hc-black .notebook-preview .showEditorSelection .code-line:hover:before {
border-left: 3px solid rgba(255, 160, 0, 1);
}
.hc-black .notebook-preview .showEditorSelection .code-line .code-line:hover:before {
border-left: none;
}
.notebook-preview img {
max-width: 100%;
max-height: 100%;
}
.notebookEditor a {
text-decoration: none;
}
.notebookEditor a:hover {
text-decoration: underline;
}
.notebook-preview a:focus,
.notebook-preview input:focus,
.notebook-preview select:focus,
.notebook-preview textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
.notebook-preview hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
.notebook-preview h1 {
padding-bottom: 0.3em;
line-height: 1.2;
border-bottom-width: 1px;
border-bottom-style: solid;
}
.notebook-preview h1, .notebook-preview h2, .notebook-preview h3 {
font-weight: normal;
}
.notebook-preview h1 code,
.notebook-preview h2 code,
.notebook-preview h3 code,
.notebook-preview h4 code,
.notebook-preview h5 code,
.notebook-preview h6 code {
font-size: inherit;
line-height: auto;
}
.notebook-preview table {
border-collapse: collapse;
}
.notebook-preview table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
.notebook-preview table > thead > tr > th,
.notebook-preview table > thead > tr > td,
.notebook-preview table > tbody > tr > th,
.notebook-preview .notebook-preview table > tbody > tr > td {
padding: 5px 10px;
}
.notebook-preview table > tbody > tr + tr > td {
border-top: 1px solid;
}
.notebook-preview blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
.notebook-preview code {
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
font-size: 12px;
line-height: 19px;
}
.notebook-preview pre {
white-space: pre-wrap;
}
.notebook-preview .mac code {
font-size: 12px;
line-height: 18px;
}
.notebook-preview pre:not(.hljs),
.notebook-preview pre.hljs code > div {
padding: 16px;
border-radius: 3px;
overflow: auto;
}
/** Theming */
.notebook-preview pre code {
color: var(--vscode-editor-foreground);
}
.notebook-preview pre {
background-color: rgba(220, 220, 220, 0.4);
}
.vs-dark .notebook-preview pre {
background-color: rgba(10, 10, 10, 0.4);
}
.hc-black .notebook-preview pre {
background-color: rgb(0, 0, 0);
}
.hc-black .notebook-preview h1 {
border-color: rgb(0, 0, 0);
}
.notebook-preview table > thead > tr > th {
border-color: rgba(0, 0, 0, 0.69);
}
.vs-dark .notebook-preview table > thead > tr > th {
border-color: rgba(255, 255, 255, 0.69);
}
.notebook-preview h1,
.notebook-preview hr,
.notebook-preview table > tbody > tr + tr > td {
border-color: rgba(0, 0, 0, 0.18);
}
.vs-dark .notebook-preview h1,
.vs-dark .notebook-preview hr,
.vs-dark .notebook-preview table > tbody > tr + tr > td {
border-color: rgba(255, 255, 255, 0.18);
}

View File

@@ -0,0 +1,18 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: column" (mouseover)="hover=true" (mouseleave)="hover=false">
<div class="notebook-text" style="flex: 0 0 auto;">
<code-component *ngIf="isEditMode" [cellModel]="cellModel" (onContentChanged)="handleContentChanged()" [model]="model" [activeCellId]="activeCellId">
</code-component>
</div>
<div style="overflow: hidden; width: 100%; height: 100%; display: flex; flex-flow: row">
<div #preview link-handler [isTrusted]="isTrusted" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto" (dblclick)="toggleEditMode()">
</div>
<div #moreactions class="moreActions" style="flex: 0 0 auto; display: flex; flex-flow:column;width: 20px; min-height: 20px; max-height: 20px; padding-top: 0px; orientation: portrait">
</div>
</div>
</div>

View File

@@ -0,0 +1,266 @@
/*---------------------------------------------------------------------------------------------
* 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!./textCell';
import 'vs/css!./media/markdown';
import 'vs/css!./media/highlight';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener } from '@angular/core';
import { localize } from 'vs/nls';
import { IColorTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import * as themeColors from 'vs/workbench/common/theme';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import * as DOM from 'vs/base/browser/dom';
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/browser/outputs/notebookMarkdown';
import { CellView } from 'sql/workbench/parts/notebook/browser/cellViews/interfaces';
import { ICellModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { NotebookModel } from 'sql/workbench/parts/notebook/browser/models/notebookModel';
import { ISanitizer, defaultSanitizer } from 'sql/workbench/parts/notebook/browser/outputs/sanitizer';
import { CellToggleMoreActions } from 'sql/workbench/parts/notebook/browser/cellToggleMoreActions';
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/browser/models/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'))
})
export class TextCellComponent extends CellView implements OnInit, OnChanges {
@ViewChild('preview', { read: ElementRef }) private output: ElementRef;
@ViewChild('moreactions', { read: ElementRef }) private moreActionsElementRef: ElementRef;
@Input() cellModel: ICellModel;
@Input() set model(value: NotebookModel) {
this._model = value;
}
@Input() set activeCellId(value: string) {
this._activeCellId = value;
}
@Input() set hover(value: boolean) {
this._hover = value;
if (!this.isActive()) {
// Only make a change if we're not active, since this has priority
this.updateMoreActions();
}
}
@HostListener('document:keydown.escape', ['$event'])
handleKeyboardEvent() {
if (this.isEditMode) {
this.toggleEditMode(false);
}
this.cellModel.active = false;
this._model.updateActiveCell(undefined);
}
private _content: string | string[];
private _lastTrustedMode: boolean;
private isEditMode: boolean;
private _sanitizer: ISanitizer;
private _model: NotebookModel;
private _activeCellId: string;
private readonly _onDidClickLink = this._register(new Emitter<URI>());
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(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
private get sanitizer(): ISanitizer {
if (this._sanitizer) {
return this._sanitizer;
}
return this._sanitizer = defaultSanitizer;
}
get model(): NotebookModel {
return this._model;
}
get activeCellId(): string {
return this._activeCellId;
}
private setLoading(isLoading: boolean): void {
this.cellModel.loaded = !isLoading;
this._changeRef.detectChanges();
}
ngOnInit() {
this._register(this.themeService.onDidColorThemeChange(this.updateTheme, this));
this.updateTheme(this.themeService.getColorTheme());
this._cellToggleMoreActions.onInit(this.moreActionsElementRef, this.model, this.cellModel);
this.setFocusAndScroll();
this._register(this.cellModel.onOutputsChanged(e => {
this.updatePreview();
}));
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
for (let propName in changes) {
if (propName === 'activeCellId') {
let changedProp = changes[propName];
this._activeCellId = changedProp.currentValue;
this.toggleUserSelect(this.isActive());
// If the activeCellId is undefined (i.e. in an active cell update), don't unnecessarily set editMode to false;
// it will be set to true in a subsequent call to toggleEditMode()
if (changedProp.previousValue !== undefined) {
this.toggleEditMode(false);
}
break;
}
}
}
public get isTrusted(): boolean {
return this.model.trustedMode;
}
public get notebookUri(): URI {
return this.model.notebookUri;
}
/**
* 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(): void {
let trustedChanged = this.cellModel && this._lastTrustedMode !== this.cellModel.trustedMode;
let cellModelSourceJoined = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source;
let contentJoined = Array.isArray(this._content) ? this._content.join('') : this._content;
let contentChanged = contentJoined !== cellModelSourceJoined || cellModelSourceJoined.length === 0;
if (trustedChanged || contentChanged) {
this._lastTrustedMode = this.cellModel.trustedMode;
if ((!cellModelSourceJoined) && !this.isEditMode) {
this._content = localize('doubleClickEdit', "Double-click to edit");
} else {
this._content = this.cellModel.source;
}
if (useInProcMarkdown(this.configurationService)) {
this.markdownRenderer.setNotebookURI(this.cellModel.notebookModel.notebookUri);
this.markdownResult = this.markdownRenderer.render({
isTrusted: true,
value: Array.isArray(this._content) ? this._content.join('') : this._content
});
this.markdownResult.element.innerHTML = this.sanitizeContent(this.markdownResult.element.innerHTML);
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);
});
}
}
}
//Sanitizes the content based on trusted mode of Cell Model
private sanitizeContent(content: string): string {
if (this.cellModel && !this.cellModel.trustedMode) {
content = this.sanitizer.sanitize(content);
}
return content;
}
// Todo: implement layout
public layout() {
}
private updateTheme(theme: IColorTheme): void {
let outputElement = <HTMLElement>this.output.nativeElement;
outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
let moreActionsEl = <HTMLElement>this.moreActionsElementRef.nativeElement;
moreActionsEl.style.borderRightColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString();
}
public handleContentChanged(): void {
this.updatePreview();
}
public toggleEditMode(editMode?: boolean): void {
this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode;
this.updateMoreActions();
this.updatePreview();
this._changeRef.detectChanges();
}
private updateMoreActions(): void {
if (!this.isEditMode && (this.isActive() || this._hover)) {
this.toggleMoreActionsButton(true);
}
else {
this.toggleMoreActionsButton(false);
}
}
private toggleUserSelect(userSelect: boolean): void {
if (!this.output) {
return;
}
if (userSelect) {
DOM.addClass(this.output.nativeElement, USER_SELECT_CLASS);
} else {
DOM.removeClass(this.output.nativeElement, USER_SELECT_CLASS);
}
}
private setFocusAndScroll(): void {
this.toggleEditMode(this.isActive());
if (this.output && this.output.nativeElement) {
(<HTMLElement>this.output.nativeElement).scrollTo({ behavior: 'smooth' });
}
}
protected isActive() {
return this.cellModel && this.cellModel.id === this.activeCellId;
}
protected toggleMoreActionsButton(isActiveOrHovered: boolean) {
this._cellToggleMoreActions.toggleVisible(!isActiveOrHovered);
}
}

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
text-cell-component {
display: block;
}
text-cell-component .notebook-preview {
user-select: none;
padding-left: 8px;
padding-right: 8px;
}
.notebook-preview.actionselect {
user-select: text;
}
text-cell-component table {
border-collapse: collapse;
border-spacing: 0;
border: none;
font-size: 12px;
table-layout: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: 1em;
display: table-row;
}
text-cell-component thead {
vertical-align: bottom;
}
text-cell-component td,
text-cell-component th,
text-cell-component tr {
text-align: left;
vertical-align: middle;
padding: 0.5em 0.5em;
line-height: normal;
white-space: normal;
max-width: none;
border: none;
}
text-cell-component th {
font-weight: bold;
}

View File

@@ -23,12 +23,11 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { URI } from 'vs/base/common/uri';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { NodeContextKey } from 'sql/workbench/parts/dataExplorer/browser/nodeContext';
import { MssqlNodeContext } from 'sql/workbench/parts/dataExplorer/browser/mssqlNodeContext';
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { TreeViewItemHandleArg } from 'sql/workbench/common/views';
import { ConnectedContext } from 'azdata';
import { TreeNodeContextKey } from 'sql/workbench/parts/objectExplorer/common/treeNodeContextKey';
@@ -36,6 +35,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ObjectExplorerActionsContext } from 'sql/workbench/parts/objectExplorer/browser/objectExplorerActions';
import { ItemContextKey } from 'sql/workbench/parts/dashboard/browser/widgets/explorer/explorerTreeContext';
import { ManageActionContext } from 'sql/workbench/browser/actions';
import { MarkdownOutputComponent } from 'sql/workbench/parts/notebook/browser/outputs/markdownOutput.component';
import { registerCellComponent } from 'sql/platform/notebooks/common/outputRegistry';
import { TextCellComponent } from 'sql/workbench/parts/notebook/browser/cellViews/textCell.component';
// Model View editor registration
const viewModelEditorDescriptor = new EditorDescriptor(
@@ -278,3 +280,16 @@ registerComponentType({
ctor: PlotlyOutputComponent,
selector: PlotlyOutputComponent.SELECTOR
});
/**
* A mime renderer component for Markdown.
*/
registerComponentType({
mimeTypes: ['text/markdown'],
rank: 60,
safe: true,
ctor: MarkdownOutputComponent,
selector: MarkdownOutputComponent.SELECTOR
});
registerCellComponent(TextCellComponent);

View File

@@ -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" [notebookUri]="notebookUri" class="notebook-preview" style="flex: 1 1 auto">
</div>
</div>

View File

@@ -0,0 +1,154 @@
/*---------------------------------------------------------------------------------------------
* 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/browser/outputs/sanitizer';
import { AngularDisposable } from 'sql/base/browser/lifecycle';
import { IMimeComponent } from 'sql/workbench/parts/notebook/browser/outputs/mimeRegistry';
import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { NotebookMarkdownRenderer } from 'sql/workbench/parts/notebook/browser/outputs/notebookMarkdown';
import { MimeModel } from 'sql/workbench/parts/notebook/browser/models/mimemodel';
import { ICellModel } from 'sql/workbench/parts/notebook/browser/models/modelInterfaces';
import { useInProcMarkdown, convertVscodeResourceToFileInSubDirectories } from 'sql/workbench/parts/notebook/browser/models/notebookUtils';
import { URI } from 'vs/base/common/uri';
@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;
private _markdownRenderer: NotebookMarkdownRenderer;
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef,
@Inject(ICommandService) private _commandService: ICommandService,
@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) {
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;
if (this._initialized) {
this.updatePreview();
}
}
public get isTrusted(): boolean {
return this._bundleOptions && this._bundleOptions.trusted;
}
public get notebookUri(): URI {
return this.cellModel.notebookModel.notebookUri;
}
//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 (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;
}
}
//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();
}
}

View File

@@ -0,0 +1,242 @@
/*---------------------------------------------------------------------------------------------
* 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 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IMarkdownString, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
import * as marked from 'vs/base/common/marked/marked';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { revive } from 'vs/base/common/marshalling';
import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
// 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: () => { }
};
}
createElement(options: MarkdownRenderOptions): 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: MarkdownRenderOptions = {}): 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.fsPath) + '/';
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
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) && !path.isAbsolute(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 if (href.slice(0, 2) === '..') {
return path.join(base, href);
} else {
return base + href;
}
}
// end marked.js adaptation
setNotebookURI(val: URI) {
this._notebookURI = val;
}
}