ya-bbcode 4.0.0 → 5.0.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/README.md +5 -10
- package/dist/ya-bbcode.cjs +453 -0
- package/dist/ya-bbcode.cjs.map +1 -0
- package/dist/ya-bbcode.d.cts +62 -0
- package/dist/ya-bbcode.d.mts +62 -0
- package/dist/ya-bbcode.mjs +452 -0
- package/dist/ya-bbcode.mjs.map +1 -0
- package/package.json +41 -27
- package/ya-bbcode.d.ts +0 -39
- package/ya-bbcode.js +0 -355
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
//#region src/ya-bbcode.ts
|
|
2
|
+
/**
|
|
3
|
+
* Escapes HTML attribute values to prevent XSS
|
|
4
|
+
* Encodes quotes, angle brackets, and other special characters
|
|
5
|
+
*/
|
|
6
|
+
function escapeHtmlAttr(value) {
|
|
7
|
+
const attrEntities = {
|
|
8
|
+
"&": "&",
|
|
9
|
+
"<": "<",
|
|
10
|
+
">": ">",
|
|
11
|
+
"\"": """,
|
|
12
|
+
"'": "'"
|
|
13
|
+
};
|
|
14
|
+
return String(value).replaceAll(/["&'<>]/g, (char) => attrEntities[char] || char);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validates and sanitizes URLs to prevent javascript: and data: URL attacks
|
|
18
|
+
* Only allows safe URL schemes based on the allowedSchemes list
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeUrl(url, allowedSchemes) {
|
|
21
|
+
const trimmed = String(url).trim();
|
|
22
|
+
if (!trimmed) return "#";
|
|
23
|
+
if (/^\.{0,2}\//.test(trimmed)) return trimmed;
|
|
24
|
+
if (trimmed.startsWith("#")) return trimmed;
|
|
25
|
+
const schemeMatch = /^([a-z][\d+.a-z-]*):/.exec(trimmed.toLowerCase());
|
|
26
|
+
if (schemeMatch) {
|
|
27
|
+
const scheme = schemeMatch[1] ?? "";
|
|
28
|
+
if (!allowedSchemes.includes(scheme)) return "#";
|
|
29
|
+
}
|
|
30
|
+
return trimmed;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse tag attributes from a tag string
|
|
34
|
+
* Supports both formats:
|
|
35
|
+
* - [tag=value] → { attr: 'value', attrs: {} }
|
|
36
|
+
* - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }
|
|
37
|
+
*/
|
|
38
|
+
function parseTagAttributes(tagContent) {
|
|
39
|
+
const firstSpace = tagContent.indexOf(" ");
|
|
40
|
+
const firstEquals = tagContent.indexOf("=");
|
|
41
|
+
if (firstSpace === -1 || firstEquals !== -1 && firstEquals < firstSpace) return {
|
|
42
|
+
attr: tagContent.split("=").slice(1).join("="),
|
|
43
|
+
attrs: {}
|
|
44
|
+
};
|
|
45
|
+
const parts = tagContent.split(/\s+/);
|
|
46
|
+
const attrs = Object.create(null);
|
|
47
|
+
let firstValue = "";
|
|
48
|
+
const dangerousKeys = new Set([
|
|
49
|
+
"__proto__",
|
|
50
|
+
"constructor",
|
|
51
|
+
"prototype"
|
|
52
|
+
]);
|
|
53
|
+
for (let i = 1; i < parts.length; i++) {
|
|
54
|
+
const part = parts[i];
|
|
55
|
+
if (!part) continue;
|
|
56
|
+
if (part.includes("=")) {
|
|
57
|
+
const eqIndex = part.indexOf("=");
|
|
58
|
+
const key = part.slice(0, eqIndex);
|
|
59
|
+
const value = part.slice(eqIndex + 1);
|
|
60
|
+
if (dangerousKeys.has(key.toLowerCase())) continue;
|
|
61
|
+
attrs[key] = value;
|
|
62
|
+
if (!firstValue && value) firstValue = value;
|
|
63
|
+
} else {
|
|
64
|
+
if (dangerousKeys.has(part.toLowerCase())) continue;
|
|
65
|
+
attrs[part] = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
attr: firstValue,
|
|
70
|
+
attrs
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var yabbcode = class {
|
|
74
|
+
tags = {
|
|
75
|
+
"url": {
|
|
76
|
+
type: "replace",
|
|
77
|
+
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? [
|
|
78
|
+
"http",
|
|
79
|
+
"https",
|
|
80
|
+
"mailto",
|
|
81
|
+
"ftp",
|
|
82
|
+
"ftps"
|
|
83
|
+
]))}">`,
|
|
84
|
+
close: "</a>"
|
|
85
|
+
},
|
|
86
|
+
"quote": {
|
|
87
|
+
type: "replace",
|
|
88
|
+
open: (attr) => `<blockquote author="${escapeHtmlAttr(attr)}">`,
|
|
89
|
+
close: "</blockquote>"
|
|
90
|
+
},
|
|
91
|
+
"b": {
|
|
92
|
+
type: "replace",
|
|
93
|
+
open: "<strong>",
|
|
94
|
+
close: "</strong>"
|
|
95
|
+
},
|
|
96
|
+
"u": {
|
|
97
|
+
type: "replace",
|
|
98
|
+
open: "<u>",
|
|
99
|
+
close: "</u>"
|
|
100
|
+
},
|
|
101
|
+
"i": {
|
|
102
|
+
type: "replace",
|
|
103
|
+
open: "<i>",
|
|
104
|
+
close: "</i>"
|
|
105
|
+
},
|
|
106
|
+
"h1": {
|
|
107
|
+
type: "replace",
|
|
108
|
+
open: "<h1>",
|
|
109
|
+
close: "</h1>"
|
|
110
|
+
},
|
|
111
|
+
"h2": {
|
|
112
|
+
type: "replace",
|
|
113
|
+
open: "<h2>",
|
|
114
|
+
close: "</h2>"
|
|
115
|
+
},
|
|
116
|
+
"h3": {
|
|
117
|
+
type: "replace",
|
|
118
|
+
open: "<h3>",
|
|
119
|
+
close: "</h3>"
|
|
120
|
+
},
|
|
121
|
+
"h4": {
|
|
122
|
+
type: "replace",
|
|
123
|
+
open: "<h4>",
|
|
124
|
+
close: "</h4>"
|
|
125
|
+
},
|
|
126
|
+
"h5": {
|
|
127
|
+
type: "replace",
|
|
128
|
+
open: "<h5>",
|
|
129
|
+
close: "</h5>"
|
|
130
|
+
},
|
|
131
|
+
"h6": {
|
|
132
|
+
type: "replace",
|
|
133
|
+
open: "<h6>",
|
|
134
|
+
close: "</h6>"
|
|
135
|
+
},
|
|
136
|
+
"code": {
|
|
137
|
+
type: "replace",
|
|
138
|
+
open: "<code>",
|
|
139
|
+
close: "</code>"
|
|
140
|
+
},
|
|
141
|
+
"strike": {
|
|
142
|
+
type: "replace",
|
|
143
|
+
open: "<span class=\"yabbcode-strike\">",
|
|
144
|
+
close: "</span>"
|
|
145
|
+
},
|
|
146
|
+
"spoiler": {
|
|
147
|
+
type: "replace",
|
|
148
|
+
open: "<span class=\"yabbcode-spoiler\">",
|
|
149
|
+
close: "</span>"
|
|
150
|
+
},
|
|
151
|
+
"list": {
|
|
152
|
+
type: "replace",
|
|
153
|
+
open: "<ul>",
|
|
154
|
+
close: "</ul>"
|
|
155
|
+
},
|
|
156
|
+
"olist": {
|
|
157
|
+
type: "replace",
|
|
158
|
+
open: "<ol>",
|
|
159
|
+
close: "</ol>"
|
|
160
|
+
},
|
|
161
|
+
"*": {
|
|
162
|
+
type: "replace",
|
|
163
|
+
open: "<li>",
|
|
164
|
+
close: null
|
|
165
|
+
},
|
|
166
|
+
"img": {
|
|
167
|
+
type: "content",
|
|
168
|
+
replace: (attr, content) => {
|
|
169
|
+
if (!content) return "";
|
|
170
|
+
return `<img src="${escapeHtmlAttr(content)}" alt="${escapeHtmlAttr(attr)}"/>`;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"noparse": { type: "ignore" }
|
|
174
|
+
};
|
|
175
|
+
regex = {
|
|
176
|
+
tags: /(\[[^\]^]+])/g,
|
|
177
|
+
newline: /\r\n|\r|\n/g,
|
|
178
|
+
placeholders: /\[TAG-[1-9]+]/g
|
|
179
|
+
};
|
|
180
|
+
config;
|
|
181
|
+
contentModules = {
|
|
182
|
+
replace: (tag, module, content) => {
|
|
183
|
+
let open = module.open;
|
|
184
|
+
let close = module.close;
|
|
185
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
186
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
187
|
+
if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
|
|
188
|
+
if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
|
|
189
|
+
return content;
|
|
190
|
+
},
|
|
191
|
+
content: (tag, module, content) => {
|
|
192
|
+
if (!tag.closing) return content;
|
|
193
|
+
const openTag = "[TAG-" + tag.index + "]";
|
|
194
|
+
const closeTag = "[TAG-" + tag.closing.index + "]";
|
|
195
|
+
const start = content.indexOf(openTag);
|
|
196
|
+
const end = content.indexOf(closeTag);
|
|
197
|
+
let replace = module.replace;
|
|
198
|
+
const innerContent = content.slice(start + openTag.length, end);
|
|
199
|
+
if (typeof replace === "function") replace = replace(tag.attr, innerContent, tag.attrs);
|
|
200
|
+
const contentStart = content.slice(0, Math.max(0, start));
|
|
201
|
+
const contentEnd = content.slice(end + closeTag.length);
|
|
202
|
+
return contentStart + replace + contentEnd;
|
|
203
|
+
},
|
|
204
|
+
ignore: (tag, _module, content) => {
|
|
205
|
+
const openTag = "[TAG-" + tag.index + "]";
|
|
206
|
+
const start = content.indexOf(openTag);
|
|
207
|
+
let closeTag = "";
|
|
208
|
+
let end = content.length;
|
|
209
|
+
if (tag.closing) {
|
|
210
|
+
closeTag = "[TAG-" + tag.closing.index + "]";
|
|
211
|
+
end = content.indexOf(closeTag);
|
|
212
|
+
}
|
|
213
|
+
let innerContent = content.slice(start + openTag.length, end);
|
|
214
|
+
innerContent = this.#ignoreLoop(tag.children, innerContent);
|
|
215
|
+
const contentStart = content.slice(0, Math.max(0, start));
|
|
216
|
+
const contentEnd = content.slice(end + closeTag.length);
|
|
217
|
+
return contentStart + innerContent + contentEnd;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
constructor(config = {}) {
|
|
221
|
+
this.config = {
|
|
222
|
+
newline: true,
|
|
223
|
+
paragraph: false,
|
|
224
|
+
cleanUnmatchable: true,
|
|
225
|
+
sanitizeHtml: true,
|
|
226
|
+
allowedSchemes: [
|
|
227
|
+
"http",
|
|
228
|
+
"https",
|
|
229
|
+
"mailto",
|
|
230
|
+
"ftp",
|
|
231
|
+
"ftps"
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
if (config.newline !== void 0) this.config.newline = config.newline;
|
|
235
|
+
if (config.paragraph !== void 0) this.config.paragraph = config.paragraph;
|
|
236
|
+
if (config.cleanUnmatchable !== void 0) this.config.cleanUnmatchable = config.cleanUnmatchable;
|
|
237
|
+
if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
|
|
238
|
+
if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
|
|
239
|
+
}
|
|
240
|
+
#ignoreLoop(tagsMap, content) {
|
|
241
|
+
for (const tag of tagsMap) {
|
|
242
|
+
content = content.replace("[TAG-" + tag.index + "]", tag.raw);
|
|
243
|
+
if (tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", tag.closing.raw);
|
|
244
|
+
if (tag.children.length > 0) content = this.#ignoreLoop(tag.children, content);
|
|
245
|
+
}
|
|
246
|
+
return content;
|
|
247
|
+
}
|
|
248
|
+
#contentLoop(tagsMap, content) {
|
|
249
|
+
if (tagsMap.length > 50) {
|
|
250
|
+
if (!tagsMap.some((tag) => {
|
|
251
|
+
const module = this.tags[tag.module];
|
|
252
|
+
return module && (module.type === "content" || module.type === "ignore");
|
|
253
|
+
})) return this.#contentLoopOptimized(tagsMap, content);
|
|
254
|
+
}
|
|
255
|
+
for (const tag of tagsMap) {
|
|
256
|
+
let module = this.tags[tag.module];
|
|
257
|
+
if (!module) module = {
|
|
258
|
+
type: "replace",
|
|
259
|
+
open: tag.raw,
|
|
260
|
+
close: tag.closing ? tag.closing.raw : ""
|
|
261
|
+
};
|
|
262
|
+
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") content = this.contentModules.replace(tag, module, content);
|
|
264
|
+
else if (module.type === "content") content = this.contentModules.content(tag, module, content);
|
|
265
|
+
else if (module.type === "ignore") content = this.contentModules.ignore(tag, module, content);
|
|
266
|
+
if (tag.children.length > 0 && module.type !== "ignore") content = this.#contentLoop(tag.children, content);
|
|
267
|
+
}
|
|
268
|
+
return content;
|
|
269
|
+
}
|
|
270
|
+
#contentLoopOptimized(tagsMap, content) {
|
|
271
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
272
|
+
const processTag = (tag) => {
|
|
273
|
+
let module = this.tags[tag.module];
|
|
274
|
+
if (!module) module = {
|
|
275
|
+
type: "replace",
|
|
276
|
+
open: tag.raw,
|
|
277
|
+
close: tag.closing ? tag.closing.raw : ""
|
|
278
|
+
};
|
|
279
|
+
if (module.type !== "replace") return;
|
|
280
|
+
let open = module.open;
|
|
281
|
+
let close = module.close;
|
|
282
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
283
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
284
|
+
if (open && !tag.isClosing) replacements.set("[TAG-" + tag.index + "]", open);
|
|
285
|
+
if (close && tag.closing) replacements.set("[TAG-" + tag.closing.index + "]", close);
|
|
286
|
+
if (tag.children.length > 0) for (const child of tag.children) processTag(child);
|
|
287
|
+
};
|
|
288
|
+
for (const tag of tagsMap) processTag(tag);
|
|
289
|
+
return content.replaceAll(/\[TAG-(\d+)]/g, (match) => replacements.get(match) || "");
|
|
290
|
+
}
|
|
291
|
+
#tagLoop(tagsMap, parent) {
|
|
292
|
+
const stack = [];
|
|
293
|
+
const processed = /* @__PURE__ */ new Set();
|
|
294
|
+
for (let i = 0; i < tagsMap.length; i++) {
|
|
295
|
+
const tag = tagsMap[i];
|
|
296
|
+
if (!tag) continue;
|
|
297
|
+
if (tag.isClosing) for (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
|
|
298
|
+
const openTag = stack[stackIndex];
|
|
299
|
+
if (!openTag) continue;
|
|
300
|
+
if (openTag.module === tag.module && !processed.has(openTag.index)) {
|
|
301
|
+
openTag.matchTag = tag.index;
|
|
302
|
+
openTag.closing = tag;
|
|
303
|
+
tag.matchTag = openTag.index;
|
|
304
|
+
processed.add(openTag.index);
|
|
305
|
+
const childStart = (openTag.stackIndex ?? 0) + 1;
|
|
306
|
+
if (childStart < i) {
|
|
307
|
+
openTag.children = [];
|
|
308
|
+
for (let childIndex = childStart; childIndex < i; childIndex++) {
|
|
309
|
+
const childTag = tagsMap[childIndex];
|
|
310
|
+
if (childTag && !childTag.isClosing && childTag.parent === null) {
|
|
311
|
+
childTag.parent = openTag.index;
|
|
312
|
+
openTag.children.push(childTag);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
stack.splice(stackIndex, 1);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
tag.stackIndex = i;
|
|
322
|
+
stack.push(tag);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const tag of stack) {
|
|
326
|
+
tag.matchTag = false;
|
|
327
|
+
const childStart = (tag.stackIndex ?? 0) + 1;
|
|
328
|
+
if (childStart < tagsMap.length) {
|
|
329
|
+
tag.children = [];
|
|
330
|
+
for (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {
|
|
331
|
+
const childTag = tagsMap[childIndex];
|
|
332
|
+
if (childTag && !childTag.isClosing && childTag.parent === null) {
|
|
333
|
+
childTag.parent = tag.index;
|
|
334
|
+
tag.children.push(childTag);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const result = [];
|
|
340
|
+
for (const tag of tagsMap) if (!tag.isClosing) {
|
|
341
|
+
if (parent === void 0 && tag.parent === null || parent !== void 0 && tag.parent === parent) {
|
|
342
|
+
if (tag.children.length > 0) tag.children = this.#tagLoop(tag.children, tag.index);
|
|
343
|
+
result.push(tag);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
clearTags() {
|
|
349
|
+
this.tags = {};
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
registerTag(tag, options) {
|
|
353
|
+
const tagName = String(tag).toLowerCase();
|
|
354
|
+
if ([
|
|
355
|
+
"__proto__",
|
|
356
|
+
"constructor",
|
|
357
|
+
"prototype"
|
|
358
|
+
].includes(tagName)) throw new Error(`Cannot register tag with dangerous name: ${tagName}`);
|
|
359
|
+
this.tags[tagName] = options;
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
parse(bbcInput) {
|
|
363
|
+
if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
|
|
364
|
+
let input = String(bbcInput);
|
|
365
|
+
if (this.config.sanitizeHtml) {
|
|
366
|
+
const htmlEntities = {
|
|
367
|
+
"&": "&",
|
|
368
|
+
"<": "<",
|
|
369
|
+
">": ">",
|
|
370
|
+
"\"": """,
|
|
371
|
+
"'": "'"
|
|
372
|
+
};
|
|
373
|
+
input = input.replaceAll(/["&'<>]/g, (char) => htmlEntities[char]);
|
|
374
|
+
}
|
|
375
|
+
let tagsMap = [];
|
|
376
|
+
const tags = String(input).match(this.regex.tags);
|
|
377
|
+
if (this.config.newline) if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
|
|
378
|
+
else input = input.replace(this.regex.newline, "<br/>");
|
|
379
|
+
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
380
|
+
if (!tags || tags.length === 0) return input;
|
|
381
|
+
for (const [i, tag] of tags.entries()) {
|
|
382
|
+
const tagContent = tag.slice(1, -1);
|
|
383
|
+
const isClosing = tagContent.startsWith("/");
|
|
384
|
+
const contentToParse = isClosing ? tagContent.slice(1) : tagContent;
|
|
385
|
+
const spaceIndex = contentToParse.indexOf(" ");
|
|
386
|
+
const equalsIndex = contentToParse.indexOf("=");
|
|
387
|
+
let moduleName;
|
|
388
|
+
let parsedAttrs;
|
|
389
|
+
if (spaceIndex === -1 && equalsIndex === -1) {
|
|
390
|
+
moduleName = contentToParse.toLowerCase();
|
|
391
|
+
parsedAttrs = {
|
|
392
|
+
attr: "",
|
|
393
|
+
attrs: {}
|
|
394
|
+
};
|
|
395
|
+
} else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
|
|
396
|
+
moduleName = (contentToParse.split("=")[0] ?? "").toLowerCase();
|
|
397
|
+
parsedAttrs = parseTagAttributes(contentToParse);
|
|
398
|
+
} else {
|
|
399
|
+
moduleName = (contentToParse.split(/\s+/)[0] ?? "").toLowerCase();
|
|
400
|
+
parsedAttrs = parseTagAttributes(contentToParse);
|
|
401
|
+
}
|
|
402
|
+
const item = {
|
|
403
|
+
index: i,
|
|
404
|
+
module: moduleName,
|
|
405
|
+
isClosing,
|
|
406
|
+
raw: tag,
|
|
407
|
+
attr: parsedAttrs.attr,
|
|
408
|
+
attrs: parsedAttrs.attrs,
|
|
409
|
+
children: [],
|
|
410
|
+
parent: null,
|
|
411
|
+
matchTag: null
|
|
412
|
+
};
|
|
413
|
+
tagsMap.push(item);
|
|
414
|
+
}
|
|
415
|
+
if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = input.replace(tag, "[TAG-" + i + "]");
|
|
416
|
+
else {
|
|
417
|
+
const tagPositions = [];
|
|
418
|
+
let lastIndex = 0;
|
|
419
|
+
for (const [i, tag] of tags.entries()) {
|
|
420
|
+
const pos = input.indexOf(tag, lastIndex);
|
|
421
|
+
if (pos !== -1) {
|
|
422
|
+
tagPositions.push({
|
|
423
|
+
pos,
|
|
424
|
+
tag,
|
|
425
|
+
placeholder: "[TAG-" + i + "]"
|
|
426
|
+
});
|
|
427
|
+
lastIndex = pos + tag.length;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (tagPositions.length > 0) {
|
|
431
|
+
const parts = [];
|
|
432
|
+
let lastPos = 0;
|
|
433
|
+
for (const { pos, tag, placeholder } of tagPositions) {
|
|
434
|
+
parts.push(input.slice(lastPos, pos));
|
|
435
|
+
parts.push(placeholder);
|
|
436
|
+
lastPos = pos + tag.length;
|
|
437
|
+
}
|
|
438
|
+
parts.push(input.slice(lastPos));
|
|
439
|
+
input = parts.join("");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
tagsMap = this.#tagLoop(tagsMap);
|
|
443
|
+
input = this.#contentLoop(tagsMap, input);
|
|
444
|
+
if (this.config.cleanUnmatchable) input = input.replace(this.regex.placeholders, "");
|
|
445
|
+
return input;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
var ya_bbcode_default = yabbcode;
|
|
449
|
+
|
|
450
|
+
//#endregion
|
|
451
|
+
export { ya_bbcode_default as default };
|
|
452
|
+
//# sourceMappingURL=ya-bbcode.mjs.map
|
|
@@ -0,0 +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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ya-bbcode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "Yet another BBCode Parser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bbc",
|
|
@@ -18,38 +18,52 @@
|
|
|
18
18
|
},
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"author": "Nodecraft Inc.",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/ya-bbcode.cjs",
|
|
23
|
+
"module": "./dist/ya-bbcode.mjs",
|
|
24
|
+
"types": "./dist/ya-bbcode.d.mts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/ya-bbcode.d.mts",
|
|
28
|
+
"import": "./dist/ya-bbcode.mjs",
|
|
29
|
+
"require": "./dist/ya-bbcode.cjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
23
32
|
"files": [
|
|
24
|
-
"
|
|
25
|
-
"ya-bbcode.d.ts"
|
|
33
|
+
"dist"
|
|
26
34
|
],
|
|
27
35
|
"scripts": {
|
|
28
|
-
"
|
|
36
|
+
"bench": "node benchmark/run.ts",
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"check-types": "tsc --noEmit",
|
|
39
|
+
"coverage": "vitest run --coverage",
|
|
29
40
|
"lint": "eslint . --fix",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"test": "
|
|
33
|
-
|
|
34
|
-
"lint-staged": {
|
|
35
|
-
"*.js": "eslint --cache"
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"test": "npm run check-types && vitest run && npm run test:imports",
|
|
43
|
+
"test:imports": "node test-imports/test-esm.mjs && node test-imports/test-cjs.cjs",
|
|
44
|
+
"test:unit": "vitest run"
|
|
36
45
|
},
|
|
37
46
|
"devDependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"eslint": "
|
|
44
|
-
"eslint
|
|
45
|
-
"
|
|
46
|
-
"eslint
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
47
|
+
"@bbob/html": "4.3.1",
|
|
48
|
+
"@bbob/parser": "4.3.1",
|
|
49
|
+
"@bbob/preset-html5": "4.3.1",
|
|
50
|
+
"@nodecraft/eslint-config": "46.0.0",
|
|
51
|
+
"@types/node": "24.10.1",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "8.48.0",
|
|
53
|
+
"@typescript-eslint/parser": "8.48.0",
|
|
54
|
+
"@vitest/coverage-v8": "4.0.14",
|
|
55
|
+
"eslint": "8.57.1",
|
|
56
|
+
"eslint-plugin-json": "4.0.1",
|
|
57
|
+
"eslint-plugin-node": "11.1.0",
|
|
58
|
+
"eslint-plugin-unicorn": "56.0.1",
|
|
59
|
+
"tinybench": "5.1.0",
|
|
60
|
+
"tsdown": "0.17.0-beta.4",
|
|
61
|
+
"typescript": "5.9.3",
|
|
62
|
+
"vitest": "4.0.14",
|
|
63
|
+
"xbbcode-parser": "0.3.1"
|
|
50
64
|
},
|
|
51
65
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
53
|
-
"npm": ">=
|
|
66
|
+
"node": ">=22",
|
|
67
|
+
"npm": ">=10"
|
|
54
68
|
}
|
|
55
|
-
}
|
|
69
|
+
}
|
package/ya-bbcode.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export = yabbcode
|
|
2
|
-
|
|
3
|
-
declare class yabbcode {
|
|
4
|
-
/**
|
|
5
|
-
* Create a new bbcode parser
|
|
6
|
-
*/
|
|
7
|
-
constructor(config?: {
|
|
8
|
-
newline?: boolean
|
|
9
|
-
paragraph?: boolean
|
|
10
|
-
cleanUnmatchable?: boolean
|
|
11
|
-
sanitizeHtml?: boolean
|
|
12
|
-
})
|
|
13
|
-
/**
|
|
14
|
-
* Parse bbcode
|
|
15
|
-
* @param bbc bbcode to parse
|
|
16
|
-
*/
|
|
17
|
-
parse(bbc: any): string
|
|
18
|
-
/**
|
|
19
|
-
* Remove all default or registered tags
|
|
20
|
-
*/
|
|
21
|
-
clearTags(): this
|
|
22
|
-
/**
|
|
23
|
-
* Add custom tags
|
|
24
|
-
* @param tag tag name
|
|
25
|
-
* @param options options to parse the tag
|
|
26
|
-
*/
|
|
27
|
-
registerTag(tag: string, options: {
|
|
28
|
-
type: 'replace'
|
|
29
|
-
open: ((attr: string) => string) | string
|
|
30
|
-
close: ((attr: string) => string) | string | null
|
|
31
|
-
}): this
|
|
32
|
-
registerTag(tag: string, options: {
|
|
33
|
-
type: 'content',
|
|
34
|
-
replace: (attr: string, content: string) => string
|
|
35
|
-
}): this
|
|
36
|
-
registerTag(tag: string, options: {
|
|
37
|
-
type: 'ignore'
|
|
38
|
-
}): this
|
|
39
|
-
}
|