ya-bbcode 5.1.0 → 5.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ya-bbcode.cjs +113 -82
- package/dist/ya-bbcode.cjs.map +1 -1
- package/dist/ya-bbcode.mjs +113 -82
- package/dist/ya-bbcode.mjs.map +1 -1
- package/package.json +4 -1
package/dist/ya-bbcode.cjs
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
// src/ya-bbcode.ts
|
|
2
|
+
const PLACEHOLDER_START = "\0";
|
|
3
|
+
const PLACEHOLDER_END = ";";
|
|
4
|
+
function placeholder(index) {
|
|
5
|
+
return PLACEHOLDER_START + index + PLACEHOLDER_END;
|
|
6
|
+
}
|
|
7
|
+
function replaceFirst(content, search, replacement) {
|
|
8
|
+
const index = content.indexOf(search);
|
|
9
|
+
if (index === -1) return content;
|
|
10
|
+
return content.slice(0, index) + replacement + content.slice(index + search.length);
|
|
11
|
+
}
|
|
12
|
+
const sanitizedEntities = {
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">",
|
|
16
|
+
""": "\"",
|
|
17
|
+
"'": "'"
|
|
18
|
+
};
|
|
19
|
+
function unescapeSanitized(value) {
|
|
20
|
+
return value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity) => sanitizedEntities[entity]);
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* Escapes HTML attribute values to prevent XSS
|
|
4
24
|
* Encodes quotes, angle brackets, and other special characters
|
|
@@ -13,19 +33,34 @@ function escapeHtmlAttr(value) {
|
|
|
13
33
|
};
|
|
14
34
|
return String(value).replaceAll(/["&'<>]/g, (char) => attrEntities[char] || char);
|
|
15
35
|
}
|
|
36
|
+
function normalizeUrl(url) {
|
|
37
|
+
let start = 0;
|
|
38
|
+
let end = url.length;
|
|
39
|
+
while (start < end && url.charAt(start) <= " ") start++;
|
|
40
|
+
while (end > start && url.charAt(end - 1) <= " ") end--;
|
|
41
|
+
return url.slice(start, end).replaceAll(/[\t\n\r]/g, "");
|
|
42
|
+
}
|
|
43
|
+
const defaultAllowedSchemes = [
|
|
44
|
+
"http",
|
|
45
|
+
"https",
|
|
46
|
+
"mailto",
|
|
47
|
+
"ftp",
|
|
48
|
+
"ftps"
|
|
49
|
+
];
|
|
16
50
|
/**
|
|
17
51
|
* Validates and sanitizes URLs to prevent javascript: and data: URL attacks
|
|
18
52
|
* Only allows safe URL schemes based on the allowedSchemes list
|
|
53
|
+
* Returns undefined for empty or disallowed URLs
|
|
19
54
|
*/
|
|
20
55
|
function sanitizeUrl(url, allowedSchemes) {
|
|
21
|
-
const trimmed = String(url)
|
|
22
|
-
if (!trimmed) return
|
|
56
|
+
const trimmed = normalizeUrl(String(url));
|
|
57
|
+
if (!trimmed) return;
|
|
23
58
|
if (/^\.{0,2}\//.test(trimmed)) return trimmed;
|
|
24
59
|
if (trimmed.startsWith("#")) return trimmed;
|
|
25
60
|
const schemeMatch = /^([a-z][\d+.a-z-]*):/.exec(trimmed.toLowerCase());
|
|
26
61
|
if (schemeMatch) {
|
|
27
62
|
const scheme = schemeMatch[1] ?? "";
|
|
28
|
-
if (!allowedSchemes.includes(scheme)) return
|
|
63
|
+
if (!allowedSchemes.includes(scheme)) return;
|
|
29
64
|
}
|
|
30
65
|
return trimmed;
|
|
31
66
|
}
|
|
@@ -74,18 +109,12 @@ var yabbcode = class {
|
|
|
74
109
|
tags = {
|
|
75
110
|
"url": {
|
|
76
111
|
type: "replace",
|
|
77
|
-
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ??
|
|
78
|
-
"http",
|
|
79
|
-
"https",
|
|
80
|
-
"mailto",
|
|
81
|
-
"ftp",
|
|
82
|
-
"ftps"
|
|
83
|
-
]))}">`,
|
|
112
|
+
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? "#")}">`,
|
|
84
113
|
close: "</a>"
|
|
85
114
|
},
|
|
86
115
|
"quote": {
|
|
87
116
|
type: "replace",
|
|
88
|
-
open: (attr) => `<blockquote author="${escapeHtmlAttr(attr)}">`,
|
|
117
|
+
open: (attr) => `<blockquote author="${escapeHtmlAttr(this.#unsanitize(attr))}">`,
|
|
89
118
|
close: "</blockquote>"
|
|
90
119
|
},
|
|
91
120
|
"b": {
|
|
@@ -172,8 +201,9 @@ var yabbcode = class {
|
|
|
172
201
|
"img": {
|
|
173
202
|
type: "content",
|
|
174
203
|
replace: (attr, content) => {
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
const src = sanitizeUrl(this.#unsanitize(content.replaceAll(this.regex.placeholders, "")), this.config.allowedSchemes ?? defaultAllowedSchemes);
|
|
205
|
+
if (src === void 0) return "";
|
|
206
|
+
return `<img src="${escapeHtmlAttr(src)}" alt="${escapeHtmlAttr(this.#unsanitize(attr))}"/>`;
|
|
177
207
|
}
|
|
178
208
|
},
|
|
179
209
|
"noparse": { type: "ignore" }
|
|
@@ -181,7 +211,7 @@ var yabbcode = class {
|
|
|
181
211
|
regex = {
|
|
182
212
|
tags: /(\[[^\]^]+\])/g,
|
|
183
213
|
newline: /\r\n|\r|\n/g,
|
|
184
|
-
placeholders: /\
|
|
214
|
+
placeholders: /\0(\d+);/g
|
|
185
215
|
};
|
|
186
216
|
config;
|
|
187
217
|
contentModules = {
|
|
@@ -190,14 +220,14 @@ var yabbcode = class {
|
|
|
190
220
|
let close = module.close;
|
|
191
221
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
192
222
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
193
|
-
if (open && !tag.isClosing) content = content
|
|
194
|
-
if (close && tag.closing) content = content
|
|
223
|
+
if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
|
|
224
|
+
if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
|
|
195
225
|
return content;
|
|
196
226
|
},
|
|
197
227
|
content: (tag, module, content) => {
|
|
198
228
|
if (!tag.closing) return content;
|
|
199
|
-
const openTag =
|
|
200
|
-
const closeTag =
|
|
229
|
+
const openTag = placeholder(tag.index);
|
|
230
|
+
const closeTag = placeholder(tag.closing.index);
|
|
201
231
|
const start = content.indexOf(openTag);
|
|
202
232
|
const end = content.indexOf(closeTag);
|
|
203
233
|
let replace = module.replace;
|
|
@@ -208,16 +238,15 @@ var yabbcode = class {
|
|
|
208
238
|
return contentStart + replace + contentEnd;
|
|
209
239
|
},
|
|
210
240
|
ignore: (tag, _module, content) => {
|
|
211
|
-
const openTag =
|
|
241
|
+
const openTag = placeholder(tag.index);
|
|
212
242
|
const start = content.indexOf(openTag);
|
|
213
243
|
let closeTag = "";
|
|
214
244
|
let end = content.length;
|
|
215
245
|
if (tag.closing) {
|
|
216
|
-
closeTag =
|
|
246
|
+
closeTag = placeholder(tag.closing.index);
|
|
217
247
|
end = content.indexOf(closeTag);
|
|
218
248
|
}
|
|
219
|
-
|
|
220
|
-
innerContent = this.#ignoreLoop(tag.children, innerContent);
|
|
249
|
+
const innerContent = content.slice(start + openTag.length, end);
|
|
221
250
|
const contentStart = content.slice(0, Math.max(0, start));
|
|
222
251
|
const contentEnd = content.slice(end + closeTag.length);
|
|
223
252
|
return contentStart + innerContent + contentEnd;
|
|
@@ -229,13 +258,7 @@ var yabbcode = class {
|
|
|
229
258
|
paragraph: false,
|
|
230
259
|
cleanUnmatchable: true,
|
|
231
260
|
sanitizeHtml: true,
|
|
232
|
-
allowedSchemes: [
|
|
233
|
-
"http",
|
|
234
|
-
"https",
|
|
235
|
-
"mailto",
|
|
236
|
-
"ftp",
|
|
237
|
-
"ftps"
|
|
238
|
-
]
|
|
261
|
+
allowedSchemes: [...defaultAllowedSchemes]
|
|
239
262
|
};
|
|
240
263
|
if (config.newline !== void 0) this.config.newline = config.newline;
|
|
241
264
|
if (config.paragraph !== void 0) this.config.paragraph = config.paragraph;
|
|
@@ -243,18 +266,37 @@ var yabbcode = class {
|
|
|
243
266
|
if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
|
|
244
267
|
if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
|
|
245
268
|
}
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return
|
|
269
|
+
#getTag(name) {
|
|
270
|
+
if (!Object.hasOwn(this.tags, name)) return;
|
|
271
|
+
return this.tags[name];
|
|
272
|
+
}
|
|
273
|
+
#convertNewlines(input) {
|
|
274
|
+
if (!this.config.newline) return input;
|
|
275
|
+
if (this.config.paragraph) return input.replace(this.regex.newline, "</p><p>");
|
|
276
|
+
return input.replace(this.regex.newline, "<br/>");
|
|
277
|
+
}
|
|
278
|
+
#applyLayout(input) {
|
|
279
|
+
input = this.#convertNewlines(input);
|
|
280
|
+
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
281
|
+
return input;
|
|
282
|
+
}
|
|
283
|
+
#unsanitize(value) {
|
|
284
|
+
if (!this.config.sanitizeHtml) return value;
|
|
285
|
+
return unescapeSanitized(value);
|
|
286
|
+
}
|
|
287
|
+
#findIgnoredTags(tagsMap) {
|
|
288
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
289
|
+
let ignoreModule;
|
|
290
|
+
for (const tag of tagsMap) if (ignoreModule === void 0) {
|
|
291
|
+
if (!tag.isClosing && this.#getTag(tag.module)?.type === "ignore") ignoreModule = tag.module;
|
|
292
|
+
} else if (tag.isClosing && tag.module === ignoreModule) ignoreModule = void 0;
|
|
293
|
+
else ignored.add(tag.index);
|
|
294
|
+
return ignored;
|
|
253
295
|
}
|
|
254
296
|
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
255
297
|
if (tagsMap.length > 50) {
|
|
256
298
|
const hasSpecialType = (tag) => {
|
|
257
|
-
const module = this
|
|
299
|
+
const module = this.#getTag(tag.module);
|
|
258
300
|
if (module) {
|
|
259
301
|
if (module.type === "content" || module.type === "ignore") return true;
|
|
260
302
|
if (module.type === "replace" && module.autoClose) return true;
|
|
@@ -266,7 +308,7 @@ var yabbcode = class {
|
|
|
266
308
|
for (let i = 0; i < tagsMap.length; i++) {
|
|
267
309
|
const tag = tagsMap[i];
|
|
268
310
|
if (!tag) continue;
|
|
269
|
-
let module = this
|
|
311
|
+
let module = this.#getTag(tag.module);
|
|
270
312
|
if (!module) module = {
|
|
271
313
|
type: "replace",
|
|
272
314
|
open: tag.raw,
|
|
@@ -277,7 +319,7 @@ var yabbcode = class {
|
|
|
277
319
|
const replaceModule = module;
|
|
278
320
|
let open = replaceModule.open;
|
|
279
321
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
280
|
-
if (open && !tag.isClosing) content = content
|
|
322
|
+
if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
|
|
281
323
|
if (tag.children.length > 0) {
|
|
282
324
|
const closingIndex = tag.closing?.index;
|
|
283
325
|
content = this.#contentLoop(tag.children, content, closingIndex);
|
|
@@ -289,16 +331,16 @@ var yabbcode = class {
|
|
|
289
331
|
for (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {
|
|
290
332
|
const sibling = tagsMap[siblingIndex];
|
|
291
333
|
if (sibling?.module === tag.module) {
|
|
292
|
-
insertBefore =
|
|
334
|
+
insertBefore = placeholder(sibling.index);
|
|
293
335
|
break;
|
|
294
336
|
}
|
|
295
337
|
}
|
|
296
|
-
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore =
|
|
297
|
-
if (insertBefore && autoClose) content = content
|
|
338
|
+
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore = placeholder(parentClosingTagIndex);
|
|
339
|
+
if (insertBefore && autoClose) content = replaceFirst(content, insertBefore, autoClose + insertBefore);
|
|
298
340
|
}
|
|
299
341
|
let close = replaceModule.close;
|
|
300
342
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
301
|
-
if (close && tag.closing) content = content
|
|
343
|
+
if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
|
|
302
344
|
} else if (module.type === "content") {
|
|
303
345
|
if (tag.children.length > 0) content = this.#contentLoop(tag.children, content, tag.closing?.index);
|
|
304
346
|
content = this.contentModules.content(tag, module, content);
|
|
@@ -309,7 +351,7 @@ var yabbcode = class {
|
|
|
309
351
|
#contentLoopOptimized(tagsMap, content) {
|
|
310
352
|
const replacements = /* @__PURE__ */ new Map();
|
|
311
353
|
const processTag = (tag) => {
|
|
312
|
-
let module = this
|
|
354
|
+
let module = this.#getTag(tag.module);
|
|
313
355
|
if (!module) module = {
|
|
314
356
|
type: "replace",
|
|
315
357
|
open: tag.raw,
|
|
@@ -320,12 +362,12 @@ var yabbcode = class {
|
|
|
320
362
|
let close = module.close;
|
|
321
363
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
322
364
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
323
|
-
if (open && !tag.isClosing) replacements.set(
|
|
324
|
-
if (close && tag.closing) replacements.set(
|
|
365
|
+
if (open && !tag.isClosing) replacements.set(placeholder(tag.index), open);
|
|
366
|
+
if (close && tag.closing) replacements.set(placeholder(tag.closing.index), close);
|
|
325
367
|
for (const child of tag.children) processTag(child);
|
|
326
368
|
};
|
|
327
369
|
for (const tag of tagsMap) processTag(tag);
|
|
328
|
-
return content.replaceAll(
|
|
370
|
+
return content.replaceAll(this.regex.placeholders, (match) => replacements.get(match) ?? match);
|
|
329
371
|
}
|
|
330
372
|
#tagLoop(tagsMap, parent) {
|
|
331
373
|
const stack = [];
|
|
@@ -397,7 +439,7 @@ var yabbcode = class {
|
|
|
397
439
|
}
|
|
398
440
|
parse(bbcInput) {
|
|
399
441
|
if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
|
|
400
|
-
let input = String(bbcInput);
|
|
442
|
+
let input = String(bbcInput).replaceAll(PLACEHOLDER_START, "");
|
|
401
443
|
if (this.config.sanitizeHtml) {
|
|
402
444
|
const htmlEntities = {
|
|
403
445
|
"&": "&",
|
|
@@ -410,12 +452,7 @@ var yabbcode = class {
|
|
|
410
452
|
}
|
|
411
453
|
let tagsMap = [];
|
|
412
454
|
const tags = String(input).match(this.regex.tags);
|
|
413
|
-
if (
|
|
414
|
-
if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
|
|
415
|
-
else input = input.replace(this.regex.newline, "<br/>");
|
|
416
|
-
}
|
|
417
|
-
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
418
|
-
if (!tags || tags.length === 0) return input;
|
|
455
|
+
if (!tags || tags.length === 0) return this.#applyLayout(input);
|
|
419
456
|
for (const [i, tag] of tags.entries()) {
|
|
420
457
|
const tagContent = tag.slice(1, -1);
|
|
421
458
|
const isClosing = tagContent.startsWith("/");
|
|
@@ -441,7 +478,7 @@ var yabbcode = class {
|
|
|
441
478
|
index: i,
|
|
442
479
|
module: moduleName,
|
|
443
480
|
isClosing,
|
|
444
|
-
raw: tag,
|
|
481
|
+
raw: this.#convertNewlines(tag),
|
|
445
482
|
attr: parsedAttrs.attr,
|
|
446
483
|
attrs: parsedAttrs.attrs,
|
|
447
484
|
children: [],
|
|
@@ -450,36 +487,30 @@ var yabbcode = class {
|
|
|
450
487
|
};
|
|
451
488
|
tagsMap.push(item);
|
|
452
489
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
lastIndex = pos + tag.length;
|
|
466
|
-
}
|
|
467
|
-
if (tagPositions.length > 0) {
|
|
468
|
-
const parts = [];
|
|
469
|
-
let lastPos = 0;
|
|
470
|
-
for (const { pos, tag, placeholder } of tagPositions) {
|
|
471
|
-
parts.push(input.slice(lastPos, pos));
|
|
472
|
-
parts.push(placeholder);
|
|
473
|
-
lastPos = pos + tag.length;
|
|
474
|
-
}
|
|
475
|
-
parts.push(input.slice(lastPos));
|
|
476
|
-
input = parts.join("");
|
|
477
|
-
}
|
|
490
|
+
const ignored = this.#findIgnoredTags(tagsMap);
|
|
491
|
+
tagsMap = tagsMap.filter((tag) => !ignored.has(tag.index));
|
|
492
|
+
const parts = [];
|
|
493
|
+
let searchFrom = 0;
|
|
494
|
+
let copiedUpTo = 0;
|
|
495
|
+
for (const [i, tag] of tags.entries()) {
|
|
496
|
+
const pos = input.indexOf(tag, searchFrom);
|
|
497
|
+
if (pos === -1) continue;
|
|
498
|
+
searchFrom = pos + tag.length;
|
|
499
|
+
if (ignored.has(i)) continue;
|
|
500
|
+
parts.push(input.slice(copiedUpTo, pos), placeholder(i));
|
|
501
|
+
copiedUpTo = searchFrom;
|
|
478
502
|
}
|
|
503
|
+
parts.push(input.slice(copiedUpTo));
|
|
504
|
+
input = parts.join("");
|
|
505
|
+
input = this.#applyLayout(input);
|
|
479
506
|
tagsMap = this.#tagLoop(tagsMap);
|
|
480
507
|
input = this.#contentLoop(tagsMap, input);
|
|
481
|
-
if (this.config.cleanUnmatchable)
|
|
482
|
-
return input
|
|
508
|
+
if (this.config.cleanUnmatchable) return input.replaceAll(this.regex.placeholders, "");
|
|
509
|
+
return input.replaceAll(this.regex.placeholders, (_match, index) => {
|
|
510
|
+
let raw = tags[Number(index)] ?? "";
|
|
511
|
+
if (!this.config.sanitizeHtml) raw = escapeHtmlAttr(raw);
|
|
512
|
+
return this.#convertNewlines(raw);
|
|
513
|
+
});
|
|
483
514
|
}
|
|
484
515
|
};
|
|
485
516
|
|
package/dist/ya-bbcode.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.cjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string {\n\tconst trimmed = String(url).trim();\n\tif (!trimmed) {\n\t\treturn '#';\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn '#';\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? ['http', 'https', 'mailto', 'ftp', 'ftps']))}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(attr)}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\tif (!content) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(content)}\" alt=\"${escapeHtmlAttr(attr)}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+\\])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\[TAG-[1-9]+\\]/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t//content = content\n\t\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst closeTag = '[TAG-' + tag.closing.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = '[TAG-' + tag.closing.index + ']';\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tlet innerContent = content.slice(start + openTag.length, end);\n\t\t\tinnerContent = this.#ignoreLoop(tag.children, innerContent);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: ['http', 'https', 'mailto', 'ftp', 'ftps'],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t#ignoreLoop(tagsMap: TagItem[], content: string): string {\n\t\tfor (const tag of tagsMap) {\n\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', tag.raw);\n\t\t\tif (tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);\n\t\t\t}\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\tcontent = this.#ignoreLoop(tag.children, content);\n\t\t\t}\n\t\t}\n\t\treturn content;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.tags[tag.module];\n\t\t\t\tif (module) {\n\t\t\t\t\tif (module.type === 'content' || module.type === 'ignore') {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (module.type === 'replace' && (module as ReplaceTag).autoClose) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn tag.children.some(child => hasSpecialType(child));\n\t\t\t};\n\t\t\tconst hasSpecialTypes = tagsMap.some(tag => hasSpecialType(tag));\n\n\t\t\t// If only replace-type tags without autoClose, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tconst replaceModule = module as ReplaceTag;\n\n\t\t\t\t// Step 1: Process opening tag\n\t\t\t\tlet open = replaceModule.open;\n\t\t\t\tif (typeof(open) === 'function') {\n\t\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', open);\n\t\t\t\t}\n\n\t\t\t\t// Step 2: Process children (before closing, so parent's close placeholder is still available)\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tconst closingIndex = tag.closing?.index;\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, closingIndex);\n\t\t\t\t}\n\n\t\t\t\t// Step 3: Handle autoClose for tags without explicit closing\n\t\t\t\tif (replaceModule.autoClose && !tag.closing) {\n\t\t\t\t\tlet autoClose = replaceModule.autoClose;\n\t\t\t\t\tif (typeof(autoClose) === 'function') {\n\t\t\t\t\t\tautoClose = autoClose(tag.attr, tag.attrs);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\t\t\t\t\tlet insertBefore: string | undefined;\n\t\t\t\t\tfor (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {\n\t\t\t\t\t\tconst sibling = tagsMap[siblingIndex];\n\t\t\t\t\t\tif (sibling?.module === tag.module) {\n\t\t\t\t\t\t\tinsertBefore = '[TAG-' + sibling.index + ']';\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (insertBefore === undefined && parentClosingTagIndex !== undefined) {\n\t\t\t\t\t\tinsertBefore = '[TAG-' + parentClosingTagIndex + ']';\n\t\t\t\t\t}\n\n\t\t\t\t\tif (insertBefore && autoClose) {\n\t\t\t\t\t\tcontent = content.replace(insertBefore, autoClose + insertBefore);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Step 4: Process closing tag (after children)\n\t\t\t\tlet close = replaceModule.close;\n\t\t\t\tif (typeof(close) === 'function') {\n\t\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (close && tag.closing) {\n\t\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', close);\n\t\t\t\t}\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\t// Resolve children first so replace() receives final HTML it can escape, rather than placeholders that expand into raw HTML after escaping\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, tag.closing?.index);\n\t\t\t\t}\n\t\t\t\tcontent = this.contentModules.content(tag, module as ContentTag, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module as IgnoreTag, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tfor (const child of tag.children) {\n\t\t\t\tprocessTag(child);\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Single-pass replacement using regex\n\t\tconst placeholderRegex = /\\[TAG-(\\d+)\\]/g;\n\t\treturn content.replaceAll(placeholderRegex, (match: string) => replacements.get(match) || '');\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (!(childStart < tagsMap.length)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttag.children = [];\n\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\ttag.children.push(childTag);\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (tag.isClosing) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\tif (!shouldInclude) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t}\n\t\t\tresult.push(tag);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) {\n\t\t\treturn '';\n\t\t}\n\t\tlet input = String(bbcInput);\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\tif (this.config.newline) {\n\t\t\tif (this.config.paragraph) {\n\t\t\t\tinput = input.replace(this.regex.newline, '</p><p>');\n\t\t\t} else {\n\t\t\t\tinput = input.replace(this.regex.newline, '<br/>');\n\t\t\t}\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn input;\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=', 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/, 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\traw: tag,\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\t// Replace tags with placeholders - optimized for different input sizes\n\t\tif (tags.length <= 3) {\n\t\t\t// Fast path for few tags - direct replacement is faster\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tinput = input.replace(tag, '[TAG-' + i + ']');\n\t\t\t}\n\t\t} else {\n\t\t\t// Batch replacement for many tags\n\t\t\tconst tagPositions = [];\n\t\t\tlet lastIndex = 0;\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tconst pos = input.indexOf(tag, lastIndex);\n\t\t\t\tif (pos === -1) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttagPositions.push({ pos, tag: tag, placeholder: '[TAG-' + i + ']' });\n\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t}\n\n\t\t\tif (tagPositions.length > 0) {\n\t\t\t\tconst parts = [];\n\t\t\t\tlet lastPos = 0;\n\t\t\t\tfor (const { pos, tag, placeholder } of tagPositions) {\n\t\t\t\t\tparts.push(input.slice(lastPos, pos));\n\t\t\t\t\tparts.push(placeholder);\n\t\t\t\t\tlastPos = pos + tag.length;\n\t\t\t\t}\n\t\t\t\tparts.push(input.slice(lastPos));\n\t\t\t\tinput = parts.join('');\n\t\t\t}\n\t\t}\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\t// put back all non-found matches?\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\tinput = input.replace(this.regex.placeholders, '');\n\t\t}\n\t\treturn input;\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ya-bbcode.cjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n// NUL is stripped from input so placeholders can't be forged; a distinct end stops `\\0 1 \\0 2 \\0` reading as placeholder 2.\nconst PLACEHOLDER_START = '\\0';\nconst PLACEHOLDER_END = ';';\n\nfunction placeholder(index: number): string {\n\treturn PLACEHOLDER_START + index + PLACEHOLDER_END;\n}\n\n// String#replace expands `$&`, `$'` and `` $` `` in string replacements, which user-controlled attributes could abuse.\nfunction replaceFirst(content: string, search: string, replacement: string): string {\n\tconst index = content.indexOf(search);\n\tif (index === -1) {\n\t\treturn content;\n\t}\n\treturn content.slice(0, index) + replacement + content.slice(index + search.length);\n}\n\nconst sanitizedEntities = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'"': '\"',\n\t''': '\\'',\n};\n\nfunction unescapeSanitized(value: string): string {\n\treturn value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity: string) => sanitizedEntities[entity as keyof typeof sanitizedEntities]);\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n// Mirrors the WHATWG URL parser, which trims C0 controls and spaces and drops tabs/newlines, so `java\\tscript:` can't slip through.\nfunction normalizeUrl(url: string): string {\n\tlet start = 0;\n\tlet end = url.length;\n\t// Comparing against ' ' matches every character from U+0000 to U+0020\n\twhile (start < end && url.charAt(start) <= ' ') {\n\t\tstart++;\n\t}\n\twhile (end > start && url.charAt(end - 1) <= ' ') {\n\t\tend--;\n\t}\n\treturn url.slice(start, end).replaceAll(/[\\t\\n\\r]/g, '');\n}\n\nconst defaultAllowedSchemes = ['http', 'https', 'mailto', 'ftp', 'ftps'];\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n * Returns undefined for empty or disallowed URLs\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string | undefined {\n\tconst trimmed = normalizeUrl(String(url));\n\tif (!trimmed) {\n\t\treturn undefined;\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? '#')}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(this.#unsanitize(attr))}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\t// Leftover placeholders are resolved after this check, so a removed one could hide a scheme like `[/b]javascript:`\n\t\t\t\tconst url = this.#unsanitize(content.replaceAll(this.regex.placeholders, ''));\n\t\t\t\tconst src = sanitizeUrl(url, this.config.allowedSchemes ?? defaultAllowedSchemes);\n\t\t\t\tif (src === undefined) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(src)}\" alt=\"${escapeHtmlAttr(this.#unsanitize(attr))}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+\\])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\0(\\d+);/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst closeTag = placeholder(tag.closing.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = placeholder(tag.closing.index);\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: [...defaultAllowedSchemes],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t// Tag names come from user input, so inherited keys like `constructor` must not resolve to Object.prototype members.\n\t#getTag(name: string): TagDefinition | undefined {\n\t\tif (!Object.hasOwn(this.tags, name)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn this.tags[name];\n\t}\n\n\t#convertNewlines(input: string): string {\n\t\tif (!this.config.newline) {\n\t\t\treturn input;\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\treturn input.replace(this.regex.newline, '</p><p>');\n\t\t}\n\t\treturn input.replace(this.regex.newline, '<br/>');\n\t}\n\n\t#applyLayout(input: string): string {\n\t\tinput = this.#convertNewlines(input);\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\t\treturn input;\n\t}\n\n\t// Built-in attribute tags escape for themselves, so undo parse()'s sanitizing first to avoid double-escaping `&`.\n\t#unsanitize(value: string): string {\n\t\tif (!this.config.sanitizeHtml) {\n\t\t\treturn value;\n\t\t}\n\t\treturn unescapeSanitized(value);\n\t}\n\n\t// Tags inside an ignore tag are plain text, so they must never get a placeholder that could pair with a tag outside it.\n\t#findIgnoredTags(tagsMap: TagItem[]): Set<number> {\n\t\tconst ignored = new Set<number>();\n\t\tlet ignoreModule: string | undefined;\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (ignoreModule === undefined) {\n\t\t\t\tif (!tag.isClosing && this.#getTag(tag.module)?.type === 'ignore') {\n\t\t\t\t\tignoreModule = tag.module;\n\t\t\t\t}\n\t\t\t} else if (tag.isClosing && tag.module === ignoreModule) {\n\t\t\t\tignoreModule = undefined;\n\t\t\t} else {\n\t\t\t\tignored.add(tag.index);\n\t\t\t}\n\t\t}\n\t\treturn ignored;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.#getTag(tag.module);\n\t\t\t\tif (module) {\n\t\t\t\t\tif (module.type === 'content' || module.type === 'ignore') {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (module.type === 'replace' && (module as ReplaceTag).autoClose) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn tag.children.some(child => hasSpecialType(child));\n\t\t\t};\n\t\t\tconst hasSpecialTypes = tagsMap.some(tag => hasSpecialType(tag));\n\n\t\t\t// If only replace-type tags without autoClose, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tconst replaceModule = module as ReplaceTag;\n\n\t\t\t\t// Step 1: Process opening tag\n\t\t\t\tlet open = replaceModule.open;\n\t\t\t\tif (typeof(open) === 'function') {\n\t\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t\t}\n\n\t\t\t\t// Step 2: Process children (before closing, so parent's close placeholder is still available)\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tconst closingIndex = tag.closing?.index;\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, closingIndex);\n\t\t\t\t}\n\n\t\t\t\t// Step 3: Handle autoClose for tags without explicit closing\n\t\t\t\tif (replaceModule.autoClose && !tag.closing) {\n\t\t\t\t\tlet autoClose = replaceModule.autoClose;\n\t\t\t\t\tif (typeof(autoClose) === 'function') {\n\t\t\t\t\t\tautoClose = autoClose(tag.attr, tag.attrs);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\t\t\t\t\tlet insertBefore: string | undefined;\n\t\t\t\t\tfor (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {\n\t\t\t\t\t\tconst sibling = tagsMap[siblingIndex];\n\t\t\t\t\t\tif (sibling?.module === tag.module) {\n\t\t\t\t\t\t\tinsertBefore = placeholder(sibling.index);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (insertBefore === undefined && parentClosingTagIndex !== undefined) {\n\t\t\t\t\t\tinsertBefore = placeholder(parentClosingTagIndex);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (insertBefore && autoClose) {\n\t\t\t\t\t\tcontent = replaceFirst(content, insertBefore, autoClose + insertBefore);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Step 4: Process closing tag (after children)\n\t\t\t\tlet close = replaceModule.close;\n\t\t\t\tif (typeof(close) === 'function') {\n\t\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (close && tag.closing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t\t}\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\t// Resolve children first so replace() receives final HTML it can escape, rather than placeholders that expand into raw HTML after escaping\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, tag.closing?.index);\n\t\t\t\t}\n\t\t\t\tcontent = this.contentModules.content(tag, module as ContentTag, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module as IgnoreTag, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set(placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set(placeholder(tag.closing.index), close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tfor (const child of tag.children) {\n\t\t\t\tprocessTag(child);\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Unmatched placeholders are kept so parse() handles them per cleanUnmatchable\n\t\treturn content.replaceAll(this.regex.placeholders, (match: string) => replacements.get(match) ?? match);\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (!(childStart < tagsMap.length)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttag.children = [];\n\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\ttag.children.push(childTag);\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (tag.isClosing) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\tif (!shouldInclude) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t}\n\t\t\tresult.push(tag);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) {\n\t\t\treturn '';\n\t\t}\n\t\tlet input = String(bbcInput).replaceAll(PLACEHOLDER_START, '');\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn this.#applyLayout(input);\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=', 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/, 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\t// Layout runs after placeholders go in, so restored tag text needs its newlines converted to match the text around it\n\t\t\t\traw: this.#convertNewlines(tag),\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\tconst ignored = this.#findIgnoredTags(tagsMap);\n\t\ttagsMap = tagsMap.filter(tag => !ignored.has(tag.index));\n\n\t\t// Ignored tags are still located so the search moves past them, since their text can contain a later tag's text\n\t\tconst parts: string[] = [];\n\t\tlet searchFrom = 0;\n\t\tlet copiedUpTo = 0;\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst pos = input.indexOf(tag, searchFrom);\n\t\t\tif (pos === -1) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsearchFrom = pos + tag.length;\n\t\t\tif (ignored.has(i)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparts.push(input.slice(copiedUpTo, pos), placeholder(i));\n\t\t\tcopiedUpTo = searchFrom;\n\t\t}\n\t\tparts.push(input.slice(copiedUpTo));\n\t\tinput = parts.join('');\n\t\t// Only after placeholders are in, so a tag spanning lines still matches its raw text\n\t\tinput = this.#applyLayout(input);\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\t// Placeholders are internal markers, so leftovers become nothing or the original BBCode, never leak\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\treturn input.replaceAll(this.regex.placeholders, '');\n\t\t}\n\t\t// Unknown tags are restored before content tags escape their output, but leftovers land after, possibly inside an attribute, so raw input must be escaped here\n\t\treturn input.replaceAll(this.regex.placeholders, (_match: string, index: string) => {\n\t\t\tlet raw = tags[Number(index)] ?? '';\n\t\t\tif (!this.config.sanitizeHtml) {\n\t\t\t\traw = escapeHtmlAttr(raw);\n\t\t\t}\n\t\t\treturn this.#convertNewlines(raw);\n\t\t});\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
package/dist/ya-bbcode.mjs
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
// src/ya-bbcode.ts
|
|
2
|
+
const PLACEHOLDER_START = "\0";
|
|
3
|
+
const PLACEHOLDER_END = ";";
|
|
4
|
+
function placeholder(index) {
|
|
5
|
+
return PLACEHOLDER_START + index + PLACEHOLDER_END;
|
|
6
|
+
}
|
|
7
|
+
function replaceFirst(content, search, replacement) {
|
|
8
|
+
const index = content.indexOf(search);
|
|
9
|
+
if (index === -1) return content;
|
|
10
|
+
return content.slice(0, index) + replacement + content.slice(index + search.length);
|
|
11
|
+
}
|
|
12
|
+
const sanitizedEntities = {
|
|
13
|
+
"&": "&",
|
|
14
|
+
"<": "<",
|
|
15
|
+
">": ">",
|
|
16
|
+
""": "\"",
|
|
17
|
+
"'": "'"
|
|
18
|
+
};
|
|
19
|
+
function unescapeSanitized(value) {
|
|
20
|
+
return value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity) => sanitizedEntities[entity]);
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* Escapes HTML attribute values to prevent XSS
|
|
4
24
|
* Encodes quotes, angle brackets, and other special characters
|
|
@@ -13,19 +33,34 @@ function escapeHtmlAttr(value) {
|
|
|
13
33
|
};
|
|
14
34
|
return String(value).replaceAll(/["&'<>]/g, (char) => attrEntities[char] || char);
|
|
15
35
|
}
|
|
36
|
+
function normalizeUrl(url) {
|
|
37
|
+
let start = 0;
|
|
38
|
+
let end = url.length;
|
|
39
|
+
while (start < end && url.charAt(start) <= " ") start++;
|
|
40
|
+
while (end > start && url.charAt(end - 1) <= " ") end--;
|
|
41
|
+
return url.slice(start, end).replaceAll(/[\t\n\r]/g, "");
|
|
42
|
+
}
|
|
43
|
+
const defaultAllowedSchemes = [
|
|
44
|
+
"http",
|
|
45
|
+
"https",
|
|
46
|
+
"mailto",
|
|
47
|
+
"ftp",
|
|
48
|
+
"ftps"
|
|
49
|
+
];
|
|
16
50
|
/**
|
|
17
51
|
* Validates and sanitizes URLs to prevent javascript: and data: URL attacks
|
|
18
52
|
* Only allows safe URL schemes based on the allowedSchemes list
|
|
53
|
+
* Returns undefined for empty or disallowed URLs
|
|
19
54
|
*/
|
|
20
55
|
function sanitizeUrl(url, allowedSchemes) {
|
|
21
|
-
const trimmed = String(url)
|
|
22
|
-
if (!trimmed) return
|
|
56
|
+
const trimmed = normalizeUrl(String(url));
|
|
57
|
+
if (!trimmed) return;
|
|
23
58
|
if (/^\.{0,2}\//.test(trimmed)) return trimmed;
|
|
24
59
|
if (trimmed.startsWith("#")) return trimmed;
|
|
25
60
|
const schemeMatch = /^([a-z][\d+.a-z-]*):/.exec(trimmed.toLowerCase());
|
|
26
61
|
if (schemeMatch) {
|
|
27
62
|
const scheme = schemeMatch[1] ?? "";
|
|
28
|
-
if (!allowedSchemes.includes(scheme)) return
|
|
63
|
+
if (!allowedSchemes.includes(scheme)) return;
|
|
29
64
|
}
|
|
30
65
|
return trimmed;
|
|
31
66
|
}
|
|
@@ -74,18 +109,12 @@ var yabbcode = class {
|
|
|
74
109
|
tags = {
|
|
75
110
|
"url": {
|
|
76
111
|
type: "replace",
|
|
77
|
-
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ??
|
|
78
|
-
"http",
|
|
79
|
-
"https",
|
|
80
|
-
"mailto",
|
|
81
|
-
"ftp",
|
|
82
|
-
"ftps"
|
|
83
|
-
]))}">`,
|
|
112
|
+
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? "#")}">`,
|
|
84
113
|
close: "</a>"
|
|
85
114
|
},
|
|
86
115
|
"quote": {
|
|
87
116
|
type: "replace",
|
|
88
|
-
open: (attr) => `<blockquote author="${escapeHtmlAttr(attr)}">`,
|
|
117
|
+
open: (attr) => `<blockquote author="${escapeHtmlAttr(this.#unsanitize(attr))}">`,
|
|
89
118
|
close: "</blockquote>"
|
|
90
119
|
},
|
|
91
120
|
"b": {
|
|
@@ -172,8 +201,9 @@ var yabbcode = class {
|
|
|
172
201
|
"img": {
|
|
173
202
|
type: "content",
|
|
174
203
|
replace: (attr, content) => {
|
|
175
|
-
|
|
176
|
-
|
|
204
|
+
const src = sanitizeUrl(this.#unsanitize(content.replaceAll(this.regex.placeholders, "")), this.config.allowedSchemes ?? defaultAllowedSchemes);
|
|
205
|
+
if (src === void 0) return "";
|
|
206
|
+
return `<img src="${escapeHtmlAttr(src)}" alt="${escapeHtmlAttr(this.#unsanitize(attr))}"/>`;
|
|
177
207
|
}
|
|
178
208
|
},
|
|
179
209
|
"noparse": { type: "ignore" }
|
|
@@ -181,7 +211,7 @@ var yabbcode = class {
|
|
|
181
211
|
regex = {
|
|
182
212
|
tags: /(\[[^\]^]+\])/g,
|
|
183
213
|
newline: /\r\n|\r|\n/g,
|
|
184
|
-
placeholders: /\
|
|
214
|
+
placeholders: /\0(\d+);/g
|
|
185
215
|
};
|
|
186
216
|
config;
|
|
187
217
|
contentModules = {
|
|
@@ -190,14 +220,14 @@ var yabbcode = class {
|
|
|
190
220
|
let close = module.close;
|
|
191
221
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
192
222
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
193
|
-
if (open && !tag.isClosing) content = content
|
|
194
|
-
if (close && tag.closing) content = content
|
|
223
|
+
if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
|
|
224
|
+
if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
|
|
195
225
|
return content;
|
|
196
226
|
},
|
|
197
227
|
content: (tag, module, content) => {
|
|
198
228
|
if (!tag.closing) return content;
|
|
199
|
-
const openTag =
|
|
200
|
-
const closeTag =
|
|
229
|
+
const openTag = placeholder(tag.index);
|
|
230
|
+
const closeTag = placeholder(tag.closing.index);
|
|
201
231
|
const start = content.indexOf(openTag);
|
|
202
232
|
const end = content.indexOf(closeTag);
|
|
203
233
|
let replace = module.replace;
|
|
@@ -208,16 +238,15 @@ var yabbcode = class {
|
|
|
208
238
|
return contentStart + replace + contentEnd;
|
|
209
239
|
},
|
|
210
240
|
ignore: (tag, _module, content) => {
|
|
211
|
-
const openTag =
|
|
241
|
+
const openTag = placeholder(tag.index);
|
|
212
242
|
const start = content.indexOf(openTag);
|
|
213
243
|
let closeTag = "";
|
|
214
244
|
let end = content.length;
|
|
215
245
|
if (tag.closing) {
|
|
216
|
-
closeTag =
|
|
246
|
+
closeTag = placeholder(tag.closing.index);
|
|
217
247
|
end = content.indexOf(closeTag);
|
|
218
248
|
}
|
|
219
|
-
|
|
220
|
-
innerContent = this.#ignoreLoop(tag.children, innerContent);
|
|
249
|
+
const innerContent = content.slice(start + openTag.length, end);
|
|
221
250
|
const contentStart = content.slice(0, Math.max(0, start));
|
|
222
251
|
const contentEnd = content.slice(end + closeTag.length);
|
|
223
252
|
return contentStart + innerContent + contentEnd;
|
|
@@ -229,13 +258,7 @@ var yabbcode = class {
|
|
|
229
258
|
paragraph: false,
|
|
230
259
|
cleanUnmatchable: true,
|
|
231
260
|
sanitizeHtml: true,
|
|
232
|
-
allowedSchemes: [
|
|
233
|
-
"http",
|
|
234
|
-
"https",
|
|
235
|
-
"mailto",
|
|
236
|
-
"ftp",
|
|
237
|
-
"ftps"
|
|
238
|
-
]
|
|
261
|
+
allowedSchemes: [...defaultAllowedSchemes]
|
|
239
262
|
};
|
|
240
263
|
if (config.newline !== void 0) this.config.newline = config.newline;
|
|
241
264
|
if (config.paragraph !== void 0) this.config.paragraph = config.paragraph;
|
|
@@ -243,18 +266,37 @@ var yabbcode = class {
|
|
|
243
266
|
if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
|
|
244
267
|
if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
|
|
245
268
|
}
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return
|
|
269
|
+
#getTag(name) {
|
|
270
|
+
if (!Object.hasOwn(this.tags, name)) return;
|
|
271
|
+
return this.tags[name];
|
|
272
|
+
}
|
|
273
|
+
#convertNewlines(input) {
|
|
274
|
+
if (!this.config.newline) return input;
|
|
275
|
+
if (this.config.paragraph) return input.replace(this.regex.newline, "</p><p>");
|
|
276
|
+
return input.replace(this.regex.newline, "<br/>");
|
|
277
|
+
}
|
|
278
|
+
#applyLayout(input) {
|
|
279
|
+
input = this.#convertNewlines(input);
|
|
280
|
+
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
281
|
+
return input;
|
|
282
|
+
}
|
|
283
|
+
#unsanitize(value) {
|
|
284
|
+
if (!this.config.sanitizeHtml) return value;
|
|
285
|
+
return unescapeSanitized(value);
|
|
286
|
+
}
|
|
287
|
+
#findIgnoredTags(tagsMap) {
|
|
288
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
289
|
+
let ignoreModule;
|
|
290
|
+
for (const tag of tagsMap) if (ignoreModule === void 0) {
|
|
291
|
+
if (!tag.isClosing && this.#getTag(tag.module)?.type === "ignore") ignoreModule = tag.module;
|
|
292
|
+
} else if (tag.isClosing && tag.module === ignoreModule) ignoreModule = void 0;
|
|
293
|
+
else ignored.add(tag.index);
|
|
294
|
+
return ignored;
|
|
253
295
|
}
|
|
254
296
|
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
255
297
|
if (tagsMap.length > 50) {
|
|
256
298
|
const hasSpecialType = (tag) => {
|
|
257
|
-
const module = this
|
|
299
|
+
const module = this.#getTag(tag.module);
|
|
258
300
|
if (module) {
|
|
259
301
|
if (module.type === "content" || module.type === "ignore") return true;
|
|
260
302
|
if (module.type === "replace" && module.autoClose) return true;
|
|
@@ -266,7 +308,7 @@ var yabbcode = class {
|
|
|
266
308
|
for (let i = 0; i < tagsMap.length; i++) {
|
|
267
309
|
const tag = tagsMap[i];
|
|
268
310
|
if (!tag) continue;
|
|
269
|
-
let module = this
|
|
311
|
+
let module = this.#getTag(tag.module);
|
|
270
312
|
if (!module) module = {
|
|
271
313
|
type: "replace",
|
|
272
314
|
open: tag.raw,
|
|
@@ -277,7 +319,7 @@ var yabbcode = class {
|
|
|
277
319
|
const replaceModule = module;
|
|
278
320
|
let open = replaceModule.open;
|
|
279
321
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
280
|
-
if (open && !tag.isClosing) content = content
|
|
322
|
+
if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
|
|
281
323
|
if (tag.children.length > 0) {
|
|
282
324
|
const closingIndex = tag.closing?.index;
|
|
283
325
|
content = this.#contentLoop(tag.children, content, closingIndex);
|
|
@@ -289,16 +331,16 @@ var yabbcode = class {
|
|
|
289
331
|
for (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {
|
|
290
332
|
const sibling = tagsMap[siblingIndex];
|
|
291
333
|
if (sibling?.module === tag.module) {
|
|
292
|
-
insertBefore =
|
|
334
|
+
insertBefore = placeholder(sibling.index);
|
|
293
335
|
break;
|
|
294
336
|
}
|
|
295
337
|
}
|
|
296
|
-
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore =
|
|
297
|
-
if (insertBefore && autoClose) content = content
|
|
338
|
+
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore = placeholder(parentClosingTagIndex);
|
|
339
|
+
if (insertBefore && autoClose) content = replaceFirst(content, insertBefore, autoClose + insertBefore);
|
|
298
340
|
}
|
|
299
341
|
let close = replaceModule.close;
|
|
300
342
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
301
|
-
if (close && tag.closing) content = content
|
|
343
|
+
if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
|
|
302
344
|
} else if (module.type === "content") {
|
|
303
345
|
if (tag.children.length > 0) content = this.#contentLoop(tag.children, content, tag.closing?.index);
|
|
304
346
|
content = this.contentModules.content(tag, module, content);
|
|
@@ -309,7 +351,7 @@ var yabbcode = class {
|
|
|
309
351
|
#contentLoopOptimized(tagsMap, content) {
|
|
310
352
|
const replacements = /* @__PURE__ */ new Map();
|
|
311
353
|
const processTag = (tag) => {
|
|
312
|
-
let module = this
|
|
354
|
+
let module = this.#getTag(tag.module);
|
|
313
355
|
if (!module) module = {
|
|
314
356
|
type: "replace",
|
|
315
357
|
open: tag.raw,
|
|
@@ -320,12 +362,12 @@ var yabbcode = class {
|
|
|
320
362
|
let close = module.close;
|
|
321
363
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
322
364
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
323
|
-
if (open && !tag.isClosing) replacements.set(
|
|
324
|
-
if (close && tag.closing) replacements.set(
|
|
365
|
+
if (open && !tag.isClosing) replacements.set(placeholder(tag.index), open);
|
|
366
|
+
if (close && tag.closing) replacements.set(placeholder(tag.closing.index), close);
|
|
325
367
|
for (const child of tag.children) processTag(child);
|
|
326
368
|
};
|
|
327
369
|
for (const tag of tagsMap) processTag(tag);
|
|
328
|
-
return content.replaceAll(
|
|
370
|
+
return content.replaceAll(this.regex.placeholders, (match) => replacements.get(match) ?? match);
|
|
329
371
|
}
|
|
330
372
|
#tagLoop(tagsMap, parent) {
|
|
331
373
|
const stack = [];
|
|
@@ -397,7 +439,7 @@ var yabbcode = class {
|
|
|
397
439
|
}
|
|
398
440
|
parse(bbcInput) {
|
|
399
441
|
if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
|
|
400
|
-
let input = String(bbcInput);
|
|
442
|
+
let input = String(bbcInput).replaceAll(PLACEHOLDER_START, "");
|
|
401
443
|
if (this.config.sanitizeHtml) {
|
|
402
444
|
const htmlEntities = {
|
|
403
445
|
"&": "&",
|
|
@@ -410,12 +452,7 @@ var yabbcode = class {
|
|
|
410
452
|
}
|
|
411
453
|
let tagsMap = [];
|
|
412
454
|
const tags = String(input).match(this.regex.tags);
|
|
413
|
-
if (
|
|
414
|
-
if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
|
|
415
|
-
else input = input.replace(this.regex.newline, "<br/>");
|
|
416
|
-
}
|
|
417
|
-
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
418
|
-
if (!tags || tags.length === 0) return input;
|
|
455
|
+
if (!tags || tags.length === 0) return this.#applyLayout(input);
|
|
419
456
|
for (const [i, tag] of tags.entries()) {
|
|
420
457
|
const tagContent = tag.slice(1, -1);
|
|
421
458
|
const isClosing = tagContent.startsWith("/");
|
|
@@ -441,7 +478,7 @@ var yabbcode = class {
|
|
|
441
478
|
index: i,
|
|
442
479
|
module: moduleName,
|
|
443
480
|
isClosing,
|
|
444
|
-
raw: tag,
|
|
481
|
+
raw: this.#convertNewlines(tag),
|
|
445
482
|
attr: parsedAttrs.attr,
|
|
446
483
|
attrs: parsedAttrs.attrs,
|
|
447
484
|
children: [],
|
|
@@ -450,36 +487,30 @@ var yabbcode = class {
|
|
|
450
487
|
};
|
|
451
488
|
tagsMap.push(item);
|
|
452
489
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
lastIndex = pos + tag.length;
|
|
466
|
-
}
|
|
467
|
-
if (tagPositions.length > 0) {
|
|
468
|
-
const parts = [];
|
|
469
|
-
let lastPos = 0;
|
|
470
|
-
for (const { pos, tag, placeholder } of tagPositions) {
|
|
471
|
-
parts.push(input.slice(lastPos, pos));
|
|
472
|
-
parts.push(placeholder);
|
|
473
|
-
lastPos = pos + tag.length;
|
|
474
|
-
}
|
|
475
|
-
parts.push(input.slice(lastPos));
|
|
476
|
-
input = parts.join("");
|
|
477
|
-
}
|
|
490
|
+
const ignored = this.#findIgnoredTags(tagsMap);
|
|
491
|
+
tagsMap = tagsMap.filter((tag) => !ignored.has(tag.index));
|
|
492
|
+
const parts = [];
|
|
493
|
+
let searchFrom = 0;
|
|
494
|
+
let copiedUpTo = 0;
|
|
495
|
+
for (const [i, tag] of tags.entries()) {
|
|
496
|
+
const pos = input.indexOf(tag, searchFrom);
|
|
497
|
+
if (pos === -1) continue;
|
|
498
|
+
searchFrom = pos + tag.length;
|
|
499
|
+
if (ignored.has(i)) continue;
|
|
500
|
+
parts.push(input.slice(copiedUpTo, pos), placeholder(i));
|
|
501
|
+
copiedUpTo = searchFrom;
|
|
478
502
|
}
|
|
503
|
+
parts.push(input.slice(copiedUpTo));
|
|
504
|
+
input = parts.join("");
|
|
505
|
+
input = this.#applyLayout(input);
|
|
479
506
|
tagsMap = this.#tagLoop(tagsMap);
|
|
480
507
|
input = this.#contentLoop(tagsMap, input);
|
|
481
|
-
if (this.config.cleanUnmatchable)
|
|
482
|
-
return input
|
|
508
|
+
if (this.config.cleanUnmatchable) return input.replaceAll(this.regex.placeholders, "");
|
|
509
|
+
return input.replaceAll(this.regex.placeholders, (_match, index) => {
|
|
510
|
+
let raw = tags[Number(index)] ?? "";
|
|
511
|
+
if (!this.config.sanitizeHtml) raw = escapeHtmlAttr(raw);
|
|
512
|
+
return this.#convertNewlines(raw);
|
|
513
|
+
});
|
|
483
514
|
}
|
|
484
515
|
};
|
|
485
516
|
|
package/dist/ya-bbcode.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.mjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string {\n\tconst trimmed = String(url).trim();\n\tif (!trimmed) {\n\t\treturn '#';\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn '#';\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? ['http', 'https', 'mailto', 'ftp', 'ftps']))}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(attr)}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\tif (!content) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(content)}\" alt=\"${escapeHtmlAttr(attr)}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+\\])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\[TAG-[1-9]+\\]/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t//content = content\n\t\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst closeTag = '[TAG-' + tag.closing.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = '[TAG-' + tag.closing.index + ']';\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tlet innerContent = content.slice(start + openTag.length, end);\n\t\t\tinnerContent = this.#ignoreLoop(tag.children, innerContent);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: ['http', 'https', 'mailto', 'ftp', 'ftps'],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t#ignoreLoop(tagsMap: TagItem[], content: string): string {\n\t\tfor (const tag of tagsMap) {\n\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', tag.raw);\n\t\t\tif (tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);\n\t\t\t}\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\tcontent = this.#ignoreLoop(tag.children, content);\n\t\t\t}\n\t\t}\n\t\treturn content;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.tags[tag.module];\n\t\t\t\tif (module) {\n\t\t\t\t\tif (module.type === 'content' || module.type === 'ignore') {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (module.type === 'replace' && (module as ReplaceTag).autoClose) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn tag.children.some(child => hasSpecialType(child));\n\t\t\t};\n\t\t\tconst hasSpecialTypes = tagsMap.some(tag => hasSpecialType(tag));\n\n\t\t\t// If only replace-type tags without autoClose, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tconst replaceModule = module as ReplaceTag;\n\n\t\t\t\t// Step 1: Process opening tag\n\t\t\t\tlet open = replaceModule.open;\n\t\t\t\tif (typeof(open) === 'function') {\n\t\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', open);\n\t\t\t\t}\n\n\t\t\t\t// Step 2: Process children (before closing, so parent's close placeholder is still available)\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tconst closingIndex = tag.closing?.index;\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, closingIndex);\n\t\t\t\t}\n\n\t\t\t\t// Step 3: Handle autoClose for tags without explicit closing\n\t\t\t\tif (replaceModule.autoClose && !tag.closing) {\n\t\t\t\t\tlet autoClose = replaceModule.autoClose;\n\t\t\t\t\tif (typeof(autoClose) === 'function') {\n\t\t\t\t\t\tautoClose = autoClose(tag.attr, tag.attrs);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\t\t\t\t\tlet insertBefore: string | undefined;\n\t\t\t\t\tfor (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {\n\t\t\t\t\t\tconst sibling = tagsMap[siblingIndex];\n\t\t\t\t\t\tif (sibling?.module === tag.module) {\n\t\t\t\t\t\t\tinsertBefore = '[TAG-' + sibling.index + ']';\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (insertBefore === undefined && parentClosingTagIndex !== undefined) {\n\t\t\t\t\t\tinsertBefore = '[TAG-' + parentClosingTagIndex + ']';\n\t\t\t\t\t}\n\n\t\t\t\t\tif (insertBefore && autoClose) {\n\t\t\t\t\t\tcontent = content.replace(insertBefore, autoClose + insertBefore);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Step 4: Process closing tag (after children)\n\t\t\t\tlet close = replaceModule.close;\n\t\t\t\tif (typeof(close) === 'function') {\n\t\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (close && tag.closing) {\n\t\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', close);\n\t\t\t\t}\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\t// Resolve children first so replace() receives final HTML it can escape, rather than placeholders that expand into raw HTML after escaping\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, tag.closing?.index);\n\t\t\t\t}\n\t\t\t\tcontent = this.contentModules.content(tag, module as ContentTag, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module as IgnoreTag, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tfor (const child of tag.children) {\n\t\t\t\tprocessTag(child);\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Single-pass replacement using regex\n\t\tconst placeholderRegex = /\\[TAG-(\\d+)\\]/g;\n\t\treturn content.replaceAll(placeholderRegex, (match: string) => replacements.get(match) || '');\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (!(childStart < tagsMap.length)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttag.children = [];\n\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\ttag.children.push(childTag);\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (tag.isClosing) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\tif (!shouldInclude) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t}\n\t\t\tresult.push(tag);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) {\n\t\t\treturn '';\n\t\t}\n\t\tlet input = String(bbcInput);\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\tif (this.config.newline) {\n\t\t\tif (this.config.paragraph) {\n\t\t\t\tinput = input.replace(this.regex.newline, '</p><p>');\n\t\t\t} else {\n\t\t\t\tinput = input.replace(this.regex.newline, '<br/>');\n\t\t\t}\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn input;\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=', 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/, 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\traw: tag,\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\t// Replace tags with placeholders - optimized for different input sizes\n\t\tif (tags.length <= 3) {\n\t\t\t// Fast path for few tags - direct replacement is faster\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tinput = input.replace(tag, '[TAG-' + i + ']');\n\t\t\t}\n\t\t} else {\n\t\t\t// Batch replacement for many tags\n\t\t\tconst tagPositions = [];\n\t\t\tlet lastIndex = 0;\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tconst pos = input.indexOf(tag, lastIndex);\n\t\t\t\tif (pos === -1) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttagPositions.push({ pos, tag: tag, placeholder: '[TAG-' + i + ']' });\n\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t}\n\n\t\t\tif (tagPositions.length > 0) {\n\t\t\t\tconst parts = [];\n\t\t\t\tlet lastPos = 0;\n\t\t\t\tfor (const { pos, tag, placeholder } of tagPositions) {\n\t\t\t\t\tparts.push(input.slice(lastPos, pos));\n\t\t\t\t\tparts.push(placeholder);\n\t\t\t\t\tlastPos = pos + tag.length;\n\t\t\t\t}\n\t\t\t\tparts.push(input.slice(lastPos));\n\t\t\t\tinput = parts.join('');\n\t\t\t}\n\t\t}\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\t// put back all non-found matches?\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\tinput = input.replace(this.regex.placeholders, '');\n\t\t}\n\t\treturn input;\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ya-bbcode.mjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n// NUL is stripped from input so placeholders can't be forged; a distinct end stops `\\0 1 \\0 2 \\0` reading as placeholder 2.\nconst PLACEHOLDER_START = '\\0';\nconst PLACEHOLDER_END = ';';\n\nfunction placeholder(index: number): string {\n\treturn PLACEHOLDER_START + index + PLACEHOLDER_END;\n}\n\n// String#replace expands `$&`, `$'` and `` $` `` in string replacements, which user-controlled attributes could abuse.\nfunction replaceFirst(content: string, search: string, replacement: string): string {\n\tconst index = content.indexOf(search);\n\tif (index === -1) {\n\t\treturn content;\n\t}\n\treturn content.slice(0, index) + replacement + content.slice(index + search.length);\n}\n\nconst sanitizedEntities = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'"': '\"',\n\t''': '\\'',\n};\n\nfunction unescapeSanitized(value: string): string {\n\treturn value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity: string) => sanitizedEntities[entity as keyof typeof sanitizedEntities]);\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n// Mirrors the WHATWG URL parser, which trims C0 controls and spaces and drops tabs/newlines, so `java\\tscript:` can't slip through.\nfunction normalizeUrl(url: string): string {\n\tlet start = 0;\n\tlet end = url.length;\n\t// Comparing against ' ' matches every character from U+0000 to U+0020\n\twhile (start < end && url.charAt(start) <= ' ') {\n\t\tstart++;\n\t}\n\twhile (end > start && url.charAt(end - 1) <= ' ') {\n\t\tend--;\n\t}\n\treturn url.slice(start, end).replaceAll(/[\\t\\n\\r]/g, '');\n}\n\nconst defaultAllowedSchemes = ['http', 'https', 'mailto', 'ftp', 'ftps'];\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n * Returns undefined for empty or disallowed URLs\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string | undefined {\n\tconst trimmed = normalizeUrl(String(url));\n\tif (!trimmed) {\n\t\treturn undefined;\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? '#')}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(this.#unsanitize(attr))}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\t// Leftover placeholders are resolved after this check, so a removed one could hide a scheme like `[/b]javascript:`\n\t\t\t\tconst url = this.#unsanitize(content.replaceAll(this.regex.placeholders, ''));\n\t\t\t\tconst src = sanitizeUrl(url, this.config.allowedSchemes ?? defaultAllowedSchemes);\n\t\t\t\tif (src === undefined) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(src)}\" alt=\"${escapeHtmlAttr(this.#unsanitize(attr))}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+\\])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\0(\\d+);/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst closeTag = placeholder(tag.closing.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = placeholder(tag.closing.index);\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: [...defaultAllowedSchemes],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t// Tag names come from user input, so inherited keys like `constructor` must not resolve to Object.prototype members.\n\t#getTag(name: string): TagDefinition | undefined {\n\t\tif (!Object.hasOwn(this.tags, name)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn this.tags[name];\n\t}\n\n\t#convertNewlines(input: string): string {\n\t\tif (!this.config.newline) {\n\t\t\treturn input;\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\treturn input.replace(this.regex.newline, '</p><p>');\n\t\t}\n\t\treturn input.replace(this.regex.newline, '<br/>');\n\t}\n\n\t#applyLayout(input: string): string {\n\t\tinput = this.#convertNewlines(input);\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\t\treturn input;\n\t}\n\n\t// Built-in attribute tags escape for themselves, so undo parse()'s sanitizing first to avoid double-escaping `&`.\n\t#unsanitize(value: string): string {\n\t\tif (!this.config.sanitizeHtml) {\n\t\t\treturn value;\n\t\t}\n\t\treturn unescapeSanitized(value);\n\t}\n\n\t// Tags inside an ignore tag are plain text, so they must never get a placeholder that could pair with a tag outside it.\n\t#findIgnoredTags(tagsMap: TagItem[]): Set<number> {\n\t\tconst ignored = new Set<number>();\n\t\tlet ignoreModule: string | undefined;\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (ignoreModule === undefined) {\n\t\t\t\tif (!tag.isClosing && this.#getTag(tag.module)?.type === 'ignore') {\n\t\t\t\t\tignoreModule = tag.module;\n\t\t\t\t}\n\t\t\t} else if (tag.isClosing && tag.module === ignoreModule) {\n\t\t\t\tignoreModule = undefined;\n\t\t\t} else {\n\t\t\t\tignored.add(tag.index);\n\t\t\t}\n\t\t}\n\t\treturn ignored;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.#getTag(tag.module);\n\t\t\t\tif (module) {\n\t\t\t\t\tif (module.type === 'content' || module.type === 'ignore') {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (module.type === 'replace' && (module as ReplaceTag).autoClose) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn tag.children.some(child => hasSpecialType(child));\n\t\t\t};\n\t\t\tconst hasSpecialTypes = tagsMap.some(tag => hasSpecialType(tag));\n\n\t\t\t// If only replace-type tags without autoClose, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tconst replaceModule = module as ReplaceTag;\n\n\t\t\t\t// Step 1: Process opening tag\n\t\t\t\tlet open = replaceModule.open;\n\t\t\t\tif (typeof(open) === 'function') {\n\t\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t\t}\n\n\t\t\t\t// Step 2: Process children (before closing, so parent's close placeholder is still available)\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tconst closingIndex = tag.closing?.index;\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, closingIndex);\n\t\t\t\t}\n\n\t\t\t\t// Step 3: Handle autoClose for tags without explicit closing\n\t\t\t\tif (replaceModule.autoClose && !tag.closing) {\n\t\t\t\t\tlet autoClose = replaceModule.autoClose;\n\t\t\t\t\tif (typeof(autoClose) === 'function') {\n\t\t\t\t\t\tautoClose = autoClose(tag.attr, tag.attrs);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\t\t\t\t\tlet insertBefore: string | undefined;\n\t\t\t\t\tfor (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {\n\t\t\t\t\t\tconst sibling = tagsMap[siblingIndex];\n\t\t\t\t\t\tif (sibling?.module === tag.module) {\n\t\t\t\t\t\t\tinsertBefore = placeholder(sibling.index);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (insertBefore === undefined && parentClosingTagIndex !== undefined) {\n\t\t\t\t\t\tinsertBefore = placeholder(parentClosingTagIndex);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (insertBefore && autoClose) {\n\t\t\t\t\t\tcontent = replaceFirst(content, insertBefore, autoClose + insertBefore);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Step 4: Process closing tag (after children)\n\t\t\t\tlet close = replaceModule.close;\n\t\t\t\tif (typeof(close) === 'function') {\n\t\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (close && tag.closing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t\t}\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\t// Resolve children first so replace() receives final HTML it can escape, rather than placeholders that expand into raw HTML after escaping\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, tag.closing?.index);\n\t\t\t\t}\n\t\t\t\tcontent = this.contentModules.content(tag, module as ContentTag, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module as IgnoreTag, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set(placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set(placeholder(tag.closing.index), close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tfor (const child of tag.children) {\n\t\t\t\tprocessTag(child);\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Unmatched placeholders are kept so parse() handles them per cleanUnmatchable\n\t\treturn content.replaceAll(this.regex.placeholders, (match: string) => replacements.get(match) ?? match);\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (!(childStart < tagsMap.length)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttag.children = [];\n\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\ttag.children.push(childTag);\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (tag.isClosing) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\tif (!shouldInclude) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t}\n\t\t\tresult.push(tag);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) {\n\t\t\treturn '';\n\t\t}\n\t\tlet input = String(bbcInput).replaceAll(PLACEHOLDER_START, '');\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn this.#applyLayout(input);\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=', 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/, 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\t// Layout runs after placeholders go in, so restored tag text needs its newlines converted to match the text around it\n\t\t\t\traw: this.#convertNewlines(tag),\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\tconst ignored = this.#findIgnoredTags(tagsMap);\n\t\ttagsMap = tagsMap.filter(tag => !ignored.has(tag.index));\n\n\t\t// Ignored tags are still located so the search moves past them, since their text can contain a later tag's text\n\t\tconst parts: string[] = [];\n\t\tlet searchFrom = 0;\n\t\tlet copiedUpTo = 0;\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst pos = input.indexOf(tag, searchFrom);\n\t\t\tif (pos === -1) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsearchFrom = pos + tag.length;\n\t\t\tif (ignored.has(i)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparts.push(input.slice(copiedUpTo, pos), placeholder(i));\n\t\t\tcopiedUpTo = searchFrom;\n\t\t}\n\t\tparts.push(input.slice(copiedUpTo));\n\t\tinput = parts.join('');\n\t\t// Only after placeholders are in, so a tag spanning lines still matches its raw text\n\t\tinput = this.#applyLayout(input);\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\t// Placeholders are internal markers, so leftovers become nothing or the original BBCode, never leak\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\treturn input.replaceAll(this.regex.placeholders, '');\n\t\t}\n\t\t// Unknown tags are restored before content tags escape their output, but leftovers land after, possibly inside an attribute, so raw input must be escaped here\n\t\treturn input.replaceAll(this.regex.placeholders, (_match: string, index: string) => {\n\t\t\tlet raw = tags[Number(index)] ?? '';\n\t\t\tif (!this.config.sanitizeHtml) {\n\t\t\t\traw = escapeHtmlAttr(raw);\n\t\t\t}\n\t\t\treturn this.#convertNewlines(raw);\n\t\t});\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ya-bbcode",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.2",
|
|
4
4
|
"description": "Yet another BBCode Parser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bbc",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"lint:ci": "eslint .",
|
|
42
42
|
"prepublishOnly": "npm run build",
|
|
43
43
|
"test": "npm run check-types && vitest run && npm run test:imports",
|
|
44
|
+
"test:fuzz": "vitest run --config vitest.fuzz.config.ts",
|
|
44
45
|
"test:imports": "node test-imports/test-esm.mjs && node test-imports/test-cjs.cjs",
|
|
45
46
|
"test:unit": "vitest run"
|
|
46
47
|
},
|
|
@@ -52,6 +53,8 @@
|
|
|
52
53
|
"@types/node": "26.6.6",
|
|
53
54
|
"@vitest/coverage-v8": "5.1.0",
|
|
54
55
|
"eslint": "10.14.0",
|
|
56
|
+
"fast-check": "4.10.2",
|
|
57
|
+
"parse5": "8.1.1",
|
|
55
58
|
"semantic-release": "25.0.9",
|
|
56
59
|
"tinybench": "6.2.3",
|
|
57
60
|
"tsdown": "0.23.7",
|