bbcode-compiler 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -16
- package/dist/index.js +866 -1038
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +914 -1063
- package/dist/index.umd.cjs.map +1 -1
- package/dist/src/generateHtml.d.ts +2 -0
- package/dist/src/generateHtml.d.ts.map +1 -0
- package/dist/{generator → src/generator}/Generator.d.ts +2 -3
- package/dist/src/generator/Generator.d.ts.map +1 -0
- package/dist/{generator → src/generator}/transforms/Transform.d.ts +1 -2
- package/dist/src/generator/transforms/Transform.d.ts.map +1 -0
- package/dist/src/generator/transforms/htmlTransforms.d.ts +3 -0
- package/dist/src/generator/transforms/htmlTransforms.d.ts.map +1 -0
- package/dist/{generator → src/generator}/utils/getTagImmediateAttrVal.d.ts +1 -2
- package/dist/src/generator/utils/getTagImmediateAttrVal.d.ts.map +1 -0
- package/dist/{generator → src/generator}/utils/getTagImmediateText.d.ts +1 -2
- package/dist/src/generator/utils/getTagImmediateText.d.ts.map +1 -0
- package/dist/{generator → src/generator}/utils/getWidthHeightAttr.d.ts +1 -2
- package/dist/src/generator/utils/getWidthHeightAttr.d.ts.map +1 -0
- package/dist/src/generator/utils/isDangerousUrl.d.ts.map +1 -0
- package/dist/{generator → src/generator}/utils/isOrderedList.d.ts +1 -2
- package/dist/src/generator/utils/isOrderedList.d.ts.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/{lexer → src/lexer}/Lexer.d.ts +1 -2
- package/dist/src/lexer/Lexer.d.ts.map +1 -0
- package/dist/{lexer → src/lexer}/Token.d.ts +1 -2
- package/dist/src/lexer/Token.d.ts.map +1 -0
- package/dist/src/lexer/TokenType.d.ts +5 -0
- package/dist/src/lexer/TokenType.d.ts.map +1 -0
- package/dist/{parser → src/parser}/AstNode.d.ts +10 -18
- package/dist/src/parser/AstNode.d.ts.map +1 -0
- package/dist/{parser → src/parser}/Parser.d.ts +3 -4
- package/dist/src/parser/Parser.d.ts.map +1 -0
- package/dist/src/parser/nodeIsType.d.ts +13 -0
- package/dist/src/parser/nodeIsType.d.ts.map +1 -0
- package/package.json +81 -82
- package/src/generateHtml.ts +4 -4
- package/src/generator/Generator.ts +7 -7
- package/src/generator/transforms/Transform.ts +1 -1
- package/src/generator/transforms/htmlTransforms.ts +6 -6
- package/src/generator/utils/getTagImmediateAttrVal.ts +1 -1
- package/src/generator/utils/getTagImmediateText.ts +4 -4
- package/src/generator/utils/getWidthHeightAttr.ts +1 -1
- package/src/generator/utils/isOrderedList.ts +1 -1
- package/src/index.ts +15 -15
- package/src/lexer/Lexer.ts +9 -9
- package/src/lexer/Token.ts +12 -12
- package/src/lexer/TokenType.ts +39 -40
- package/src/parser/AstNode.ts +30 -29
- package/src/parser/Parser.ts +28 -28
- package/src/parser/nodeIsType.ts +8 -8
- package/dist/generateHtml.d.ts +0 -2
- package/dist/generateHtml.d.ts.map +0 -1
- package/dist/generator/Generator.d.ts.map +0 -1
- package/dist/generator/transforms/Transform.d.ts.map +0 -1
- package/dist/generator/transforms/htmlTransforms.d.ts +0 -4
- package/dist/generator/transforms/htmlTransforms.d.ts.map +0 -1
- package/dist/generator/utils/getTagImmediateAttrVal.d.ts.map +0 -1
- package/dist/generator/utils/getTagImmediateText.d.ts.map +0 -1
- package/dist/generator/utils/getWidthHeightAttr.d.ts.map +0 -1
- package/dist/generator/utils/isDangerousUrl.d.ts.map +0 -1
- package/dist/generator/utils/isOrderedList.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lexer/Lexer.d.ts.map +0 -1
- package/dist/lexer/Token.d.ts.map +0 -1
- package/dist/lexer/TokenType.d.ts +0 -17
- package/dist/lexer/TokenType.d.ts.map +0 -1
- package/dist/parser/AstNode.d.ts.map +0 -1
- package/dist/parser/Parser.d.ts.map +0 -1
- package/dist/parser/nodeIsType.d.ts +0 -14
- package/dist/parser/nodeIsType.d.ts.map +0 -1
- /package/dist/{generator → src/generator}/utils/isDangerousUrl.d.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -1,1061 +1,889 @@
|
|
|
1
|
-
|
|
2
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1
|
+
//#region src/parser/nodeIsType.ts
|
|
4
2
|
function nodeIsType(node, nodeType) {
|
|
5
|
-
|
|
3
|
+
return node.nodeType === nodeType;
|
|
6
4
|
}
|
|
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
|
-
class TextNode extends AstNode {
|
|
69
|
-
constructor(str) {
|
|
70
|
-
super();
|
|
71
|
-
__publicField(this, "nodeType", "TextNode");
|
|
72
|
-
__publicField(this, "str");
|
|
73
|
-
this.str = str;
|
|
74
|
-
}
|
|
75
|
-
isValid() {
|
|
76
|
-
return super.isValid() && this.children.length === 0;
|
|
77
|
-
}
|
|
78
|
-
toShortString() {
|
|
79
|
-
return `${super.toShortString()} "${this.str}"`;
|
|
80
|
-
}
|
|
81
|
-
toJSON() {
|
|
82
|
-
const json = super.toJSON();
|
|
83
|
-
json.data = {
|
|
84
|
-
str: this.str
|
|
85
|
-
};
|
|
86
|
-
return json;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
class LinebreakNode extends AstNode {
|
|
90
|
-
constructor() {
|
|
91
|
-
super(...arguments);
|
|
92
|
-
__publicField(this, "nodeType", "LinebreakNode");
|
|
93
|
-
}
|
|
94
|
-
toShortString() {
|
|
95
|
-
return `${super.toShortString()} "\\n"`;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const _AttrNode = class _AttrNode extends AstNode {
|
|
99
|
-
constructor() {
|
|
100
|
-
super(...arguments);
|
|
101
|
-
__publicField(this, "nodeType", "AttrNode");
|
|
102
|
-
}
|
|
103
|
-
get key() {
|
|
104
|
-
switch (this.children.length) {
|
|
105
|
-
case 1: {
|
|
106
|
-
return _AttrNode.DEFAULT_KEY;
|
|
107
|
-
}
|
|
108
|
-
case 2: {
|
|
109
|
-
if (!nodeIsType(
|
|
110
|
-
this.children[0],
|
|
111
|
-
"TextNode"
|
|
112
|
-
/* TextNode */
|
|
113
|
-
)) {
|
|
114
|
-
throw new Error("Invalid TextNode");
|
|
115
|
-
}
|
|
116
|
-
return this.children[0].str.trim();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
throw new Error("Invalid AttrNode");
|
|
120
|
-
}
|
|
121
|
-
get val() {
|
|
122
|
-
switch (this.children.length) {
|
|
123
|
-
case 1: {
|
|
124
|
-
if (!nodeIsType(
|
|
125
|
-
this.children[0],
|
|
126
|
-
"TextNode"
|
|
127
|
-
/* TextNode */
|
|
128
|
-
)) {
|
|
129
|
-
throw new Error("Invalid TextNode");
|
|
130
|
-
}
|
|
131
|
-
return this.children[0].str.trim();
|
|
132
|
-
}
|
|
133
|
-
case 2: {
|
|
134
|
-
if (!nodeIsType(
|
|
135
|
-
this.children[1],
|
|
136
|
-
"TextNode"
|
|
137
|
-
/* TextNode */
|
|
138
|
-
)) {
|
|
139
|
-
throw new Error("Invalid TextNode");
|
|
140
|
-
}
|
|
141
|
-
return this.children[1].str.trim();
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
throw new Error("Invalid AttrNode");
|
|
145
|
-
}
|
|
146
|
-
isValid() {
|
|
147
|
-
return super.isValid() && (this.children.length >= 1 && this.children.length <= 2);
|
|
148
|
-
}
|
|
149
|
-
toShortString() {
|
|
150
|
-
let s = super.toShortString();
|
|
151
|
-
switch (this.children.length) {
|
|
152
|
-
case 1: {
|
|
153
|
-
s += ` VAL="${this.val}"`;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case 2: {
|
|
157
|
-
s += ` KEY="${this.key}" VAL="${this.val}"`;
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return s;
|
|
162
|
-
}
|
|
163
|
-
toJSON() {
|
|
164
|
-
const json = {
|
|
165
|
-
type: this.nodeType
|
|
166
|
-
};
|
|
167
|
-
switch (this.children.length) {
|
|
168
|
-
case 1: {
|
|
169
|
-
json.data = {
|
|
170
|
-
key: this.key
|
|
171
|
-
};
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
case 2: {
|
|
175
|
-
json.data = {
|
|
176
|
-
key: this.key,
|
|
177
|
-
val: this.val
|
|
178
|
-
};
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return json;
|
|
183
|
-
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region src/parser/AstNode.ts
|
|
7
|
+
/**
|
|
8
|
+
|
|
9
|
+
Haven't formally verified this grammar but it should be LL(2)
|
|
10
|
+
|
|
11
|
+
The root's intermediate state has StartTag/EndTag because it's easier to first parse them as independant nodes
|
|
12
|
+
than to parse a StartTag and find the matching EndTag since we can only lookahead by 1 token
|
|
13
|
+
|
|
14
|
+
Trying to lookahead by 4 tokens after each advancement to determine the end of the sub-root will greatly affect performance
|
|
15
|
+
1 "["
|
|
16
|
+
2 "/"
|
|
17
|
+
3 "LABEL"
|
|
18
|
+
4 "]"
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
Root <- (Text | Linebreak | Tag)*
|
|
23
|
+
|
|
24
|
+
Text <-
|
|
25
|
+
| {XSS Characters}.
|
|
26
|
+
| STR.
|
|
27
|
+
|
|
28
|
+
Linebreak <-
|
|
29
|
+
| LINEBREAK.
|
|
30
|
+
|
|
31
|
+
Tag <- StartTag Root EndTag
|
|
32
|
+
StartTag <- L_BRACKET Text Attr* R_BRACKET
|
|
33
|
+
EndTag <- L_BRACKET BACKSLASH Text R_BRACKET
|
|
34
|
+
|
|
35
|
+
Attr <-
|
|
36
|
+
| STR EQUALS STR
|
|
37
|
+
| EQUALS STR
|
|
38
|
+
| STR
|
|
39
|
+
|
|
40
|
+
*/
|
|
41
|
+
var AstNode = class {
|
|
42
|
+
children;
|
|
43
|
+
constructor(children = new Array()) {
|
|
44
|
+
this.children = children;
|
|
45
|
+
}
|
|
46
|
+
addChild(node) {
|
|
47
|
+
this.children.push(node);
|
|
48
|
+
}
|
|
49
|
+
isValid() {
|
|
50
|
+
for (const child of this.children) if (!child.isValid()) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
toShortString() {
|
|
54
|
+
return this.nodeType;
|
|
55
|
+
}
|
|
56
|
+
toString(depth = 0) {
|
|
57
|
+
let s = " ".repeat(depth * 2) + this.toShortString();
|
|
58
|
+
for (const child of this.children) s += "\n" + child.toString(depth + 1);
|
|
59
|
+
return s;
|
|
60
|
+
}
|
|
61
|
+
toJSON() {
|
|
62
|
+
const json = { type: this.nodeType };
|
|
63
|
+
if (this.children.length > 0) json.children = this.children.map((child) => child.toJSON());
|
|
64
|
+
return json;
|
|
65
|
+
}
|
|
184
66
|
};
|
|
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
|
-
}
|
|
67
|
+
var RootNode = class extends AstNode {
|
|
68
|
+
nodeType = "RootNode";
|
|
69
|
+
isValid() {
|
|
70
|
+
for (const child of this.children) if (child.nodeType !== "TagNode" && child.nodeType !== "TextNode" && child.nodeType !== "LinebreakNode") return false;
|
|
71
|
+
return super.isValid() && this.children.length > 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var TextNode = class extends AstNode {
|
|
75
|
+
nodeType = "TextNode";
|
|
76
|
+
str;
|
|
77
|
+
constructor(str) {
|
|
78
|
+
super();
|
|
79
|
+
this.str = str;
|
|
80
|
+
}
|
|
81
|
+
isValid() {
|
|
82
|
+
return super.isValid() && this.children.length === 0;
|
|
83
|
+
}
|
|
84
|
+
toShortString() {
|
|
85
|
+
return `${super.toShortString()} "${this.str}"`;
|
|
86
|
+
}
|
|
87
|
+
toJSON() {
|
|
88
|
+
const json = super.toJSON();
|
|
89
|
+
json.data = { str: this.str };
|
|
90
|
+
return json;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
var LinebreakNode = class extends AstNode {
|
|
94
|
+
nodeType = "LinebreakNode";
|
|
95
|
+
toShortString() {
|
|
96
|
+
return `${super.toShortString()} "\\n"`;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var AttrNode = class AttrNode extends AstNode {
|
|
100
|
+
nodeType = "AttrNode";
|
|
101
|
+
static DEFAULT_KEY = "default";
|
|
102
|
+
get key() {
|
|
103
|
+
switch (this.children.length) {
|
|
104
|
+
case 1: return AttrNode.DEFAULT_KEY;
|
|
105
|
+
case 2:
|
|
106
|
+
if (!nodeIsType(this.children[0], "TextNode")) throw new Error("Invalid TextNode");
|
|
107
|
+
return this.children[0].str.trim();
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Invalid AttrNode");
|
|
110
|
+
}
|
|
111
|
+
get val() {
|
|
112
|
+
switch (this.children.length) {
|
|
113
|
+
case 1:
|
|
114
|
+
if (!nodeIsType(this.children[0], "TextNode")) throw new Error("Invalid TextNode");
|
|
115
|
+
return this.children[0].str.trim();
|
|
116
|
+
case 2:
|
|
117
|
+
if (!nodeIsType(this.children[1], "TextNode")) throw new Error("Invalid TextNode");
|
|
118
|
+
return this.children[1].str.trim();
|
|
119
|
+
}
|
|
120
|
+
throw new Error("Invalid AttrNode");
|
|
121
|
+
}
|
|
122
|
+
isValid() {
|
|
123
|
+
return super.isValid() && this.children.length >= 1 && this.children.length <= 2;
|
|
124
|
+
}
|
|
125
|
+
toShortString() {
|
|
126
|
+
let s = super.toShortString();
|
|
127
|
+
switch (this.children.length) {
|
|
128
|
+
case 1:
|
|
129
|
+
s += ` VAL="${this.val}"`;
|
|
130
|
+
break;
|
|
131
|
+
case 2:
|
|
132
|
+
s += ` KEY="${this.key}" VAL="${this.val}"`;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
return s;
|
|
136
|
+
}
|
|
137
|
+
toJSON() {
|
|
138
|
+
const json = { type: this.nodeType };
|
|
139
|
+
switch (this.children.length) {
|
|
140
|
+
case 1:
|
|
141
|
+
json.data = { key: this.key };
|
|
142
|
+
break;
|
|
143
|
+
case 2:
|
|
144
|
+
json.data = {
|
|
145
|
+
key: this.key,
|
|
146
|
+
val: this.val
|
|
147
|
+
};
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return json;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var StartTagNode = class extends AstNode {
|
|
154
|
+
nodeType = "StartTagNode";
|
|
155
|
+
tagName;
|
|
156
|
+
ogTag;
|
|
157
|
+
constructor(tagName, ogTag, attrNodes = []) {
|
|
158
|
+
super(attrNodes);
|
|
159
|
+
this.tagName = tagName.toLowerCase();
|
|
160
|
+
this.ogTag = ogTag;
|
|
161
|
+
}
|
|
162
|
+
isValid() {
|
|
163
|
+
for (const child of this.children) if (child.nodeType !== "AttrNode") return false;
|
|
164
|
+
return super.isValid();
|
|
165
|
+
}
|
|
166
|
+
toShortString() {
|
|
167
|
+
return `${super.toShortString()} ${this.ogTag}`;
|
|
168
|
+
}
|
|
169
|
+
toJSON() {
|
|
170
|
+
const json = super.toJSON();
|
|
171
|
+
json.data = { tag: this.tagName };
|
|
172
|
+
return json;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var EndTagNode = class extends AstNode {
|
|
176
|
+
nodeType = "EndTagNode";
|
|
177
|
+
tagName;
|
|
178
|
+
ogTag;
|
|
179
|
+
constructor(tagName, ogTag) {
|
|
180
|
+
super();
|
|
181
|
+
this.tagName = tagName;
|
|
182
|
+
this.ogTag = ogTag;
|
|
183
|
+
}
|
|
184
|
+
isValid() {
|
|
185
|
+
return super.isValid() && this.children.length === 0;
|
|
186
|
+
}
|
|
187
|
+
toShortString() {
|
|
188
|
+
return `${super.toShortString()} ${this.ogTag}`;
|
|
189
|
+
}
|
|
190
|
+
toJSON() {
|
|
191
|
+
const json = super.toJSON();
|
|
192
|
+
json.data = { tag: this.tagName };
|
|
193
|
+
return json;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
var TagNode = class extends AstNode {
|
|
197
|
+
nodeType = "TagNode";
|
|
198
|
+
_startTag;
|
|
199
|
+
_endTag;
|
|
200
|
+
constructor(startTag, endTag) {
|
|
201
|
+
super();
|
|
202
|
+
this._startTag = startTag;
|
|
203
|
+
this._endTag = endTag;
|
|
204
|
+
}
|
|
205
|
+
get tagName() {
|
|
206
|
+
return this._startTag.tagName;
|
|
207
|
+
}
|
|
208
|
+
get attributes() {
|
|
209
|
+
return this._startTag.children;
|
|
210
|
+
}
|
|
211
|
+
get ogStartTag() {
|
|
212
|
+
return this._startTag.ogTag;
|
|
213
|
+
}
|
|
214
|
+
get ogEndTag() {
|
|
215
|
+
if (!this._endTag) return "";
|
|
216
|
+
if (nodeIsType(this._endTag, "LinebreakNode")) return "\n";
|
|
217
|
+
else return this._endTag.ogTag;
|
|
218
|
+
}
|
|
219
|
+
isValid() {
|
|
220
|
+
if (this._endTag && nodeIsType(this._endTag, "EndTagNode") && this._startTag.tagName !== this._endTag.tagName) return false;
|
|
221
|
+
if (this.children.length === 1 && this.children[0].nodeType !== "RootNode") return false;
|
|
222
|
+
if (this.children.length > 2) return false;
|
|
223
|
+
return super.isValid() && this._startTag.isValid() && (this._endTag?.isValid() ?? true);
|
|
224
|
+
}
|
|
225
|
+
toString(depth = 0) {
|
|
226
|
+
let s = " ".repeat(depth * 2) + this.toShortString() + ` [${this.tagName}]`;
|
|
227
|
+
for (const attrNode of this._startTag.children) s += "\n" + attrNode.toString(depth + 1);
|
|
228
|
+
for (const child of this.children) s += "\n" + child.toString(depth + 1);
|
|
229
|
+
return s;
|
|
230
|
+
}
|
|
231
|
+
toJSON() {
|
|
232
|
+
const json = super.toJSON();
|
|
233
|
+
json.data = { startTag: this._startTag.toJSON() };
|
|
234
|
+
if (this._endTag) json.data.endTag = this._endTag.toJSON();
|
|
235
|
+
return json;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/generator/utils/getTagImmediateAttrVal.ts
|
|
240
|
+
/**
|
|
241
|
+
* Gets the text of the immediate attribute of the current TagNode
|
|
242
|
+
*
|
|
243
|
+
* [url=https://en.wikipedia.org]English Wikipedia[/url]
|
|
244
|
+
*
|
|
245
|
+
* TagNode [url]
|
|
246
|
+
* AttrNode VAL="https://en.wikipedia.org" (returns this string)
|
|
247
|
+
* TextNode "https://en.wikipedia.org"
|
|
248
|
+
* RootNode
|
|
249
|
+
* TextNode "English Wikipedia"
|
|
250
|
+
*/
|
|
308
251
|
function getTagImmediateAttrVal(tagNode) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
const attrNode = tagNode.attributes[0];
|
|
313
|
-
return attrNode.val;
|
|
252
|
+
if (tagNode.attributes.length !== 1) return;
|
|
253
|
+
return tagNode.attributes[0].val;
|
|
314
254
|
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/generator/utils/getTagImmediateText.ts
|
|
257
|
+
/**
|
|
258
|
+
* Gets the text of the immediate descendant of the current TagNode
|
|
259
|
+
*
|
|
260
|
+
* [url]https://en.wikipedia.org[/url]
|
|
261
|
+
*
|
|
262
|
+
* TagNode [url]
|
|
263
|
+
* RootNode
|
|
264
|
+
* TextNode "https://en.wikipedia.org" (returns this string)
|
|
265
|
+
*/
|
|
315
266
|
function getTagImmediateText(tagNode) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (child.children.length !== 1) {
|
|
324
|
-
return void 0;
|
|
325
|
-
}
|
|
326
|
-
const textNode = child.children[0];
|
|
327
|
-
if (!nodeIsType(textNode, AstNodeType.TextNode)) {
|
|
328
|
-
return void 0;
|
|
329
|
-
}
|
|
330
|
-
return textNode.str;
|
|
267
|
+
if (tagNode.children.length !== 1) return;
|
|
268
|
+
const child = tagNode.children[0];
|
|
269
|
+
if (!nodeIsType(child, "RootNode")) return;
|
|
270
|
+
if (child.children.length !== 1) return;
|
|
271
|
+
const textNode = child.children[0];
|
|
272
|
+
if (!nodeIsType(textNode, "TextNode")) return;
|
|
273
|
+
return textNode.str;
|
|
331
274
|
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/generator/utils/getWidthHeightAttr.ts
|
|
277
|
+
/**
|
|
278
|
+
* Gets the width/height attributes of the TagNode if they exist
|
|
279
|
+
*
|
|
280
|
+
* [img 500x300]https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png[/img]
|
|
281
|
+
*
|
|
282
|
+
* RootNode
|
|
283
|
+
* TagNode [img] (returns width:500, height:300)
|
|
284
|
+
* AttrNode VAL="500x300"
|
|
285
|
+
* TextNode " 500x300"
|
|
286
|
+
* RootNode
|
|
287
|
+
* TextNode "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png"
|
|
288
|
+
*
|
|
289
|
+
* [img width=500 height=300]https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png[/img]
|
|
290
|
+
*
|
|
291
|
+
* RootNode
|
|
292
|
+
* TagNode [img] (returns width:500, height:300)
|
|
293
|
+
* AttrNode KEY="width" VAL="500"
|
|
294
|
+
* TextNode " width"
|
|
295
|
+
* TextNode "500"
|
|
296
|
+
* AttrNode KEY="height" VAL="300"
|
|
297
|
+
* TextNode " height"
|
|
298
|
+
* TextNode "300"
|
|
299
|
+
* RootNode
|
|
300
|
+
* TextNode "https://upload.wikimedia.org/wikipedia/commons/7/70/Example.png
|
|
301
|
+
*/
|
|
332
302
|
function getWidthHeightAttr(tagNode) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
width,
|
|
350
|
-
height
|
|
351
|
-
};
|
|
303
|
+
let width;
|
|
304
|
+
let height;
|
|
305
|
+
for (const child of tagNode.attributes) {
|
|
306
|
+
if (child.key === "width") width = child.val;
|
|
307
|
+
if (child.key === "height") height = child.val;
|
|
308
|
+
const matches = /(\d+)x(\d+)/.exec(child.val);
|
|
309
|
+
if (matches) {
|
|
310
|
+
width = matches[1];
|
|
311
|
+
height = matches[2];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
width,
|
|
316
|
+
height
|
|
317
|
+
};
|
|
352
318
|
}
|
|
353
|
-
|
|
354
|
-
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/generator/utils/isDangerousUrl.ts
|
|
321
|
+
var dangerousUriRe = /^(vbscript|javascript|file|data):/;
|
|
322
|
+
var safeDataUriRe = /^data:image\/(gif|png|jpeg|webp);/;
|
|
355
323
|
function isDangerousUrl(url) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
if (safeDataUriRe.test(normalizedUrl)) {
|
|
361
|
-
return false;
|
|
362
|
-
}
|
|
363
|
-
return true;
|
|
324
|
+
const normalizedUrl = url.trim().toLowerCase();
|
|
325
|
+
if (!dangerousUriRe.test(normalizedUrl)) return false;
|
|
326
|
+
if (safeDataUriRe.test(normalizedUrl)) return false;
|
|
327
|
+
return true;
|
|
364
328
|
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/generator/utils/isOrderedList.ts
|
|
331
|
+
/**
|
|
332
|
+
* Determines if the StartTag has an attribute of "1" to indicate that it's an ordered list
|
|
333
|
+
*
|
|
334
|
+
* [list=1]
|
|
335
|
+
*
|
|
336
|
+
* TagNode [list]
|
|
337
|
+
* AttrNode VAL="1"
|
|
338
|
+
* TextNode "1"
|
|
339
|
+
* RootNode
|
|
340
|
+
* TagNode [*]
|
|
341
|
+
* RootNode
|
|
342
|
+
* TextNode "Entry 1"
|
|
343
|
+
* TagNode [*]
|
|
344
|
+
* RootNode
|
|
345
|
+
* TextNode "Entry 2"
|
|
346
|
+
*/
|
|
365
347
|
function isOrderedList(node) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (val === "1") {
|
|
369
|
-
return true;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return false;
|
|
348
|
+
for (const child of node.attributes) if (child.val === "1") return true;
|
|
349
|
+
return false;
|
|
373
350
|
}
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
{
|
|
548
|
-
name: "td",
|
|
549
|
-
start: () => {
|
|
550
|
-
return "<td>";
|
|
551
|
-
},
|
|
552
|
-
end: () => {
|
|
553
|
-
return "</td>";
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
name: "code",
|
|
558
|
-
start: () => {
|
|
559
|
-
return "<code>";
|
|
560
|
-
},
|
|
561
|
-
end: () => {
|
|
562
|
-
return "</code>";
|
|
563
|
-
}
|
|
564
|
-
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/generator/transforms/htmlTransforms.ts
|
|
353
|
+
var htmlTransforms = [
|
|
354
|
+
{
|
|
355
|
+
name: "b",
|
|
356
|
+
start: () => {
|
|
357
|
+
return "<strong>";
|
|
358
|
+
},
|
|
359
|
+
end: () => {
|
|
360
|
+
return "</strong>";
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "i",
|
|
365
|
+
start: () => {
|
|
366
|
+
return "<em>";
|
|
367
|
+
},
|
|
368
|
+
end: () => {
|
|
369
|
+
return "</em>";
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: "u",
|
|
374
|
+
start: () => {
|
|
375
|
+
return "<ins>";
|
|
376
|
+
},
|
|
377
|
+
end: () => {
|
|
378
|
+
return "</ins>";
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "s",
|
|
383
|
+
start: () => {
|
|
384
|
+
return "<del>";
|
|
385
|
+
},
|
|
386
|
+
end: () => {
|
|
387
|
+
return "</del>";
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "style",
|
|
392
|
+
start: (tagNode) => {
|
|
393
|
+
let style = "";
|
|
394
|
+
for (const child of tagNode.attributes) switch (child.key) {
|
|
395
|
+
case "color":
|
|
396
|
+
style += `color:${child.val};`;
|
|
397
|
+
continue;
|
|
398
|
+
case "size":
|
|
399
|
+
if (/^\d+$/.test(child.val)) style += `font-size:${child.val}%;`;
|
|
400
|
+
else style += `font-size:${child.val};`;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
return `<span style="${style}">`;
|
|
404
|
+
},
|
|
405
|
+
end: () => {
|
|
406
|
+
return "</span>";
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "color",
|
|
411
|
+
start: (tagNode) => {
|
|
412
|
+
return `<span style="color:${getTagImmediateAttrVal(tagNode)};">`;
|
|
413
|
+
},
|
|
414
|
+
end: () => {
|
|
415
|
+
return "</span>";
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "hr",
|
|
420
|
+
isStandalone: true,
|
|
421
|
+
start: () => {
|
|
422
|
+
return "<hr />";
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "br",
|
|
427
|
+
isStandalone: true,
|
|
428
|
+
start: () => {
|
|
429
|
+
return "<br />";
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: "list",
|
|
434
|
+
start: (tagNode) => {
|
|
435
|
+
return isOrderedList(tagNode) ? "<ol>" : "<ul>";
|
|
436
|
+
},
|
|
437
|
+
end: (tagNode) => {
|
|
438
|
+
return isOrderedList(tagNode) ? "</ol>" : "</ul>";
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "*",
|
|
443
|
+
isLinebreakTerminated: true,
|
|
444
|
+
start: () => {
|
|
445
|
+
return "<li>";
|
|
446
|
+
},
|
|
447
|
+
end: () => {
|
|
448
|
+
return "</li>";
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "img",
|
|
453
|
+
skipChildren: true,
|
|
454
|
+
start: (tagNode) => {
|
|
455
|
+
const src = getTagImmediateText(tagNode);
|
|
456
|
+
if (!src) return false;
|
|
457
|
+
if (isDangerousUrl(src)) return false;
|
|
458
|
+
const { width, height } = getWidthHeightAttr(tagNode);
|
|
459
|
+
let str = `<img src="${src}"`;
|
|
460
|
+
if (width) str += ` width="${width}"`;
|
|
461
|
+
if (height) str += ` height="${height}"`;
|
|
462
|
+
str += ">";
|
|
463
|
+
return str;
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: "url",
|
|
468
|
+
start: (tagNode) => {
|
|
469
|
+
const href = getTagImmediateAttrVal(tagNode) ?? getTagImmediateText(tagNode);
|
|
470
|
+
if (!href) return false;
|
|
471
|
+
if (isDangerousUrl(href)) return false;
|
|
472
|
+
return `<a href="${href}">`;
|
|
473
|
+
},
|
|
474
|
+
end: () => {
|
|
475
|
+
return "</a>";
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
name: "quote",
|
|
480
|
+
start: (tagNode) => {
|
|
481
|
+
const author = getTagImmediateAttrVal(tagNode);
|
|
482
|
+
return author ? `<blockquote><strong>${author}</strong>` : "<blockquote>";
|
|
483
|
+
},
|
|
484
|
+
end: () => {
|
|
485
|
+
return "</blockquote>";
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
name: "table",
|
|
490
|
+
start: () => {
|
|
491
|
+
return "<table>";
|
|
492
|
+
},
|
|
493
|
+
end: () => {
|
|
494
|
+
return "</table>";
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "tr",
|
|
499
|
+
start: () => {
|
|
500
|
+
return "<tr>";
|
|
501
|
+
},
|
|
502
|
+
end: () => {
|
|
503
|
+
return "</tr>";
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "td",
|
|
508
|
+
start: () => {
|
|
509
|
+
return "<td>";
|
|
510
|
+
},
|
|
511
|
+
end: () => {
|
|
512
|
+
return "</td>";
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "code",
|
|
517
|
+
start: () => {
|
|
518
|
+
return "<code>";
|
|
519
|
+
},
|
|
520
|
+
end: () => {
|
|
521
|
+
return "</code>";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
565
524
|
];
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
}
|
|
599
|
-
} else if (nodeIsType(node, AstNodeType.TextNode)) {
|
|
600
|
-
output += node.str;
|
|
601
|
-
} else if (nodeIsType(node, AstNodeType.LinebreakNode)) {
|
|
602
|
-
output += "\n";
|
|
603
|
-
} else {
|
|
604
|
-
for (const child of node.children) {
|
|
605
|
-
output += stringify(child);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return output;
|
|
609
|
-
};
|
|
610
|
-
return stringify(root);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
var TokenType = /* @__PURE__ */ ((TokenType2) => {
|
|
614
|
-
TokenType2[TokenType2["STR"] = 0] = "STR";
|
|
615
|
-
TokenType2[TokenType2["LINEBREAK"] = 1] = "LINEBREAK";
|
|
616
|
-
TokenType2[TokenType2["L_BRACKET"] = 2] = "L_BRACKET";
|
|
617
|
-
TokenType2[TokenType2["R_BRACKET"] = 3] = "R_BRACKET";
|
|
618
|
-
TokenType2[TokenType2["BACKSLASH"] = 4] = "BACKSLASH";
|
|
619
|
-
TokenType2[TokenType2["EQUALS"] = 5] = "EQUALS";
|
|
620
|
-
TokenType2[TokenType2["XSS_AMP"] = 6] = "XSS_AMP";
|
|
621
|
-
TokenType2[TokenType2["XSS_LT"] = 7] = "XSS_LT";
|
|
622
|
-
TokenType2[TokenType2["XSS_GT"] = 8] = "XSS_GT";
|
|
623
|
-
TokenType2[TokenType2["XSS_D_QUOTE"] = 9] = "XSS_D_QUOTE";
|
|
624
|
-
TokenType2[TokenType2["XSS_S_QUOTE"] = 10] = "XSS_S_QUOTE";
|
|
625
|
-
return TokenType2;
|
|
626
|
-
})(TokenType || {});
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/generator/Generator.ts
|
|
527
|
+
var Generator = class {
|
|
528
|
+
transforms;
|
|
529
|
+
constructor(transforms = htmlTransforms) {
|
|
530
|
+
this.transforms = new Map(transforms.map((transform) => [transform.name, transform]));
|
|
531
|
+
}
|
|
532
|
+
generate(root) {
|
|
533
|
+
const stringify = (node) => {
|
|
534
|
+
let output = "";
|
|
535
|
+
if (nodeIsType(node, "TagNode")) {
|
|
536
|
+
const tagName = node.tagName;
|
|
537
|
+
const transform = this.transforms.get(tagName);
|
|
538
|
+
if (!transform) throw new Error(`Unrecognized bbcode ${node.tagName}`);
|
|
539
|
+
const renderedStartTag = transform.start(node);
|
|
540
|
+
const renderedEndTag = transform.end?.(node) ?? "";
|
|
541
|
+
const isInvalidTag = renderedStartTag === false;
|
|
542
|
+
if (isInvalidTag) output += node.ogStartTag;
|
|
543
|
+
else output += renderedStartTag;
|
|
544
|
+
if (!transform.skipChildren || isInvalidTag) for (const child of node.children) output += stringify(child);
|
|
545
|
+
if (isInvalidTag) output += node.ogEndTag;
|
|
546
|
+
else output += renderedEndTag;
|
|
547
|
+
} else if (nodeIsType(node, "TextNode")) output += node.str;
|
|
548
|
+
else if (nodeIsType(node, "LinebreakNode")) output += "\n";
|
|
549
|
+
else for (const child of node.children) output += stringify(child);
|
|
550
|
+
return output;
|
|
551
|
+
};
|
|
552
|
+
return stringify(root);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/lexer/TokenType.ts
|
|
627
557
|
function tokenTypeToString(tokenType) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
case 6:
|
|
642
|
-
return "XSS_AMP";
|
|
643
|
-
case 7:
|
|
644
|
-
return "XSS_LT";
|
|
645
|
-
case 8:
|
|
646
|
-
return "XSS_GT";
|
|
647
|
-
case 9:
|
|
648
|
-
return "XSS_D_QUOTE";
|
|
649
|
-
case 10:
|
|
650
|
-
return "XSS_S_QUOTE";
|
|
651
|
-
}
|
|
558
|
+
switch (tokenType) {
|
|
559
|
+
case "STR": return "STR";
|
|
560
|
+
case "LINEBREAK": return "LINEBREAK";
|
|
561
|
+
case "L_BRACKET": return "L_BRACKET";
|
|
562
|
+
case "R_BRACKET": return "R_BRACKET";
|
|
563
|
+
case "BACKSLASH": return "BACKSLASH";
|
|
564
|
+
case "EQUALS": return "EQUALS";
|
|
565
|
+
case "XSS_AMP": return "XSS_AMP";
|
|
566
|
+
case "XSS_LT": return "XSS_LT";
|
|
567
|
+
case "XSS_GT": return "XSS_GT";
|
|
568
|
+
case "XSS_D_QUOTE": return "XSS_D_QUOTE";
|
|
569
|
+
case "XSS_S_QUOTE": return "XSS_S_QUOTE";
|
|
570
|
+
}
|
|
652
571
|
}
|
|
653
572
|
function isStringToken(tokenType) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
return false;
|
|
573
|
+
switch (tokenType) {
|
|
574
|
+
case "XSS_AMP":
|
|
575
|
+
case "XSS_LT":
|
|
576
|
+
case "XSS_GT":
|
|
577
|
+
case "XSS_D_QUOTE":
|
|
578
|
+
case "XSS_S_QUOTE":
|
|
579
|
+
case "STR": return true;
|
|
580
|
+
}
|
|
581
|
+
return false;
|
|
665
582
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
/* XSS_S_QUOTE */
|
|
583
|
+
var symbolTable = {
|
|
584
|
+
"\n": "LINEBREAK",
|
|
585
|
+
"[": "L_BRACKET",
|
|
586
|
+
"]": "R_BRACKET",
|
|
587
|
+
"/": "BACKSLASH",
|
|
588
|
+
"=": "EQUALS",
|
|
589
|
+
"&": "XSS_AMP",
|
|
590
|
+
"<": "XSS_LT",
|
|
591
|
+
">": "XSS_GT",
|
|
592
|
+
"\"": "XSS_D_QUOTE",
|
|
593
|
+
"'": "XSS_S_QUOTE"
|
|
678
594
|
};
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/lexer/Lexer.ts
|
|
597
|
+
var Lexer = class {
|
|
598
|
+
tokenize(input) {
|
|
599
|
+
const tokens = new Array();
|
|
600
|
+
const re = /\n|\[\/|\[(\w+|\*)|\]|=|&|<|>|'|"/g;
|
|
601
|
+
let offset = 0;
|
|
602
|
+
while (true) {
|
|
603
|
+
const match = re.exec(input);
|
|
604
|
+
if (!match) break;
|
|
605
|
+
const length = match.index - offset;
|
|
606
|
+
if (length > 0) tokens.push({
|
|
607
|
+
type: "STR",
|
|
608
|
+
offset,
|
|
609
|
+
length
|
|
610
|
+
});
|
|
611
|
+
offset = match.index;
|
|
612
|
+
if (match[0] === "[/") {
|
|
613
|
+
tokens.push({
|
|
614
|
+
type: "L_BRACKET",
|
|
615
|
+
offset,
|
|
616
|
+
length: 1
|
|
617
|
+
});
|
|
618
|
+
offset += 1;
|
|
619
|
+
tokens.push({
|
|
620
|
+
type: "BACKSLASH",
|
|
621
|
+
offset,
|
|
622
|
+
length: 1
|
|
623
|
+
});
|
|
624
|
+
offset += 1;
|
|
625
|
+
} else if (match[0].startsWith("[")) {
|
|
626
|
+
tokens.push({
|
|
627
|
+
type: "L_BRACKET",
|
|
628
|
+
offset,
|
|
629
|
+
length: 1
|
|
630
|
+
});
|
|
631
|
+
offset += 1;
|
|
632
|
+
const length = match[0].length - 1;
|
|
633
|
+
tokens.push({
|
|
634
|
+
type: "STR",
|
|
635
|
+
offset,
|
|
636
|
+
length
|
|
637
|
+
});
|
|
638
|
+
offset += length;
|
|
639
|
+
} else {
|
|
640
|
+
tokens.push({
|
|
641
|
+
type: symbolTable[match[0]] ?? "STR",
|
|
642
|
+
offset,
|
|
643
|
+
length: 1
|
|
644
|
+
});
|
|
645
|
+
offset += 1;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const length = input.length - offset;
|
|
649
|
+
if (length > 0) tokens.push({
|
|
650
|
+
type: "STR",
|
|
651
|
+
offset,
|
|
652
|
+
length
|
|
653
|
+
});
|
|
654
|
+
return tokens;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/lexer/Token.ts
|
|
745
659
|
function stringifyTokens(ogText, tokens) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
case TokenType.XSS_D_QUOTE: {
|
|
786
|
-
s += """;
|
|
787
|
-
break;
|
|
788
|
-
}
|
|
789
|
-
case TokenType.XSS_S_QUOTE: {
|
|
790
|
-
s += "'";
|
|
791
|
-
break;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return s;
|
|
796
|
-
}
|
|
797
|
-
class Parser {
|
|
798
|
-
constructor(transforms = htmlTransforms) {
|
|
799
|
-
__publicField(this, "tags");
|
|
800
|
-
__publicField(this, "linebreakTerminatedTags");
|
|
801
|
-
__publicField(this, "standaloneTags");
|
|
802
|
-
this.tags = new Set(transforms.map((transform) => transform.name));
|
|
803
|
-
this.linebreakTerminatedTags = new Set(transforms.filter((transform) => transform.isLinebreakTerminated).map((transform) => transform.name.toLowerCase()));
|
|
804
|
-
this.standaloneTags = new Set(transforms.filter((transform) => transform.isStandalone).map((transform) => transform.name.toLowerCase()));
|
|
805
|
-
}
|
|
806
|
-
parse(ogText, tokens) {
|
|
807
|
-
let idx = 0;
|
|
808
|
-
const parseLabel = () => {
|
|
809
|
-
const slice = tokens.slice(idx, idx + 1);
|
|
810
|
-
const label = stringifyTokens(ogText, slice);
|
|
811
|
-
idx += 1;
|
|
812
|
-
return label.toLowerCase();
|
|
813
|
-
};
|
|
814
|
-
const parseText = (endOnQuotes = false, endOnSpace = false) => {
|
|
815
|
-
const startIdx = idx;
|
|
816
|
-
while (idx < tokens.length) {
|
|
817
|
-
if (!isStringToken(tokens[idx].type)) {
|
|
818
|
-
break;
|
|
819
|
-
}
|
|
820
|
-
if (endOnQuotes && (tokens[idx].type === TokenType.XSS_S_QUOTE || tokens[idx].type === TokenType.XSS_D_QUOTE)) {
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
if (endOnSpace && !endOnQuotes) {
|
|
824
|
-
const origStr = stringifyTokens(ogText, [tokens[idx]]);
|
|
825
|
-
const spaceIdx = origStr.indexOf(" ");
|
|
826
|
-
if (spaceIdx >= 0) {
|
|
827
|
-
const oldToken = {
|
|
828
|
-
type: TokenType.STR,
|
|
829
|
-
offset: tokens[idx].offset,
|
|
830
|
-
length: spaceIdx
|
|
831
|
-
};
|
|
832
|
-
const newToken = {
|
|
833
|
-
type: TokenType.STR,
|
|
834
|
-
offset: tokens[idx].offset + spaceIdx,
|
|
835
|
-
length: tokens[idx].length - spaceIdx
|
|
836
|
-
};
|
|
837
|
-
tokens.splice(idx + 0, 1, oldToken);
|
|
838
|
-
tokens.splice(idx + 1, 0, newToken);
|
|
839
|
-
idx += 1;
|
|
840
|
-
break;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
idx += 1;
|
|
844
|
-
}
|
|
845
|
-
const slice = tokens.slice(startIdx, idx);
|
|
846
|
-
const str = stringifyTokens(ogText, slice);
|
|
847
|
-
return new TextNode(str);
|
|
848
|
-
};
|
|
849
|
-
const parseAttr = () => {
|
|
850
|
-
if (idx + 1 >= tokens.length) {
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
const attrNode = new AttrNode();
|
|
854
|
-
if (tokens[idx].type === TokenType.EQUALS && isStringToken(tokens[idx + 1].type)) {
|
|
855
|
-
idx += 1;
|
|
856
|
-
const openedWithQuotes = tokens[idx].type === TokenType.XSS_S_QUOTE || tokens[idx].type === TokenType.XSS_D_QUOTE;
|
|
857
|
-
if (openedWithQuotes) {
|
|
858
|
-
idx += 1;
|
|
859
|
-
}
|
|
860
|
-
const valNode = parseText(openedWithQuotes, true);
|
|
861
|
-
attrNode.addChild(valNode);
|
|
862
|
-
if (openedWithQuotes) {
|
|
863
|
-
if (tokens[idx].type !== TokenType.XSS_S_QUOTE && tokens[idx].type !== TokenType.XSS_D_QUOTE) {
|
|
864
|
-
return null;
|
|
865
|
-
}
|
|
866
|
-
idx += 1;
|
|
867
|
-
}
|
|
868
|
-
} else if (isStringToken(tokens[idx].type) && tokens[idx + 1].type === TokenType.EQUALS && (idx + 2 < tokens.length && isStringToken(tokens[idx + 2].type))) {
|
|
869
|
-
const keyNode = parseText();
|
|
870
|
-
attrNode.addChild(keyNode);
|
|
871
|
-
idx += 1;
|
|
872
|
-
const openedWithQuotes = tokens[idx].type === TokenType.XSS_S_QUOTE || tokens[idx].type === TokenType.XSS_D_QUOTE;
|
|
873
|
-
if (openedWithQuotes) {
|
|
874
|
-
idx += 1;
|
|
875
|
-
}
|
|
876
|
-
const valNode = parseText(openedWithQuotes, true);
|
|
877
|
-
if (openedWithQuotes) {
|
|
878
|
-
if (tokens[idx].type !== TokenType.XSS_S_QUOTE && tokens[idx].type !== TokenType.XSS_D_QUOTE) {
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
idx += 1;
|
|
882
|
-
}
|
|
883
|
-
attrNode.addChild(valNode);
|
|
884
|
-
} else if (isStringToken(tokens[idx].type) && tokens[idx + 1].type !== TokenType.EQUALS) {
|
|
885
|
-
const valNode = parseText();
|
|
886
|
-
attrNode.addChild(valNode);
|
|
887
|
-
} else {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
return attrNode;
|
|
891
|
-
};
|
|
892
|
-
const parseTag = () => {
|
|
893
|
-
if (idx + 1 >= tokens.length) {
|
|
894
|
-
return null;
|
|
895
|
-
}
|
|
896
|
-
if (tokens[idx].type !== TokenType.L_BRACKET) {
|
|
897
|
-
return null;
|
|
898
|
-
}
|
|
899
|
-
if (isStringToken(tokens[idx + 1].type)) {
|
|
900
|
-
const startIdx = idx;
|
|
901
|
-
idx += 1;
|
|
902
|
-
const labelText = parseLabel();
|
|
903
|
-
if (!this.tags.has(labelText)) {
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
const attrNodes = new Array();
|
|
907
|
-
while (true) {
|
|
908
|
-
const attrNode = parseAttr();
|
|
909
|
-
if (attrNode === null) {
|
|
910
|
-
break;
|
|
911
|
-
}
|
|
912
|
-
attrNodes.push(attrNode);
|
|
913
|
-
}
|
|
914
|
-
if (tokens[idx].type !== TokenType.R_BRACKET) {
|
|
915
|
-
return null;
|
|
916
|
-
}
|
|
917
|
-
idx += 1;
|
|
918
|
-
const slice = tokens.slice(startIdx, idx);
|
|
919
|
-
const ogTag = stringifyTokens(ogText, slice);
|
|
920
|
-
const startTagNode = new StartTagNode(labelText, ogTag, attrNodes);
|
|
921
|
-
return startTagNode;
|
|
922
|
-
}
|
|
923
|
-
if (tokens[idx + 1].type === TokenType.BACKSLASH) {
|
|
924
|
-
const startIdx = idx;
|
|
925
|
-
idx += 1;
|
|
926
|
-
idx += 1;
|
|
927
|
-
const labelText = parseLabel();
|
|
928
|
-
if (!this.tags.has(labelText)) {
|
|
929
|
-
return null;
|
|
930
|
-
}
|
|
931
|
-
if (tokens[idx].type !== TokenType.R_BRACKET) {
|
|
932
|
-
return null;
|
|
933
|
-
}
|
|
934
|
-
idx += 1;
|
|
935
|
-
const slice = tokens.slice(startIdx, idx);
|
|
936
|
-
const ogTag = stringifyTokens(ogText, slice);
|
|
937
|
-
const endTagNode = new EndTagNode(labelText, ogTag);
|
|
938
|
-
return endTagNode;
|
|
939
|
-
}
|
|
940
|
-
return null;
|
|
941
|
-
};
|
|
942
|
-
const parseRoot = () => {
|
|
943
|
-
const root2 = new RootNode();
|
|
944
|
-
while (idx < tokens.length) {
|
|
945
|
-
if (tokens[idx].type === TokenType.L_BRACKET) {
|
|
946
|
-
const startIdx = idx;
|
|
947
|
-
const tagNode = parseTag();
|
|
948
|
-
if (tagNode !== null) {
|
|
949
|
-
root2.addChild(tagNode);
|
|
950
|
-
} else {
|
|
951
|
-
const invalidTokens = tokens.slice(startIdx, idx);
|
|
952
|
-
const str = stringifyTokens(ogText, invalidTokens);
|
|
953
|
-
const textNode = new TextNode(str);
|
|
954
|
-
root2.addChild(textNode);
|
|
955
|
-
}
|
|
956
|
-
} else if (tokens[idx].type === TokenType.LINEBREAK) {
|
|
957
|
-
idx += 1;
|
|
958
|
-
root2.addChild(new LinebreakNode());
|
|
959
|
-
} else {
|
|
960
|
-
const startIdx = idx;
|
|
961
|
-
while (idx < tokens.length && tokens[idx].type !== TokenType.L_BRACKET && tokens[idx].type !== TokenType.LINEBREAK) {
|
|
962
|
-
idx += 1;
|
|
963
|
-
}
|
|
964
|
-
const slice = tokens.slice(startIdx, idx);
|
|
965
|
-
const str = stringifyTokens(ogText, slice);
|
|
966
|
-
root2.addChild(new TextNode(str));
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return root2;
|
|
970
|
-
};
|
|
971
|
-
let root = parseRoot();
|
|
972
|
-
root = this.matchTagNodes(root);
|
|
973
|
-
return root;
|
|
974
|
-
}
|
|
975
|
-
// ------------------------------------------------------------------------
|
|
976
|
-
// Post Parsing Transforms
|
|
977
|
-
// ------------------------------------------------------------------------
|
|
978
|
-
matchTagNodes(rootNode) {
|
|
979
|
-
const transformedRoot = new RootNode();
|
|
980
|
-
for (let i = 0; i < rootNode.children.length; i++) {
|
|
981
|
-
const child = rootNode.children[i];
|
|
982
|
-
if (nodeIsType(child, AstNodeType.StartTagNode)) {
|
|
983
|
-
const endTag = this.findMatchingEndTag(rootNode.children, i, child.tagName);
|
|
984
|
-
const isStandalone = this.standaloneTags.has(child.tagName);
|
|
985
|
-
if (endTag || isStandalone) {
|
|
986
|
-
const tagNode = new TagNode(child, endTag == null ? void 0 : endTag.node);
|
|
987
|
-
transformedRoot.addChild(tagNode);
|
|
988
|
-
if (endTag) {
|
|
989
|
-
const subRoot = new RootNode(rootNode.children.slice(i + 1, endTag.idx));
|
|
990
|
-
i = endTag.idx;
|
|
991
|
-
const transformedSubRoot = this.matchTagNodes(subRoot);
|
|
992
|
-
tagNode.addChild(transformedSubRoot);
|
|
993
|
-
}
|
|
994
|
-
} else {
|
|
995
|
-
transformedRoot.addChild(new TextNode(child.ogTag));
|
|
996
|
-
}
|
|
997
|
-
} else if (nodeIsType(child, AstNodeType.EndTagNode)) {
|
|
998
|
-
transformedRoot.addChild(new TextNode(child.ogTag));
|
|
999
|
-
} else if (nodeIsType(child, AstNodeType.TextNode)) {
|
|
1000
|
-
transformedRoot.addChild(child);
|
|
1001
|
-
} else if (nodeIsType(child, AstNodeType.LinebreakNode)) {
|
|
1002
|
-
transformedRoot.addChild(child);
|
|
1003
|
-
} else {
|
|
1004
|
-
throw new Error("Unexpected child of RootNode");
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
return transformedRoot;
|
|
1008
|
-
}
|
|
1009
|
-
findMatchingEndTag(siblings, startIdx, tagName) {
|
|
1010
|
-
if (this.standaloneTags.has(tagName)) {
|
|
1011
|
-
return null;
|
|
1012
|
-
}
|
|
1013
|
-
for (let i = startIdx; i < siblings.length; i++) {
|
|
1014
|
-
const sibling = siblings[i];
|
|
1015
|
-
const isEndTag = nodeIsType(sibling, AstNodeType.LinebreakNode) && this.linebreakTerminatedTags.has(tagName) || nodeIsType(sibling, AstNodeType.EndTagNode) && sibling.tagName === tagName;
|
|
1016
|
-
if (isEndTag) {
|
|
1017
|
-
return {
|
|
1018
|
-
idx: i,
|
|
1019
|
-
node: sibling
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return null;
|
|
1024
|
-
}
|
|
660
|
+
let s = "";
|
|
661
|
+
for (const token of tokens) switch (token.type) {
|
|
662
|
+
case "STR":
|
|
663
|
+
s += ogText.substring(token.offset, token.offset + token.length);
|
|
664
|
+
break;
|
|
665
|
+
case "LINEBREAK":
|
|
666
|
+
s += "\n";
|
|
667
|
+
break;
|
|
668
|
+
case "L_BRACKET":
|
|
669
|
+
s += "[";
|
|
670
|
+
break;
|
|
671
|
+
case "R_BRACKET":
|
|
672
|
+
s += "]";
|
|
673
|
+
break;
|
|
674
|
+
case "BACKSLASH":
|
|
675
|
+
s += "/";
|
|
676
|
+
break;
|
|
677
|
+
case "EQUALS":
|
|
678
|
+
s += "=";
|
|
679
|
+
break;
|
|
680
|
+
case "XSS_AMP":
|
|
681
|
+
s += "&";
|
|
682
|
+
break;
|
|
683
|
+
case "XSS_LT":
|
|
684
|
+
s += "<";
|
|
685
|
+
break;
|
|
686
|
+
case "XSS_GT":
|
|
687
|
+
s += ">";
|
|
688
|
+
break;
|
|
689
|
+
case "XSS_D_QUOTE":
|
|
690
|
+
s += """;
|
|
691
|
+
break;
|
|
692
|
+
case "XSS_S_QUOTE":
|
|
693
|
+
s += "'";
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
return s;
|
|
1025
697
|
}
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/parser/Parser.ts
|
|
700
|
+
var Parser = class {
|
|
701
|
+
tags;
|
|
702
|
+
linebreakTerminatedTags;
|
|
703
|
+
standaloneTags;
|
|
704
|
+
constructor(transforms = htmlTransforms) {
|
|
705
|
+
this.tags = new Set(transforms.map((transform) => transform.name));
|
|
706
|
+
this.linebreakTerminatedTags = new Set(transforms.filter((transform) => transform.isLinebreakTerminated).map((transform) => transform.name.toLowerCase()));
|
|
707
|
+
this.standaloneTags = new Set(transforms.filter((transform) => transform.isStandalone).map((transform) => transform.name.toLowerCase()));
|
|
708
|
+
}
|
|
709
|
+
parse(ogText, tokens) {
|
|
710
|
+
let idx = 0;
|
|
711
|
+
const parseLabel = () => {
|
|
712
|
+
const label = stringifyTokens(ogText, tokens.slice(idx, idx + 1));
|
|
713
|
+
idx += 1;
|
|
714
|
+
return label.toLowerCase();
|
|
715
|
+
};
|
|
716
|
+
const parseText = (endOnQuotes = false, endOnSpace = false) => {
|
|
717
|
+
const startIdx = idx;
|
|
718
|
+
while (idx < tokens.length) {
|
|
719
|
+
if (!isStringToken(tokens[idx].type)) break;
|
|
720
|
+
if (endOnQuotes && (tokens[idx].type === "XSS_S_QUOTE" || tokens[idx].type === "XSS_D_QUOTE")) break;
|
|
721
|
+
/**
|
|
722
|
+
* SPECIAL CASE:
|
|
723
|
+
* If we encounter a space, then we must split the current token into 2 tokens and only consume the first part
|
|
724
|
+
*
|
|
725
|
+
* a b -> a b
|
|
726
|
+
* | | |
|
|
727
|
+
* | | idx (new)
|
|
728
|
+
* | |
|
|
729
|
+
* idx consumed
|
|
730
|
+
*
|
|
731
|
+
* Note: We only handle endOnSpace special case when we don't expect the current text to endOnQuotes
|
|
732
|
+
* If it endOnQuotes, then it implies that it opened with quotes (and thus we need an enclosing/matching quote)
|
|
733
|
+
*/
|
|
734
|
+
if (endOnSpace && !endOnQuotes) {
|
|
735
|
+
const spaceIdx = stringifyTokens(ogText, [tokens[idx]]).indexOf(" ");
|
|
736
|
+
if (spaceIdx >= 0) {
|
|
737
|
+
const oldToken = {
|
|
738
|
+
type: "STR",
|
|
739
|
+
offset: tokens[idx].offset,
|
|
740
|
+
length: spaceIdx
|
|
741
|
+
};
|
|
742
|
+
const newToken = {
|
|
743
|
+
type: "STR",
|
|
744
|
+
offset: tokens[idx].offset + spaceIdx,
|
|
745
|
+
length: tokens[idx].length - spaceIdx
|
|
746
|
+
};
|
|
747
|
+
tokens.splice(idx + 0, 1, oldToken);
|
|
748
|
+
tokens.splice(idx + 1, 0, newToken);
|
|
749
|
+
idx += 1;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
idx += 1;
|
|
754
|
+
}
|
|
755
|
+
return new TextNode(stringifyTokens(ogText, tokens.slice(startIdx, idx)));
|
|
756
|
+
};
|
|
757
|
+
const parseAttr = () => {
|
|
758
|
+
if (idx + 1 >= tokens.length) return null;
|
|
759
|
+
const attrNode = new AttrNode();
|
|
760
|
+
if (tokens[idx].type === "EQUALS" && isStringToken(tokens[idx + 1].type)) {
|
|
761
|
+
idx += 1;
|
|
762
|
+
const openedWithQuotes = tokens[idx].type === "XSS_S_QUOTE" || tokens[idx].type === "XSS_D_QUOTE";
|
|
763
|
+
if (openedWithQuotes) idx += 1;
|
|
764
|
+
const valNode = parseText(openedWithQuotes, true);
|
|
765
|
+
attrNode.addChild(valNode);
|
|
766
|
+
if (openedWithQuotes) {
|
|
767
|
+
if (tokens[idx].type !== "XSS_S_QUOTE" && tokens[idx].type !== "XSS_D_QUOTE") return null;
|
|
768
|
+
idx += 1;
|
|
769
|
+
}
|
|
770
|
+
} else if (isStringToken(tokens[idx].type) && tokens[idx + 1].type === "EQUALS" && idx + 2 < tokens.length && isStringToken(tokens[idx + 2].type)) {
|
|
771
|
+
const keyNode = parseText();
|
|
772
|
+
attrNode.addChild(keyNode);
|
|
773
|
+
idx += 1;
|
|
774
|
+
const openedWithQuotes = tokens[idx].type === "XSS_S_QUOTE" || tokens[idx].type === "XSS_D_QUOTE";
|
|
775
|
+
if (openedWithQuotes) idx += 1;
|
|
776
|
+
const valNode = parseText(openedWithQuotes, true);
|
|
777
|
+
if (openedWithQuotes) {
|
|
778
|
+
if (tokens[idx].type !== "XSS_S_QUOTE" && tokens[idx].type !== "XSS_D_QUOTE") return null;
|
|
779
|
+
idx += 1;
|
|
780
|
+
}
|
|
781
|
+
attrNode.addChild(valNode);
|
|
782
|
+
} else if (isStringToken(tokens[idx].type) && tokens[idx + 1].type !== "EQUALS") {
|
|
783
|
+
const valNode = parseText();
|
|
784
|
+
attrNode.addChild(valNode);
|
|
785
|
+
} else return null;
|
|
786
|
+
return attrNode;
|
|
787
|
+
};
|
|
788
|
+
const parseTag = () => {
|
|
789
|
+
if (idx + 1 >= tokens.length) return null;
|
|
790
|
+
if (tokens[idx].type !== "L_BRACKET") return null;
|
|
791
|
+
if (isStringToken(tokens[idx + 1].type)) {
|
|
792
|
+
const startIdx = idx;
|
|
793
|
+
idx += 1;
|
|
794
|
+
const labelText = parseLabel();
|
|
795
|
+
if (!this.tags.has(labelText)) return null;
|
|
796
|
+
const attrNodes = new Array();
|
|
797
|
+
while (true) {
|
|
798
|
+
const attrNode = parseAttr();
|
|
799
|
+
if (attrNode === null) break;
|
|
800
|
+
attrNodes.push(attrNode);
|
|
801
|
+
}
|
|
802
|
+
if (tokens[idx].type !== "R_BRACKET") return null;
|
|
803
|
+
idx += 1;
|
|
804
|
+
return new StartTagNode(labelText, stringifyTokens(ogText, tokens.slice(startIdx, idx)), attrNodes);
|
|
805
|
+
}
|
|
806
|
+
if (tokens[idx + 1].type === "BACKSLASH") {
|
|
807
|
+
const startIdx = idx;
|
|
808
|
+
idx += 1;
|
|
809
|
+
idx += 1;
|
|
810
|
+
const labelText = parseLabel();
|
|
811
|
+
if (!this.tags.has(labelText)) return null;
|
|
812
|
+
if (tokens[idx].type !== "R_BRACKET") return null;
|
|
813
|
+
idx += 1;
|
|
814
|
+
return new EndTagNode(labelText, stringifyTokens(ogText, tokens.slice(startIdx, idx)));
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
};
|
|
818
|
+
const parseRoot = () => {
|
|
819
|
+
const root = new RootNode();
|
|
820
|
+
while (idx < tokens.length) if (tokens[idx].type === "L_BRACKET") {
|
|
821
|
+
const startIdx = idx;
|
|
822
|
+
const tagNode = parseTag();
|
|
823
|
+
if (tagNode !== null) root.addChild(tagNode);
|
|
824
|
+
else {
|
|
825
|
+
const textNode = new TextNode(stringifyTokens(ogText, tokens.slice(startIdx, idx)));
|
|
826
|
+
root.addChild(textNode);
|
|
827
|
+
}
|
|
828
|
+
} else if (tokens[idx].type === "LINEBREAK") {
|
|
829
|
+
idx += 1;
|
|
830
|
+
root.addChild(new LinebreakNode());
|
|
831
|
+
} else {
|
|
832
|
+
const startIdx = idx;
|
|
833
|
+
while (idx < tokens.length && tokens[idx].type !== "L_BRACKET" && tokens[idx].type !== "LINEBREAK") idx += 1;
|
|
834
|
+
const str = stringifyTokens(ogText, tokens.slice(startIdx, idx));
|
|
835
|
+
root.addChild(new TextNode(str));
|
|
836
|
+
}
|
|
837
|
+
return root;
|
|
838
|
+
};
|
|
839
|
+
let root = parseRoot();
|
|
840
|
+
root = this.matchTagNodes(root);
|
|
841
|
+
return root;
|
|
842
|
+
}
|
|
843
|
+
matchTagNodes(rootNode) {
|
|
844
|
+
const transformedRoot = new RootNode();
|
|
845
|
+
for (let i = 0; i < rootNode.children.length; i++) {
|
|
846
|
+
const child = rootNode.children[i];
|
|
847
|
+
if (nodeIsType(child, "StartTagNode")) {
|
|
848
|
+
const endTag = this.findMatchingEndTag(rootNode.children, i, child.tagName);
|
|
849
|
+
const isStandalone = this.standaloneTags.has(child.tagName);
|
|
850
|
+
if (endTag || isStandalone) {
|
|
851
|
+
const tagNode = new TagNode(child, endTag?.node);
|
|
852
|
+
transformedRoot.addChild(tagNode);
|
|
853
|
+
if (endTag) {
|
|
854
|
+
const subRoot = new RootNode(rootNode.children.slice(i + 1, endTag.idx));
|
|
855
|
+
i = endTag.idx;
|
|
856
|
+
const transformedSubRoot = this.matchTagNodes(subRoot);
|
|
857
|
+
tagNode.addChild(transformedSubRoot);
|
|
858
|
+
}
|
|
859
|
+
} else transformedRoot.addChild(new TextNode(child.ogTag));
|
|
860
|
+
} else if (nodeIsType(child, "EndTagNode")) transformedRoot.addChild(new TextNode(child.ogTag));
|
|
861
|
+
else if (nodeIsType(child, "TextNode")) transformedRoot.addChild(child);
|
|
862
|
+
else if (nodeIsType(child, "LinebreakNode")) transformedRoot.addChild(child);
|
|
863
|
+
else throw new Error("Unexpected child of RootNode");
|
|
864
|
+
}
|
|
865
|
+
return transformedRoot;
|
|
866
|
+
}
|
|
867
|
+
findMatchingEndTag(siblings, startIdx, tagName) {
|
|
868
|
+
if (this.standaloneTags.has(tagName)) return null;
|
|
869
|
+
for (let i = startIdx; i < siblings.length; i++) {
|
|
870
|
+
const sibling = siblings[i];
|
|
871
|
+
if (nodeIsType(sibling, "LinebreakNode") && this.linebreakTerminatedTags.has(tagName) || nodeIsType(sibling, "EndTagNode") && sibling.tagName === tagName) return {
|
|
872
|
+
idx: i,
|
|
873
|
+
node: sibling
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/generateHtml.ts
|
|
1026
881
|
function generateHtml(input, transforms = htmlTransforms) {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const root = parser.parse(input, tokens);
|
|
1031
|
-
const generator = new Generator(transforms);
|
|
1032
|
-
return generator.generate(root);
|
|
882
|
+
const tokens = new Lexer().tokenize(input);
|
|
883
|
+
const root = new Parser(transforms).parse(input, tokens);
|
|
884
|
+
return new Generator(transforms).generate(root);
|
|
1033
885
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
EndTagNode,
|
|
1039
|
-
Generator,
|
|
1040
|
-
Lexer,
|
|
1041
|
-
LinebreakNode,
|
|
1042
|
-
Parser,
|
|
1043
|
-
RootNode,
|
|
1044
|
-
StartTagNode,
|
|
1045
|
-
TagNode,
|
|
1046
|
-
TextNode,
|
|
1047
|
-
TokenType,
|
|
1048
|
-
generateHtml,
|
|
1049
|
-
getTagImmediateAttrVal,
|
|
1050
|
-
getTagImmediateText,
|
|
1051
|
-
getWidthHeightAttr,
|
|
1052
|
-
htmlTransforms,
|
|
1053
|
-
isDangerousUrl,
|
|
1054
|
-
isOrderedList,
|
|
1055
|
-
isStringToken,
|
|
1056
|
-
nodeIsType,
|
|
1057
|
-
stringifyTokens,
|
|
1058
|
-
symbolTable,
|
|
1059
|
-
tokenTypeToString
|
|
1060
|
-
};
|
|
1061
|
-
//# sourceMappingURL=index.js.map
|
|
886
|
+
//#endregion
|
|
887
|
+
export { AstNode, AttrNode, EndTagNode, Generator, Lexer, LinebreakNode, Parser, RootNode, StartTagNode, TagNode, TextNode, generateHtml, getTagImmediateAttrVal, getTagImmediateText, getWidthHeightAttr, htmlTransforms, isDangerousUrl, isOrderedList, isStringToken, nodeIsType, stringifyTokens, symbolTable, tokenTypeToString };
|
|
888
|
+
|
|
889
|
+
//# sourceMappingURL=index.js.map
|