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