diff --git a/src/vs/base/browser/htmlContentRenderer.ts b/src/vs/base/browser/htmlContentRenderer.ts index cf346270dca..26ebffbc1b7 100644 --- a/src/vs/base/browser/htmlContentRenderer.ts +++ b/src/vs/base/browser/htmlContentRenderer.ts @@ -11,6 +11,7 @@ import * as marked from 'vs/base/common/marked/marked'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { URI } from 'vs/base/common/uri'; export interface IContentActionHandler { callback: (content: string, event?: IMouseEvent) => void; @@ -52,6 +53,14 @@ export function renderFormattedText(formattedText: string, options: RenderOption export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions = {}): HTMLElement { const element = createElement(options); + const _href = function (href: string): string { + const data = markdown.uris && markdown.uris[href]; + if (data) { + href = URI.revive(data).toString(true); + } + return href; + }; + // signal to code-block render that the // element has been created let signalInnerHTML: Function; @@ -59,6 +68,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions const renderer = new marked.Renderer(); renderer.image = (href: string, title: string, text: string) => { + href = _href(href); let dimensions: string[] = []; if (href) { const splitted = href.split('|').map(s => s.trim()); @@ -99,6 +109,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions if (href === text) { // raw link case text = removeMarkdownEscapes(text); } + href = _href(href); title = removeMarkdownEscapes(title); href = removeMarkdownEscapes(href); if ( diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index f470327b88b..0ec7be4955a 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from 'vs/base/common/arrays'; +import { UriComponents } from 'vs/base/common/uri'; export interface IMarkdownString { value: string; isTrusted?: boolean; + uris?: { [href: string]: UriComponents }; } export class MarkdownString implements IMarkdownString { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1a6f3755419..429c70fbabb 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -392,9 +392,13 @@ declare namespace monaco { static readonly WinCtrl: number; static chord(firstPart: number, secondPart: number): number; } + export interface IMarkdownString { value: string; isTrusted?: boolean; + uris?: { + [href: string]: UriComponents; + }; } export interface IKeyboardEvent { diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index d655abe4e72..8d3b6988fb1 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -274,12 +274,6 @@ export interface ISerializedSignatureHelpProviderMetadata { readonly retriggerCharacters: ReadonlyArray; } -export interface IMarkdownStringDto { - isTrusted: boolean; - value: string; - uris: { [n: string]: UriComponents }; -} - export interface MainThreadLanguageFeaturesShape extends IDisposable { $unregister(handle: number): void; $registerOutlineSupport(handle: number, selector: ISerializedDocumentFilter[], extensionId: string): void; diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 7db09363924..0bbe5c7e66b 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -19,7 +19,7 @@ import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; import * as htmlContent from 'vs/base/common/htmlContent'; import * as languageSelector from 'vs/editor/common/modes/languageSelector'; -import { WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto, IMarkdownStringDto } from 'vs/workbench/api/node/extHost.protocol'; +import { WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { MarkerSeverity, IRelatedInformation, IMarkerData, MarkerTag } from 'vs/platform/markers/common/markers'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; @@ -210,38 +210,32 @@ export namespace MarkdownString { } export function from(markup: vscode.MarkdownString | vscode.MarkedString): htmlContent.IMarkdownString { + let res: htmlContent.IMarkdownString; if (isCodeblock(markup)) { const { language, value } = markup; - return { value: '```' + language + '\n' + value + '\n```\n' }; + res = { value: '```' + language + '\n' + value + '\n```\n' }; } else if (htmlContent.isMarkdownString(markup)) { - return markup; + res = markup; } else if (typeof markup === 'string') { - return { value: markup }; + res = { value: markup }; } else { - return { value: '' }; + res = { value: '' }; } - } - export function from2(markup: vscode.MarkedString | vscode.MarkdownString): IMarkdownStringDto { - let { value, isTrusted } = from(markup); - - let uris = Object.create(null); + // extract uris into a separate object + res.uris = Object.create(null); let renderer = new marked.Renderer(); - renderer.image = renderer.link = (href: string): string => { try { - uris[href] = URI.parse(href, true); + res.uris[href] = URI.parse(href, true); } catch (e) { // ignore } return ''; }; - marked(value, { renderer }); - return { - isTrusted, - value, - uris - }; + marked(res.value, { renderer }); + + return res; } export function to(value: htmlContent.IMarkdownString): vscode.MarkdownString { diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts new file mode 100644 index 00000000000..3b217231a5f --- /dev/null +++ b/src/vs/workbench/test/electron-browser/api/extHostTypeConverter.test.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as assert from 'assert'; +import { MarkdownString } from 'vs/workbench/api/node/extHostTypeConverters'; +import { isEmptyObject } from 'vs/base/common/types'; +import { size } from 'vs/base/common/collections'; + +suite('ExtHostTypeConverter', function () { + + test('MarkdownConvert - uris', function () { + + let data = MarkdownString.from('Hello'); + assert.equal(isEmptyObject(data.uris), true); + assert.equal(data.value, 'Hello'); + + data = MarkdownString.from('Hello [link](foo)'); + assert.equal(data.value, 'Hello [link](foo)'); + assert.equal(isEmptyObject(data.uris), true); // no scheme, no uri + + data = MarkdownString.from('Hello [link](www.noscheme.bad)'); + assert.equal(data.value, 'Hello [link](www.noscheme.bad)'); + assert.equal(isEmptyObject(data.uris), true); // no scheme, no uri + + data = MarkdownString.from('Hello [link](foo:path)'); + assert.equal(data.value, 'Hello [link](foo:path)'); + assert.equal(size(data.uris), 1); + assert.ok(!!data.uris['foo:path']); + + data = MarkdownString.from('hello@foo.bar'); + assert.equal(data.value, 'hello@foo.bar'); + assert.equal(size(data.uris), 1); + assert.ok(!!data.uris['mailto:hello@foo.bar']); + + data = MarkdownString.from('*hello* [click](command:me)'); + assert.equal(data.value, '*hello* [click](command:me)'); + assert.equal(size(data.uris), 1); + assert.ok(!!data.uris['command:me']); + + data = MarkdownString.from('*hello* [click](file:///somepath/here). [click](file:///somepath/here)'); + assert.equal(data.value, '*hello* [click](file:///somepath/here). [click](file:///somepath/here)'); + assert.equal(size(data.uris), 1); + assert.ok(!!data.uris['file:///somepath/here']); + + data = MarkdownString.from('*hello* [click](file:///somepath/here). [click](file:///somepath/here)'); + assert.equal(data.value, '*hello* [click](file:///somepath/here). [click](file:///somepath/here)'); + assert.equal(size(data.uris), 1); + assert.ok(!!data.uris['file:///somepath/here']); + + data = MarkdownString.from('*hello* [click](file:///somepath/here). [click](file:///somepath/here2)'); + assert.equal(data.value, '*hello* [click](file:///somepath/here). [click](file:///somepath/here2)'); + assert.equal(size(data.uris), 2); + assert.ok(!!data.uris['file:///somepath/here']); + assert.ok(!!data.uris['file:///somepath/here2']); + }); +});