ya-bbcode 4.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 +8 -11
- package/dist/ya-bbcode.cjs +487 -0
- package/dist/ya-bbcode.cjs.map +1 -0
- package/dist/ya-bbcode.d.cts +64 -0
- package/dist/ya-bbcode.d.mts +64 -0
- package/dist/ya-bbcode.mjs +487 -0
- package/dist/ya-bbcode.mjs.map +1 -0
- package/package.json +39 -27
- package/ya-bbcode.d.ts +0 -39
- package/ya-bbcode.js +0 -355
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
newline?: boolean;
|
|
3
|
+
paragraph?: boolean;
|
|
4
|
+
cleanUnmatchable?: boolean;
|
|
5
|
+
sanitizeHtml?: boolean;
|
|
6
|
+
allowedSchemes?: string[];
|
|
7
|
+
}
|
|
8
|
+
interface TagAttributes {
|
|
9
|
+
[key: string]: string | boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ReplaceTag {
|
|
12
|
+
type: 'replace';
|
|
13
|
+
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
interface ContentTag {
|
|
22
|
+
type: 'content';
|
|
23
|
+
replace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;
|
|
24
|
+
}
|
|
25
|
+
interface IgnoreTag {
|
|
26
|
+
type: 'ignore';
|
|
27
|
+
}
|
|
28
|
+
type TagDefinition = ReplaceTag | ContentTag | IgnoreTag;
|
|
29
|
+
interface TagsMap {
|
|
30
|
+
[key: string]: TagDefinition;
|
|
31
|
+
}
|
|
32
|
+
interface TagItem {
|
|
33
|
+
index: number;
|
|
34
|
+
module: string;
|
|
35
|
+
raw: string;
|
|
36
|
+
attr: string;
|
|
37
|
+
attrs: TagAttributes;
|
|
38
|
+
isClosing: boolean;
|
|
39
|
+
matchTag: number | false | null;
|
|
40
|
+
closing?: TagItem;
|
|
41
|
+
children: TagItem[];
|
|
42
|
+
parent: number | null;
|
|
43
|
+
stackIndex?: number;
|
|
44
|
+
}
|
|
45
|
+
declare class yabbcode {
|
|
46
|
+
#private;
|
|
47
|
+
tags: TagsMap;
|
|
48
|
+
regex: {
|
|
49
|
+
tags: RegExp;
|
|
50
|
+
newline: RegExp;
|
|
51
|
+
placeholders: RegExp;
|
|
52
|
+
};
|
|
53
|
+
config: Config;
|
|
54
|
+
contentModules: {
|
|
55
|
+
replace: (tag: TagItem, module: ReplaceTag, content: string) => string;
|
|
56
|
+
content: (tag: TagItem, module: ContentTag, content: string) => string;
|
|
57
|
+
ignore: (tag: TagItem, _module: IgnoreTag, content: string) => string;
|
|
58
|
+
};
|
|
59
|
+
constructor(config?: Config);
|
|
60
|
+
clearTags(): this;
|
|
61
|
+
registerTag(tag: string, options: TagDefinition): this;
|
|
62
|
+
parse(bbcInput: string | number | boolean): string;
|
|
63
|
+
}
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
interface Config {
|
|
2
|
+
newline?: boolean;
|
|
3
|
+
paragraph?: boolean;
|
|
4
|
+
cleanUnmatchable?: boolean;
|
|
5
|
+
sanitizeHtml?: boolean;
|
|
6
|
+
allowedSchemes?: string[];
|
|
7
|
+
}
|
|
8
|
+
interface TagAttributes {
|
|
9
|
+
[key: string]: string | boolean;
|
|
10
|
+
}
|
|
11
|
+
interface ReplaceTag {
|
|
12
|
+
type: 'replace';
|
|
13
|
+
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
interface ContentTag {
|
|
22
|
+
type: 'content';
|
|
23
|
+
replace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;
|
|
24
|
+
}
|
|
25
|
+
interface IgnoreTag {
|
|
26
|
+
type: 'ignore';
|
|
27
|
+
}
|
|
28
|
+
type TagDefinition = ReplaceTag | ContentTag | IgnoreTag;
|
|
29
|
+
interface TagsMap {
|
|
30
|
+
[key: string]: TagDefinition;
|
|
31
|
+
}
|
|
32
|
+
interface TagItem {
|
|
33
|
+
index: number;
|
|
34
|
+
module: string;
|
|
35
|
+
raw: string;
|
|
36
|
+
attr: string;
|
|
37
|
+
attrs: TagAttributes;
|
|
38
|
+
isClosing: boolean;
|
|
39
|
+
matchTag: number | false | null;
|
|
40
|
+
closing?: TagItem;
|
|
41
|
+
children: TagItem[];
|
|
42
|
+
parent: number | null;
|
|
43
|
+
stackIndex?: number;
|
|
44
|
+
}
|
|
45
|
+
declare class yabbcode {
|
|
46
|
+
#private;
|
|
47
|
+
tags: TagsMap;
|
|
48
|
+
regex: {
|
|
49
|
+
tags: RegExp;
|
|
50
|
+
newline: RegExp;
|
|
51
|
+
placeholders: RegExp;
|
|
52
|
+
};
|
|
53
|
+
config: Config;
|
|
54
|
+
contentModules: {
|
|
55
|
+
replace: (tag: TagItem, module: ReplaceTag, content: string) => string;
|
|
56
|
+
content: (tag: TagItem, module: ContentTag, content: string) => string;
|
|
57
|
+
ignore: (tag: TagItem, _module: IgnoreTag, content: string) => string;
|
|
58
|
+
};
|
|
59
|
+
constructor(config?: Config);
|
|
60
|
+
clearTags(): this;
|
|
61
|
+
registerTag(tag: string, options: TagDefinition): this;
|
|
62
|
+
parse(bbcInput: string | number | boolean): string;
|
|
63
|
+
}
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// 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 = /* @__PURE__ */ 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
|
+
"hr": {
|
|
142
|
+
type: "replace",
|
|
143
|
+
open: "<hr/>",
|
|
144
|
+
close: null
|
|
145
|
+
},
|
|
146
|
+
"strike": {
|
|
147
|
+
type: "replace",
|
|
148
|
+
open: "<span class=\"yabbcode-strike\">",
|
|
149
|
+
close: "</span>"
|
|
150
|
+
},
|
|
151
|
+
"spoiler": {
|
|
152
|
+
type: "replace",
|
|
153
|
+
open: "<span class=\"yabbcode-spoiler\">",
|
|
154
|
+
close: "</span>"
|
|
155
|
+
},
|
|
156
|
+
"list": {
|
|
157
|
+
type: "replace",
|
|
158
|
+
open: "<ul>",
|
|
159
|
+
close: "</ul>"
|
|
160
|
+
},
|
|
161
|
+
"olist": {
|
|
162
|
+
type: "replace",
|
|
163
|
+
open: "<ol>",
|
|
164
|
+
close: "</ol>"
|
|
165
|
+
},
|
|
166
|
+
"*": {
|
|
167
|
+
type: "replace",
|
|
168
|
+
open: "<li>",
|
|
169
|
+
close: "</li>",
|
|
170
|
+
autoClose: "</li>"
|
|
171
|
+
},
|
|
172
|
+
"img": {
|
|
173
|
+
type: "content",
|
|
174
|
+
replace: (attr, content) => {
|
|
175
|
+
if (!content) return "";
|
|
176
|
+
return `<img src="${escapeHtmlAttr(content)}" alt="${escapeHtmlAttr(attr)}"/>`;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"noparse": { type: "ignore" }
|
|
180
|
+
};
|
|
181
|
+
regex = {
|
|
182
|
+
tags: /(\[[^\]^]+\])/g,
|
|
183
|
+
newline: /\r\n|\r|\n/g,
|
|
184
|
+
placeholders: /\[TAG-[1-9]+\]/g
|
|
185
|
+
};
|
|
186
|
+
config;
|
|
187
|
+
contentModules = {
|
|
188
|
+
replace: (tag, module, content) => {
|
|
189
|
+
let open = module.open;
|
|
190
|
+
let close = module.close;
|
|
191
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
192
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
193
|
+
if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
|
|
194
|
+
if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
|
|
195
|
+
return content;
|
|
196
|
+
},
|
|
197
|
+
content: (tag, module, content) => {
|
|
198
|
+
if (!tag.closing) return content;
|
|
199
|
+
const openTag = "[TAG-" + tag.index + "]";
|
|
200
|
+
const closeTag = "[TAG-" + tag.closing.index + "]";
|
|
201
|
+
const start = content.indexOf(openTag);
|
|
202
|
+
const end = content.indexOf(closeTag);
|
|
203
|
+
let replace = module.replace;
|
|
204
|
+
const innerContent = content.slice(start + openTag.length, end);
|
|
205
|
+
if (typeof replace === "function") replace = replace(tag.attr, innerContent, tag.attrs);
|
|
206
|
+
const contentStart = content.slice(0, Math.max(0, start));
|
|
207
|
+
const contentEnd = content.slice(end + closeTag.length);
|
|
208
|
+
return contentStart + replace + contentEnd;
|
|
209
|
+
},
|
|
210
|
+
ignore: (tag, _module, content) => {
|
|
211
|
+
const openTag = "[TAG-" + tag.index + "]";
|
|
212
|
+
const start = content.indexOf(openTag);
|
|
213
|
+
let closeTag = "";
|
|
214
|
+
let end = content.length;
|
|
215
|
+
if (tag.closing) {
|
|
216
|
+
closeTag = "[TAG-" + tag.closing.index + "]";
|
|
217
|
+
end = content.indexOf(closeTag);
|
|
218
|
+
}
|
|
219
|
+
let innerContent = content.slice(start + openTag.length, end);
|
|
220
|
+
innerContent = this.#ignoreLoop(tag.children, innerContent);
|
|
221
|
+
const contentStart = content.slice(0, Math.max(0, start));
|
|
222
|
+
const contentEnd = content.slice(end + closeTag.length);
|
|
223
|
+
return contentStart + innerContent + contentEnd;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
constructor(config = {}) {
|
|
227
|
+
this.config = {
|
|
228
|
+
newline: true,
|
|
229
|
+
paragraph: false,
|
|
230
|
+
cleanUnmatchable: true,
|
|
231
|
+
sanitizeHtml: true,
|
|
232
|
+
allowedSchemes: [
|
|
233
|
+
"http",
|
|
234
|
+
"https",
|
|
235
|
+
"mailto",
|
|
236
|
+
"ftp",
|
|
237
|
+
"ftps"
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
if (config.newline !== void 0) this.config.newline = config.newline;
|
|
241
|
+
if (config.paragraph !== void 0) this.config.paragraph = config.paragraph;
|
|
242
|
+
if (config.cleanUnmatchable !== void 0) this.config.cleanUnmatchable = config.cleanUnmatchable;
|
|
243
|
+
if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
|
|
244
|
+
if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
|
|
245
|
+
}
|
|
246
|
+
#ignoreLoop(tagsMap, content) {
|
|
247
|
+
for (const tag of tagsMap) {
|
|
248
|
+
content = content.replace("[TAG-" + tag.index + "]", tag.raw);
|
|
249
|
+
if (tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", tag.closing.raw);
|
|
250
|
+
if (tag.children.length > 0) content = this.#ignoreLoop(tag.children, content);
|
|
251
|
+
}
|
|
252
|
+
return content;
|
|
253
|
+
}
|
|
254
|
+
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
255
|
+
if (tagsMap.length > 50) {
|
|
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);
|
|
265
|
+
}
|
|
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 = {
|
|
271
|
+
type: "replace",
|
|
272
|
+
open: tag.raw,
|
|
273
|
+
close: tag.closing ? tag.closing.raw : ""
|
|
274
|
+
};
|
|
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);
|
|
306
|
+
}
|
|
307
|
+
return content;
|
|
308
|
+
}
|
|
309
|
+
#contentLoopOptimized(tagsMap, content) {
|
|
310
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
311
|
+
const processTag = (tag) => {
|
|
312
|
+
let module = this.tags[tag.module];
|
|
313
|
+
if (!module) module = {
|
|
314
|
+
type: "replace",
|
|
315
|
+
open: tag.raw,
|
|
316
|
+
close: tag.closing ? tag.closing.raw : ""
|
|
317
|
+
};
|
|
318
|
+
if (module.type !== "replace") return;
|
|
319
|
+
let open = module.open;
|
|
320
|
+
let close = module.close;
|
|
321
|
+
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
322
|
+
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
323
|
+
if (open && !tag.isClosing) replacements.set("[TAG-" + tag.index + "]", open);
|
|
324
|
+
if (close && tag.closing) replacements.set("[TAG-" + tag.closing.index + "]", close);
|
|
325
|
+
for (const child of tag.children) processTag(child);
|
|
326
|
+
};
|
|
327
|
+
for (const tag of tagsMap) processTag(tag);
|
|
328
|
+
return content.replaceAll(/\[TAG-(\d+)\]/g, (match) => replacements.get(match) || "");
|
|
329
|
+
}
|
|
330
|
+
#tagLoop(tagsMap, parent) {
|
|
331
|
+
const stack = [];
|
|
332
|
+
const processed = /* @__PURE__ */ new Set();
|
|
333
|
+
for (let i = 0; i < tagsMap.length; i++) {
|
|
334
|
+
const tag = tagsMap[i];
|
|
335
|
+
if (!tag) continue;
|
|
336
|
+
if (tag.isClosing) for (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {
|
|
337
|
+
const openTag = stack[stackIndex];
|
|
338
|
+
if (!openTag) continue;
|
|
339
|
+
if (openTag.module === tag.module && !processed.has(openTag.index)) {
|
|
340
|
+
openTag.matchTag = tag.index;
|
|
341
|
+
openTag.closing = tag;
|
|
342
|
+
tag.matchTag = openTag.index;
|
|
343
|
+
processed.add(openTag.index);
|
|
344
|
+
const childStart = (openTag.stackIndex ?? 0) + 1;
|
|
345
|
+
if (childStart < i) {
|
|
346
|
+
openTag.children = [];
|
|
347
|
+
for (let childIndex = childStart; childIndex < i; childIndex++) {
|
|
348
|
+
const childTag = tagsMap[childIndex];
|
|
349
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
350
|
+
childTag.parent = openTag.index;
|
|
351
|
+
openTag.children.push(childTag);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
stack.splice(stackIndex, 1);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
tag.stackIndex = i;
|
|
360
|
+
stack.push(tag);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const tag of stack) {
|
|
364
|
+
tag.matchTag = false;
|
|
365
|
+
const childStart = (tag.stackIndex ?? 0) + 1;
|
|
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);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const result = [];
|
|
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);
|
|
381
|
+
}
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
clearTags() {
|
|
385
|
+
this.tags = {};
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
registerTag(tag, options) {
|
|
389
|
+
const tagName = String(tag).toLowerCase();
|
|
390
|
+
if ([
|
|
391
|
+
"__proto__",
|
|
392
|
+
"constructor",
|
|
393
|
+
"prototype"
|
|
394
|
+
].includes(tagName)) throw new Error(`Cannot register tag with dangerous name: ${tagName}`);
|
|
395
|
+
this.tags[tagName] = options;
|
|
396
|
+
return this;
|
|
397
|
+
}
|
|
398
|
+
parse(bbcInput) {
|
|
399
|
+
if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
|
|
400
|
+
let input = String(bbcInput);
|
|
401
|
+
if (this.config.sanitizeHtml) {
|
|
402
|
+
const htmlEntities = {
|
|
403
|
+
"&": "&",
|
|
404
|
+
"<": "<",
|
|
405
|
+
">": ">",
|
|
406
|
+
"\"": """,
|
|
407
|
+
"'": "'"
|
|
408
|
+
};
|
|
409
|
+
input = input.replaceAll(/["&'<>]/g, (char) => htmlEntities[char]);
|
|
410
|
+
}
|
|
411
|
+
let tagsMap = [];
|
|
412
|
+
const tags = String(input).match(this.regex.tags);
|
|
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
|
+
}
|
|
417
|
+
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
418
|
+
if (!tags || tags.length === 0) return input;
|
|
419
|
+
for (const [i, tag] of tags.entries()) {
|
|
420
|
+
const tagContent = tag.slice(1, -1);
|
|
421
|
+
const isClosing = tagContent.startsWith("/");
|
|
422
|
+
const contentToParse = isClosing ? tagContent.slice(1) : tagContent;
|
|
423
|
+
const spaceIndex = contentToParse.indexOf(" ");
|
|
424
|
+
const equalsIndex = contentToParse.indexOf("=");
|
|
425
|
+
let moduleName;
|
|
426
|
+
let parsedAttrs;
|
|
427
|
+
if (spaceIndex === -1 && equalsIndex === -1) {
|
|
428
|
+
moduleName = contentToParse.toLowerCase();
|
|
429
|
+
parsedAttrs = {
|
|
430
|
+
attr: "",
|
|
431
|
+
attrs: {}
|
|
432
|
+
};
|
|
433
|
+
} else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
|
|
434
|
+
moduleName = (contentToParse.split("=", 1)[0] ?? "").toLowerCase();
|
|
435
|
+
parsedAttrs = parseTagAttributes(contentToParse);
|
|
436
|
+
} else {
|
|
437
|
+
moduleName = (contentToParse.split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
438
|
+
parsedAttrs = parseTagAttributes(contentToParse);
|
|
439
|
+
}
|
|
440
|
+
const item = {
|
|
441
|
+
index: i,
|
|
442
|
+
module: moduleName,
|
|
443
|
+
isClosing,
|
|
444
|
+
raw: tag,
|
|
445
|
+
attr: parsedAttrs.attr,
|
|
446
|
+
attrs: parsedAttrs.attrs,
|
|
447
|
+
children: [],
|
|
448
|
+
parent: null,
|
|
449
|
+
matchTag: null
|
|
450
|
+
};
|
|
451
|
+
tagsMap.push(item);
|
|
452
|
+
}
|
|
453
|
+
if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = input.replace(tag, "[TAG-" + i + "]");
|
|
454
|
+
else {
|
|
455
|
+
const tagPositions = [];
|
|
456
|
+
let lastIndex = 0;
|
|
457
|
+
for (const [i, tag] of tags.entries()) {
|
|
458
|
+
const pos = input.indexOf(tag, lastIndex);
|
|
459
|
+
if (pos === -1) continue;
|
|
460
|
+
tagPositions.push({
|
|
461
|
+
pos,
|
|
462
|
+
tag,
|
|
463
|
+
placeholder: "[TAG-" + i + "]"
|
|
464
|
+
});
|
|
465
|
+
lastIndex = pos + tag.length;
|
|
466
|
+
}
|
|
467
|
+
if (tagPositions.length > 0) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
let lastPos = 0;
|
|
470
|
+
for (const { pos, tag, placeholder } of tagPositions) {
|
|
471
|
+
parts.push(input.slice(lastPos, pos));
|
|
472
|
+
parts.push(placeholder);
|
|
473
|
+
lastPos = pos + tag.length;
|
|
474
|
+
}
|
|
475
|
+
parts.push(input.slice(lastPos));
|
|
476
|
+
input = parts.join("");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
tagsMap = this.#tagLoop(tagsMap);
|
|
480
|
+
input = this.#contentLoop(tagsMap, input);
|
|
481
|
+
if (this.config.cleanUnmatchable) input = input.replace(this.regex.placeholders, "");
|
|
482
|
+
return input;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export { yabbcode as default };
|
|
487
|
+
//# sourceMappingURL=ya-bbcode.mjs.map
|