bbcode-compiler 0.1.0
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/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/generateHtml.d.ts +2 -0
- package/dist/generateHtml.d.ts.map +1 -0
- package/dist/generateHtml.js +13 -0
- package/dist/generateHtml.js.map +1 -0
- package/dist/generator/Generator.d.ts +8 -0
- package/dist/generator/Generator.d.ts.map +1 -0
- package/dist/generator/Generator.js +54 -0
- package/dist/generator/Generator.js.map +1 -0
- package/dist/generator/transforms/Transform.d.ts +10 -0
- package/dist/generator/transforms/Transform.d.ts.map +1 -0
- package/dist/generator/transforms/Transform.js +2 -0
- package/dist/generator/transforms/Transform.js.map +1 -0
- package/dist/generator/transforms/htmlTransforms.d.ts +3 -0
- package/dist/generator/transforms/htmlTransforms.d.ts.map +1 -0
- package/dist/generator/transforms/htmlTransforms.js +198 -0
- package/dist/generator/transforms/htmlTransforms.js.map +1 -0
- package/dist/generator/utils/getTagImmediateAttrVal.d.ts +14 -0
- package/dist/generator/utils/getTagImmediateAttrVal.d.ts.map +1 -0
- package/dist/generator/utils/getTagImmediateAttrVal.js +19 -0
- package/dist/generator/utils/getTagImmediateAttrVal.js.map +1 -0
- package/dist/generator/utils/getTagImmediateText.d.ts +12 -0
- package/dist/generator/utils/getTagImmediateText.d.ts.map +1 -0
- package/dist/generator/utils/getTagImmediateText.js +29 -0
- package/dist/generator/utils/getTagImmediateText.js.map +1 -0
- package/dist/generator/utils/getWidthHeightAttr.d.ts +31 -0
- package/dist/generator/utils/getWidthHeightAttr.d.ts.map +1 -0
- package/dist/generator/utils/getWidthHeightAttr.js +47 -0
- package/dist/generator/utils/getWidthHeightAttr.js.map +1 -0
- package/dist/generator/utils/isDangerousUrl.d.ts +2 -0
- package/dist/generator/utils/isDangerousUrl.d.ts.map +1 -0
- package/dist/generator/utils/isDangerousUrl.js +14 -0
- package/dist/generator/utils/isDangerousUrl.js.map +1 -0
- package/dist/generator/utils/isOrderedList.d.ts +19 -0
- package/dist/generator/utils/isOrderedList.d.ts.map +1 -0
- package/dist/generator/utils/isOrderedList.js +26 -0
- package/dist/generator/utils/isOrderedList.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/Lexer.d.ts +5 -0
- package/dist/lexer/Lexer.d.ts.map +1 -0
- package/dist/lexer/Lexer.js +81 -0
- package/dist/lexer/Lexer.js.map +1 -0
- package/dist/lexer/Token.d.ts +8 -0
- package/dist/lexer/Token.d.ts.map +1 -0
- package/dist/lexer/Token.js +54 -0
- package/dist/lexer/Token.js.map +1 -0
- package/dist/lexer/TokenType.d.ts +17 -0
- package/dist/lexer/TokenType.d.ts.map +1 -0
- package/dist/lexer/TokenType.js +41 -0
- package/dist/lexer/TokenType.js.map +1 -0
- package/dist/parser/AstNode.d.ts +105 -0
- package/dist/parser/AstNode.d.ts.map +1 -0
- package/dist/parser/AstNode.js +263 -0
- package/dist/parser/AstNode.js.map +1 -0
- package/dist/parser/Parser.d.ts +11 -0
- package/dist/parser/Parser.d.ts.map +1 -0
- package/dist/parser/Parser.js +265 -0
- package/dist/parser/Parser.js.map +1 -0
- package/dist/parser/nodeIsType.d.ts +13 -0
- package/dist/parser/nodeIsType.d.ts.map +1 -0
- package/dist/parser/nodeIsType.js +5 -0
- package/dist/parser/nodeIsType.js.map +1 -0
- package/package.json +68 -0
- package/src/generateHtml.ts +15 -0
- package/src/generator/Generator.ts +60 -0
- package/src/generator/transforms/Transform.ts +15 -0
- package/src/generator/transforms/htmlTransforms.ts +205 -0
- package/src/generator/utils/getTagImmediateAttrVal.ts +21 -0
- package/src/generator/utils/getTagImmediateText.ts +33 -0
- package/src/generator/utils/getWidthHeightAttr.ts +51 -0
- package/src/generator/utils/isDangerousUrl.ts +17 -0
- package/src/generator/utils/isOrderedList.ts +28 -0
- package/src/index.ts +18 -0
- package/src/lexer/Lexer.ts +89 -0
- package/src/lexer/Token.ts +64 -0
- package/src/lexer/TokenType.ts +65 -0
- package/src/parser/AstNode.ts +338 -0
- package/src/parser/Parser.ts +316 -0
- package/src/parser/nodeIsType.ts +15 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const enum TokenType {
|
|
2
|
+
STR,
|
|
3
|
+
LINEBREAK,
|
|
4
|
+
|
|
5
|
+
// BBCode symbols
|
|
6
|
+
L_BRACKET,
|
|
7
|
+
R_BRACKET,
|
|
8
|
+
BACKSLASH,
|
|
9
|
+
EQUALS,
|
|
10
|
+
|
|
11
|
+
// XSS symbols
|
|
12
|
+
XSS_AMP,
|
|
13
|
+
XSS_LT,
|
|
14
|
+
XSS_GT,
|
|
15
|
+
XSS_D_QUOTE,
|
|
16
|
+
XSS_S_QUOTE,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function tokenTypeToString(tokenType: TokenType): string {
|
|
20
|
+
switch (tokenType) {
|
|
21
|
+
case TokenType.STR: return 'STR'
|
|
22
|
+
case TokenType.LINEBREAK: return 'LINEBREAK'
|
|
23
|
+
|
|
24
|
+
case TokenType.L_BRACKET: return 'L_BRACKET'
|
|
25
|
+
case TokenType.R_BRACKET: return 'R_BRACKET'
|
|
26
|
+
case TokenType.BACKSLASH: return 'BACKSLASH'
|
|
27
|
+
case TokenType.EQUALS: return 'EQUALS'
|
|
28
|
+
|
|
29
|
+
case TokenType.XSS_AMP: return 'XSS_AMP'
|
|
30
|
+
case TokenType.XSS_LT: return 'XSS_LT'
|
|
31
|
+
case TokenType.XSS_GT: return 'XSS_GT'
|
|
32
|
+
case TokenType.XSS_D_QUOTE: return 'XSS_D_QUOTE'
|
|
33
|
+
case TokenType.XSS_S_QUOTE: return 'XSS_S_QUOTE'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isStringToken(tokenType: TokenType): boolean {
|
|
38
|
+
switch (tokenType) {
|
|
39
|
+
case TokenType.XSS_AMP:
|
|
40
|
+
case TokenType.XSS_LT:
|
|
41
|
+
case TokenType.XSS_GT:
|
|
42
|
+
case TokenType.XSS_D_QUOTE:
|
|
43
|
+
case TokenType.XSS_S_QUOTE:
|
|
44
|
+
case TokenType.STR: {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const symbolTable: Record<string, TokenType | undefined> = {
|
|
53
|
+
'\n': TokenType.LINEBREAK,
|
|
54
|
+
|
|
55
|
+
'[': TokenType.L_BRACKET,
|
|
56
|
+
']': TokenType.R_BRACKET,
|
|
57
|
+
'/': TokenType.BACKSLASH,
|
|
58
|
+
'=': TokenType.EQUALS,
|
|
59
|
+
|
|
60
|
+
'&': TokenType.XSS_AMP,
|
|
61
|
+
'<': TokenType.XSS_LT,
|
|
62
|
+
'>': TokenType.XSS_GT,
|
|
63
|
+
'"': TokenType.XSS_D_QUOTE,
|
|
64
|
+
"'": TokenType.XSS_S_QUOTE,
|
|
65
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
|
|
3
|
+
Haven't formally verified this grammar but it should be LL(2)
|
|
4
|
+
|
|
5
|
+
The root's intermediate state has StartTag/EndTag because it's easier to first parse them as independant nodes
|
|
6
|
+
than to parse a StartTag and find the matching EndTag since we can only lookahead by 1 token
|
|
7
|
+
|
|
8
|
+
Trying to lookahead by 4 tokens after each advancement to determine the end of the sub-root will greatly affect performance
|
|
9
|
+
1 "["
|
|
10
|
+
2 "/"
|
|
11
|
+
3 "LABEL"
|
|
12
|
+
4 "]"
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Root <- (Text | Linebreak | Tag)*
|
|
17
|
+
|
|
18
|
+
Text <-
|
|
19
|
+
| {XSS Characters}.
|
|
20
|
+
| STR.
|
|
21
|
+
|
|
22
|
+
Linebreak <-
|
|
23
|
+
| LINEBREAK.
|
|
24
|
+
|
|
25
|
+
Tag <- StartTag Root EndTag
|
|
26
|
+
StartTag <- L_BRACKET Text Attr* R_BRACKET
|
|
27
|
+
EndTag <- L_BRACKET BACKSLASH Text R_BRACKET
|
|
28
|
+
|
|
29
|
+
Attr <-
|
|
30
|
+
| STR EQUALS STR
|
|
31
|
+
| EQUALS STR
|
|
32
|
+
| STR
|
|
33
|
+
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { nodeIsType } from './nodeIsType'
|
|
37
|
+
|
|
38
|
+
// ----------------------------------------------------------------------------
|
|
39
|
+
// AstNode
|
|
40
|
+
// ----------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export const enum AstNodeType {
|
|
43
|
+
RootNode,
|
|
44
|
+
TextNode,
|
|
45
|
+
LinebreakNode,
|
|
46
|
+
TagNode,
|
|
47
|
+
StartTagNode,
|
|
48
|
+
EndTagNode,
|
|
49
|
+
AttrNode,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function nodeTypeToString(nodeType: AstNodeType): string {
|
|
53
|
+
switch (nodeType) {
|
|
54
|
+
case AstNodeType.RootNode: return 'RootNode'
|
|
55
|
+
case AstNodeType.TextNode: return 'TextNode'
|
|
56
|
+
case AstNodeType.LinebreakNode: return 'LinebreakNode'
|
|
57
|
+
case AstNodeType.TagNode: return 'TagNode'
|
|
58
|
+
case AstNodeType.StartTagNode: return 'StartTagNode'
|
|
59
|
+
case AstNodeType.EndTagNode: return 'EndTagNode'
|
|
60
|
+
case AstNodeType.AttrNode: return 'AttrNode'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export abstract class AstNode {
|
|
65
|
+
readonly abstract nodeType: AstNodeType
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line no-use-before-define
|
|
68
|
+
readonly children: Array<AstNode>
|
|
69
|
+
|
|
70
|
+
constructor(children: Array<AstNode> = []) {
|
|
71
|
+
this.children = children
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
addChild(node: AstNode): void {
|
|
75
|
+
this.children.push(node)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isValid(): boolean {
|
|
79
|
+
for (const child of this.children) {
|
|
80
|
+
if (!child.isValid()) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
toShortString(): string {
|
|
89
|
+
return nodeTypeToString(this.nodeType)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// For debugging purposes only
|
|
93
|
+
// Pretty-prints AST
|
|
94
|
+
toString(depth = 0): string {
|
|
95
|
+
let s = ' '.repeat(depth * 2) + this.toShortString()
|
|
96
|
+
|
|
97
|
+
for (const child of this.children) {
|
|
98
|
+
s += '\n' + child.toString(depth + 1)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return s
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ----------------------------------------------------------------------------
|
|
106
|
+
// Root
|
|
107
|
+
// ----------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export class RootNode extends AstNode {
|
|
110
|
+
readonly nodeType = AstNodeType.RootNode
|
|
111
|
+
|
|
112
|
+
override isValid(): boolean {
|
|
113
|
+
for (const child of this.children) {
|
|
114
|
+
if (child.nodeType !== AstNodeType.TagNode &&
|
|
115
|
+
child.nodeType !== AstNodeType.TextNode &&
|
|
116
|
+
child.nodeType !== AstNodeType.LinebreakNode) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return super.isValid() && this.children.length > 0
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ----------------------------------------------------------------------------
|
|
126
|
+
// Text
|
|
127
|
+
// ----------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export class TextNode extends AstNode {
|
|
130
|
+
readonly nodeType = AstNodeType.TextNode
|
|
131
|
+
readonly str: string
|
|
132
|
+
|
|
133
|
+
constructor(str: string) {
|
|
134
|
+
super()
|
|
135
|
+
this.str = str
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
override isValid(): boolean {
|
|
139
|
+
return super.isValid() && this.children.length === 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
override toShortString(): string {
|
|
143
|
+
return `${super.toShortString()} "${this.str}"`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class LinebreakNode extends AstNode {
|
|
148
|
+
readonly nodeType = AstNodeType.LinebreakNode
|
|
149
|
+
|
|
150
|
+
override toShortString(): string {
|
|
151
|
+
return `${super.toShortString()} "\\n"`
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ----------------------------------------------------------------------------
|
|
156
|
+
// Tag
|
|
157
|
+
// ----------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export class StartTagNode extends AstNode {
|
|
160
|
+
readonly nodeType = AstNodeType.StartTagNode
|
|
161
|
+
readonly tagName: string
|
|
162
|
+
readonly ogTag: string
|
|
163
|
+
|
|
164
|
+
constructor(tagName: string, ogTag: string, attrNodes: Array<AttrNode> = []) {
|
|
165
|
+
super(attrNodes)
|
|
166
|
+
this.tagName = tagName.toLowerCase()
|
|
167
|
+
this.ogTag = ogTag
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override isValid(): boolean {
|
|
171
|
+
for (const child of this.children) {
|
|
172
|
+
if (child.nodeType !== AstNodeType.AttrNode) {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return super.isValid()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override toShortString(): string {
|
|
181
|
+
return `${super.toShortString()} ${this.ogTag}`
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export class EndTagNode extends AstNode {
|
|
186
|
+
readonly nodeType = AstNodeType.EndTagNode
|
|
187
|
+
readonly tagName: string
|
|
188
|
+
readonly ogTag: string
|
|
189
|
+
|
|
190
|
+
constructor(tagName: string, ogTag: string) {
|
|
191
|
+
super()
|
|
192
|
+
this.tagName = tagName
|
|
193
|
+
this.ogTag = ogTag
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
override isValid(): boolean {
|
|
197
|
+
return super.isValid() && this.children.length === 0
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override toShortString(): string {
|
|
201
|
+
return `${super.toShortString()} ${this.ogTag}`
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class TagNode extends AstNode {
|
|
206
|
+
readonly nodeType = AstNodeType.TagNode
|
|
207
|
+
private readonly _startTag: StartTagNode
|
|
208
|
+
private readonly _endTag?: EndTagNode | LinebreakNode
|
|
209
|
+
|
|
210
|
+
constructor(startTag: StartTagNode, endTag?: EndTagNode | LinebreakNode) {
|
|
211
|
+
super()
|
|
212
|
+
this._startTag = startTag
|
|
213
|
+
this._endTag = endTag
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
get tagName(): string {
|
|
217
|
+
return this._startTag.tagName
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
get attributes(): Array<AttrNode> {
|
|
221
|
+
return this._startTag.children as Array<AttrNode>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get ogStartTag(): string {
|
|
225
|
+
return this._startTag.ogTag
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get ogEndTag(): string {
|
|
229
|
+
if (!this._endTag) {
|
|
230
|
+
return ''
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (nodeIsType(this._endTag, AstNodeType.LinebreakNode)) {
|
|
234
|
+
return '\n'
|
|
235
|
+
} else {
|
|
236
|
+
return this._endTag.ogTag
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override isValid(): boolean {
|
|
241
|
+
if (this._endTag && nodeIsType(this._endTag, AstNodeType.EndTagNode) && this._startTag.tagName !== this._endTag.tagName) {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.children.length === 1 && this.children[0].nodeType !== AstNodeType.RootNode) {
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (this.children.length > 2) {
|
|
250
|
+
return false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return super.isValid() && this._startTag.isValid() && (this._endTag?.isValid() ?? true)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
override toString(depth = 0): string {
|
|
257
|
+
let s = ' '.repeat(depth * 2) + this.toShortString() + ` [${this.tagName}]`
|
|
258
|
+
|
|
259
|
+
for (const attrNode of this._startTag.children) {
|
|
260
|
+
s += '\n' + attrNode.toString(depth + 1)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const child of this.children) {
|
|
264
|
+
s += '\n' + child.toString(depth + 1)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return s
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ----------------------------------------------------------------------------
|
|
272
|
+
// Attr
|
|
273
|
+
// ----------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
export class AttrNode extends AstNode {
|
|
276
|
+
readonly nodeType = AstNodeType.AttrNode
|
|
277
|
+
|
|
278
|
+
static readonly DEFAULT_KEY = 'default'
|
|
279
|
+
|
|
280
|
+
get key(): string {
|
|
281
|
+
switch (this.children.length) {
|
|
282
|
+
case 1: {
|
|
283
|
+
return AttrNode.DEFAULT_KEY
|
|
284
|
+
}
|
|
285
|
+
case 2: {
|
|
286
|
+
if (!nodeIsType(this.children[0], AstNodeType.TextNode)) {
|
|
287
|
+
throw new Error('Invalid TextNode')
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return this.children[0].str.trim()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
throw new Error('Invalid AttrNode')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get val(): string {
|
|
298
|
+
switch (this.children.length) {
|
|
299
|
+
case 1: {
|
|
300
|
+
if (!nodeIsType(this.children[0], AstNodeType.TextNode)) {
|
|
301
|
+
throw new Error('Invalid TextNode')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this.children[0].str.trim()
|
|
305
|
+
}
|
|
306
|
+
case 2: {
|
|
307
|
+
if (!nodeIsType(this.children[1], AstNodeType.TextNode)) {
|
|
308
|
+
throw new Error('Invalid TextNode')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return this.children[1].str.trim()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
throw new Error('Invalid AttrNode')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
override isValid(): boolean {
|
|
319
|
+
return super.isValid() && (this.children.length >= 1 && this.children.length <= 2)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
override toShortString(): string {
|
|
323
|
+
let s = super.toShortString()
|
|
324
|
+
|
|
325
|
+
switch (this.children.length) {
|
|
326
|
+
case 1: {
|
|
327
|
+
s += ` VAL="${this.val}"`
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
case 2: {
|
|
331
|
+
s += ` KEY="${this.key}" VAL="${this.val}"`
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return s
|
|
337
|
+
}
|
|
338
|
+
}
|