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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/generateHtml.d.ts +2 -0
  4. package/dist/generateHtml.d.ts.map +1 -0
  5. package/dist/generateHtml.js +13 -0
  6. package/dist/generateHtml.js.map +1 -0
  7. package/dist/generator/Generator.d.ts +8 -0
  8. package/dist/generator/Generator.d.ts.map +1 -0
  9. package/dist/generator/Generator.js +54 -0
  10. package/dist/generator/Generator.js.map +1 -0
  11. package/dist/generator/transforms/Transform.d.ts +10 -0
  12. package/dist/generator/transforms/Transform.d.ts.map +1 -0
  13. package/dist/generator/transforms/Transform.js +2 -0
  14. package/dist/generator/transforms/Transform.js.map +1 -0
  15. package/dist/generator/transforms/htmlTransforms.d.ts +3 -0
  16. package/dist/generator/transforms/htmlTransforms.d.ts.map +1 -0
  17. package/dist/generator/transforms/htmlTransforms.js +198 -0
  18. package/dist/generator/transforms/htmlTransforms.js.map +1 -0
  19. package/dist/generator/utils/getTagImmediateAttrVal.d.ts +14 -0
  20. package/dist/generator/utils/getTagImmediateAttrVal.d.ts.map +1 -0
  21. package/dist/generator/utils/getTagImmediateAttrVal.js +19 -0
  22. package/dist/generator/utils/getTagImmediateAttrVal.js.map +1 -0
  23. package/dist/generator/utils/getTagImmediateText.d.ts +12 -0
  24. package/dist/generator/utils/getTagImmediateText.d.ts.map +1 -0
  25. package/dist/generator/utils/getTagImmediateText.js +29 -0
  26. package/dist/generator/utils/getTagImmediateText.js.map +1 -0
  27. package/dist/generator/utils/getWidthHeightAttr.d.ts +31 -0
  28. package/dist/generator/utils/getWidthHeightAttr.d.ts.map +1 -0
  29. package/dist/generator/utils/getWidthHeightAttr.js +47 -0
  30. package/dist/generator/utils/getWidthHeightAttr.js.map +1 -0
  31. package/dist/generator/utils/isDangerousUrl.d.ts +2 -0
  32. package/dist/generator/utils/isDangerousUrl.d.ts.map +1 -0
  33. package/dist/generator/utils/isDangerousUrl.js +14 -0
  34. package/dist/generator/utils/isDangerousUrl.js.map +1 -0
  35. package/dist/generator/utils/isOrderedList.d.ts +19 -0
  36. package/dist/generator/utils/isOrderedList.d.ts.map +1 -0
  37. package/dist/generator/utils/isOrderedList.js +26 -0
  38. package/dist/generator/utils/isOrderedList.js.map +1 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +16 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/lexer/Lexer.d.ts +5 -0
  44. package/dist/lexer/Lexer.d.ts.map +1 -0
  45. package/dist/lexer/Lexer.js +81 -0
  46. package/dist/lexer/Lexer.js.map +1 -0
  47. package/dist/lexer/Token.d.ts +8 -0
  48. package/dist/lexer/Token.d.ts.map +1 -0
  49. package/dist/lexer/Token.js +54 -0
  50. package/dist/lexer/Token.js.map +1 -0
  51. package/dist/lexer/TokenType.d.ts +17 -0
  52. package/dist/lexer/TokenType.d.ts.map +1 -0
  53. package/dist/lexer/TokenType.js +41 -0
  54. package/dist/lexer/TokenType.js.map +1 -0
  55. package/dist/parser/AstNode.d.ts +105 -0
  56. package/dist/parser/AstNode.d.ts.map +1 -0
  57. package/dist/parser/AstNode.js +263 -0
  58. package/dist/parser/AstNode.js.map +1 -0
  59. package/dist/parser/Parser.d.ts +11 -0
  60. package/dist/parser/Parser.d.ts.map +1 -0
  61. package/dist/parser/Parser.js +265 -0
  62. package/dist/parser/Parser.js.map +1 -0
  63. package/dist/parser/nodeIsType.d.ts +13 -0
  64. package/dist/parser/nodeIsType.d.ts.map +1 -0
  65. package/dist/parser/nodeIsType.js +5 -0
  66. package/dist/parser/nodeIsType.js.map +1 -0
  67. package/package.json +68 -0
  68. package/src/generateHtml.ts +15 -0
  69. package/src/generator/Generator.ts +60 -0
  70. package/src/generator/transforms/Transform.ts +15 -0
  71. package/src/generator/transforms/htmlTransforms.ts +205 -0
  72. package/src/generator/utils/getTagImmediateAttrVal.ts +21 -0
  73. package/src/generator/utils/getTagImmediateText.ts +33 -0
  74. package/src/generator/utils/getWidthHeightAttr.ts +51 -0
  75. package/src/generator/utils/isDangerousUrl.ts +17 -0
  76. package/src/generator/utils/isOrderedList.ts +28 -0
  77. package/src/index.ts +18 -0
  78. package/src/lexer/Lexer.ts +89 -0
  79. package/src/lexer/Token.ts +64 -0
  80. package/src/lexer/TokenType.ts +65 -0
  81. package/src/parser/AstNode.ts +338 -0
  82. package/src/parser/Parser.ts +316 -0
  83. 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
+ }