/
TagNode.ts
205 lines (177 loc) · 5.68 KB
/
TagNode.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
import { strict as assert } from "assert";
import { IToken } from "chevrotain";
import { Range } from "../Range";
// The TagNode represents a tag found by the visitor.
interface TagNode {
type: "line" | "block";
// The name of the tag (without -start or -end).
tagName: string;
// For block tags, range from the first character of the tag (start)
// token to the last character of the tag (end) token. For line tags,
// range from tag token start to end.
range: Range;
// Range from the beginning of the line on which the tag (start) token
// appears to the end of the line on which the tag (end) token appears.
lineRange: Range;
// Potentially useful tokens contained in the node
newlines: IToken[];
lineComments: IToken[];
// The comment tokens on the line the tag was found in, if any.
associatedTokens: IToken[];
// The comment context the tag was found in.
inContext: TagNodeContext;
// Block tags have an inner range that includes the lines between the
// attribute list and the end tag token.
contentRange?: Range;
// The child tag nodes.
children?: TagNode[];
// Attributes come from JSON and their schema depends on the tag.
attributes?: TagNodeAttributes;
// Shorthand args are passed after a tag name.
shorthandArgs?: string[];
}
// A line tag applies to a specific line and does not have -start or -end
// tags.
export interface LineTagNode extends TagNode {
type: "line";
id: undefined;
contentRange: undefined;
children: undefined;
attributes: undefined;
}
// A block tag applies to a range of lines and has -start and -end tags.
export interface BlockTagNode extends TagNode {
type: "block";
contentRange: Range;
children: AnyTagNode[];
attributes: TagNodeAttributes;
}
export type AnyTagNode = LineTagNode | BlockTagNode;
export type TagNodeContext =
| "none"
| "stringLiteral"
| "lineComment"
| "blockComment";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TagNodeAttributes = { [member: string]: any };
interface VisitorContext {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
Newline?: IToken[];
LineComment?: IToken[];
}
export class TagNodeImpl implements TagNode {
type: "line" | "block";
tagName: string;
get inContext(): TagNodeContext {
return this._context[this._context.length - 1] || "none";
}
_context = Array<TagNodeContext>();
range: Range;
lineRange: Range;
// Only available in block tags
get id(): string[] | undefined {
return this.attributes?.id;
}
contentRange?: Range;
children?: TagNodeImpl[];
shorthandArgs?: string[];
attributes?: TagNodeAttributes;
newlines: IToken[] = [];
lineComments: IToken[] = [];
associatedTokens: IToken[] = [];
// Imports potentially useful tokens from a visitor context object.
// Only use this in visitors that do not create the TagNodeImpl.
addTokensFromContext(context: VisitorContext): void {
this.newlines.push(...(context.Newline ?? []));
this.lineComments.push(...(context.LineComment ?? []));
}
// Block Tags operate on their inner range and can have children, IDs, and
// attributes.
makeChildBlockTag(tagName: string, context: VisitorContext): TagNodeImpl {
assert(this.children);
const tag = new TagNodeImpl("block", tagName, context, this);
return tag;
}
// Line Tags operate on the line they appear in and cannot have children,
// IDs, or attributes.
makeChildLineTag(tagName: string, context: VisitorContext): TagNodeImpl {
assert(this.children);
return new TagNodeImpl("line", tagName, context, this);
}
withErasedBlockTag(
context: VisitorContext,
callback: (erasedBlockTag: TagNodeImpl) => void
): void {
assert(this.children);
// We erase whatever element created the context and add what would have
// been that element's children to the parent node. This is definitely
// weird, but only used internally...
const node = new TagNodeImpl(
"block",
"__this_should_not_be_here___please_file_a_bug__",
context
);
callback(node);
assert(node.children); // Enforced by setting type to "block"
this.children.push(...node.children);
this.newlines.push(...node.newlines);
this.lineComments.push(...node.lineComments);
this.associatedTokens.push(...node.associatedTokens);
}
// The root tag is the root node of a parsed document and contains all
// other nodes in the document.
static rootTag(): TagNodeImpl {
const tag = new TagNodeImpl("block", "__root__", {});
return tag;
}
private constructor(
type: "block" | "line",
tagName: string,
context: VisitorContext,
parentToAttachTo?: TagNodeImpl
) {
this.type = type;
if (type === "block") {
this.children = [];
}
this.tagName = tagName;
// FIXME: ranges should always be valid, so pass them in the constructor
this.range = {
start: {
column: -1,
line: -1,
offset: -1,
},
end: {
column: -1,
line: -1,
offset: -1,
},
};
this.lineRange = {
start: {
column: -1,
line: -1,
offset: -1,
},
end: {
column: -1,
line: -1,
offset: -1,
},
};
this.addTokensFromContext(context);
if (parentToAttachTo !== undefined) {
this._context = [...parentToAttachTo._context];
assert(parentToAttachTo.children);
parentToAttachTo.children.push(this);
}
}
asBlockTagNode = (): BlockTagNode | undefined => {
return this.type === "block" ? (this as BlockTagNode) : undefined;
};
asLineTagNode = (): LineTagNode | undefined => {
return this.type === "line" ? (this as LineTagNode) : undefined;
};
}