Merge from vscode b12f623603e2fc1c5b3037115fa37c1a6acc4165 (#6760)

This commit is contained in:
Anthony Dresser
2019-08-15 02:19:31 -07:00
committed by GitHub
parent 4966ed8b42
commit 58bfba4b47
161 changed files with 2072 additions and 1317 deletions

View File

@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';
export interface IContentActionHandler {
callback: (content: string, event?: IMouseEvent) => void;
readonly disposeables: DisposableStore;
}
export interface FormattedTextRenderOptions {
readonly className?: string;
readonly inline?: boolean;
readonly actionHandler?: IContentActionHandler;
}
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
const element = createElement(options);
element.textContent = text;
return element;
}
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
const element = createElement(options);
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
return element;
}
export function createElement(options: FormattedTextRenderOptions): HTMLElement {
const tagName = options.inline ? 'span' : 'div';
const element = document.createElement(tagName);
if (options.className) {
element.className = options.className;
}
return element;
}
class StringStream {
private source: string;
private index: number;
constructor(source: string) {
this.source = source;
this.index = 0;
}
public eos(): boolean {
return this.index >= this.source.length;
}
public next(): string {
const next = this.peek();
this.advance();
return next;
}
public peek(): string {
return this.source[this.index];
}
public advance(): void {
this.index++;
}
}
const enum FormatType {
Invalid,
Root,
Text,
Bold,
Italics,
Action,
ActionClose,
NewLine
}
interface IFormatParseTree {
type: FormatType;
content?: string;
index?: number;
children?: IFormatParseTree[];
}
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
let child: Node | undefined;
if (treeNode.type === FormatType.Text) {
child = document.createTextNode(treeNode.content || '');
} else if (treeNode.type === FormatType.Bold) {
child = document.createElement('b');
} else if (treeNode.type === FormatType.Italics) {
child = document.createElement('i');
} else if (treeNode.type === FormatType.Action && actionHandler) {
const a = document.createElement('a');
a.href = '#';
actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
actionHandler.callback(String(treeNode.index), event);
}));
child = a;
} else if (treeNode.type === FormatType.NewLine) {
child = document.createElement('br');
} else if (treeNode.type === FormatType.Root) {
child = element;
}
if (child && element !== child) {
element.appendChild(child);
}
if (child && Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
_renderFormattedText(child!, nodeChild, actionHandler);
});
}
}
function parseFormattedText(content: string): IFormatParseTree {
const root: IFormatParseTree = {
type: FormatType.Root,
children: []
};
let actionViewItemIndex = 0;
let current = root;
const stack: IFormatParseTree[] = [];
const stream = new StringStream(content);
while (!stream.eos()) {
let next = stream.next();
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
if (isEscapedFormatType) {
next = stream.next(); // unread the backslash if it escapes a format tag type
}
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
stream.advance();
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
const type = formatTagType(next);
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
current = stack.pop()!;
} else {
const newCurrent: IFormatParseTree = {
type: type,
children: []
};
if (type === FormatType.Action) {
newCurrent.index = actionViewItemIndex;
actionViewItemIndex++;
}
current.children!.push(newCurrent);
stack.push(current);
current = newCurrent;
}
} else if (next === '\n') {
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
current.children!.push({
type: FormatType.NewLine
});
} else {
if (current.type !== FormatType.Text) {
const textCurrent: IFormatParseTree = {
type: FormatType.Text,
content: next
};
current.children!.push(textCurrent);
stack.push(current);
current = textCurrent;
} else {
current.content += next;
}
}
}
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
if (stack.length) {
// incorrectly formatted string literal
}
return root;
}
function isFormatTag(char: string): boolean {
return formatTagType(char) !== FormatType.Invalid;
}
function formatTagType(char: string): FormatType {
switch (char) {
case '*':
return FormatType.Bold;
case '_':
return FormatType.Italics;
case '[':
return FormatType.Action;
case ']':
return FormatType.ActionClose;
default:
return FormatType.Invalid;
}
}

View File

@@ -4,55 +4,25 @@
*--------------------------------------------------------------------------------------------*/
import * as DOM from 'vs/base/browser/dom';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { escape } from 'vs/base/common/strings';
import { removeMarkdownEscapes, IMarkdownString, parseHrefAndDimensions } from 'vs/base/common/htmlContent';
import * as marked from 'vs/base/common/marked/marked';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
import { onUnexpectedError } from 'vs/base/common/errors';
import { URI } from 'vs/base/common/uri';
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import * as marked from 'vs/base/common/marked/marked';
import { parse } from 'vs/base/common/marshalling';
import { cloneAndChange } from 'vs/base/common/objects';
import { escape } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
export interface IContentActionHandler {
callback: (content: string, event?: IMouseEvent) => void;
readonly disposeables: DisposableStore;
}
export interface RenderOptions {
className?: string;
inline?: boolean;
actionHandler?: IContentActionHandler;
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
codeBlockRenderer?: (modeId: string, value: string) => Promise<string>;
codeBlockRenderCallback?: () => void;
}
function createElement(options: RenderOptions): HTMLElement {
const tagName = options.inline ? 'span' : 'div';
const element = document.createElement(tagName);
if (options.className) {
element.className = options.className;
}
return element;
}
export function renderText(text: string, options: RenderOptions = {}): HTMLElement {
const element = createElement(options);
element.textContent = text;
return element;
}
export function renderFormattedText(formattedText: string, options: RenderOptions = {}): HTMLElement {
const element = createElement(options);
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
return element;
}
/**
* Create html nodes for the given content element.
*/
export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement {
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}): HTMLElement {
const element = createElement(options);
const _uriMassage = function (part: string): string {
@@ -219,190 +189,3 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions
return element;
}
// --- formatted string parsing
class StringStream {
private source: string;
private index: number;
constructor(source: string) {
this.source = source;
this.index = 0;
}
public eos(): boolean {
return this.index >= this.source.length;
}
public next(): string {
const next = this.peek();
this.advance();
return next;
}
public peek(): string {
return this.source[this.index];
}
public advance(): void {
this.index++;
}
}
const enum FormatType {
Invalid,
Root,
Text,
Bold,
Italics,
Action,
ActionClose,
NewLine
}
interface IFormatParseTree {
type: FormatType;
content?: string;
index?: number;
children?: IFormatParseTree[];
}
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
let child: Node | undefined;
if (treeNode.type === FormatType.Text) {
child = document.createTextNode(treeNode.content || '');
}
else if (treeNode.type === FormatType.Bold) {
child = document.createElement('b');
}
else if (treeNode.type === FormatType.Italics) {
child = document.createElement('i');
}
else if (treeNode.type === FormatType.Action && actionHandler) {
const a = document.createElement('a');
a.href = '#';
actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
actionHandler.callback(String(treeNode.index), event);
}));
child = a;
}
else if (treeNode.type === FormatType.NewLine) {
child = document.createElement('br');
}
else if (treeNode.type === FormatType.Root) {
child = element;
}
if (child && element !== child) {
element.appendChild(child);
}
if (child && Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
_renderFormattedText(child!, nodeChild, actionHandler);
});
}
}
function parseFormattedText(content: string): IFormatParseTree {
const root: IFormatParseTree = {
type: FormatType.Root,
children: []
};
let actionViewItemIndex = 0;
let current = root;
const stack: IFormatParseTree[] = [];
const stream = new StringStream(content);
while (!stream.eos()) {
let next = stream.next();
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
if (isEscapedFormatType) {
next = stream.next(); // unread the backslash if it escapes a format tag type
}
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
stream.advance();
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
const type = formatTagType(next);
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
current = stack.pop()!;
} else {
const newCurrent: IFormatParseTree = {
type: type,
children: []
};
if (type === FormatType.Action) {
newCurrent.index = actionViewItemIndex;
actionViewItemIndex++;
}
current.children!.push(newCurrent);
stack.push(current);
current = newCurrent;
}
} else if (next === '\n') {
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
current.children!.push({
type: FormatType.NewLine
});
} else {
if (current.type !== FormatType.Text) {
const textCurrent: IFormatParseTree = {
type: FormatType.Text,
content: next
};
current.children!.push(textCurrent);
stack.push(current);
current = textCurrent;
} else {
current.content += next;
}
}
}
if (current.type === FormatType.Text) {
current = stack.pop()!;
}
if (stack.length) {
// incorrectly formatted string literal
}
return root;
}
function isFormatTag(char: string): boolean {
return formatTagType(char) !== FormatType.Invalid;
}
function formatTagType(char: string): FormatType {
switch (char) {
case '*':
return FormatType.Bold;
case '_':
return FormatType.Italics;
case '[':
return FormatType.Action;
case ']':
return FormatType.ActionClose;
default:
return FormatType.Invalid;
}
}

View File

@@ -8,7 +8,8 @@ import 'vs/css!./inputBox';
import * as nls from 'vs/nls';
import * as Bal from 'vs/base/browser/browser';
import * as dom from 'vs/base/browser/dom';
import { RenderOptions, renderFormattedText, renderText } from 'vs/base/browser/htmlContentRenderer';
import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer';
import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { IAction } from 'vs/base/common/actions';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
@@ -472,7 +473,7 @@ export class InputBox extends Widget {
div = dom.append(container, $('.monaco-inputbox-container'));
layout();
const renderOptions: RenderOptions = {
const renderOptions: MarkdownRenderOptions = {
inline: true,
className: 'monaco-inputbox-message'
};

View File

@@ -18,7 +18,7 @@ import { domEvent } from 'vs/base/browser/event';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { ISelectBoxDelegate, ISelectOptionItem, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
import { isMacintosh } from 'vs/base/common/platform';
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
// {{SQL CARBON EDIT}} import color
import { Color } from 'vs/base/common/color';

View File

@@ -729,3 +729,18 @@ export function getNLines(str: string, n = 1): string {
str.substr(0, idx) :
str;
}
/**
* Produces 'a'-'z', followed by 'A'-'Z'... followed by 'a'-'z', etc.
*/
export function singleLetterHash(n: number): string {
const LETTERS_CNT = (CharCode.Z - CharCode.A + 1);
n = n % (2 * LETTERS_CNT);
if (n < LETTERS_CNT) {
return String.fromCharCode(CharCode.a + n);
}
return String.fromCharCode(CharCode.A + n - LETTERS_CNT);
}

View File

@@ -2,12 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as marked from 'vs/base/common/marked/marked';
import { renderMarkdown, renderText, renderFormattedText } from 'vs/base/browser/htmlContentRenderer';
import { renderText, renderFormattedText } from 'vs/base/browser/formattedTextRenderer';
import { DisposableStore } from 'vs/base/common/lifecycle';
suite('HtmlContent', () => {
suite('FormattedTextRenderer', () => {
const store = new DisposableStore();
setup(() => {
@@ -101,36 +101,4 @@ suite('HtmlContent', () => {
assert.strictEqual(result.children.length, 0);
assert.strictEqual(result.innerHTML, '**bold**');
});
test('image rendering conforms to default', () => {
const markdown = { value: `![image](someimageurl 'caption')` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image rendering conforms to default without title', () => {
const markdown = { value: `![image](someimageurl)` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image width from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
});
test('image height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
});
test('image width and height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
});
});

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as marked from 'vs/base/common/marked/marked';
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
suite('MarkdownRenderer', () => {
test('image rendering conforms to default', () => {
const markdown = { value: `![image](someimageurl 'caption')` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image rendering conforms to default without title', () => {
const markdown = { value: `![image](someimageurl)` };
const result: HTMLElement = renderMarkdown(markdown);
const renderer = new marked.Renderer();
const imageFromMarked = marked(markdown.value, {
sanitize: true,
renderer
}).trim();
assert.strictEqual(result.innerHTML, imageFromMarked);
});
test('image width from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100"></p>`);
});
test('image height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" height="100"></p>`);
});
test('image width and height from title params', () => {
let result: HTMLElement = renderMarkdown({ value: `![image](someimageurl|height=200,width=100 'caption')` });
assert.strictEqual(result.innerHTML, `<p><img src="someimageurl" alt="image" title="caption" width="100" height="200"></p>`);
});
});

View File

@@ -19,26 +19,31 @@ function getWorker(workerId: string, label: string): Worker | Promise<Worker> {
// ESM-comment-begin
if (typeof require === 'function') {
// check if the JS lives on a different origin
const workerMain = require.toUrl('./' + workerId);
if (/^(http:)|(https:)|(file:)/.test(workerMain)) {
const currentUrl = String(window.location);
const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
if (workerMain.substring(0, currentOrigin.length) !== currentOrigin) {
// this is the cross-origin case
// i.e. the webpage is running at a different origin than where the scripts are loaded from
const workerBaseUrl = workerMain.substr(0, workerMain.length - 'vs/base/worker/workerMain.js'.length);
const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${workerMain}');/*${label}*/`;
const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`;
return new Worker(url);
}
}
return new Worker(workerMain + '#' + label);
const workerUrl = getWorkerBootstrapUrl(workerMain, label);
return new Worker(workerUrl, { name: label });
}
// ESM-comment-end
throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`);
}
export function getWorkerBootstrapUrl(scriptPath: string, label: string): string {
if (/^(http:)|(https:)|(file:)/.test(scriptPath)) {
const currentUrl = String(window.location);
const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) {
// this is the cross-origin case
// i.e. the webpage is running at a different origin than where the scripts are loaded from
const myPath = 'vs/base/worker/defaultWorkerFactory.js';
const workerBaseUrl = require.toUrl(myPath).slice(0, -myPath.length);
const js = `/*${label}*/self.MonacoEnvironment={baseUrl: '${workerBaseUrl}'};importScripts('${scriptPath}');/*${label}*/`;
const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(js)}`;
return url;
}
}
return scriptPath + '#' + label;
}
function isPromiseLike<T>(obj: any): obj is PromiseLike<T> {
if (typeof obj.then === 'function') {
return true;