Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/formattedTextRenderer.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as DOM from './dom.js';
7
import { IKeyboardEvent } from './keyboardEvent.js';
8
import { IMouseEvent } from './mouseEvent.js';
9
import { DisposableStore } from '../common/lifecycle.js';
10
11
export interface IContentActionHandler {
12
readonly callback: (content: string, event: IMouseEvent | IKeyboardEvent) => void;
13
readonly disposables: DisposableStore;
14
}
15
16
export interface FormattedTextRenderOptions {
17
readonly actionHandler?: IContentActionHandler;
18
readonly renderCodeSegments?: boolean;
19
}
20
21
export function renderText(text: string, _options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement {
22
const element = target ?? document.createElement('div');
23
element.textContent = text;
24
return element;
25
}
26
27
export function renderFormattedText(formattedText: string, options?: FormattedTextRenderOptions, target?: HTMLElement): HTMLElement {
28
const element = target ?? document.createElement('div');
29
element.textContent = '';
30
_renderFormattedText(element, parseFormattedText(formattedText, !!options?.renderCodeSegments), options?.actionHandler, options?.renderCodeSegments);
31
return element;
32
}
33
34
class StringStream {
35
private source: string;
36
private index: number;
37
38
constructor(source: string) {
39
this.source = source;
40
this.index = 0;
41
}
42
43
public eos(): boolean {
44
return this.index >= this.source.length;
45
}
46
47
public next(): string {
48
const next = this.peek();
49
this.advance();
50
return next;
51
}
52
53
public peek(): string {
54
return this.source[this.index];
55
}
56
57
public advance(): void {
58
this.index++;
59
}
60
}
61
62
const enum FormatType {
63
Invalid,
64
Root,
65
Text,
66
Bold,
67
Italics,
68
Action,
69
ActionClose,
70
Code,
71
NewLine
72
}
73
74
interface IFormatParseTree {
75
type: FormatType;
76
content?: string;
77
index?: number;
78
children?: IFormatParseTree[];
79
}
80
81
function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler, renderCodeSegments?: boolean) {
82
let child: Node | undefined;
83
84
if (treeNode.type === FormatType.Text) {
85
child = document.createTextNode(treeNode.content || '');
86
} else if (treeNode.type === FormatType.Bold) {
87
child = document.createElement('b');
88
} else if (treeNode.type === FormatType.Italics) {
89
child = document.createElement('i');
90
} else if (treeNode.type === FormatType.Code && renderCodeSegments) {
91
child = document.createElement('code');
92
} else if (treeNode.type === FormatType.Action && actionHandler) {
93
const a = document.createElement('a');
94
actionHandler.disposables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
95
actionHandler.callback(String(treeNode.index), event);
96
}));
97
98
child = a;
99
} else if (treeNode.type === FormatType.NewLine) {
100
child = document.createElement('br');
101
} else if (treeNode.type === FormatType.Root) {
102
child = element;
103
}
104
105
if (child && element !== child) {
106
element.appendChild(child);
107
}
108
109
if (child && Array.isArray(treeNode.children)) {
110
treeNode.children.forEach((nodeChild) => {
111
_renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);
112
});
113
}
114
}
115
116
function parseFormattedText(content: string, parseCodeSegments: boolean): IFormatParseTree {
117
118
const root: IFormatParseTree = {
119
type: FormatType.Root,
120
children: []
121
};
122
123
let actionViewItemIndex = 0;
124
let current = root;
125
const stack: IFormatParseTree[] = [];
126
const stream = new StringStream(content);
127
128
while (!stream.eos()) {
129
let next = stream.next();
130
131
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek(), parseCodeSegments) !== FormatType.Invalid);
132
if (isEscapedFormatType) {
133
next = stream.next(); // unread the backslash if it escapes a format tag type
134
}
135
136
if (!isEscapedFormatType && isFormatTag(next, parseCodeSegments) && next === stream.peek()) {
137
stream.advance();
138
139
if (current.type === FormatType.Text) {
140
current = stack.pop()!;
141
}
142
143
const type = formatTagType(next, parseCodeSegments);
144
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
145
current = stack.pop()!;
146
} else {
147
const newCurrent: IFormatParseTree = {
148
type: type,
149
children: []
150
};
151
152
if (type === FormatType.Action) {
153
newCurrent.index = actionViewItemIndex;
154
actionViewItemIndex++;
155
}
156
157
current.children!.push(newCurrent);
158
stack.push(current);
159
current = newCurrent;
160
}
161
} else if (next === '\n') {
162
if (current.type === FormatType.Text) {
163
current = stack.pop()!;
164
}
165
166
current.children!.push({
167
type: FormatType.NewLine
168
});
169
170
} else {
171
if (current.type !== FormatType.Text) {
172
const textCurrent: IFormatParseTree = {
173
type: FormatType.Text,
174
content: next
175
};
176
current.children!.push(textCurrent);
177
stack.push(current);
178
current = textCurrent;
179
180
} else {
181
current.content += next;
182
}
183
}
184
}
185
186
if (current.type === FormatType.Text) {
187
current = stack.pop()!;
188
}
189
190
if (stack.length) {
191
// incorrectly formatted string literal
192
}
193
194
return root;
195
}
196
197
function isFormatTag(char: string, supportCodeSegments: boolean): boolean {
198
return formatTagType(char, supportCodeSegments) !== FormatType.Invalid;
199
}
200
201
function formatTagType(char: string, supportCodeSegments: boolean): FormatType {
202
switch (char) {
203
case '*':
204
return FormatType.Bold;
205
case '_':
206
return FormatType.Italics;
207
case '[':
208
return FormatType.Action;
209
case ']':
210
return FormatType.ActionClose;
211
case '`':
212
return supportCodeSegments ? FormatType.Code : FormatType.Invalid;
213
default:
214
return FormatType.Invalid;
215
}
216
}
217
218