ya-bbcode 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +4 -2
- package/dist/ya-bbcode.cjs +98 -64
- package/dist/ya-bbcode.cjs.map +1 -1
- package/dist/ya-bbcode.d.cts +6 -4
- package/dist/ya-bbcode.d.mts +6 -4
- package/dist/ya-bbcode.mjs +84 -49
- package/dist/ya-bbcode.mjs.map +1 -1
- package/package.json +15 -17
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2018 Nodecraft Inc.
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Nodecraft Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -3,8 +3,7 @@ Yet another BBCode parser.
|
|
|
3
3
|
`npm install ya-bbcode --save`
|
|
4
4
|
|
|
5
5
|
[](https://badge.fury.io/js/ya-bbcode)
|
|
6
|
-
[](https://coveralls.io/github/nodecraft/ya-bbcode)
|
|
6
|
+
[](https://github.com/nodecraft/ya-bbcode/actions/workflows/ci.yml)
|
|
8
7
|
|
|
9
8
|
## Usage
|
|
10
9
|
|
|
@@ -37,3 +36,6 @@ parser.clearTags();
|
|
|
37
36
|
- **Zero dependencies**: Lightweight with no external dependencies
|
|
38
37
|
- **Correct parsing**: Parent nodes are parsed before children in nested format
|
|
39
38
|
- **Customizable**: Easy to add or override tags
|
|
39
|
+
|
|
40
|
+
### Changelog
|
|
41
|
+
Release notes are published on [GitHub Releases](https://github.com/nodecraft/ya-bbcode/releases). Notes for 5.0.0 and earlier are in [CHANGELOG.md](CHANGELOG.md).
|
package/dist/ya-bbcode.cjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/ya-bbcode.ts
|
|
1
|
+
// src/ya-bbcode.ts
|
|
3
2
|
/**
|
|
4
3
|
* Escapes HTML attribute values to prevent XSS
|
|
5
4
|
* Encodes quotes, angle brackets, and other special characters
|
|
@@ -46,7 +45,7 @@ function parseTagAttributes(tagContent) {
|
|
|
46
45
|
const parts = tagContent.split(/\s+/);
|
|
47
46
|
const attrs = Object.create(null);
|
|
48
47
|
let firstValue = "";
|
|
49
|
-
const dangerousKeys = new Set([
|
|
48
|
+
const dangerousKeys = /* @__PURE__ */ new Set([
|
|
50
49
|
"__proto__",
|
|
51
50
|
"constructor",
|
|
52
51
|
"prototype"
|
|
@@ -139,6 +138,11 @@ var yabbcode = class {
|
|
|
139
138
|
open: "<code>",
|
|
140
139
|
close: "</code>"
|
|
141
140
|
},
|
|
141
|
+
"hr": {
|
|
142
|
+
type: "replace",
|
|
143
|
+
open: "<hr/>",
|
|
144
|
+
close: null
|
|
145
|
+
},
|
|
142
146
|
"strike": {
|
|
143
147
|
type: "replace",
|
|
144
148
|
open: "<span class=\"yabbcode-strike\">",
|
|
@@ -162,7 +166,8 @@ var yabbcode = class {
|
|
|
162
166
|
"*": {
|
|
163
167
|
type: "replace",
|
|
164
168
|
open: "<li>",
|
|
165
|
-
close:
|
|
169
|
+
close: "</li>",
|
|
170
|
+
autoClose: "</li>"
|
|
166
171
|
},
|
|
167
172
|
"img": {
|
|
168
173
|
type: "content",
|
|
@@ -174,28 +179,28 @@ var yabbcode = class {
|
|
|
174
179
|
"noparse": { type: "ignore" }
|
|
175
180
|
};
|
|
176
181
|
regex = {
|
|
177
|
-
tags: /(\[[^\]^]
|
|
182
|
+
tags: /(\[[^\]^]+\])/g,
|
|
178
183
|
newline: /\r\n|\r|\n/g,
|
|
179
|
-
placeholders: /\[TAG-[1-9]
|
|
184
|
+
placeholders: /\[TAG-[1-9]+\]/g
|
|
180
185
|
};
|
|
181
186
|
config;
|
|
182
187
|
contentModules = {
|
|
183
|
-
replace: (tag, module
|
|
184
|
-
let open = module
|
|
185
|
-
let close = module
|
|
188
|
+
replace: (tag, module, content) => {
|
|
189
|
+
let open = module.open;
|
|
190
|
+
let close = module.close;
|
|
186
191
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
187
192
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
188
193
|
if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
|
|
189
194
|
if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
|
|
190
195
|
return content;
|
|
191
196
|
},
|
|
192
|
-
content: (tag, module
|
|
197
|
+
content: (tag, module, content) => {
|
|
193
198
|
if (!tag.closing) return content;
|
|
194
199
|
const openTag = "[TAG-" + tag.index + "]";
|
|
195
200
|
const closeTag = "[TAG-" + tag.closing.index + "]";
|
|
196
201
|
const start = content.indexOf(openTag);
|
|
197
202
|
const end = content.indexOf(closeTag);
|
|
198
|
-
let replace = module
|
|
203
|
+
let replace = module.replace;
|
|
199
204
|
const innerContent = content.slice(start + openTag.length, end);
|
|
200
205
|
if (typeof replace === "function") replace = replace(tag.attr, innerContent, tag.attrs);
|
|
201
206
|
const contentStart = content.slice(0, Math.max(0, start));
|
|
@@ -246,48 +251,81 @@ var yabbcode = class {
|
|
|
246
251
|
}
|
|
247
252
|
return content;
|
|
248
253
|
}
|
|
249
|
-
#contentLoop(tagsMap, content) {
|
|
254
|
+
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
250
255
|
if (tagsMap.length > 50) {
|
|
251
|
-
|
|
252
|
-
const module
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
const hasSpecialType = (tag) => {
|
|
257
|
+
const module = this.tags[tag.module];
|
|
258
|
+
if (module) {
|
|
259
|
+
if (module.type === "content" || module.type === "ignore") return true;
|
|
260
|
+
if (module.type === "replace" && module.autoClose) return true;
|
|
261
|
+
}
|
|
262
|
+
return tag.children.some((child) => hasSpecialType(child));
|
|
263
|
+
};
|
|
264
|
+
if (!tagsMap.some((tag) => hasSpecialType(tag))) return this.#contentLoopOptimized(tagsMap, content);
|
|
255
265
|
}
|
|
256
|
-
for (
|
|
257
|
-
|
|
258
|
-
if (!
|
|
266
|
+
for (let i = 0; i < tagsMap.length; i++) {
|
|
267
|
+
const tag = tagsMap[i];
|
|
268
|
+
if (!tag) continue;
|
|
269
|
+
let module = this.tags[tag.module];
|
|
270
|
+
if (!module) module = {
|
|
259
271
|
type: "replace",
|
|
260
272
|
open: tag.raw,
|
|
261
273
|
close: tag.closing ? tag.closing.raw : ""
|
|
262
274
|
};
|
|
263
|
-
if (!this.contentModules[module
|
|
264
|
-
if (module
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
275
|
+
if (!this.contentModules[module.type]) throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
|
|
276
|
+
if (module.type === "replace") {
|
|
277
|
+
const replaceModule = module;
|
|
278
|
+
let open = replaceModule.open;
|
|
279
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
280
|
+
if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
|
|
281
|
+
if (tag.children.length > 0) {
|
|
282
|
+
const closingIndex = tag.closing?.index;
|
|
283
|
+
content = this.#contentLoop(tag.children, content, closingIndex);
|
|
284
|
+
}
|
|
285
|
+
if (replaceModule.autoClose && !tag.closing) {
|
|
286
|
+
let autoClose = replaceModule.autoClose;
|
|
287
|
+
if (typeof autoClose === "function") autoClose = autoClose(tag.attr, tag.attrs);
|
|
288
|
+
let insertBefore;
|
|
289
|
+
for (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {
|
|
290
|
+
const sibling = tagsMap[siblingIndex];
|
|
291
|
+
if (sibling?.module === tag.module) {
|
|
292
|
+
insertBefore = "[TAG-" + sibling.index + "]";
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore = "[TAG-" + parentClosingTagIndex + "]";
|
|
297
|
+
if (insertBefore && autoClose) content = content.replace(insertBefore, autoClose + insertBefore);
|
|
298
|
+
}
|
|
299
|
+
let close = replaceModule.close;
|
|
300
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
301
|
+
if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
|
|
302
|
+
} else if (module.type === "content") {
|
|
303
|
+
if (tag.children.length > 0) content = this.#contentLoop(tag.children, content, tag.closing?.index);
|
|
304
|
+
content = this.contentModules.content(tag, module, content);
|
|
305
|
+
} else if (module.type === "ignore") content = this.contentModules.ignore(tag, module, content);
|
|
268
306
|
}
|
|
269
307
|
return content;
|
|
270
308
|
}
|
|
271
309
|
#contentLoopOptimized(tagsMap, content) {
|
|
272
310
|
const replacements = /* @__PURE__ */ new Map();
|
|
273
311
|
const processTag = (tag) => {
|
|
274
|
-
let module
|
|
275
|
-
if (!module
|
|
312
|
+
let module = this.tags[tag.module];
|
|
313
|
+
if (!module) module = {
|
|
276
314
|
type: "replace",
|
|
277
315
|
open: tag.raw,
|
|
278
316
|
close: tag.closing ? tag.closing.raw : ""
|
|
279
317
|
};
|
|
280
|
-
if (module
|
|
281
|
-
let open = module
|
|
282
|
-
let close = module
|
|
318
|
+
if (module.type !== "replace") return;
|
|
319
|
+
let open = module.open;
|
|
320
|
+
let close = module.close;
|
|
283
321
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
284
322
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
285
323
|
if (open && !tag.isClosing) replacements.set("[TAG-" + tag.index + "]", open);
|
|
286
324
|
if (close && tag.closing) replacements.set("[TAG-" + tag.closing.index + "]", close);
|
|
287
|
-
|
|
325
|
+
for (const child of tag.children) processTag(child);
|
|
288
326
|
};
|
|
289
327
|
for (const tag of tagsMap) processTag(tag);
|
|
290
|
-
return content.replaceAll(/\[TAG-(\d+)]/g, (match) => replacements.get(match) || "");
|
|
328
|
+
return content.replaceAll(/\[TAG-(\d+)\]/g, (match) => replacements.get(match) || "");
|
|
291
329
|
}
|
|
292
330
|
#tagLoop(tagsMap, parent) {
|
|
293
331
|
const stack = [];
|
|
@@ -308,10 +346,9 @@ var yabbcode = class {
|
|
|
308
346
|
openTag.children = [];
|
|
309
347
|
for (let childIndex = childStart; childIndex < i; childIndex++) {
|
|
310
348
|
const childTag = tagsMap[childIndex];
|
|
311
|
-
if (childTag && !childTag.isClosing && childTag.parent === null)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
349
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
350
|
+
childTag.parent = openTag.index;
|
|
351
|
+
openTag.children.push(childTag);
|
|
315
352
|
}
|
|
316
353
|
}
|
|
317
354
|
stack.splice(stackIndex, 1);
|
|
@@ -326,23 +363,21 @@ var yabbcode = class {
|
|
|
326
363
|
for (const tag of stack) {
|
|
327
364
|
tag.matchTag = false;
|
|
328
365
|
const childStart = (tag.stackIndex ?? 0) + 1;
|
|
329
|
-
if (childStart < tagsMap.length)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
}
|
|
366
|
+
if (!(childStart < tagsMap.length)) continue;
|
|
367
|
+
tag.children = [];
|
|
368
|
+
for (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {
|
|
369
|
+
const childTag = tagsMap[childIndex];
|
|
370
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
371
|
+
childTag.parent = tag.index;
|
|
372
|
+
tag.children.push(childTag);
|
|
338
373
|
}
|
|
339
374
|
}
|
|
340
375
|
const result = [];
|
|
341
|
-
for (const tag of tagsMap)
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
376
|
+
for (const tag of tagsMap) {
|
|
377
|
+
if (tag.isClosing) continue;
|
|
378
|
+
if (!(parent === void 0 && tag.parent === null || parent !== void 0 && tag.parent === parent)) continue;
|
|
379
|
+
if (tag.children.length > 0) tag.children = this.#tagLoop(tag.children, tag.index);
|
|
380
|
+
result.push(tag);
|
|
346
381
|
}
|
|
347
382
|
return result;
|
|
348
383
|
}
|
|
@@ -375,8 +410,10 @@ var yabbcode = class {
|
|
|
375
410
|
}
|
|
376
411
|
let tagsMap = [];
|
|
377
412
|
const tags = String(input).match(this.regex.tags);
|
|
378
|
-
if (this.config.newline)
|
|
379
|
-
|
|
413
|
+
if (this.config.newline) {
|
|
414
|
+
if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
|
|
415
|
+
else input = input.replace(this.regex.newline, "<br/>");
|
|
416
|
+
}
|
|
380
417
|
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
381
418
|
if (!tags || tags.length === 0) return input;
|
|
382
419
|
for (const [i, tag] of tags.entries()) {
|
|
@@ -394,10 +431,10 @@ var yabbcode = class {
|
|
|
394
431
|
attrs: {}
|
|
395
432
|
};
|
|
396
433
|
} else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
|
|
397
|
-
moduleName = (contentToParse.split("=")[0] ?? "").toLowerCase();
|
|
434
|
+
moduleName = (contentToParse.split("=", 1)[0] ?? "").toLowerCase();
|
|
398
435
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
399
436
|
} else {
|
|
400
|
-
moduleName = (contentToParse.split(/\s
|
|
437
|
+
moduleName = (contentToParse.split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
401
438
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
402
439
|
}
|
|
403
440
|
const item = {
|
|
@@ -419,14 +456,13 @@ var yabbcode = class {
|
|
|
419
456
|
let lastIndex = 0;
|
|
420
457
|
for (const [i, tag] of tags.entries()) {
|
|
421
458
|
const pos = input.indexOf(tag, lastIndex);
|
|
422
|
-
if (pos
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
459
|
+
if (pos === -1) continue;
|
|
460
|
+
tagPositions.push({
|
|
461
|
+
pos,
|
|
462
|
+
tag,
|
|
463
|
+
placeholder: "[TAG-" + i + "]"
|
|
464
|
+
});
|
|
465
|
+
lastIndex = pos + tag.length;
|
|
430
466
|
}
|
|
431
467
|
if (tagPositions.length > 0) {
|
|
432
468
|
const parts = [];
|
|
@@ -446,8 +482,6 @@ var yabbcode = class {
|
|
|
446
482
|
return input;
|
|
447
483
|
}
|
|
448
484
|
};
|
|
449
|
-
var ya_bbcode_default = yabbcode;
|
|
450
485
|
|
|
451
|
-
|
|
452
|
-
module.exports = ya_bbcode_default;
|
|
486
|
+
module.exports = yabbcode;
|
|
453
487
|
//# sourceMappingURL=ya-bbcode.cjs.map
|
package/dist/ya-bbcode.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.cjs","names":["attrEntities: { [key: string]: string; }","attrs: TagAttributes","module","#ignoreLoop","#contentLoopOptimized","#contentLoop","#tagLoop","tagsMap: TagItem[]","moduleName: string","parsedAttrs: { attr: string; attrs: TagAttributes; }","item: TagItem"],"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}\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) { return '#'; }\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) { continue; }\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'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: null,\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) { return content; }\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): 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 (most common case for large docs)\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content or ignore type tags\n\t\t\tconst hasSpecialTypes = tagsMap.some((tag) => {\n\t\t\t\tconst module = this.tags[tag.module];\n\t\t\t\treturn module && (module.type === 'content' || module.type === 'ignore');\n\t\t\t});\n\n\t\t\t// If only replace-type tags, 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 (const tag of tagsMap) {\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\t\t\tif (module.type === 'replace') {\n\t\t\t\tcontent = this.contentModules.replace(tag, module, content);\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\tcontent = this.contentModules.content(tag, module, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module, content);\n\t\t\t}\n\t\t\tif (tag.children.length > 0 && module.type !== 'ignore') {\n\t\t\t\tcontent = this.#contentLoop(tag.children, 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') { return; }\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\tif (tag.children.length > 0) {\n\t\t\t\tfor (const child of tag.children) {\n\t\t\t\t\tprocessTag(child);\n\t\t\t\t}\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) { continue; }\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) { continue; }\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\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t\t}\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\ttag.children = [];\n\t\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\tif (childTag && !childTag.isClosing && childTag.parent === null) {\n\t\t\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\t\t\ttag.children.push(childTag);\n\t\t\t\t\t}\n\t\t\t\t}\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\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\t\tif (shouldInclude) {\n\t\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t\t\t}\n\t\t\t\t\tresult.push(tag);\n\t\t\t\t}\n\t\t\t}\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) { return ''; }\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('=')[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+/)[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\ttagPositions.push({ pos, tag: tag, placeholder: '[TAG-' + i + ']' });\n\t\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t\t}\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/**\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":";;;;;AA0DA,SAAS,eAAe,OAAuB;CAC9C,MAAM,eAA2C;EAChD,KAAK;EACL,KAAK;EACL,KAAK;EACL,MAAK;EACL,KAAM;CACP;CACA,OAAO,OAAO,KAAK,CAAC,CAAC,WAAW,aAAa,SAAiB,aAAa,SAAS,IAAI;AACzF;;;;;AAMA,SAAS,YAAY,KAAa,gBAAkC;CACnE,MAAM,UAAU,OAAO,GAAG,CAAC,CAAC,KAAK;CACjC,IAAI,CAAC,SACJ,OAAO;CAIR,IAAI,aAAa,KAAK,OAAO,GAC5B,OAAO;CAIR,IAAI,QAAQ,WAAW,GAAG,GACzB,OAAO;CAIR,MAAM,cAAc,uBAAuB,KAAK,QAAQ,YAAY,CAAC;CACrE,IAAI,aAAa;EAChB,MAAM,SAAS,YAAY,MAAM;EACjC,IAAI,CAAC,eAAe,SAAS,MAAM,GAElC,OAAO;CAET;CAEA,OAAO;AACR;;;;;;;AAQA,SAAS,mBAAmB,YAA6D;CAExF,MAAM,aAAa,WAAW,QAAQ,GAAG;CACzC,MAAM,cAAc,WAAW,QAAQ,GAAG;CAG1C,IAAI,eAAe,MAAO,gBAAgB,MAAM,cAAc,YAE7D,OAAO;EACN,MAFa,WAAW,MAAM,GAEpB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;EAC7B,OAAO,CAAC;CACT;CAID,MAAM,QAAQ,WAAW,MAAM,KAAK;CAEpC,MAAM,QAAuB,OAAO,OAAO,IAAI;CAC/C,IAAI,aAAa;CAGjB,MAAM,gCAAgB,IAAI,IAAI;EAAC;EAAa;EAAe;CAAW,CAAC;CAEvE,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MACJ;EAGD,IAAI,KAAK,SAAS,GAAG,GAAG;GACvB,MAAM,UAAU,KAAK,QAAQ,GAAG;GAChC,MAAM,MAAM,KAAK,MAAM,GAAG,OAAO;GACjC,MAAM,QAAQ,KAAK,MAAM,UAAU,CAAC;GAGpC,IAAI,cAAc,IAAI,IAAI,YAAY,CAAC,GACtC;GAGD,MAAM,OAAO;GAEb,IAAI,CAAC,cAAc,OAClB,aAAa;EAEf,OAAO;GAEN,IAAI,cAAc,IAAI,KAAK,YAAY,CAAC,GACvC;GAED,MAAM,QAAQ;EACf;CACD;CAEA,OAAO;EAAE,MAAM;EAAY;CAAM;AAClC;AAEA,IAAM,WAAN,MAAe;CACd,OAAgB;EACf,OAAO;GACN,MAAM;GACN,OAAM,SAAQ,YAAY,eAAe,YAAY,MAAM,KAAK,OAAO,kBAAkB;IAAC;IAAQ;IAAS;IAAU;IAAO;GAAM,CAAC,CAAC,EAAE;GACtI,OAAO;EACR;EACA,SAAS;GACR,MAAM;GACN,OAAM,SAAQ,uBAAuB,eAAe,IAAI,EAAE;GAC1D,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,QAAQ;GACP,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,UAAU;GACT,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,WAAW;GACV,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,QAAQ;GACP,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,SAAS;GACR,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;GACP,WAAW;EACZ;EACA,OAAO;GACN,MAAM;GACN,UAAU,MAAM,YAAY;IAC3B,IAAI,CAAC,SACJ,OAAO;IAER,OAAO,aAAa,eAAe,OAAO,EAAE,SAAS,eAAe,IAAI,EAAE;GAC3E;EACD;EACA,WAAW,EACV,MAAM,SACP;CACD;CAEA,QAAQ;EACP,MAAM;EACN,SAAS;EACT,cAAc;CACf;CAEA;CAEA,iBAAiB;EAChB,UAAU,KAAc,QAAoB,YAA4B;GACvE,IAAI,OAAO,OAAO;GAClB,IAAI,QAAQ,OAAO;GACnB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;GAEhC,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;GAGlC,IAAI,QAAQ,CAAC,IAAI,WAEhB,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,KAAK,IAAI;GAE1D,IAAI,SAAS,IAAI,SAChB,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,QAAQ,KAAK,KAAK;GAEnE,OAAO;EACR;EACA,UAAU,KAAc,QAAoB,YAA4B;GACvE,IAAI,CAAC,IAAI,SACR,OAAO;GAER,MAAM,UAAU,UAAU,IAAI,QAAQ;GACtC,MAAM,WAAW,UAAU,IAAI,QAAQ,QAAQ;GAC/C,MAAM,QAAQ,QAAQ,QAAQ,OAAO;GACrC,MAAM,MAAM,QAAQ,QAAQ,QAAQ;GACpC,IAAI,UAAU,OAAO;GAErB,MAAM,eAAe,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG;GAC9D,IAAI,OAAO,YAAa,YACvB,UAAU,QAAQ,IAAI,MAAM,cAAc,IAAI,KAAK;GAGpD,MAAM,eAAe,QAAQ,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;GACxD,MAAM,aAAa,QAAQ,MAAM,MAAM,SAAS,MAAM;GAEtD,OAAO,eAAe,UAAU;EACjC;EACA,SAAS,KAAc,SAAoB,YAA4B;GACtE,MAAM,UAAU,UAAU,IAAI,QAAQ;GACtC,MAAM,QAAQ,QAAQ,QAAQ,OAAO;GACrC,IAAI,WAAW;GACf,IAAI,MAAM,QAAQ;GAClB,IAAI,IAAI,SAAS;IAChB,WAAW,UAAU,IAAI,QAAQ,QAAQ;IACzC,MAAM,QAAQ,QAAQ,QAAQ;GAC/B;GACA,IAAI,eAAe,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG;GAC5D,eAAe,KAAK,YAAY,IAAI,UAAU,YAAY;GAC1D,MAAM,eAAe,QAAQ,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;GACxD,MAAM,aAAa,QAAQ,MAAM,MAAM,SAAS,MAAM;GACtD,OAAO,eAAe,eAAe;EACtC;CACD;CAEA,YAAY,SAAiB,CAAC,GAAG;EAChC,KAAK,SAAS;GACb,SAAS;GACT,WAAW;GACX,kBAAkB;GAClB,cAAc;GACd,gBAAgB;IAAC;IAAQ;IAAS;IAAU;IAAO;GAAM;EAC1D;EACA,IAAI,OAAO,YAAY,QACtB,KAAK,OAAO,UAAU,OAAO;EAE9B,IAAI,OAAO,cAAc,QACxB,KAAK,OAAO,YAAY,OAAO;EAEhC,IAAI,OAAO,qBAAqB,QAC/B,KAAK,OAAO,mBAAmB,OAAO;EAEvC,IAAI,OAAO,iBAAiB,QAC3B,KAAK,OAAO,eAAe,OAAO;EAEnC,IAAI,OAAO,mBAAmB,QAC7B,KAAK,OAAO,iBAAiB,OAAO;CAEtC;CAEA,YAAY,SAAoB,SAAyB;EACxD,KAAK,MAAM,OAAO,SAAS;GAC1B,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,KAAK,IAAI,GAAG;GAC5D,IAAI,IAAI,SACP,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,QAAQ,KAAK,IAAI,QAAQ,GAAG;GAE7E,IAAI,IAAI,SAAS,SAAS,GACzB,UAAU,KAAK,YAAY,IAAI,UAAU,OAAO;EAElD;EACA,OAAO;CACR;CAEA,aAAa,SAAoB,SAAiB,uBAAwC;EAGzF,IAAI,QAAQ,SAAS,IAAI;GAKxB,MAAM,kBAAkB,QAA0B;IACjD,MAAM,SAAS,KAAK,KAAK,IAAI;IAC7B,IAAI,QAAQ;KACX,IAAI,OAAO,SAAS,aAAa,OAAO,SAAS,UAChD,OAAO;KAER,IAAI,OAAO,SAAS,aAAc,OAAsB,WACvD,OAAO;IAET;IACA,OAAO,IAAI,SAAS,MAAK,UAAS,eAAe,KAAK,CAAC;GACxD;GAIA,IAAI,CAHoB,QAAQ,MAAK,QAAO,eAAe,GAAG,CAG3C,GAClB,OAAO,KAAK,sBAAsB,SAAS,OAAO;EAEpD;EAEA,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACxC,MAAM,MAAM,QAAQ;GACpB,IAAI,CAAC,KACJ;GAGD,IAAI,SAAS,KAAK,KAAK,IAAI;GAC3B,IAAI,CAAC,QAEJ,SAAS;IACR,MAAM;IACN,MAAM,IAAI;IACV,OAAO,IAAI,UAAU,IAAI,QAAQ,MAAM;GACxC;GAED,IAAI,CAAC,KAAK,eAAe,OAAO,OAC/B,MAAM,IAAI,MAAM,qDAAqD,OAAO,OAAO,yBAAyB,IAAI,SAAS,GAAG;GAG7H,IAAI,OAAO,SAAS,WAAW;IAC9B,MAAM,gBAAgB;IAGtB,IAAI,OAAO,cAAc;IACzB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;IAEhC,IAAI,QAAQ,CAAC,IAAI,WAChB,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,KAAK,IAAI;IAI1D,IAAI,IAAI,SAAS,SAAS,GAAG;KAC5B,MAAM,eAAe,IAAI,SAAS;KAClC,UAAU,KAAK,aAAa,IAAI,UAAU,SAAS,YAAY;IAChE;IAGA,IAAI,cAAc,aAAa,CAAC,IAAI,SAAS;KAC5C,IAAI,YAAY,cAAc;KAC9B,IAAI,OAAO,cAAe,YACzB,YAAY,UAAU,IAAI,MAAM,IAAI,KAAK;KAI1C,IAAI;KACJ,KAAK,IAAI,eAAe,IAAI,GAAG,eAAe,QAAQ,QAAQ,gBAAgB;MAC7E,MAAM,UAAU,QAAQ;MACxB,IAAI,SAAS,WAAW,IAAI,QAAQ;OACnC,eAAe,UAAU,QAAQ,QAAQ;OACzC;MACD;KACD;KACA,IAAI,iBAAiB,UAAa,0BAA0B,QAC3D,eAAe,UAAU,wBAAwB;KAGlD,IAAI,gBAAgB,WACnB,UAAU,QAAQ,QAAQ,cAAc,YAAY,YAAY;IAElE;IAGA,IAAI,QAAQ,cAAc;IAC1B,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;IAElC,IAAI,SAAS,IAAI,SAChB,UAAU,QAAQ,QAAQ,UAAU,IAAI,QAAQ,QAAQ,KAAK,KAAK;GAEpE,OAAO,IAAI,OAAO,SAAS,WAAW;IAErC,IAAI,IAAI,SAAS,SAAS,GACzB,UAAU,KAAK,aAAa,IAAI,UAAU,SAAS,IAAI,SAAS,KAAK;IAEtE,UAAU,KAAK,eAAe,QAAQ,KAAK,QAAsB,OAAO;GACzE,OAAO,IAAI,OAAO,SAAS,UAC1B,UAAU,KAAK,eAAe,OAAO,KAAK,QAAqB,OAAO;EAExE;EAEA,OAAO;CACR;CAEA,sBAAsB,SAAoB,SAAyB;EAGlE,MAAM,+BAAe,IAAI,IAAoB;EAE7C,MAAM,cAAc,QAAiB;GACpC,IAAI,SAAS,KAAK,KAAK,IAAI;GAC3B,IAAI,CAAC,QACJ,SAAS;IACR,MAAM;IACN,MAAM,IAAI;IACV,OAAO,IAAI,UAAU,IAAI,QAAQ,MAAM;GACxC;GAID,IAAI,OAAO,SAAS,WACnB;GAGD,IAAI,OAAO,OAAO;GAClB,IAAI,QAAQ,OAAO;GACnB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;GAEhC,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;GAElC,IAAI,QAAQ,CAAC,IAAI,WAChB,aAAa,IAAI,UAAU,IAAI,QAAQ,KAAK,IAAI;GAEjD,IAAI,SAAS,IAAI,SAChB,aAAa,IAAI,UAAU,IAAI,QAAQ,QAAQ,KAAK,KAAK;GAI1D,KAAK,MAAM,SAAS,IAAI,UACvB,WAAW,KAAK;EAElB;EAGA,KAAK,MAAM,OAAO,SACjB,WAAW,GAAG;EAKf,OAAO,QAAQ,WAAW,mBAAmB,UAAkB,aAAa,IAAI,KAAK,KAAK,EAAE;CAC7F;CAEA,SAAS,SAAoB,QAA4B;EAExD,MAAM,QAAQ,CAAC;EACf,MAAM,4BAAY,IAAI,IAAI;EAE1B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACxC,MAAM,MAAM,QAAQ;GACpB,IAAI,CAAC,KACJ;GAGD,IAAI,IAAI,WAEP,KAAK,IAAI,aAAa,MAAM,SAAS,GAAG,cAAc,GAAG,cAAc;IACtE,MAAM,UAAU,MAAM;IACtB,IAAI,CAAC,SACJ;IAED,IAAI,QAAQ,WAAW,IAAI,UAAU,CAAC,UAAU,IAAI,QAAQ,KAAK,GAAG;KAEnE,QAAQ,WAAW,IAAI;KACvB,QAAQ,UAAU;KAClB,IAAI,WAAW,QAAQ;KACvB,UAAU,IAAI,QAAQ,KAAK;KAG3B,MAAM,cAAc,QAAQ,cAAc,KAAK;KAC/C,IAAI,aAAa,GAAG;MACnB,QAAQ,WAAW,CAAC;MACpB,KAAK,IAAI,aAAa,YAAY,aAAa,GAAG,cAAc;OAC/D,MAAM,WAAW,QAAQ;OACzB,IAAI,EAAE,YAAY,CAAC,SAAS,aAAa,SAAS,WAAW,OAC5D;OAGD,SAAS,SAAS,QAAQ;OAC1B,QAAQ,SAAS,KAAK,QAAQ;MAC/B;KACD;KAGA,MAAM,OAAO,YAAY,CAAC;KAC1B;IACD;GACD;QACM;IAEN,IAAI,aAAa;IACjB,MAAM,KAAK,GAAG;GACf;EACD;EAGA,KAAK,MAAM,OAAO,OAAO;GACxB,IAAI,WAAW;GACf,MAAM,cAAc,IAAI,cAAc,KAAK;GAC3C,IAAI,EAAE,aAAa,QAAQ,SAC1B;GAGD,IAAI,WAAW,CAAC;GAChB,KAAK,IAAI,aAAa,YAAY,aAAa,QAAQ,QAAQ,cAAc;IAC5E,MAAM,WAAW,QAAQ;IACzB,IAAI,EAAE,YAAY,CAAC,SAAS,aAAa,SAAS,WAAW,OAC5D;IAGD,SAAS,SAAS,IAAI;IACtB,IAAI,SAAS,KAAK,QAAQ;GAC3B;EACD;EAGA,MAAM,SAAS,CAAC;EAChB,KAAK,MAAM,OAAO,SAAS;GAC1B,IAAI,IAAI,WACP;GAKD,IAAI,EAFmB,WAAW,UAAa,IAAI,WAAW,QAC5D,WAAW,UAAa,IAAI,WAAW,SAExC;GAGD,IAAI,IAAI,SAAS,SAAS,GACzB,IAAI,WAAW,KAAK,SAAS,IAAI,UAAU,IAAI,KAAK;GAErD,OAAO,KAAK,GAAG;EAChB;EAEA,OAAO;CACR;CAEA,YAAkB;EACjB,KAAK,OAAO,CAAC;EACb,OAAO;CACR;CAEA,YAAY,KAAa,SAA8B;EACtD,MAAM,UAAU,OAAO,GAAG,CAAC,CAAC,YAAY;EAIxC,IAAI;GADmB;GAAa;GAAe;EACnC,CAAC,CAAC,SAAS,OAAO,GACjC,MAAM,IAAI,MAAM,4CAA4C,SAAS;EAGtE,KAAK,KAAK,WAAW;EACrB,OAAO;CACR;CAEA,MAAM,UAA6C;EAClD,IACC,OAAO,aAAc,aACjB,OAAO,aAAc,YAAY,OAAO,MAAM,OAAO,QAAQ,CAAC,GAElE,OAAO;EAER,IAAI,QAAQ,OAAO,QAAQ;EAC3B,IAAI,KAAK,OAAO,cAAc;GAE7B,MAAM,eAAe;IAAE,KAAK;IAAS,KAAK;IAAQ,KAAK;IAAQ,MAAK;IAAU,KAAM;GAAQ;GAC5F,QAAQ,MAAM,WAAW,aAAa,SAAiB,aAAa,KAAkC;EACvG;EAGA,IAAI,UAAqB,CAAC;EAE1B,MAAM,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI;EAEhD,IAAI,KAAK,OAAO,SAAS;GACxB,IAAI,KAAK,OAAO,WACf,QAAQ,MAAM,QAAQ,KAAK,MAAM,SAAS,SAAS;QAEnD,QAAQ,MAAM,QAAQ,KAAK,MAAM,SAAS,OAAO;EAEnD;EACA,IAAI,KAAK,OAAO,WACf,QAAQ,QAAQ,QAAQ;EAIzB,IAAI,CAAC,QAAQ,KAAK,WAAW,GAC5B,OAAO;EAGR,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GAAG;GACtC,MAAM,aAAa,IAAI,MAAM,GAAG,EAAE;GAClC,MAAM,YAAY,WAAW,WAAW,GAAG;GAC3C,MAAM,iBAAiB,YAAY,WAAW,MAAM,CAAC,IAAI;GAGzD,MAAM,aAAa,eAAe,QAAQ,GAAG;GAC7C,MAAM,cAAc,eAAe,QAAQ,GAAG;GAC9C,IAAI;GACJ,IAAI;GAGJ,IAAI,eAAe,MAAM,gBAAgB,IAAI;IAE5C,aAAa,eAAe,YAAY;IACxC,cAAc;KAAE,MAAM;KAAI,OAAO,CAAC;IAAE;GACrC,OAAO,IAAI,eAAe,MAAO,gBAAgB,MAAM,cAAc,YAAa;IAEjF,cAAc,eAAe,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,GAAE,CAAE,YAAY;IACjE,cAAc,mBAAmB,cAAc;GAChD,OAAO;IAEN,cAAc,eAAe,MAAM,OAAO,CAAC,CAAC,CAAC,MAAM,GAAE,CAAE,YAAY;IACnE,cAAc,mBAAmB,cAAc;GAChD;GAEA,MAAM,OAAgB;IACrB,OAAO;IACP,QAAQ;IACR;IACA,KAAK;IACL,MAAM,YAAY;IAClB,OAAO,YAAY;IACnB,UAAU,CAAC;IACX,QAAQ;IACR,UAAU;GACX;GAEA,QAAQ,KAAK,IAAI;EAClB;EAGA,IAAI,KAAK,UAAU,GAElB,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GACnC,QAAQ,MAAM,QAAQ,KAAK,UAAU,IAAI,GAAG;OAEvC;GAEN,MAAM,eAAe,CAAC;GACtB,IAAI,YAAY;GAChB,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GAAG;IACtC,MAAM,MAAM,MAAM,QAAQ,KAAK,SAAS;IACxC,IAAI,QAAQ,IACX;IAGD,aAAa,KAAK;KAAE;KAAU;KAAK,aAAa,UAAU,IAAI;IAAI,CAAC;IACnE,YAAY,MAAM,IAAI;GACvB;GAEA,IAAI,aAAa,SAAS,GAAG;IAC5B,MAAM,QAAQ,CAAC;IACf,IAAI,UAAU;IACd,KAAK,MAAM,EAAE,KAAK,KAAK,iBAAiB,cAAc;KACrD,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;KACpC,MAAM,KAAK,WAAW;KACtB,UAAU,MAAM,IAAI;IACrB;IACA,MAAM,KAAK,MAAM,MAAM,OAAO,CAAC;IAC/B,QAAQ,MAAM,KAAK,EAAE;GACtB;EACD;EAEA,UAAU,KAAK,SAAS,OAAO;EAE/B,QAAQ,KAAK,aAAa,SAAS,KAAK;EACxC,IAAI,KAAK,OAAO,kBACf,QAAQ,MAAM,QAAQ,KAAK,MAAM,cAAc,EAAE;EAElD,OAAO;CACR;AACD"}
|
package/dist/ya-bbcode.d.cts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/ya-bbcode.d.ts
|
|
2
1
|
interface Config {
|
|
3
2
|
newline?: boolean;
|
|
4
3
|
paragraph?: boolean;
|
|
@@ -13,6 +12,11 @@ interface ReplaceTag {
|
|
|
13
12
|
type: 'replace';
|
|
14
13
|
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
15
14
|
close: ((attr: string, attrs: TagAttributes) => string) | string | null;
|
|
15
|
+
/**
|
|
16
|
+
* HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).
|
|
17
|
+
* Useful for tags like [*] that don't have explicit closing tags in BBCode.
|
|
18
|
+
*/
|
|
19
|
+
autoClose?: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
16
20
|
}
|
|
17
21
|
interface ContentTag {
|
|
18
22
|
type: 'content';
|
|
@@ -57,6 +61,4 @@ declare class yabbcode {
|
|
|
57
61
|
registerTag(tag: string, options: TagDefinition): this;
|
|
58
62
|
parse(bbcInput: string | number | boolean): string;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
62
|
-
//# sourceMappingURL=ya-bbcode.d.cts.map
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
package/dist/ya-bbcode.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/ya-bbcode.d.ts
|
|
2
1
|
interface Config {
|
|
3
2
|
newline?: boolean;
|
|
4
3
|
paragraph?: boolean;
|
|
@@ -13,6 +12,11 @@ interface ReplaceTag {
|
|
|
13
12
|
type: 'replace';
|
|
14
13
|
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
15
14
|
close: ((attr: string, attrs: TagAttributes) => string) | string | null;
|
|
15
|
+
/**
|
|
16
|
+
* HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).
|
|
17
|
+
* Useful for tags like [*] that don't have explicit closing tags in BBCode.
|
|
18
|
+
*/
|
|
19
|
+
autoClose?: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
16
20
|
}
|
|
17
21
|
interface ContentTag {
|
|
18
22
|
type: 'content';
|
|
@@ -57,6 +61,4 @@ declare class yabbcode {
|
|
|
57
61
|
registerTag(tag: string, options: TagDefinition): this;
|
|
58
62
|
parse(bbcInput: string | number | boolean): string;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
62
|
-
//# sourceMappingURL=ya-bbcode.d.mts.map
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
package/dist/ya-bbcode.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/ya-bbcode.ts
|
|
2
2
|
/**
|
|
3
3
|
* Escapes HTML attribute values to prevent XSS
|
|
4
4
|
* Encodes quotes, angle brackets, and other special characters
|
|
@@ -45,7 +45,7 @@ function parseTagAttributes(tagContent) {
|
|
|
45
45
|
const parts = tagContent.split(/\s+/);
|
|
46
46
|
const attrs = Object.create(null);
|
|
47
47
|
let firstValue = "";
|
|
48
|
-
const dangerousKeys = new Set([
|
|
48
|
+
const dangerousKeys = /* @__PURE__ */ new Set([
|
|
49
49
|
"__proto__",
|
|
50
50
|
"constructor",
|
|
51
51
|
"prototype"
|
|
@@ -138,6 +138,11 @@ var yabbcode = class {
|
|
|
138
138
|
open: "<code>",
|
|
139
139
|
close: "</code>"
|
|
140
140
|
},
|
|
141
|
+
"hr": {
|
|
142
|
+
type: "replace",
|
|
143
|
+
open: "<hr/>",
|
|
144
|
+
close: null
|
|
145
|
+
},
|
|
141
146
|
"strike": {
|
|
142
147
|
type: "replace",
|
|
143
148
|
open: "<span class=\"yabbcode-strike\">",
|
|
@@ -161,7 +166,8 @@ var yabbcode = class {
|
|
|
161
166
|
"*": {
|
|
162
167
|
type: "replace",
|
|
163
168
|
open: "<li>",
|
|
164
|
-
close:
|
|
169
|
+
close: "</li>",
|
|
170
|
+
autoClose: "</li>"
|
|
165
171
|
},
|
|
166
172
|
"img": {
|
|
167
173
|
type: "content",
|
|
@@ -173,9 +179,9 @@ var yabbcode = class {
|
|
|
173
179
|
"noparse": { type: "ignore" }
|
|
174
180
|
};
|
|
175
181
|
regex = {
|
|
176
|
-
tags: /(\[[^\]^]
|
|
182
|
+
tags: /(\[[^\]^]+\])/g,
|
|
177
183
|
newline: /\r\n|\r|\n/g,
|
|
178
|
-
placeholders: /\[TAG-[1-9]
|
|
184
|
+
placeholders: /\[TAG-[1-9]+\]/g
|
|
179
185
|
};
|
|
180
186
|
config;
|
|
181
187
|
contentModules = {
|
|
@@ -245,14 +251,21 @@ var yabbcode = class {
|
|
|
245
251
|
}
|
|
246
252
|
return content;
|
|
247
253
|
}
|
|
248
|
-
#contentLoop(tagsMap, content) {
|
|
254
|
+
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
249
255
|
if (tagsMap.length > 50) {
|
|
250
|
-
|
|
256
|
+
const hasSpecialType = (tag) => {
|
|
251
257
|
const module = this.tags[tag.module];
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
if (module) {
|
|
259
|
+
if (module.type === "content" || module.type === "ignore") return true;
|
|
260
|
+
if (module.type === "replace" && module.autoClose) return true;
|
|
261
|
+
}
|
|
262
|
+
return tag.children.some((child) => hasSpecialType(child));
|
|
263
|
+
};
|
|
264
|
+
if (!tagsMap.some((tag) => hasSpecialType(tag))) return this.#contentLoopOptimized(tagsMap, content);
|
|
254
265
|
}
|
|
255
|
-
for (
|
|
266
|
+
for (let i = 0; i < tagsMap.length; i++) {
|
|
267
|
+
const tag = tagsMap[i];
|
|
268
|
+
if (!tag) continue;
|
|
256
269
|
let module = this.tags[tag.module];
|
|
257
270
|
if (!module) module = {
|
|
258
271
|
type: "replace",
|
|
@@ -260,10 +273,36 @@ var yabbcode = class {
|
|
|
260
273
|
close: tag.closing ? tag.closing.raw : ""
|
|
261
274
|
};
|
|
262
275
|
if (!this.contentModules[module.type]) throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
|
|
263
|
-
if (module.type === "replace")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
276
|
+
if (module.type === "replace") {
|
|
277
|
+
const replaceModule = module;
|
|
278
|
+
let open = replaceModule.open;
|
|
279
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
280
|
+
if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
|
|
281
|
+
if (tag.children.length > 0) {
|
|
282
|
+
const closingIndex = tag.closing?.index;
|
|
283
|
+
content = this.#contentLoop(tag.children, content, closingIndex);
|
|
284
|
+
}
|
|
285
|
+
if (replaceModule.autoClose && !tag.closing) {
|
|
286
|
+
let autoClose = replaceModule.autoClose;
|
|
287
|
+
if (typeof autoClose === "function") autoClose = autoClose(tag.attr, tag.attrs);
|
|
288
|
+
let insertBefore;
|
|
289
|
+
for (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {
|
|
290
|
+
const sibling = tagsMap[siblingIndex];
|
|
291
|
+
if (sibling?.module === tag.module) {
|
|
292
|
+
insertBefore = "[TAG-" + sibling.index + "]";
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore = "[TAG-" + parentClosingTagIndex + "]";
|
|
297
|
+
if (insertBefore && autoClose) content = content.replace(insertBefore, autoClose + insertBefore);
|
|
298
|
+
}
|
|
299
|
+
let close = replaceModule.close;
|
|
300
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
301
|
+
if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
|
|
302
|
+
} else if (module.type === "content") {
|
|
303
|
+
if (tag.children.length > 0) content = this.#contentLoop(tag.children, content, tag.closing?.index);
|
|
304
|
+
content = this.contentModules.content(tag, module, content);
|
|
305
|
+
} else if (module.type === "ignore") content = this.contentModules.ignore(tag, module, content);
|
|
267
306
|
}
|
|
268
307
|
return content;
|
|
269
308
|
}
|
|
@@ -283,10 +322,10 @@ var yabbcode = class {
|
|
|
283
322
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
284
323
|
if (open && !tag.isClosing) replacements.set("[TAG-" + tag.index + "]", open);
|
|
285
324
|
if (close && tag.closing) replacements.set("[TAG-" + tag.closing.index + "]", close);
|
|
286
|
-
|
|
325
|
+
for (const child of tag.children) processTag(child);
|
|
287
326
|
};
|
|
288
327
|
for (const tag of tagsMap) processTag(tag);
|
|
289
|
-
return content.replaceAll(/\[TAG-(\d+)]/g, (match) => replacements.get(match) || "");
|
|
328
|
+
return content.replaceAll(/\[TAG-(\d+)\]/g, (match) => replacements.get(match) || "");
|
|
290
329
|
}
|
|
291
330
|
#tagLoop(tagsMap, parent) {
|
|
292
331
|
const stack = [];
|
|
@@ -307,10 +346,9 @@ var yabbcode = class {
|
|
|
307
346
|
openTag.children = [];
|
|
308
347
|
for (let childIndex = childStart; childIndex < i; childIndex++) {
|
|
309
348
|
const childTag = tagsMap[childIndex];
|
|
310
|
-
if (childTag && !childTag.isClosing && childTag.parent === null)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
349
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
350
|
+
childTag.parent = openTag.index;
|
|
351
|
+
openTag.children.push(childTag);
|
|
314
352
|
}
|
|
315
353
|
}
|
|
316
354
|
stack.splice(stackIndex, 1);
|
|
@@ -325,23 +363,21 @@ var yabbcode = class {
|
|
|
325
363
|
for (const tag of stack) {
|
|
326
364
|
tag.matchTag = false;
|
|
327
365
|
const childStart = (tag.stackIndex ?? 0) + 1;
|
|
328
|
-
if (childStart < tagsMap.length)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
}
|
|
366
|
+
if (!(childStart < tagsMap.length)) continue;
|
|
367
|
+
tag.children = [];
|
|
368
|
+
for (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {
|
|
369
|
+
const childTag = tagsMap[childIndex];
|
|
370
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
371
|
+
childTag.parent = tag.index;
|
|
372
|
+
tag.children.push(childTag);
|
|
337
373
|
}
|
|
338
374
|
}
|
|
339
375
|
const result = [];
|
|
340
|
-
for (const tag of tagsMap)
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
376
|
+
for (const tag of tagsMap) {
|
|
377
|
+
if (tag.isClosing) continue;
|
|
378
|
+
if (!(parent === void 0 && tag.parent === null || parent !== void 0 && tag.parent === parent)) continue;
|
|
379
|
+
if (tag.children.length > 0) tag.children = this.#tagLoop(tag.children, tag.index);
|
|
380
|
+
result.push(tag);
|
|
345
381
|
}
|
|
346
382
|
return result;
|
|
347
383
|
}
|
|
@@ -374,8 +410,10 @@ var yabbcode = class {
|
|
|
374
410
|
}
|
|
375
411
|
let tagsMap = [];
|
|
376
412
|
const tags = String(input).match(this.regex.tags);
|
|
377
|
-
if (this.config.newline)
|
|
378
|
-
|
|
413
|
+
if (this.config.newline) {
|
|
414
|
+
if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
|
|
415
|
+
else input = input.replace(this.regex.newline, "<br/>");
|
|
416
|
+
}
|
|
379
417
|
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
380
418
|
if (!tags || tags.length === 0) return input;
|
|
381
419
|
for (const [i, tag] of tags.entries()) {
|
|
@@ -393,10 +431,10 @@ var yabbcode = class {
|
|
|
393
431
|
attrs: {}
|
|
394
432
|
};
|
|
395
433
|
} else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
|
|
396
|
-
moduleName = (contentToParse.split("=")[0] ?? "").toLowerCase();
|
|
434
|
+
moduleName = (contentToParse.split("=", 1)[0] ?? "").toLowerCase();
|
|
397
435
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
398
436
|
} else {
|
|
399
|
-
moduleName = (contentToParse.split(/\s
|
|
437
|
+
moduleName = (contentToParse.split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
400
438
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
401
439
|
}
|
|
402
440
|
const item = {
|
|
@@ -418,14 +456,13 @@ var yabbcode = class {
|
|
|
418
456
|
let lastIndex = 0;
|
|
419
457
|
for (const [i, tag] of tags.entries()) {
|
|
420
458
|
const pos = input.indexOf(tag, lastIndex);
|
|
421
|
-
if (pos
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
459
|
+
if (pos === -1) continue;
|
|
460
|
+
tagPositions.push({
|
|
461
|
+
pos,
|
|
462
|
+
tag,
|
|
463
|
+
placeholder: "[TAG-" + i + "]"
|
|
464
|
+
});
|
|
465
|
+
lastIndex = pos + tag.length;
|
|
429
466
|
}
|
|
430
467
|
if (tagPositions.length > 0) {
|
|
431
468
|
const parts = [];
|
|
@@ -445,8 +482,6 @@ var yabbcode = class {
|
|
|
445
482
|
return input;
|
|
446
483
|
}
|
|
447
484
|
};
|
|
448
|
-
var ya_bbcode_default = yabbcode;
|
|
449
485
|
|
|
450
|
-
|
|
451
|
-
export { ya_bbcode_default as default };
|
|
486
|
+
export { yabbcode as default };
|
|
452
487
|
//# sourceMappingURL=ya-bbcode.mjs.map
|
package/dist/ya-bbcode.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.mjs","names":["attrEntities: { [key: string]: string; }","attrs: TagAttributes","#ignoreLoop","#contentLoopOptimized","#contentLoop","#tagLoop","tagsMap: TagItem[]","moduleName: string","parsedAttrs: { attr: string; attrs: TagAttributes; }","item: TagItem"],"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}\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) { return '#'; }\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) { continue; }\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'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: null,\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) { return content; }\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): 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 (most common case for large docs)\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content or ignore type tags\n\t\t\tconst hasSpecialTypes = tagsMap.some((tag) => {\n\t\t\t\tconst module = this.tags[tag.module];\n\t\t\t\treturn module && (module.type === 'content' || module.type === 'ignore');\n\t\t\t});\n\n\t\t\t// If only replace-type tags, 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 (const tag of tagsMap) {\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\t\t\tif (module.type === 'replace') {\n\t\t\t\tcontent = this.contentModules.replace(tag, module, content);\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\tcontent = this.contentModules.content(tag, module, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module, content);\n\t\t\t}\n\t\t\tif (tag.children.length > 0 && module.type !== 'ignore') {\n\t\t\t\tcontent = this.#contentLoop(tag.children, 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') { return; }\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\tif (tag.children.length > 0) {\n\t\t\t\tfor (const child of tag.children) {\n\t\t\t\t\tprocessTag(child);\n\t\t\t\t}\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) { continue; }\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) { continue; }\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\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t\t}\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\ttag.children = [];\n\t\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\tif (childTag && !childTag.isClosing && childTag.parent === null) {\n\t\t\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\t\t\ttag.children.push(childTag);\n\t\t\t\t\t}\n\t\t\t\t}\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\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\t\tif (shouldInclude) {\n\t\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t\t\t}\n\t\t\t\t\tresult.push(tag);\n\t\t\t\t}\n\t\t\t}\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) { return ''; }\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('=')[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+/)[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\ttagPositions.push({ pos, tag: tag, placeholder: '[TAG-' + i + ']' });\n\t\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t\t}\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/**\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ya-bbcode",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Yet another BBCode Parser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bbc",
|
|
@@ -38,28 +38,26 @@
|
|
|
38
38
|
"check-types": "tsc --noEmit",
|
|
39
39
|
"coverage": "vitest run --coverage",
|
|
40
40
|
"lint": "eslint . --fix",
|
|
41
|
+
"lint:ci": "eslint .",
|
|
41
42
|
"prepublishOnly": "npm run build",
|
|
42
43
|
"test": "npm run check-types && vitest run && npm run test:imports",
|
|
43
44
|
"test:imports": "node test-imports/test-esm.mjs && node test-imports/test-cjs.cjs",
|
|
44
45
|
"test:unit": "vitest run"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
|
-
"@bbob/html": "4.
|
|
48
|
-
"@bbob/parser": "4.
|
|
49
|
-
"@bbob/preset-html5": "4.
|
|
50
|
-
"@nodecraft/eslint-config": "
|
|
51
|
-
"@types/node": "
|
|
52
|
-
"@
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"eslint
|
|
59
|
-
"
|
|
60
|
-
"tsdown": "0.17.0-beta.4",
|
|
61
|
-
"typescript": "5.9.3",
|
|
62
|
-
"vitest": "4.0.14",
|
|
48
|
+
"@bbob/html": "4.4.1",
|
|
49
|
+
"@bbob/parser": "4.4.1",
|
|
50
|
+
"@bbob/preset-html5": "4.4.1",
|
|
51
|
+
"@nodecraft/eslint-config": "53.0.0",
|
|
52
|
+
"@types/node": "26.6.6",
|
|
53
|
+
"@vitest/coverage-v8": "5.1.0",
|
|
54
|
+
"eslint": "10.14.0",
|
|
55
|
+
"semantic-release": "25.0.9",
|
|
56
|
+
"tinybench": "6.2.3",
|
|
57
|
+
"tsdown": "0.23.7",
|
|
58
|
+
"typescript": "6.0.3",
|
|
59
|
+
"typescript-eslint": "8.72.1",
|
|
60
|
+
"vitest": "5.1.0",
|
|
63
61
|
"xbbcode-parser": "0.3.1"
|
|
64
62
|
},
|
|
65
63
|
"engines": {
|