generated from PythonCoderAS/typescript-project-template
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnode.ts
403 lines (349 loc) · 9.42 KB
/
node.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
/**
* Parameters for constructing a new Node object.
*/
export interface NodeConstructorParams {
/**
* The name of the node.
*
* Ref: {@link Node.name}
* @example `[b]Hello World![/b]` -> `b`
*/
name: string;
/**
* The attributes of the node.
*
* Ref: {@link Node.attributes}
* @example `[img width=100 height=100]https://example.com/image.png[/img]` -> `{ width: "100", height: "100" }`
*/
attributes?: { [key: string]: string };
/**
* The value of the node.
*
* Ref: {@link Node.value}
* @example `[color=red]Hello World![/color]` -> `red`
*/
value?: string;
}
/**
* The abstract base class for all nodes.
*/
export abstract class BaseNode {
/**
* The name of the node.
*/
abstract name: string;
/**
* Clones the code, creating a new deep-copied node.
*/
abstract clone(): BaseNode;
/**
* Converts the node into a textual representation of the node as BBCode.
* A node should return the exact same string as the input to {@link Parser.parse} provided `indented` is `false`.
* @returns The textual representation of the node as BBCode.
*/
abstract toString(): string;
/**
* Gets the textual representation of the node as BBCode.
* An alias for {@link BaseNode.toString}.
*/
toBBCode(): string {
return this.toString();
}
/**
* Converts the node into a tree-like overview of all children.
* @param {number} indentWidth The number of spaces to indent the output. Defaults to `2`.
* @returns The tree-like overview of all children.
*/
nodeTree(indentWidth: number = 2): string {
return this.nodeTreeHeading() + this.nodeTreeChildren(indentWidth);
}
/**
* Gets the heading of the node tree.
* @returns The heading of the node tree.
*/
protected abstract nodeTreeHeading(): string;
/**
* Gets the children of the node tree.
* @param indentWidth The number of spaces to indent the output.
* @returns The children of the node tree.
*/
protected abstract nodeTreeChildren(indentWidth: number): string;
}
/**
* Indents a string.
* @param input The string to indent.
* @param indentWidth The number of spaces to indent the string.
* @param indentLevel The current level of indentation.
* @returns The indented string.
*/
function indentString(
input: string,
indentWidth: number,
indentLevel: number
): string {
const lines = input.split("\n");
const indent = " ".repeat(indentWidth * indentLevel);
return lines.map((line) => indent + line).join("\n");
}
/**
* An interface for nodes that can hold children.
*/
export interface ChildrenHolder {
/**
* The children of the node. A node can have any number of children of all types.
*/
children: BaseNode[];
/**
* Adds a child to the node. If the node is a {@link TextNode}, attempts to flatten the node with a previous TextNode.
* @param child The child to add.
*/
addChild(child: BaseNode): void;
}
/**
* A type for attributes.
*
* A BBCode attribute is a key-value pair, where the key is a string and the value is a string. A tag can have multiple attributes.
* @example `[img width=100 height=100]https://example.com/image.png[/img]` -> `{ width: "100", height: "100" }`
*/
export type AttributeType = { [key: string]: string };
/**
* An interface for nodes that can hold attributes.
*/
export interface AttributeHolder {
/**
* The attributes of the node.
* This should be public, so it can be directly accessed and modified.
*/
attributes: AttributeType;
/**
* Sets the attribute of the node.
* @param key The key of the attribute.
* @param value The value of the attribute.
* @deprecated Use {@link AttributeHolder.attributes} instead.
*/
setAttribute(key: string, value: string): void;
}
export interface ValueHolder {
/**
* Stores the [simple parameterized value](https://www.bbcode.org/reference.php) of the tag.
*
* @example `[b=red]Hello World![/b]` -> `red`
*/
value?: string;
/**
* Sets the value of the node.
*
* @param {string} value The value of the node.
* @deprecated Use {@link ValueHolder.value} instead.
*/
setValue(value: string): void;
}
export abstract class ChildrenHolderNode
extends BaseNode
implements ChildrenHolder
{
/**
* The children of the node.
*
* Ref: {@link ChildrenHolder.children}
*/
public children: BaseNode[] = [];
addChild(child: BaseNode): void {
if (child instanceof TextNode) {
const previousChild = this.children[this.children.length - 1];
if (previousChild instanceof TextNode) {
// We flatten the text nodes.
previousChild.text += child.text;
return;
}
}
this.children.push(child.clone());
}
protected nodeTreeChildren(indentWidth: number): string {
if (this.children.length === 0) {
return ` {}`;
}
let nodeString = ` {`;
this.children.forEach((child) => {
const childOutput = `\n${indentString(
child.nodeTree(indentWidth),
indentWidth,
1
)}`;
nodeString += childOutput;
});
nodeString += "\n}";
return nodeString;
}
}
/**
* A BBCode node. This represents a tag and it's children.
*/
export class Node
extends ChildrenHolderNode
implements AttributeHolder, ValueHolder
{
/**
* The name of the tag.
*
* @example `[b]Hello World![/b]` -> `b`
*/
public name: string;
/**
* The attributes of the tag.
*
* Ref: {@link AttributeType}
*/
public attributes: AttributeType;
/**
* The value of the tag.
*
* Ref: {@link ValueHolder.value}
*/
value?: string;
constructor(params: NodeConstructorParams) {
super();
this.name = params.name;
this.attributes = params.attributes || {};
this.value = params.value;
}
clone(): Node {
const node = new Node({
name: this.name,
attributes: this.attributes,
value: this.value,
});
node.children = this.children.map((child) => child.clone());
return node;
}
setValue(value: string): void {
this.value = value;
}
setAttribute(key: string, value: string): void {
this.attributes[key] = value;
}
/**
* Makes the opening tag of the node. This will include the name, and all attributes.
* @returns The opening tag of the node.
*/
makeOpeningTag(): string {
let nodeString = `[${this.name}`;
if (this.value) {
nodeString += `=${this.value}`;
}
Object.entries(this.attributes).forEach(([key, value]) => {
nodeString += ` ${key}=${value}`;
});
nodeString += "]";
return nodeString;
}
toString(): string {
let nodeString = this.makeOpeningTag();
nodeString += this.children.map((child) => child.toString()).join("");
nodeString += `[/${this.name}]`;
return nodeString;
}
protected nodeTreeHeading(): string {
let nodeString = `Node [${this.name}]`;
const allAttrs = [];
if (this.value) {
allAttrs.push(`${this.value}`);
}
if (Object.keys(this.attributes).length > 0) {
nodeString += ` (${allAttrs
.concat(
Object.entries(this.attributes).map(
([key, value]) => `${key}=${value}`
)
)
.join(", ")})`;
}
return nodeString;
}
}
/**
* A node that represents a chunk of text. This node is *not* a tag.
*/
export class TextNode extends BaseNode {
/**
* The text of the node.
*/
text: string;
readonly name = "TextNode";
/**
* Create a new TextNode.
* @param {string} text The text of the node.
*/
constructor(text: string) {
super();
this.text = text;
}
clone(): TextNode {
return new TextNode(this.text);
}
toString(): string {
return this.text;
}
// eslint-disable-next-line class-methods-use-this
protected nodeTreeHeading(): string {
return `TextNode`;
}
protected nodeTreeChildren(indentWidth: number): string {
return ` {\n${indentString(this.text, indentWidth, 1)}\n}`;
}
}
/**
* The root node that represents the head of the AST. It only stores children.
*/
export class RootNode extends ChildrenHolderNode {
name = "RootNode";
children: BaseNode[];
/**
* Create a new RootNode.
* @param {BaseNode[]} children The children of the node.
*/
constructor(children: BaseNode[] = []) {
super();
this.children = children;
}
addChild(child: BaseNode): void {
if (child instanceof TextNode) {
const previousChild = this.children[this.children.length - 1];
if (previousChild instanceof TextNode) {
// We flatten the text nodes.
previousChild.text += child.text;
return;
}
}
this.children.push(child.clone());
}
clone(): RootNode {
return new RootNode(this.children.map((child) => child.clone()));
}
/**
* The textual representation of the BBCode AST. It should return a string equivalent to the input to {@link Parser.parse}.
*/
toString(): string {
return this.children.map((child) => child.toString()).join("");
}
// eslint-disable-next-line class-methods-use-this
protected nodeTreeHeading(): string {
return `RootNode`;
}
}
/**
* A node that represents a list item. It is similar to the root node.
*/
export class ListItemNode extends RootNode {
name = "*";
toString(): string {
return `[*]${super.toString()}`;
}
clone(): ListItemNode {
return new ListItemNode(this.children.map((child) => child.clone()));
}
// eslint-disable-next-line class-methods-use-this
protected nodeTreeHeading(): string {
return `ListItemNode`;
}
}