Skip to content

Commit 82601dc

Browse files
committed
Split formatted text renderer into own file
1 parent ec07311 commit 82601dc

7 files changed

Lines changed: 314 additions & 298 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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 'vs/base/browser/dom';
7+
import { createElement, IContentActionHandler, RenderOptions } from 'vs/base/browser/htmlContentRenderer';
8+
9+
export function renderText(text: string, options: RenderOptions = {}): HTMLElement {
10+
const element = createElement(options);
11+
element.textContent = text;
12+
return element;
13+
}
14+
15+
export function renderFormattedText(formattedText: string, options: RenderOptions = {}): HTMLElement {
16+
const element = createElement(options);
17+
_renderFormattedText(element, parseFormattedText(formattedText), options.actionHandler);
18+
return element;
19+
}
20+
21+
class StringStream {
22+
private source: string;
23+
private index: number;
24+
25+
constructor(source: string) {
26+
this.source = source;
27+
this.index = 0;
28+
}
29+
30+
public eos(): boolean {
31+
return this.index >= this.source.length;
32+
}
33+
34+
public next(): string {
35+
const next = this.peek();
36+
this.advance();
37+
return next;
38+
}
39+
40+
public peek(): string {
41+
return this.source[this.index];
42+
}
43+
44+
public advance(): void {
45+
this.index++;
46+
}
47+
}
48+
49+
const enum FormatType {
50+
Invalid,
51+
Root,
52+
Text,
53+
Bold,
54+
Italics,
55+
Action,
56+
ActionClose,
57+
NewLine
58+
}
59+
60+
interface IFormatParseTree {
61+
type: FormatType;
62+
content?: string;
63+
index?: number;
64+
children?: IFormatParseTree[];
65+
}
66+
67+
export function _renderFormattedText(element: Node, treeNode: IFormatParseTree, actionHandler?: IContentActionHandler) {
68+
let child: Node | undefined;
69+
70+
if (treeNode.type === FormatType.Text) {
71+
child = document.createTextNode(treeNode.content || '');
72+
} else if (treeNode.type === FormatType.Bold) {
73+
child = document.createElement('b');
74+
} else if (treeNode.type === FormatType.Italics) {
75+
child = document.createElement('i');
76+
} else if (treeNode.type === FormatType.Action && actionHandler) {
77+
const a = document.createElement('a');
78+
a.href = '#';
79+
actionHandler.disposeables.add(DOM.addStandardDisposableListener(a, 'click', (event) => {
80+
actionHandler.callback(String(treeNode.index), event);
81+
}));
82+
83+
child = a;
84+
} else if (treeNode.type === FormatType.NewLine) {
85+
child = document.createElement('br');
86+
} else if (treeNode.type === FormatType.Root) {
87+
child = element;
88+
}
89+
90+
if (child && element !== child) {
91+
element.appendChild(child);
92+
}
93+
94+
if (child && Array.isArray(treeNode.children)) {
95+
treeNode.children.forEach((nodeChild) => {
96+
_renderFormattedText(child!, nodeChild, actionHandler);
97+
});
98+
}
99+
}
100+
101+
export function parseFormattedText(content: string): IFormatParseTree {
102+
103+
const root: IFormatParseTree = {
104+
type: FormatType.Root,
105+
children: []
106+
};
107+
108+
let actionViewItemIndex = 0;
109+
let current = root;
110+
const stack: IFormatParseTree[] = [];
111+
const stream = new StringStream(content);
112+
113+
while (!stream.eos()) {
114+
let next = stream.next();
115+
116+
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== FormatType.Invalid);
117+
if (isEscapedFormatType) {
118+
next = stream.next(); // unread the backslash if it escapes a format tag type
119+
}
120+
121+
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
122+
stream.advance();
123+
124+
if (current.type === FormatType.Text) {
125+
current = stack.pop()!;
126+
}
127+
128+
const type = formatTagType(next);
129+
if (current.type === type || (current.type === FormatType.Action && type === FormatType.ActionClose)) {
130+
current = stack.pop()!;
131+
} else {
132+
const newCurrent: IFormatParseTree = {
133+
type: type,
134+
children: []
135+
};
136+
137+
if (type === FormatType.Action) {
138+
newCurrent.index = actionViewItemIndex;
139+
actionViewItemIndex++;
140+
}
141+
142+
current.children!.push(newCurrent);
143+
stack.push(current);
144+
current = newCurrent;
145+
}
146+
} else if (next === '\n') {
147+
if (current.type === FormatType.Text) {
148+
current = stack.pop()!;
149+
}
150+
151+
current.children!.push({
152+
type: FormatType.NewLine
153+
});
154+
155+
} else {
156+
if (current.type !== FormatType.Text) {
157+
const textCurrent: IFormatParseTree = {
158+
type: FormatType.Text,
159+
content: next
160+
};
161+
current.children!.push(textCurrent);
162+
stack.push(current);
163+
current = textCurrent;
164+
165+
} else {
166+
current.content += next;
167+
}
168+
}
169+
}
170+
171+
if (current.type === FormatType.Text) {
172+
current = stack.pop()!;
173+
}
174+
175+
if (stack.length) {
176+
// incorrectly formatted string literal
177+
}
178+
179+
return root;
180+
}
181+
182+
function isFormatTag(char: string): boolean {
183+
return formatTagType(char) !== FormatType.Invalid;
184+
}
185+
186+
function formatTagType(char: string): FormatType {
187+
switch (char) {
188+
case '*':
189+
return FormatType.Bold;
190+
case '_':
191+
return FormatType.Italics;
192+
case '[':
193+
return FormatType.Action;
194+
case ']':
195+
return FormatType.ActionClose;
196+
default:
197+
return FormatType.Invalid;
198+
}
199+
}

0 commit comments

Comments
 (0)