Skip to content

Commit 0a6309b

Browse files
author
Jackson Kearl
authored
Support markdown styling in task descriptions. (#121338)
* Support markdown styling in task descriptions. Closes #120050 * Remove extra export * Fix typo * Dont use default values in internal-only functions * Preformatted => Code * Add tests
1 parent 2881355 commit 0a6309b

File tree

4 files changed

+47
-14
lines changed

4 files changed

+47
-14
lines changed

src/vs/base/browser/formattedTextRenderer.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface FormattedTextRenderOptions {
1616
readonly className?: string;
1717
readonly inline?: boolean;
1818
readonly actionHandler?: IContentActionHandler;
19+
readonly renderCodeSegements?: boolean;
1920
}
2021

2122
export function renderText(text: string, options: FormattedTextRenderOptions = {}): HTMLElement {
@@ -26,7 +27,7 @@ export function renderText(text: string, options: FormattedTextRenderOptions = {
2627

2728
export function renderFormattedText(formattedText: string, options: FormattedTextRenderOptions = {}): HTMLElement {
2829
const element = createElement(options);
29-
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
30+
_renderFormattedText(element, parseFormattedText(formattedText, !!options.renderCodeSegements), options.actionHandler, options.renderCodeSegements);
3031
return element;
3132
}
3233

@@ -75,6 +76,7 @@ const enum FormatType {
7576
Italics,
7677
Action,
7778
ActionClose,
79+
Code,
7880
NewLine
7981
}
8082

@@ -85,7 +87,7 @@ interface IFormatParseTree {
8587
children?: IFormatParseTree[];
8688
}
8789

88-
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
90+
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegements?: boolean) {
8991
let child: Node | undefined;
9092

9193
if (treeNode.type === FormatType.Text) {
@@ -94,6 +96,8 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
9496
child = document.createElement('b');
9597
} else if (treeNode.type === FormatType.Italics) {
9698
child = document.createElement('i');
99+
} else if (treeNode.type === FormatType.Code && renderCodeSegements) {
100+
child = document.createElement('code');
97101
} else if (treeNode.type === FormatType.Action && actionHandler) {
98102
const a = document.createElement('a');
99103
a.href = '#';
@@ -114,12 +118,12 @@ function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionH
114118

115119
if (child && Array.isArray(treeNode.children)) {
116120
treeNode.children.forEach((nodeChild) => {
117-
_renderFormattedText(child!, nodeChild, actionHandler);
121+
_renderFormattedText(child!, nodeChild, actionHandler, renderCodeSegements);
118122
});
119123
}
120124
}
121125

122-
function parseFormattedText(content: string): IFormatParseTree {
126+
function parseFormattedText(content: string, parseCodeSegments: boolean): IFormatParseTree {
123127

124128
const root: IFormatParseTree = {
125129
type: FormatType.Root,
@@ -134,19 +138,19 @@ function parseFormattedText(content: string): IFormatParseTree {
134138
while (!stream.eos()) {
135139
let next = stream.next();
136140

137-
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
141+
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== FormatType.Invalid);
138142
if (isEscapedFormatType) {
139143
next = stream.next(); // unread the backslash if it escapes a format tag type
140144
}
141145

142-
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
146+
if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {
143147
stream.advance();
144148

145149
if (current.type === FormatType.Text) {
146150
current = stack.pop()!;
147151
}
148152

149-
const type = formatTagType(next);
153+
const type = formatTagType(next, parseCodeSegments);
150154
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
151155
current = stack.pop()!;
152156
} else {
@@ -200,11 +204,11 @@ function parseFormattedText(content: string): IFormatParseTree {
200204
return root;
201205
}
202206

203-
function isFormatTag(char: string): boolean {
204-
return formatTagType(char) !== FormatType.Invalid;
207+
function isFormatTag(char: string, supportCodeSegments: boolean): boolean {
208+
return formatTagType(char, supportCodeSegments) !== FormatType.Invalid;
205209
}
206210

207-
function formatTagType(char: string): FormatType {
211+
function formatTagType(char: string, supportCodeSegments: boolean): FormatType {
208212
switch (char) {
209213
case '*':
210214
return FormatType.Bold;
@@ -214,6 +218,8 @@ function formatTagType(char: string): FormatType {
214218
return FormatType.Action;
215219
case ']':
216220
return FormatType.ActionClose;
221+
case '`':
222+
return supportCodeSegments ? FormatType.Code : FormatType.Invalid;
217223
default:
218224
return FormatType.Invalid;
219225
}

src/vs/base/test/browser/formattedTextRenderer.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,14 @@ suite('FormattedTextRenderer', () => {
4444
result = renderFormattedText('__italics__');
4545
assert.strictEqual(result.innerHTML, '<i>italics</i>');
4646

47-
result = renderFormattedText('this string has **bold** and __italics__');
48-
assert.strictEqual(result.innerHTML, 'this string has <b>bold</b> and <i>italics</i>');
47+
result = renderFormattedText('``code``');
48+
assert.strictEqual(result.innerHTML, '``code``');
49+
50+
result = renderFormattedText('``code``', { renderCodeSegements: true });
51+
assert.strictEqual(result.innerHTML, '<code>code</code>');
52+
53+
result = renderFormattedText('this string has **bold**, __italics__, and ``code``!!', { renderCodeSegements: true });
54+
assert.strictEqual(result.innerHTML, 'this string has <b>bold</b>, <i>italics</i>, and <code>code</code>!!');
4955
});
5056

5157
test('no formatting', () => {
@@ -96,6 +102,26 @@ suite('FormattedTextRenderer', () => {
96102
assert.strictEqual(callbackCalled, true);
97103
});
98104

105+
test('fancier action', () => {
106+
let callbackCalled = false;
107+
let result: HTMLElement = renderFormattedText('``__**[[action]]**__``', {
108+
renderCodeSegements: true,
109+
actionHandler: {
110+
callback(content) {
111+
assert.strictEqual(content, '0');
112+
callbackCalled = true;
113+
},
114+
disposeables: store
115+
}
116+
});
117+
assert.strictEqual(result.innerHTML, '<code><i><b><a href="#">action</a></b></i></code>');
118+
119+
let event: MouseEvent = <any>document.createEvent('MouseEvent');
120+
event.initEvent('click', true, true);
121+
result.firstChild!.firstChild!.firstChild!.firstChild!.dispatchEvent(event);
122+
assert.strictEqual(callbackCalled, true);
123+
});
124+
99125
test('escaped formatting', () => {
100126
let result: HTMLElement = renderFormattedText('\\*\\*bold\\*\\*');
101127
assert.strictEqual(result.children.length, 0);

src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ ExtensionsRegistry.registerExtensionPoint({
235235
},
236236
description: {
237237
type: 'string',
238-
description: localize('walkthroughs.tasks.description', "Description of task. Use markdown-style links fo commands or external links: [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms)")
238+
description: localize('walkthroughs.tasks.description', "Description of task. Supports ``preformatted``, __italic__, and **bold** text. Use markdown-style links for commands or external links: [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms). Links on their own line will be rendered as buttons.")
239239
},
240240
button: {
241241
deprecationMessage: localize('walkthroughs.tasks.button.deprecated', "Deprecated. Use markdown links in the description instead, i.e. [Title](command:myext.command), [Title](command:toSide:myext.command), or [Title](https://aka.ms), "),

src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { LinkedText } from 'vs/base/common/linkedText';
4545
import { Button } from 'vs/base/browser/ui/button/button';
4646
import { attachButtonStyler, attachLinkStyler } from 'vs/platform/theme/common/styler';
4747
import { Link } from 'vs/platform/opener/browser/link';
48+
import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer';
4849

4950
const SLIDE_TRANSITION_TIME_MS = 250;
5051
const configurationKey = 'workbench.startupEditor';
@@ -761,7 +762,7 @@ export class GettingStartedPage extends EditorPane {
761762
const p = append(container, $('p'));
762763
for (const node of linkedText.nodes) {
763764
if (typeof node === 'string') {
764-
append(p, document.createTextNode(node));
765+
append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true }));
765766
} else {
766767
const link = this.instantiationService.createInstance(Link, node);
767768

0 commit comments

Comments
 (0)