WYSIWYG Span Style Fixes, Refactor, and Tests (#13011)

* Refactor into own class, add tests

* Add more tests

* Test fixes

* Test fix hopefully

* Tests D vs C drive
This commit is contained in:
Chris LaFreniere
2020-10-21 16:55:41 -07:00
committed by GitHub
parent 656d727854
commit a427606050
3 changed files with 240 additions and 90 deletions

View File

@@ -7,6 +7,7 @@ import 'vs/css!./media/markdown';
import 'vs/css!./media/highlight';
import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener, ViewChildren, QueryList } from '@angular/core';
import * as Mark from 'mark.js';
import { localize } from 'vs/nls';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
@@ -15,9 +16,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import * as DOM from 'vs/base/browser/dom';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { toDisposable } from 'vs/base/common/lifecycle';
import { IMarkdownRenderResult } from 'vs/editor/contrib/markdown/markdownRenderer';
import { NotebookMarkdownRenderer } from 'sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown';
import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces';
import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces';
@@ -25,13 +28,8 @@ import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/no
import { ISanitizer, defaultSanitizer } from 'sql/workbench/services/notebook/browser/outputs/sanitizer';
import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component';
import { NotebookRange, ICellEditorProvider, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import * as turndownPluginGfm from '../turndownPluginGfm';
import TurndownService = require('turndown');
import * as Mark from 'mark.js';
import { HTMLMarkdownConverter } from 'sql/workbench/contrib/notebook/browser/htmlMarkdownConverter';
import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput';
import * as path from 'vs/base/common/path';
export const TEXT_SELECTOR: string = 'text-cell-component';
const USER_SELECT_CLASS = 'actionselect';
@@ -95,11 +93,11 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
private _model: NotebookModel;
private _activeCellId: string;
private readonly _onDidClickLink = this._register(new Emitter<URI>());
public readonly onDidClickLink = this._onDidClickLink.event;
private markdownRenderer: NotebookMarkdownRenderer;
private markdownResult: IMarkdownRenderResult;
private _htmlMarkdownConverter: HTMLMarkdownConverter;
public readonly onDidClickLink = this._onDidClickLink.event;
public previewFeaturesEnabled: boolean = false;
private turndownService;
public doubleClickEditEnabled: boolean;
constructor(
@@ -111,7 +109,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
) {
super();
this.setTurndownOptions();
this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer);
this.doubleClickEditEnabled = this._configurationService.getValue('notebook.enableDoubleClickEdit');
this._register(toDisposable(() => {
@@ -162,6 +159,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
this.updateTheme(this.themeService.getColorTheme());
this.setFocusAndScroll();
this.cellModel.isEditMode = false;
this._htmlMarkdownConverter = new HTMLMarkdownConverter(this.notebookUri);
this._register(this.cellModel.onOutputsChanged(e => {
this.updatePreview();
}));
@@ -248,7 +246,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
private updateCellSource(): void {
let textOutputElement = <HTMLElement>this.output.nativeElement;
let newCellSource: string = this.turndownService.turndown(textOutputElement.innerHTML, { gfm: true });
let newCellSource: string = this._htmlMarkdownConverter.convert(textOutputElement.innerHTML);
this.cellModel.source = newCellSource;
this._changeRef.detectChanges();
}
@@ -437,68 +435,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
return textOutput;
}
private setTurndownOptions() {
this.turndownService = new TurndownService({ 'emDelimiter': '_', 'bulletListMarker': '-', 'headingStyle': 'atx' });
this.turndownService.keep(['u', 'mark']);
this.turndownService.use(turndownPluginGfm.gfm);
this.turndownService.addRule('pre', {
filter: 'pre',
replacement: function (content, node) {
return '\n```\n' + node.textContent + '\n```\n';
}
});
this.turndownService.addRule('caption', {
filter: 'caption',
replacement: function (content, node) {
return `${node.outerHTML}
`;
}
});
this.turndownService.addRule('span', {
filter: function (node, options) {
return (
node.nodeName === 'MARK' ||
(node.nodeName === 'SPAN' &&
node.getAttribute('style') === 'background-color: yellow;')
);
},
replacement: function (content, node) {
if (node.nodeName === 'SPAN') {
return '<mark>' + node.textContent + '</mark>';
}
return node.textContent;
}
});
this.turndownService.addRule('img', {
filter: 'img',
replacement: (content, node) => {
if (node?.src) {
let imgPath = URI.parse(node.src);
const notebookFolder: string = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : '';
let relativePath = findPathRelativeToContent(notebookFolder, imgPath);
if (relativePath) {
return `![${node.alt}](${relativePath})`;
}
}
return `![${node.alt}](${node.src})`;
}
});
this.turndownService.addRule('a', {
filter: 'a',
replacement: (content, node) => {
//On Windows, if notebook is not trusted then the href attr is removed for all non-web URL links
// href contains either a hyperlink or a URI-encoded absolute path. (See resolveUrls method in notebookMarkdown.ts)
const notebookLink = node.href ? URI.parse(node.href) : URI.file(node.title);
const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : '';
let relativePath = findPathRelativeToContent(notebookFolder, notebookLink);
if (relativePath) {
return `[${node.innerText}](${relativePath})`;
}
return `[${node.innerText}](${node.href})`;
}
});
}
// Enables edit mode on double clicking active cell
private enableActiveCellEditOnDoubleClick() {
if (!this.isEditMode && this.doubleClickEditEnabled) {
@@ -509,23 +445,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges {
}
}
export function findPathRelativeToContent(notebookFolder: string, contentPath: URI | undefined): string {
if (notebookFolder) {
if (contentPath?.scheme === 'file') {
let relativePath = path.relative(notebookFolder, contentPath.fsPath);
//if path contains whitespaces then it's not identified as a link
relativePath = relativePath.replace(/\s/g, '%20');
if (relativePath.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) {
return relativePath;
} else {
// if the relative path does not contain ./ at the beginning, we need to add it so it's recognized as a link
return `.${path.join(path.sep, relativePath)}`;
}
}
}
return '';
}
function preventDefaultAndExecCommand(e: KeyboardEvent, commandId: string) {
// use preventDefault() to avoid invoking the editor's select all
e.preventDefault();