ya-bbcode 5.0.0 → 5.1.1

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 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
  [![npm version](https://badge.fury.io/js/ya-bbcode.svg)](https://badge.fury.io/js/ya-bbcode)
6
- [![Actions Status](https://github.com/nodecraft/ya-bbcode/workflows/Test/badge.svg)](https://github.com/nodecraft/ya-bbcode/actions)
7
- [![Coverage Status](https://coveralls.io/repos/github/nodecraft/ya-bbcode/badge.svg)](https://coveralls.io/github/nodecraft/ya-bbcode)
6
+ [![CI](https://github.com/nodecraft/ya-bbcode/actions/workflows/ci.yml/badge.svg)](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).
@@ -1,5 +1,24 @@
1
-
2
- //#region src/ya-bbcode.ts
1
+ // src/ya-bbcode.ts
2
+ const PLACEHOLDER_START = "\0";
3
+ const PLACEHOLDER_END = ";";
4
+ function placeholder(index) {
5
+ return PLACEHOLDER_START + index + PLACEHOLDER_END;
6
+ }
7
+ function replaceFirst(content, search, replacement) {
8
+ const index = content.indexOf(search);
9
+ if (index === -1) return content;
10
+ return content.slice(0, index) + replacement + content.slice(index + search.length);
11
+ }
12
+ const sanitizedEntities = {
13
+ "&": "&",
14
+ "&lt;": "<",
15
+ "&gt;": ">",
16
+ "&quot;": "\"",
17
+ "&#39;": "'"
18
+ };
19
+ function unescapeSanitized(value) {
20
+ return value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity) => sanitizedEntities[entity]);
21
+ }
3
22
  /**
4
23
  * Escapes HTML attribute values to prevent XSS
5
24
  * Encodes quotes, angle brackets, and other special characters
@@ -14,12 +33,19 @@ function escapeHtmlAttr(value) {
14
33
  };
15
34
  return String(value).replaceAll(/["&'<>]/g, (char) => attrEntities[char] || char);
16
35
  }
36
+ function normalizeUrl(url) {
37
+ let start = 0;
38
+ let end = url.length;
39
+ while (start < end && url.charAt(start) <= " ") start++;
40
+ while (end > start && url.charAt(end - 1) <= " ") end--;
41
+ return url.slice(start, end).replaceAll(/[\t\n\r]/g, "");
42
+ }
17
43
  /**
18
44
  * Validates and sanitizes URLs to prevent javascript: and data: URL attacks
19
45
  * Only allows safe URL schemes based on the allowedSchemes list
20
46
  */
21
47
  function sanitizeUrl(url, allowedSchemes) {
22
- const trimmed = String(url).trim();
48
+ const trimmed = normalizeUrl(String(url));
23
49
  if (!trimmed) return "#";
24
50
  if (/^\.{0,2}\//.test(trimmed)) return trimmed;
25
51
  if (trimmed.startsWith("#")) return trimmed;
@@ -46,7 +72,7 @@ function parseTagAttributes(tagContent) {
46
72
  const parts = tagContent.split(/\s+/);
47
73
  const attrs = Object.create(null);
48
74
  let firstValue = "";
49
- const dangerousKeys = new Set([
75
+ const dangerousKeys = /* @__PURE__ */ new Set([
50
76
  "__proto__",
51
77
  "constructor",
52
78
  "prototype"
@@ -75,7 +101,7 @@ var yabbcode = class {
75
101
  tags = {
76
102
  "url": {
77
103
  type: "replace",
78
- open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? [
104
+ open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? [
79
105
  "http",
80
106
  "https",
81
107
  "mailto",
@@ -86,7 +112,7 @@ var yabbcode = class {
86
112
  },
87
113
  "quote": {
88
114
  type: "replace",
89
- open: (attr) => `<blockquote author="${escapeHtmlAttr(attr)}">`,
115
+ open: (attr) => `<blockquote author="${escapeHtmlAttr(this.#unsanitize(attr))}">`,
90
116
  close: "</blockquote>"
91
117
  },
92
118
  "b": {
@@ -139,6 +165,11 @@ var yabbcode = class {
139
165
  open: "<code>",
140
166
  close: "</code>"
141
167
  },
168
+ "hr": {
169
+ type: "replace",
170
+ open: "<hr/>",
171
+ close: null
172
+ },
142
173
  "strike": {
143
174
  type: "replace",
144
175
  open: "<span class=\"yabbcode-strike\">",
@@ -162,40 +193,41 @@ var yabbcode = class {
162
193
  "*": {
163
194
  type: "replace",
164
195
  open: "<li>",
165
- close: null
196
+ close: "</li>",
197
+ autoClose: "</li>"
166
198
  },
167
199
  "img": {
168
200
  type: "content",
169
201
  replace: (attr, content) => {
170
202
  if (!content) return "";
171
- return `<img src="${escapeHtmlAttr(content)}" alt="${escapeHtmlAttr(attr)}"/>`;
203
+ return `<img src="${escapeHtmlAttr(this.#unsanitize(content))}" alt="${escapeHtmlAttr(this.#unsanitize(attr))}"/>`;
172
204
  }
173
205
  },
174
206
  "noparse": { type: "ignore" }
175
207
  };
176
208
  regex = {
177
- tags: /(\[[^\]^]+])/g,
209
+ tags: /(\[[^\]^]+\])/g,
178
210
  newline: /\r\n|\r|\n/g,
179
- placeholders: /\[TAG-[1-9]+]/g
211
+ placeholders: /\0(\d+);/g
180
212
  };
181
213
  config;
182
214
  contentModules = {
183
- replace: (tag, module$1, content) => {
184
- let open = module$1.open;
185
- let close = module$1.close;
215
+ replace: (tag, module, content) => {
216
+ let open = module.open;
217
+ let close = module.close;
186
218
  if (typeof open === "function") open = open(tag.attr, tag.attrs);
187
219
  if (typeof close === "function") close = close(tag.attr, tag.attrs);
188
- if (open && !tag.isClosing) content = content.replace("[TAG-" + tag.index + "]", open);
189
- if (close && tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", close);
220
+ if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
221
+ if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
190
222
  return content;
191
223
  },
192
- content: (tag, module$1, content) => {
224
+ content: (tag, module, content) => {
193
225
  if (!tag.closing) return content;
194
- const openTag = "[TAG-" + tag.index + "]";
195
- const closeTag = "[TAG-" + tag.closing.index + "]";
226
+ const openTag = placeholder(tag.index);
227
+ const closeTag = placeholder(tag.closing.index);
196
228
  const start = content.indexOf(openTag);
197
229
  const end = content.indexOf(closeTag);
198
- let replace = module$1.replace;
230
+ let replace = module.replace;
199
231
  const innerContent = content.slice(start + openTag.length, end);
200
232
  if (typeof replace === "function") replace = replace(tag.attr, innerContent, tag.attrs);
201
233
  const contentStart = content.slice(0, Math.max(0, start));
@@ -203,12 +235,12 @@ var yabbcode = class {
203
235
  return contentStart + replace + contentEnd;
204
236
  },
205
237
  ignore: (tag, _module, content) => {
206
- const openTag = "[TAG-" + tag.index + "]";
238
+ const openTag = placeholder(tag.index);
207
239
  const start = content.indexOf(openTag);
208
240
  let closeTag = "";
209
241
  let end = content.length;
210
242
  if (tag.closing) {
211
- closeTag = "[TAG-" + tag.closing.index + "]";
243
+ closeTag = placeholder(tag.closing.index);
212
244
  end = content.indexOf(closeTag);
213
245
  }
214
246
  let innerContent = content.slice(start + openTag.length, end);
@@ -238,56 +270,107 @@ var yabbcode = class {
238
270
  if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
239
271
  if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
240
272
  }
273
+ #getTag(name) {
274
+ if (!Object.hasOwn(this.tags, name)) return;
275
+ return this.tags[name];
276
+ }
277
+ #convertNewlines(input) {
278
+ if (!this.config.newline) return input;
279
+ if (this.config.paragraph) return input.replace(this.regex.newline, "</p><p>");
280
+ return input.replace(this.regex.newline, "<br/>");
281
+ }
282
+ #applyLayout(input) {
283
+ input = this.#convertNewlines(input);
284
+ if (this.config.paragraph) input = "<p>" + input + "</p>";
285
+ return input;
286
+ }
287
+ #unsanitize(value) {
288
+ if (!this.config.sanitizeHtml) return value;
289
+ return unescapeSanitized(value);
290
+ }
241
291
  #ignoreLoop(tagsMap, content) {
242
292
  for (const tag of tagsMap) {
243
- content = content.replace("[TAG-" + tag.index + "]", tag.raw);
244
- if (tag.closing) content = content.replace("[TAG-" + tag.closing.index + "]", tag.closing.raw);
293
+ content = replaceFirst(content, placeholder(tag.index), tag.raw);
294
+ if (tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), tag.closing.raw);
245
295
  if (tag.children.length > 0) content = this.#ignoreLoop(tag.children, content);
246
296
  }
247
297
  return content;
248
298
  }
249
- #contentLoop(tagsMap, content) {
299
+ #contentLoop(tagsMap, content, parentClosingTagIndex) {
250
300
  if (tagsMap.length > 50) {
251
- if (!tagsMap.some((tag) => {
252
- const module$1 = this.tags[tag.module];
253
- return module$1 && (module$1.type === "content" || module$1.type === "ignore");
254
- })) return this.#contentLoopOptimized(tagsMap, content);
301
+ const hasSpecialType = (tag) => {
302
+ const module = this.#getTag(tag.module);
303
+ if (module) {
304
+ if (module.type === "content" || module.type === "ignore") return true;
305
+ if (module.type === "replace" && module.autoClose) return true;
306
+ }
307
+ return tag.children.some((child) => hasSpecialType(child));
308
+ };
309
+ if (!tagsMap.some((tag) => hasSpecialType(tag))) return this.#contentLoopOptimized(tagsMap, content);
255
310
  }
256
- for (const tag of tagsMap) {
257
- let module$1 = this.tags[tag.module];
258
- if (!module$1) module$1 = {
311
+ for (let i = 0; i < tagsMap.length; i++) {
312
+ const tag = tagsMap[i];
313
+ if (!tag) continue;
314
+ let module = this.#getTag(tag.module);
315
+ if (!module) module = {
259
316
  type: "replace",
260
317
  open: tag.raw,
261
318
  close: tag.closing ? tag.closing.raw : ""
262
319
  };
263
- if (!this.contentModules[module$1.type]) throw new Error("Cannot parse content block. Invalid block type [" + module$1.type + "] provided for tag [" + tag.module + "]");
264
- if (module$1.type === "replace") content = this.contentModules.replace(tag, module$1, content);
265
- else if (module$1.type === "content") content = this.contentModules.content(tag, module$1, content);
266
- else if (module$1.type === "ignore") content = this.contentModules.ignore(tag, module$1, content);
267
- if (tag.children.length > 0 && module$1.type !== "ignore") content = this.#contentLoop(tag.children, content);
320
+ if (!this.contentModules[module.type]) throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
321
+ if (module.type === "replace") {
322
+ const replaceModule = module;
323
+ let open = replaceModule.open;
324
+ if (typeof open === "function") open = open(tag.attr, tag.attrs);
325
+ if (open && !tag.isClosing) content = replaceFirst(content, placeholder(tag.index), open);
326
+ if (tag.children.length > 0) {
327
+ const closingIndex = tag.closing?.index;
328
+ content = this.#contentLoop(tag.children, content, closingIndex);
329
+ }
330
+ if (replaceModule.autoClose && !tag.closing) {
331
+ let autoClose = replaceModule.autoClose;
332
+ if (typeof autoClose === "function") autoClose = autoClose(tag.attr, tag.attrs);
333
+ let insertBefore;
334
+ for (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {
335
+ const sibling = tagsMap[siblingIndex];
336
+ if (sibling?.module === tag.module) {
337
+ insertBefore = placeholder(sibling.index);
338
+ break;
339
+ }
340
+ }
341
+ if (insertBefore === void 0 && parentClosingTagIndex !== void 0) insertBefore = placeholder(parentClosingTagIndex);
342
+ if (insertBefore && autoClose) content = replaceFirst(content, insertBefore, autoClose + insertBefore);
343
+ }
344
+ let close = replaceModule.close;
345
+ if (typeof close === "function") close = close(tag.attr, tag.attrs);
346
+ if (close && tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), close);
347
+ } else if (module.type === "content") {
348
+ if (tag.children.length > 0) content = this.#contentLoop(tag.children, content, tag.closing?.index);
349
+ content = this.contentModules.content(tag, module, content);
350
+ } else if (module.type === "ignore") content = this.contentModules.ignore(tag, module, content);
268
351
  }
269
352
  return content;
270
353
  }
271
354
  #contentLoopOptimized(tagsMap, content) {
272
355
  const replacements = /* @__PURE__ */ new Map();
273
356
  const processTag = (tag) => {
274
- let module$1 = this.tags[tag.module];
275
- if (!module$1) module$1 = {
357
+ let module = this.#getTag(tag.module);
358
+ if (!module) module = {
276
359
  type: "replace",
277
360
  open: tag.raw,
278
361
  close: tag.closing ? tag.closing.raw : ""
279
362
  };
280
- if (module$1.type !== "replace") return;
281
- let open = module$1.open;
282
- let close = module$1.close;
363
+ if (module.type !== "replace") return;
364
+ let open = module.open;
365
+ let close = module.close;
283
366
  if (typeof open === "function") open = open(tag.attr, tag.attrs);
284
367
  if (typeof close === "function") close = close(tag.attr, tag.attrs);
285
- if (open && !tag.isClosing) replacements.set("[TAG-" + tag.index + "]", open);
286
- if (close && tag.closing) replacements.set("[TAG-" + tag.closing.index + "]", close);
287
- if (tag.children.length > 0) for (const child of tag.children) processTag(child);
368
+ if (open && !tag.isClosing) replacements.set(placeholder(tag.index), open);
369
+ if (close && tag.closing) replacements.set(placeholder(tag.closing.index), close);
370
+ for (const child of tag.children) processTag(child);
288
371
  };
289
372
  for (const tag of tagsMap) processTag(tag);
290
- return content.replaceAll(/\[TAG-(\d+)]/g, (match) => replacements.get(match) || "");
373
+ return content.replaceAll(this.regex.placeholders, (match) => replacements.get(match) ?? match);
291
374
  }
292
375
  #tagLoop(tagsMap, parent) {
293
376
  const stack = [];
@@ -308,10 +391,9 @@ var yabbcode = class {
308
391
  openTag.children = [];
309
392
  for (let childIndex = childStart; childIndex < i; childIndex++) {
310
393
  const childTag = tagsMap[childIndex];
311
- if (childTag && !childTag.isClosing && childTag.parent === null) {
312
- childTag.parent = openTag.index;
313
- openTag.children.push(childTag);
314
- }
394
+ if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
395
+ childTag.parent = openTag.index;
396
+ openTag.children.push(childTag);
315
397
  }
316
398
  }
317
399
  stack.splice(stackIndex, 1);
@@ -326,23 +408,21 @@ var yabbcode = class {
326
408
  for (const tag of stack) {
327
409
  tag.matchTag = false;
328
410
  const childStart = (tag.stackIndex ?? 0) + 1;
329
- if (childStart < tagsMap.length) {
330
- tag.children = [];
331
- for (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {
332
- const childTag = tagsMap[childIndex];
333
- if (childTag && !childTag.isClosing && childTag.parent === null) {
334
- childTag.parent = tag.index;
335
- tag.children.push(childTag);
336
- }
337
- }
411
+ if (!(childStart < tagsMap.length)) continue;
412
+ tag.children = [];
413
+ for (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {
414
+ const childTag = tagsMap[childIndex];
415
+ if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
416
+ childTag.parent = tag.index;
417
+ tag.children.push(childTag);
338
418
  }
339
419
  }
340
420
  const result = [];
341
- for (const tag of tagsMap) if (!tag.isClosing) {
342
- if (parent === void 0 && tag.parent === null || parent !== void 0 && tag.parent === parent) {
343
- if (tag.children.length > 0) tag.children = this.#tagLoop(tag.children, tag.index);
344
- result.push(tag);
345
- }
421
+ for (const tag of tagsMap) {
422
+ if (tag.isClosing) continue;
423
+ if (!(parent === void 0 && tag.parent === null || parent !== void 0 && tag.parent === parent)) continue;
424
+ if (tag.children.length > 0) tag.children = this.#tagLoop(tag.children, tag.index);
425
+ result.push(tag);
346
426
  }
347
427
  return result;
348
428
  }
@@ -362,7 +442,7 @@ var yabbcode = class {
362
442
  }
363
443
  parse(bbcInput) {
364
444
  if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
365
- let input = String(bbcInput);
445
+ let input = String(bbcInput).replaceAll(PLACEHOLDER_START, "");
366
446
  if (this.config.sanitizeHtml) {
367
447
  const htmlEntities = {
368
448
  "&": "&amp;",
@@ -375,10 +455,7 @@ var yabbcode = class {
375
455
  }
376
456
  let tagsMap = [];
377
457
  const tags = String(input).match(this.regex.tags);
378
- if (this.config.newline) if (this.config.paragraph) input = input.replace(this.regex.newline, "</p><p>");
379
- else input = input.replace(this.regex.newline, "<br/>");
380
- if (this.config.paragraph) input = "<p>" + input + "</p>";
381
- if (!tags || tags.length === 0) return input;
458
+ if (!tags || tags.length === 0) return this.#applyLayout(input);
382
459
  for (const [i, tag] of tags.entries()) {
383
460
  const tagContent = tag.slice(1, -1);
384
461
  const isClosing = tagContent.startsWith("/");
@@ -394,17 +471,17 @@ var yabbcode = class {
394
471
  attrs: {}
395
472
  };
396
473
  } else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
397
- moduleName = (contentToParse.split("=")[0] ?? "").toLowerCase();
474
+ moduleName = (contentToParse.split("=", 1)[0] ?? "").toLowerCase();
398
475
  parsedAttrs = parseTagAttributes(contentToParse);
399
476
  } else {
400
- moduleName = (contentToParse.split(/\s+/)[0] ?? "").toLowerCase();
477
+ moduleName = (contentToParse.split(/\s+/, 1)[0] ?? "").toLowerCase();
401
478
  parsedAttrs = parseTagAttributes(contentToParse);
402
479
  }
403
480
  const item = {
404
481
  index: i,
405
482
  module: moduleName,
406
483
  isClosing,
407
- raw: tag,
484
+ raw: this.#convertNewlines(tag),
408
485
  attr: parsedAttrs.attr,
409
486
  attrs: parsedAttrs.attrs,
410
487
  children: [],
@@ -413,20 +490,19 @@ var yabbcode = class {
413
490
  };
414
491
  tagsMap.push(item);
415
492
  }
416
- if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = input.replace(tag, "[TAG-" + i + "]");
493
+ if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = replaceFirst(input, tag, placeholder(i));
417
494
  else {
418
495
  const tagPositions = [];
419
496
  let lastIndex = 0;
420
497
  for (const [i, tag] of tags.entries()) {
421
498
  const pos = input.indexOf(tag, lastIndex);
422
- if (pos !== -1) {
423
- tagPositions.push({
424
- pos,
425
- tag,
426
- placeholder: "[TAG-" + i + "]"
427
- });
428
- lastIndex = pos + tag.length;
429
- }
499
+ if (pos === -1) continue;
500
+ tagPositions.push({
501
+ pos,
502
+ tag,
503
+ placeholder: placeholder(i)
504
+ });
505
+ lastIndex = pos + tag.length;
430
506
  }
431
507
  if (tagPositions.length > 0) {
432
508
  const parts = [];
@@ -440,14 +516,17 @@ var yabbcode = class {
440
516
  input = parts.join("");
441
517
  }
442
518
  }
519
+ input = this.#applyLayout(input);
443
520
  tagsMap = this.#tagLoop(tagsMap);
444
521
  input = this.#contentLoop(tagsMap, input);
445
- if (this.config.cleanUnmatchable) input = input.replace(this.regex.placeholders, "");
446
- return input;
522
+ if (this.config.cleanUnmatchable) return input.replaceAll(this.regex.placeholders, "");
523
+ return input.replaceAll(this.regex.placeholders, (_match, index) => {
524
+ let raw = tags[Number(index)] ?? "";
525
+ if (!this.config.sanitizeHtml) raw = escapeHtmlAttr(raw);
526
+ return this.#convertNewlines(raw);
527
+ });
447
528
  }
448
529
  };
449
- var ya_bbcode_default = yabbcode;
450
530
 
451
- //#endregion
452
- module.exports = ya_bbcode_default;
531
+ module.exports = yabbcode;
453
532
  //# sourceMappingURL=ya-bbcode.cjs.map