Skip to content

Commit bcacd2f

Browse files
author
Benjamin Pasero
committed
1 parent d80bc66 commit bcacd2f

5 files changed

Lines changed: 117 additions & 67 deletions

File tree

src/vs/base/browser/htmlContentRenderer.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import * as DOM from 'vs/base/browser/dom';
99
import { defaultGenerator } from 'vs/base/common/idGenerator';
1010
import { escape } from 'vs/base/common/strings';
1111
import { removeMarkdownEscapes, IMarkdownString } from 'vs/base/common/htmlContent';
12-
import { marked, MarkedRenderer, MarkedOptions } from 'vs/base/common/marked/marked';
12+
import { marked, MarkedOptions } from 'vs/base/common/marked/marked';
1313
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
14-
import { assign } from 'vs/base/common/objects';
1514
import { IDisposable } from 'vs/base/common/lifecycle';
1615

1716
export interface IContentActionHandler {
@@ -25,7 +24,6 @@ export interface RenderOptions {
2524
actionHandler?: IContentActionHandler;
2625
codeBlockRenderer?: (modeId: string, value: string) => Thenable<string>;
2726
codeBlockRenderCallback?: () => void;
28-
joinRendererConfiguration?: (renderer: MarkedRenderer) => MarkedOptions;
2927
}
3028

3129
function createElement(options: RenderOptions): HTMLElement {
@@ -166,13 +164,6 @@ export function renderMarkdown(markdown: IMarkdownString, options: RenderOptions
166164
renderer
167165
};
168166

169-
if (options.joinRendererConfiguration) {
170-
const additionalMarkedOptions = options.joinRendererConfiguration(renderer);
171-
if (additionalMarkedOptions) {
172-
assign(markedOptions, additionalMarkedOptions);
173-
}
174-
}
175-
176167
element.innerHTML = marked(markdown.value, markedOptions);
177168
signalInnerHTML();
178169

src/vs/platform/notification/common/notification.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
import Severity from 'vs/base/common/severity';
99
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1010
import { IDisposable } from 'vs/base/common/lifecycle';
11-
import { IMarkdownString } from 'vs/base/common/htmlContent';
1211
import { IAction } from 'vs/base/common/actions';
1312
import Event, { Emitter } from 'vs/base/common/event';
1413

1514
export import Severity = Severity;
1615

1716
export const INotificationService = createDecorator<INotificationService>('notificationService');
1817

19-
export type NotificationMessage = string | IMarkdownString | Error;
18+
export type NotificationMessage = string | Error;
2019

2120
export interface INotification {
2221

@@ -26,11 +25,8 @@ export interface INotification {
2625
severity: Severity;
2726

2827
/**
29-
* The message of the notification. This can either be a `string`, `Error`
30-
* or `IMarkdownString`.
31-
*
32-
* **Note:** Currently only links are supported in notifications. Links to commands can
33-
* be embedded provided that the `IMarkdownString` is trusted.
28+
* The message of the notification. This can either be a `string` or `Error`. Messages
29+
* can optionally include links in the format: `[text](link)`
3430
*/
3531
message: NotificationMessage;
3632

src/vs/workbench/browser/parts/notifications/notificationsViewer.ts

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,23 @@
66
'use strict';
77

88
import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
9-
import { renderMarkdown, IContentActionHandler } from 'vs/base/browser/htmlContentRenderer';
10-
import { clearNode, addClass, removeClass, toggleClass } from 'vs/base/browser/dom';
9+
import { clearNode, addClass, removeClass, toggleClass, addDisposableListener } from 'vs/base/browser/dom';
1110
import { IOpenerService } from 'vs/platform/opener/common/opener';
1211
import URI from 'vs/base/common/uri';
1312
import { onUnexpectedError } from 'vs/base/common/errors';
1413
import { localize } from 'vs/nls';
1514
import { ButtonGroup } from 'vs/base/browser/ui/button/button';
1615
import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler';
1716
import { IThemeService } from 'vs/platform/theme/common/themeService';
18-
import { IMarkdownString } from 'vs/base/common/htmlContent';
1917
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
2018
import { IAction, IActionRunner } from 'vs/base/common/actions';
2119
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
2220
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
2321
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
2422
import { DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
25-
import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications';
23+
import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage } from 'vs/workbench/common/notifications';
2624
import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions';
2725
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
28-
import { MarkedOptions } from 'vs/base/common/marked/marked';
2926
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
3027
import { Severity } from 'vs/platform/notification/common/notification';
3128

@@ -92,8 +89,8 @@ export class NotificationsListDelegate implements IDelegate<INotificationViewIte
9289
}
9390
this.offsetHelper.style.width = `calc(100% - ${10 /* padding */ + 24 /* severity icon */ + (actions * 24) /* 24px per action */}px)`;
9491

95-
// Render message markdown into offset helper
96-
const renderedMessage = NotificationMessageMarkdownRenderer.render(notification.message);
92+
// Render message into offset helper
93+
const renderedMessage = NotificationMessageRenderer.render(notification.message);
9794
this.offsetHelper.appendChild(renderedMessage);
9895

9996
// Compute height
@@ -131,30 +128,49 @@ export interface INotificationTemplateData {
131128
renderer: NotificationTemplateRenderer;
132129
}
133130

134-
class NotificationMessageMarkdownRenderer {
135-
136-
private static readonly MARKED_NOOP = (text?: string) => text || '';
137-
private static readonly MARKED_NOOP_TARGETS = [
138-
'blockquote', 'br', 'code', 'codespan', 'del', 'em', 'heading', 'hr', 'html',
139-
'image', 'list', 'listitem', 'paragraph', 'strong', 'table', 'tablecell',
140-
'tablerow'
141-
];
142-
143-
public static render(markdown: IMarkdownString, actionHandler?: IContentActionHandler): HTMLElement {
144-
return renderMarkdown(markdown, {
145-
inline: true,
146-
joinRendererConfiguration: renderer => {
147-
148-
// Overwrite markdown render functions as no-ops
149-
NotificationMessageMarkdownRenderer.MARKED_NOOP_TARGETS.forEach(fn => renderer[fn] = NotificationMessageMarkdownRenderer.MARKED_NOOP);
150-
151-
return {
152-
gfm: false, // disable GitHub style markdown,
153-
smartypants: false // disable some text transformations
154-
} as MarkedOptions;
155-
},
156-
actionHandler
157-
});
131+
interface IMessageActionHandler {
132+
callback: (href: string) => void;
133+
disposeables: IDisposable[];
134+
}
135+
136+
class NotificationMessageRenderer {
137+
138+
public static render(message: INotificationMessage, actionHandler?: IMessageActionHandler): HTMLElement {
139+
const messageContainer = document.createElement('span');
140+
141+
// Message has no links
142+
if (message.links.length === 0) {
143+
messageContainer.textContent = message.value;
144+
}
145+
146+
// Message has links
147+
else {
148+
let index = 0;
149+
let textBefore: string;
150+
for (let i = 0; i < message.links.length; i++) {
151+
const link = message.links[i];
152+
153+
textBefore = message.value.substring(index, link.offset);
154+
if (textBefore) {
155+
messageContainer.appendChild(document.createTextNode(textBefore));
156+
}
157+
158+
const anchor = document.createElement('a');
159+
anchor.textContent = link.name;
160+
anchor.title = link.href;
161+
anchor.href = link.href;
162+
163+
if (actionHandler) {
164+
actionHandler.disposeables.push(addDisposableListener(anchor, 'click', () => actionHandler.callback(link.href)));
165+
}
166+
167+
messageContainer.appendChild(anchor);
168+
169+
index = link.offset + link.length;
170+
}
171+
}
172+
173+
return messageContainer;
158174
}
159175
}
160176

@@ -340,8 +356,8 @@ export class NotificationTemplateRenderer {
340356

341357
private renderMessage(notification: INotificationViewItem): boolean {
342358
clearNode(this.template.message);
343-
this.template.message.appendChild(NotificationMessageMarkdownRenderer.render(notification.message, {
344-
callback: (content: string) => this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError),
359+
this.template.message.appendChild(NotificationMessageRenderer.render(notification.message, {
360+
callback: link => this.openerService.open(URI.parse(link)).then(void 0, onUnexpectedError),
345361
disposeables: this.inputDisposeables
346362
}));
347363

src/vs/workbench/common/notifications.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
'use strict';
77

8-
import { IMarkdownString } from 'vs/base/common/htmlContent';
98
import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage } from 'vs/platform/notification/common/notification';
109
import { toErrorMessage } from 'vs/base/common/errorMessage';
1110
import Event, { Emitter, once } from 'vs/base/common/event';
@@ -184,7 +183,7 @@ export class NotificationsModel implements INotificationsModel {
184183

185184
export interface INotificationViewItem {
186185
readonly severity: Severity;
187-
readonly message: IMarkdownString;
186+
readonly message: INotificationMessage;
188187
readonly source: string;
189188
readonly actions: INotificationActions;
190189
readonly progress: INotificationViewItemProgress;
@@ -320,10 +319,27 @@ export class NotificationViewItemProgress implements INotificationViewItemProgre
320319
}
321320
}
322321

322+
export interface IMessageLink {
323+
name: string;
324+
href: string;
325+
offset: number;
326+
length: number;
327+
}
328+
329+
export interface INotificationMessage {
330+
raw: string;
331+
value: string;
332+
links: IMessageLink[];
333+
}
334+
323335
export class NotificationViewItem implements INotificationViewItem {
324336

325337
private static MAX_MESSAGE_LENGTH = 1000;
326338

339+
// Example link: "Some message with [link text](http://link.href)."
340+
// RegEx: [, anything not ], ], (, http:|https:, //, no whitespace)
341+
private static LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^\)\s]+)\)/gi;
342+
327343
private _expanded: boolean;
328344
private toDispose: IDisposable[];
329345

@@ -346,15 +362,11 @@ export class NotificationViewItem implements INotificationViewItem {
346362
severity = Severity.Info;
347363
}
348364

349-
const message = NotificationViewItem.toMarkdownString(notification.message);
365+
const message = NotificationViewItem.parseNotificationMessage(notification.message);
350366
if (!message) {
351367
return null; // we need a message to show
352368
}
353369

354-
if (message.value.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
355-
message.value = `${message.value.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
356-
}
357-
358370
let actions: INotificationActions;
359371
if (notification.actions) {
360372
actions = notification.actions;
@@ -365,21 +377,42 @@ export class NotificationViewItem implements INotificationViewItem {
365377
return new NotificationViewItem(severity, message, notification.source, actions);
366378
}
367379

368-
private static toMarkdownString(input: NotificationMessage): IMarkdownString {
369-
let message: IMarkdownString;
380+
private static parseNotificationMessage(input: NotificationMessage): INotificationMessage {
381+
let message: string;
370382

371383
if (input instanceof Error) {
372-
message = { value: toErrorMessage(input, false), isTrusted: false };
384+
message = toErrorMessage(input, false);
373385
} else if (typeof input === 'string') {
374-
message = { value: input, isTrusted: false };
375-
} else if (input.value && typeof input.value === 'string') {
376386
message = input;
377387
}
378388

379-
return message;
389+
if (!message) {
390+
return null; // we need a message to show
391+
}
392+
393+
const raw = message;
394+
395+
// Make sure message is in the limits
396+
if (message.length > NotificationViewItem.MAX_MESSAGE_LENGTH) {
397+
message = `${message.substr(0, NotificationViewItem.MAX_MESSAGE_LENGTH)}...`;
398+
}
399+
400+
// Remove newlines from messages as we do not support that and it makes link parsing hard
401+
message = message.replace(/(\r\n|\n|\r)/gm, ' ').trim();
402+
403+
// Parse Links
404+
const links: IMessageLink[] = [];
405+
message.replace(NotificationViewItem.LINK_REGEX, (matchString: string, name: string, href: string, offset: number) => {
406+
links.push({ name, href, offset, length: matchString.length });
407+
408+
return matchString;
409+
});
410+
411+
412+
return { raw, value: message, links };
380413
}
381414

382-
private constructor(private _severity: Severity, private _message: IMarkdownString, private _source: string, actions?: INotificationActions) {
415+
private constructor(private _severity: Severity, private _message: INotificationMessage, private _source: string, actions?: INotificationActions) {
383416
this.toDispose = [];
384417

385418
this.setActions(actions);
@@ -452,7 +485,7 @@ export class NotificationViewItem implements INotificationViewItem {
452485
return this._progress;
453486
}
454487

455-
public get message(): IMarkdownString {
488+
public get message(): INotificationMessage {
456489
return this._message;
457490
}
458491

@@ -470,7 +503,7 @@ export class NotificationViewItem implements INotificationViewItem {
470503
}
471504

472505
public updateMessage(input: NotificationMessage): void {
473-
const message = NotificationViewItem.toMarkdownString(input);
506+
const message = NotificationViewItem.parseNotificationMessage(input);
474507
if (!message) {
475508
return;
476509
}

src/vs/workbench/test/common/notifications.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ suite('Notifications', () => {
1818
// Invalid
1919
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: '' }));
2020
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: null }));
21-
assert.ok(!NotificationViewItem.create({ severity: Severity.Error, message: { value: '', isTrusted: true } }));
2221

2322
// Duplicates
2423
let item1 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' });
@@ -107,6 +106,21 @@ suite('Notifications', () => {
107106
// Error with Action
108107
let item6 = NotificationViewItem.create({ severity: Severity.Error, message: create('Hello Error', { actions: [new Action('id', 'label')] }) });
109108
assert.equal(item6.actions.primary.length, 1);
109+
110+
// Links
111+
let item7 = NotificationViewItem.create({ severity: Severity.Info, message: 'Unable to [Link 1](http://link1.com) open [Link 2](https://link2.com) and [Invalid Link3](ftp://link3.com)' });
112+
113+
const links = item7.message.links;
114+
assert.equal(links.length, 2);
115+
assert.equal(links[0].name, 'Link 1');
116+
assert.equal(links[0].href, 'http://link1.com');
117+
assert.equal(links[0].length, '[Link 1](http://link1.com)'.length);
118+
assert.equal(links[0].offset, 'Unable to '.length);
119+
120+
assert.equal(links[1].name, 'Link 2');
121+
assert.equal(links[1].href, 'https://link2.com');
122+
assert.equal(links[1].length, '[Link 2](https://link2.com)'.length);
123+
assert.equal(links[1].offset, 'Unable to [Link 1](http://link1.com) open '.length);
110124
});
111125

112126
test('Model', () => {

0 commit comments

Comments
 (0)