ya-bbcode 5.1.2 → 6.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 +33 -0
- package/dist/ya-bbcode.cjs +412 -335
- package/dist/ya-bbcode.cjs.map +1 -1
- package/dist/ya-bbcode.d.cts +0 -20
- package/dist/ya-bbcode.d.mts +0 -20
- package/dist/ya-bbcode.mjs +412 -335
- package/dist/ya-bbcode.mjs.map +1 -1
- package/package.json +1 -1
package/dist/ya-bbcode.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.mjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n// NUL is stripped from input so placeholders can't be forged; a distinct end stops `\\0 1 \\0 2 \\0` reading as placeholder 2.\nconst PLACEHOLDER_START = '\\0';\nconst PLACEHOLDER_END = ';';\n\nfunction placeholder(index: number): string {\n\treturn PLACEHOLDER_START + index + PLACEHOLDER_END;\n}\n\n// String#replace expands `$&`, `$'` and `` $` `` in string replacements, which user-controlled attributes could abuse.\nfunction replaceFirst(content: string, search: string, replacement: string): string {\n\tconst index = content.indexOf(search);\n\tif (index === -1) {\n\t\treturn content;\n\t}\n\treturn content.slice(0, index) + replacement + content.slice(index + search.length);\n}\n\nconst sanitizedEntities = {\n\t'&': '&',\n\t'<': '<',\n\t'>': '>',\n\t'"': '\"',\n\t''': '\\'',\n};\n\nfunction unescapeSanitized(value: string): string {\n\treturn value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity: string) => sanitizedEntities[entity as keyof typeof sanitizedEntities]);\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n// Mirrors the WHATWG URL parser, which trims C0 controls and spaces and drops tabs/newlines, so `java\\tscript:` can't slip through.\nfunction normalizeUrl(url: string): string {\n\tlet start = 0;\n\tlet end = url.length;\n\t// Comparing against ' ' matches every character from U+0000 to U+0020\n\twhile (start < end && url.charAt(start) <= ' ') {\n\t\tstart++;\n\t}\n\twhile (end > start && url.charAt(end - 1) <= ' ') {\n\t\tend--;\n\t}\n\treturn url.slice(start, end).replaceAll(/[\\t\\n\\r]/g, '');\n}\n\nconst defaultAllowedSchemes = ['http', 'https', 'mailto', 'ftp', 'ftps'];\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n * Returns undefined for empty or disallowed URLs\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string | undefined {\n\tconst trimmed = normalizeUrl(String(url));\n\tif (!trimmed) {\n\t\treturn undefined;\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? '#')}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(this.#unsanitize(attr))}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\t// Leftover placeholders are resolved after this check, so a removed one could hide a scheme like `[/b]javascript:`\n\t\t\t\tconst url = this.#unsanitize(content.replaceAll(this.regex.placeholders, ''));\n\t\t\t\tconst src = sanitizeUrl(url, this.config.allowedSchemes ?? defaultAllowedSchemes);\n\t\t\t\tif (src === undefined) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(src)}\" alt=\"${escapeHtmlAttr(this.#unsanitize(attr))}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+\\])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\0(\\d+);/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) {\n\t\t\t\treturn content;\n\t\t\t}\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst closeTag = placeholder(tag.closing.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = placeholder(tag.index);\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = placeholder(tag.closing.index);\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: [...defaultAllowedSchemes],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t// Tag names come from user input, so inherited keys like `constructor` must not resolve to Object.prototype members.\n\t#getTag(name: string): TagDefinition | undefined {\n\t\tif (!Object.hasOwn(this.tags, name)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn this.tags[name];\n\t}\n\n\t#convertNewlines(input: string): string {\n\t\tif (!this.config.newline) {\n\t\t\treturn input;\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\treturn input.replace(this.regex.newline, '</p><p>');\n\t\t}\n\t\treturn input.replace(this.regex.newline, '<br/>');\n\t}\n\n\t#applyLayout(input: string): string {\n\t\tinput = this.#convertNewlines(input);\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\t\treturn input;\n\t}\n\n\t// Built-in attribute tags escape for themselves, so undo parse()'s sanitizing first to avoid double-escaping `&`.\n\t#unsanitize(value: string): string {\n\t\tif (!this.config.sanitizeHtml) {\n\t\t\treturn value;\n\t\t}\n\t\treturn unescapeSanitized(value);\n\t}\n\n\t// Tags inside an ignore tag are plain text, so they must never get a placeholder that could pair with a tag outside it.\n\t#findIgnoredTags(tagsMap: TagItem[]): Set<number> {\n\t\tconst ignored = new Set<number>();\n\t\tlet ignoreModule: string | undefined;\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (ignoreModule === undefined) {\n\t\t\t\tif (!tag.isClosing && this.#getTag(tag.module)?.type === 'ignore') {\n\t\t\t\t\tignoreModule = tag.module;\n\t\t\t\t}\n\t\t\t} else if (tag.isClosing && tag.module === ignoreModule) {\n\t\t\t\tignoreModule = undefined;\n\t\t\t} else {\n\t\t\t\tignored.add(tag.index);\n\t\t\t}\n\t\t}\n\t\treturn ignored;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.#getTag(tag.module);\n\t\t\t\tif (module) {\n\t\t\t\t\tif (module.type === 'content' || module.type === 'ignore') {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\tif (module.type === 'replace' && (module as ReplaceTag).autoClose) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn tag.children.some(child => hasSpecialType(child));\n\t\t\t};\n\t\t\tconst hasSpecialTypes = tagsMap.some(tag => hasSpecialType(tag));\n\n\t\t\t// If only replace-type tags without autoClose, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tconst replaceModule = module as ReplaceTag;\n\n\t\t\t\t// Step 1: Process opening tag\n\t\t\t\tlet open = replaceModule.open;\n\t\t\t\tif (typeof(open) === 'function') {\n\t\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.index), open);\n\t\t\t\t}\n\n\t\t\t\t// Step 2: Process children (before closing, so parent's close placeholder is still available)\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tconst closingIndex = tag.closing?.index;\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, closingIndex);\n\t\t\t\t}\n\n\t\t\t\t// Step 3: Handle autoClose for tags without explicit closing\n\t\t\t\tif (replaceModule.autoClose && !tag.closing) {\n\t\t\t\t\tlet autoClose = replaceModule.autoClose;\n\t\t\t\t\tif (typeof(autoClose) === 'function') {\n\t\t\t\t\t\tautoClose = autoClose(tag.attr, tag.attrs);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\t\t\t\t\tlet insertBefore: string | undefined;\n\t\t\t\t\tfor (let siblingIndex = i + 1; siblingIndex < tagsMap.length; siblingIndex++) {\n\t\t\t\t\t\tconst sibling = tagsMap[siblingIndex];\n\t\t\t\t\t\tif (sibling?.module === tag.module) {\n\t\t\t\t\t\t\tinsertBefore = placeholder(sibling.index);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (insertBefore === undefined && parentClosingTagIndex !== undefined) {\n\t\t\t\t\t\tinsertBefore = placeholder(parentClosingTagIndex);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (insertBefore && autoClose) {\n\t\t\t\t\t\tcontent = replaceFirst(content, insertBefore, autoClose + insertBefore);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Step 4: Process closing tag (after children)\n\t\t\t\tlet close = replaceModule.close;\n\t\t\t\tif (typeof(close) === 'function') {\n\t\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t\t}\n\t\t\t\tif (close && tag.closing) {\n\t\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), close);\n\t\t\t\t}\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\t// Resolve children first so replace() receives final HTML it can escape, rather than placeholders that expand into raw HTML after escaping\n\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\tcontent = this.#contentLoop(tag.children, content, tag.closing?.index);\n\t\t\t\t}\n\t\t\t\tcontent = this.contentModules.content(tag, module as ContentTag, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module as IgnoreTag, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.#getTag(tag.module);\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set(placeholder(tag.index), open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set(placeholder(tag.closing.index), close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tfor (const child of tag.children) {\n\t\t\t\tprocessTag(child);\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Unmatched placeholders are kept so parse() handles them per cleanUnmatchable\n\t\treturn content.replaceAll(this.regex.placeholders, (match: string) => replacements.get(match) ?? match);\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (!(childStart < tagsMap.length)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\ttag.children = [];\n\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\tif (!(childTag && !childTag.isClosing && childTag.parent === null)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\ttag.children.push(childTag);\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (tag.isClosing) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\tif (!shouldInclude) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t}\n\t\t\tresult.push(tag);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) {\n\t\t\treturn '';\n\t\t}\n\t\tlet input = String(bbcInput).replaceAll(PLACEHOLDER_START, '');\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn this.#applyLayout(input);\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=', 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/, 1)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\t// Layout runs after placeholders go in, so restored tag text needs its newlines converted to match the text around it\n\t\t\t\traw: this.#convertNewlines(tag),\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\tconst ignored = this.#findIgnoredTags(tagsMap);\n\t\ttagsMap = tagsMap.filter(tag => !ignored.has(tag.index));\n\n\t\t// Ignored tags are still located so the search moves past them, since their text can contain a later tag's text\n\t\tconst parts: string[] = [];\n\t\tlet searchFrom = 0;\n\t\tlet copiedUpTo = 0;\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst pos = input.indexOf(tag, searchFrom);\n\t\t\tif (pos === -1) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tsearchFrom = pos + tag.length;\n\t\t\tif (ignored.has(i)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tparts.push(input.slice(copiedUpTo, pos), placeholder(i));\n\t\t\tcopiedUpTo = searchFrom;\n\t\t}\n\t\tparts.push(input.slice(copiedUpTo));\n\t\tinput = parts.join('');\n\t\t// Only after placeholders are in, so a tag spanning lines still matches its raw text\n\t\tinput = this.#applyLayout(input);\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\t// Placeholders are internal markers, so leftovers become nothing or the original BBCode, never leak\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\treturn input.replaceAll(this.regex.placeholders, '');\n\t\t}\n\t\t// Unknown tags are restored before content tags escape their output, but leftovers land after, possibly inside an attribute, so raw input must be escaped here\n\t\treturn input.replaceAll(this.regex.placeholders, (_match: string, index: string) => {\n\t\t\tlet raw = tags[Number(index)] ?? '';\n\t\t\tif (!this.config.sanitizeHtml) {\n\t\t\t\traw = escapeHtmlAttr(raw);\n\t\t\t}\n\t\t\treturn this.#convertNewlines(raw);\n\t\t});\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
|
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\n// What one parse() call knows about a tag name, shared by every tag using it\ninterface TagKind {\n\tname: string;\n\tdefinition: TagDefinition | undefined;\n\t// The most recent unclosed tag of this name, so a closing tag finds its partner without searching\n\tlastOpen: OpenTag | undefined;\n}\n\n// The text between a tag's brackets and the kind it names\ninterface Spelling {\n\tcontent: string;\n\tkind: TagKind;\n}\n\n// An opening tag. Closing tags get no object of their own: once paired they are only ever a slot number.\ninterface OpenTag {\n\tslot: number;\n\t// -1 until a closing tag pairs with this one\n\tcloseSlot: number;\n\tkind: TagKind;\n\t// Text between the brackets, kept so attributes are only parsed for tags whose definition asks for them\n\tcontent: string;\n\tparent: OpenTag | undefined;\n\tfirstChild: OpenTag | undefined;\n\tnextSibling: OpenTag | undefined;\n\t// The unclosed tag of the same name opened before this one, forming a per-name stack without an array per name\n\tpreviousOpen: OpenTag | undefined;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes | undefined; // Parsed attributes\n}\n\n// One parse() call's working state, kept off the instance so a tag function can safely call parse() again\ninterface Doc {\n\tinput: string;\n\t// What a newline in text becomes, or undefined when newlines are kept\n\tlineBreak: string | undefined;\n\t// Output as [text, slot, text, slot, ..., text]: each tag owns the slot at 2n+1, undefined until something resolves it\n\tparts: (string | undefined)[];\n\t// [start, end] offsets of each slot's tag in `input`, so its original text can be restored\n\tbounds: number[];\n\t// Positions of the next \\n and \\r at or after the last text read, remembered so the input is only searched once\n\tnextLf: number;\n\tnextCr: number;\n\t// After the slot in the key, the next part still in use is the text before slot value+1; everything between was folded into a content tag\n\tskips: Map<number, number> | undefined;\n\t// Slots a content tag was given as placeholders and did not keep, so nothing may write to them again\n\tdead: Set<number> | undefined;\n}\n\n// NUL is stripped from input so placeholders can't be forged; a distinct end stops `\\0 1 \\0 2 \\0` reading as placeholder 2.\nconst PLACEHOLDER_START = '\\0';\nconst PLACEHOLDER_END = ';';\nconst PARAGRAPH_BREAK = '</p><p>';\n\nconst newlines = /\\r\\n|\\r|\\n/g;\nconst placeholders = /\\0(\\d+);/g;\nconst whitespace = /\\s/;\nconst escapable = /[\"&'<>]/g;\nconst tabOrNewline = /[\\t\\n\\r]/;\nconst relativeUrl = /^\\.{0,2}\\//;\nconst schemeName = /^[a-z][\\d+.a-z-]*$/;\n\n// Attribute and tag names that would reach Object.prototype\nconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction placeholder(index: number): string {\n\treturn PLACEHOLDER_START + index + PLACEHOLDER_END;\n}\n\n// The entity for a character matched by `escapable`\nfunction entityFor(character: string): string {\n\tswitch (character) {\n\t\tcase '&': {\n\t\t\treturn '&';\n\t\t}\n\t\tcase '<': {\n\t\t\treturn '<';\n\t\t}\n\t\tcase '>': {\n\t\t\treturn '>';\n\t\t}\n\t\tcase '\"': {\n\t\t\treturn '"';\n\t\t}\n\t\tdefault: {\n\t\t\treturn ''';\n\t\t}\n\t}\n}\n\n/**\n * Escapes text for use in HTML content or attribute values to prevent XSS\n * Encodes quotes, angle brackets, and ampersands\n */\nfunction escapeHtml(value: string): string {\n\t// test() on a global regex resumes from lastIndex, which walks the matches without a callback or match array for each one\n\tescapable.lastIndex = 0;\n\tif (!escapable.test(value)) {\n\t\treturn value;\n\t}\n\tlet escaped = '';\n\tlet copied = 0;\n\tdo {\n\t\tconst index = escapable.lastIndex - 1;\n\t\tescaped += value.slice(copied, index) + entityFor(value.charAt(index));\n\t\tcopied = index + 1;\n\t} while (escapable.test(value));\n\treturn escaped + value.slice(copied);\n}\n\nfunction unescapeSanitized(value: string): string {\n\tif (!value.includes('&')) {\n\t\treturn value;\n\t}\n\t// `&` goes last so `&lt;` becomes `<` rather than `<`\n\treturn value\n\t\t.replaceAll('<', '<')\n\t\t.replaceAll('>', '>')\n\t\t.replaceAll('"', '\"')\n\t\t.replaceAll(''', '\\'')\n\t\t.replaceAll('&', '&');\n}\n\n// Mirrors the WHATWG URL parser, which trims C0 controls and spaces and drops tabs/newlines, so `java\\tscript:` can't slip through.\nfunction normalizeUrl(url: string): string {\n\tlet start = 0;\n\tlet end = url.length;\n\t// Comparing against ' ' matches every character from U+0000 to U+0020\n\twhile (start < end && url.charAt(start) <= ' ') {\n\t\tstart++;\n\t}\n\twhile (end > start && url.charAt(end - 1) <= ' ') {\n\t\tend--;\n\t}\n\tconst trimmed = url.slice(start, end);\n\tif (!tabOrNewline.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\treturn trimmed.replaceAll(/[\\t\\n\\r]/g, '');\n}\n\nconst defaultAllowedSchemes = ['http', 'https', 'mailto', 'ftp', 'ftps'];\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n * Returns undefined for empty or disallowed URLs\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string | undefined {\n\tconst trimmed = normalizeUrl(String(url));\n\tif (!trimmed) {\n\t\treturn undefined;\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (relativeUrl.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// A scheme is whatever comes before the first colon, when that reads as a scheme name\n\tconst colon = trimmed.indexOf(':');\n\tif (colon > 0) {\n\t\tconst scheme = trimmed.slice(0, colon).toLowerCase();\n\t\t// The allow list is checked first because it settles nearly every URL without running the regex\n\t\tif (!allowedSchemes.includes(scheme) && schemeName.test(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n// The tag name is everything before the first `=` in [tag=value], or before the first whitespace in [tag key=value flag]\nfunction tagName(content: string): string {\n\tconst firstEquals = content.indexOf('=');\n\tconst firstSpace = content.indexOf(' ');\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tif (firstEquals === -1) {\n\t\t\treturn content.toLowerCase();\n\t\t}\n\t\treturn content.slice(0, firstEquals).toLowerCase();\n\t}\n\treturn content.slice(0, content.search(whitespace)).toLowerCase();\n}\n\n/**\n * Parse a tag's attributes from the text between its brackets\n * Supports both formats:\n * - [tag=value] → attr: 'value', attrs: {}\n * - [tag key=value flag] → attr: 'value', attrs: { key: 'value', flag: true }\n */\nfunction attributesOf(tag: OpenTag): TagAttributes {\n\tif (tag.attrs !== undefined) {\n\t\treturn tag.attrs;\n\t}\n\tconst content = tag.content;\n\tconst firstSpace = content.indexOf(' ');\n\tconst firstEquals = content.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tif (firstEquals !== -1) {\n\t\t\t// Everything after first =\n\t\t\ttag.attr = content.slice(firstEquals + 1);\n\t\t}\n\t\ttag.attrs = {};\n\t\treturn tag.attrs;\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = content.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\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\tconst eqIndex = part.indexOf('=');\n\t\tif (eqIndex === -1) {\n\t\t\t// Boolean flag\n\t\t\tif (!dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tattrs[part] = true;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst key = part.slice(0, eqIndex);\n\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst value = part.slice(eqIndex + 1);\n\t\tattrs[key] = value;\n\t\t// First key=value becomes the attr for backward compat\n\t\tif (!tag.attr && value) {\n\t\t\ttag.attr = value;\n\t\t}\n\t}\n\n\ttag.attrs = attrs;\n\treturn attrs;\n}\n\nfunction breakLines(text: string, lineBreak: string): string {\n\t// Passed as literals because a replacement string can carry `$` patterns, which a variable would hide\n\tif (lineBreak === PARAGRAPH_BREAK) {\n\t\treturn text.replaceAll(newlines, '</p><p>');\n\t}\n\treturn text.replaceAll(newlines, '<br/>');\n}\n\n// How many newlines one piece of text has spliced in by hand before the rest goes to the regex\nconst maxSplicedNewlines = 8;\n\nfunction indexOrEnd(input: string, character: string, from: number): number {\n\tconst index = input.indexOf(character, from);\n\tif (index === -1) {\n\t\treturn Infinity;\n\t}\n\treturn index;\n}\n\n// The text between two tags, with newlines converted when the config asks for it\nfunction textBetween(doc: Doc, start: number, end: number): string {\n\tif (start === end) {\n\t\treturn '';\n\t}\n\tconst { input, lineBreak } = doc;\n\tif (lineBreak === undefined) {\n\t\treturn input.slice(start, end);\n\t}\n\t// Text between tags holds few newlines if any, so they are found with indexOf and spliced by hand: a regex per piece costs more\n\tlet text = '';\n\tlet from = start;\n\tfor (let spliced = 0; ; spliced++) {\n\t\tif (doc.nextLf < from) {\n\t\t\tdoc.nextLf = indexOrEnd(input, '\\n', from);\n\t\t}\n\t\tif (doc.nextCr < from) {\n\t\t\tdoc.nextCr = indexOrEnd(input, '\\r', from);\n\t\t}\n\t\tconst newline = Math.min(doc.nextLf, doc.nextCr);\n\t\tif (newline >= end) {\n\t\t\tbreak;\n\t\t}\n\t\tif (spliced === maxSplicedNewlines) {\n\t\t\t// A long block of lines, such as pasted code, where one regex pass over the rest is the cheaper way\n\t\t\treturn text + breakLines(input.slice(from, end), lineBreak);\n\t\t}\n\t\ttext += input.slice(from, newline) + lineBreak;\n\t\tfrom = newline + 1;\n\t\t// \\r\\n is a single newline\n\t\tif (doc.nextCr === newline && doc.nextLf === from) {\n\t\t\tfrom++;\n\t\t}\n\t}\n\tif (from === start) {\n\t\treturn input.slice(start, end);\n\t}\n\treturn text + input.slice(from, end);\n}\n\n// A tag exactly as it was written, with its newlines converted to match the text around it\nfunction tagText(doc: Doc, slot: number, escape = false): string {\n\tlet text = doc.input.slice(doc.bounds[slot * 2], doc.bounds[(slot * 2) + 1]);\n\tif (escape) {\n\t\ttext = escapeHtml(text);\n\t}\n\tconst { lineBreak } = doc;\n\t// Tags rarely span lines, so the regex only runs for the ones that do\n\tif (lineBreak === undefined || !(text.includes('\\n') || text.includes('\\r'))) {\n\t\treturn text;\n\t}\n\treturn breakLines(text, lineBreak);\n}\n\n// Makes every unclaimed tag opened after `parent` its child. They are always the tail of `orphans`, so each tag is moved once.\nfunction adopt(parent: OpenTag, orphans: OpenTag[]): void {\n\tlet first = orphans.length;\n\twhile (first > 0 && (orphans[first - 1] as OpenTag).slot > parent.slot) {\n\t\tfirst--;\n\t}\n\tif (first === orphans.length) {\n\t\treturn;\n\t}\n\tparent.firstChild = orphans[first];\n\tfor (let i = first; i < orphans.length; i++) {\n\t\tconst child = orphans[i] as OpenTag;\n\t\tchild.parent = parent;\n\t\tchild.nextSibling = orphans[i + 1];\n\t}\n\torphans.length = first;\n}\n\n/**\n * Looks up the kind for the text between a tag's brackets, creating it the first time a name is seen.\n */\nfunction kindOf(kinds: Map<string, TagKind>, tags: TagsMap, content: string): TagKind {\n\tconst name = tagName(content);\n\tlet kind = kinds.get(name);\n\tif (kind === undefined) {\n\t\t// Tag names come from user input, so inherited keys like `constructor` must not resolve to Object.prototype members.\n\t\tlet definition: TagDefinition | undefined;\n\t\tif (Object.hasOwn(tags, name)) {\n\t\t\tdefinition = tags[name];\n\t\t}\n\t\tkind = { name, definition, lastOpen: undefined };\n\t\tkinds.set(name, kind);\n\t}\n\treturn kind;\n}\n\n// Whether `input` reads `text` at `start`, compared in place so a repeated tag needs no new string\nfunction spells(text: string, input: string, start: number, length: number): boolean {\n\tif (text.length !== length) {\n\t\treturn false;\n\t}\n\tfor (let i = 0; i < length; i++) {\n\t\tif (text.codePointAt(i) !== input.codePointAt(start + i)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n// Size of the table of recently seen spellings, and the longest spelling worth keeping in it\nconst recentSize = 64;\nconst recentMaxLength = 16;\n\n/**\n * Finds every tag in one pass, pairs opening and closing tags and links them into a tree.\n * Returns the first top-level tag, or undefined when the input has no opening tag.\n */\nfunction buildTree(doc: Doc, tags: TagsMap): OpenTag | undefined {\n\tconst { input, parts, bounds } = doc;\n\tconst kinds = new Map<string, TagKind>();\n\t// Documents repeat a few short tags, so the last spelling seen per hash is checked before allocating and hashing a new string\n\tconst recent: (Spelling | undefined)[] = [];\n\t// Opening tags no parent has claimed yet, in document order\n\tconst orphans: OpenTag[] = [];\n\tlet unclosed = 0;\n\t// The ignore tag (e.g. noparse) whose contents are currently being skipped as plain text\n\tlet ignoring: TagKind | undefined;\n\tlet textStart = 0;\n\tlet from = 0;\n\t// Cached positions of the next `]` and `^`, so input like `[[[[` is scanned once rather than once per bracket\n\tlet close = -1;\n\tlet caret = -1;\n\n\t// A tag is `[`, one or more characters other than `]` and `^`, then `]`\n\tfor (;;) {\n\t\tconst open = input.indexOf('[', from);\n\t\tif (open === -1) {\n\t\t\tbreak;\n\t\t}\n\t\tif (close <= open) {\n\t\t\tclose = input.indexOf(']', open + 1);\n\t\t\tif (close === -1) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\tif (caret <= open) {\n\t\t\tcaret = indexOrEnd(input, '^', open + 1);\n\t\t}\n\t\tif (caret < close) {\n\t\t\t// No bracket before the caret can start a tag either, as each would run into it too\n\t\t\tfrom = caret + 1;\n\t\t\tcontinue;\n\t\t}\n\t\tfrom = close + 1;\n\t\tif (close === open + 1) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet nameStart = open + 1;\n\t\tconst isClosing = input.charAt(nameStart) === '/';\n\t\tif (isClosing) {\n\t\t\tnameStart++;\n\t\t}\n\t\t// Tags inside an ignore tag are plain text, so they never get a slot that could pair with a tag outside it\n\t\tif (ignoring !== undefined && !isClosing) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst length = close - nameStart;\n\t\tconst hash = ((input.codePointAt(nameStart) ?? 0) + ((input.codePointAt(close - 1) ?? 0) * 3) + (length * 7)) % recentSize;\n\t\tlet spelling = recent[hash];\n\t\tif (spelling === undefined || !spells(spelling.content, input, nameStart, length)) {\n\t\t\tconst content = input.slice(nameStart, close);\n\t\t\tspelling = { content, kind: kindOf(kinds, tags, content) };\n\t\t\tif (length <= recentMaxLength) {\n\t\t\t\trecent[hash] = spelling;\n\t\t\t}\n\t\t}\n\t\tconst { content, kind } = spelling;\n\t\tif (ignoring !== undefined) {\n\t\t\tif (kind !== ignoring) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tignoring = undefined;\n\t\t}\n\n\t\tconst slot = bounds.length / 2;\n\t\tparts.push(textBetween(doc, textStart, open), undefined);\n\t\tbounds.push(open, close + 1);\n\t\ttextStart = close + 1;\n\n\t\tif (isClosing) {\n\t\t\tconst opener = kind.lastOpen;\n\t\t\tif (opener !== undefined) {\n\t\t\t\tkind.lastOpen = opener.previousOpen;\n\t\t\t\topener.closeSlot = slot;\n\t\t\t\tunclosed--;\n\t\t\t\tadopt(opener, orphans);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst tag: OpenTag = {\n\t\t\tslot,\n\t\t\tcloseSlot: -1,\n\t\t\tkind,\n\t\t\tcontent,\n\t\t\tparent: undefined,\n\t\t\tfirstChild: undefined,\n\t\t\tnextSibling: undefined,\n\t\t\tpreviousOpen: kind.lastOpen,\n\t\t\tattr: '',\n\t\t\tattrs: undefined,\n\t\t};\n\t\tkind.lastOpen = tag;\n\t\tunclosed++;\n\t\torphans.push(tag);\n\t\tif (kind.definition?.type === 'ignore') {\n\t\t\tignoring = kind;\n\t\t}\n\t}\n\n\tparts.push(textBetween(doc, textStart, input.length));\n\n\t// The first tag that never closes takes every unclaimed tag after it as a child\n\tif (unclosed > 0) {\n\t\tlet firstUnclosed: OpenTag | undefined;\n\t\tfor (const kind of kinds.values()) {\n\t\t\tfor (let tag = kind.lastOpen; tag !== undefined; tag = tag.previousOpen) {\n\t\t\t\tif (firstUnclosed === undefined || tag.slot < firstUnclosed.slot) {\n\t\t\t\t\tfirstUnclosed = tag;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (firstUnclosed !== undefined) {\n\t\t\tadopt(firstUnclosed, orphans);\n\t\t}\n\t}\n\n\tfor (let i = 0; i < orphans.length; i++) {\n\t\t(orphans[i] as OpenTag).nextSibling = orphans[i + 1];\n\t}\n\treturn orphans[0];\n}\n\nfunction resolve(doc: Doc, slot: number, value: string): void {\n\tif (doc.dead?.has(slot)) {\n\t\treturn;\n\t}\n\tdoc.parts[(slot * 2) + 1] = value;\n}\n\n// One piece of a replace tag's HTML, which its definition gives as a string or as a function of the tag's attributes\nfunction tagHtml(tag: OpenTag, html: ReplaceTag['close'] | undefined): string | null | undefined {\n\tif (typeof(html) !== 'function') {\n\t\treturn html;\n\t}\n\t// Parsing fills in tag.attr, so it has to happen before the call reads it\n\tconst attrs = attributesOf(tag);\n\treturn html(tag.attr, attrs);\n}\n\n// Runs before a tag's children: the opening tag is replaced first so parents always resolve before what they contain\nfunction enter(doc: Doc, tag: OpenTag): void {\n\tconst definition = tag.kind.definition;\n\tif (!definition) {\n\t\t// ignore invalid BBCode\n\t\tresolve(doc, tag.slot, tagText(doc, tag.slot));\n\t\treturn;\n\t}\n\n\tconst type: string = definition.type;\n\tif (type === 'replace') {\n\t\tconst open = tagHtml(tag, (definition as ReplaceTag).open);\n\t\tif (open) {\n\t\t\tresolve(doc, tag.slot, open);\n\t\t}\n\t} else if (type === 'ignore') {\n\t\t// Both ends go together, so a content tag that dropped one of them leaves the other as an unmatched tag\n\t\tif (doc.dead?.has(tag.slot) || doc.dead?.has(tag.closeSlot)) {\n\t\t\treturn;\n\t\t}\n\t\tdoc.parts[(tag.slot * 2) + 1] = '';\n\t\tif (tag.closeSlot !== -1) {\n\t\t\tdoc.parts[(tag.closeSlot * 2) + 1] = '';\n\t\t}\n\t} else if (type !== 'content') {\n\t\tthrow new Error('Cannot parse content block. Invalid block type [' + type + '] provided for tag [' + tag.kind.name + ']');\n\t}\n}\n\n// Closes a tag that has no closing tag of its own, e.g. a [*] list item\nfunction autoClose(doc: Doc, tag: OpenTag, definition: ReplaceTag): void {\n\tconst html = tagHtml(tag, definition.autoClose);\n\n\t// Close before the next sibling of the same tag (e.g. the next [*]), not before inline tags like [b] inside this item\n\tlet before: number | undefined;\n\tfor (let sibling = tag.nextSibling; sibling !== undefined; sibling = sibling.nextSibling) {\n\t\tif (sibling.kind === tag.kind) {\n\t\t\tbefore = sibling.slot;\n\t\t\tbreak;\n\t\t}\n\t}\n\tif (before === undefined && tag.parent !== undefined && tag.parent.closeSlot !== -1) {\n\t\tbefore = tag.parent.closeSlot;\n\t}\n\n\t// A content tag that dropped the insertion point left nothing to insert before\n\tif (before === undefined || !html || doc.dead?.has(before)) {\n\t\treturn;\n\t}\n\tdoc.parts[before * 2] += html;\n}\n\n/**\n * Writes a content tag's output back when it was given placeholders for slots that were still unresolved.\n * Each placeholder it kept goes back to its own slot with the output before it as that slot's text, so the\n * slot can still be resolved later exactly as if the content tag had never wrapped it.\n */\nfunction reseat(doc: Doc, tag: OpenTag, html: string, pending: number[], skips: Map<number, number>): void {\n\tconst { parts } = doc;\n\tdoc.dead ??= new Set();\n\tconst positions = new Map<number, number>();\n\tfor (const [index, slot] of pending.entries()) {\n\t\tpositions.set(slot, index);\n\t}\n\t// The last slot kept so far: everything between it and the next kept slot is no longer part of the output\n\tlet anchor = tag.slot;\n\t// Index in `pending` of the first slot not yet found in the output\n\tlet next = 0;\n\tlet written = 0;\n\tfor (const match of html.matchAll(placeholders)) {\n\t\tconst slot = Number(match[1]);\n\t\tconst found = positions.get(slot);\n\t\tif (found === undefined || found < next) {\n\t\t\t// Duplicated, reordered or never handed out: it stays in the text, where parse() cleans it up at the end\n\t\t\tcontinue;\n\t\t}\n\t\tfor (; next < found; next++) {\n\t\t\tdoc.dead.add(pending[next] as number);\n\t\t}\n\t\tnext = found + 1;\n\t\tparts[slot * 2] = html.slice(written, match.index);\n\t\tparts[(slot * 2) + 1] = undefined;\n\t\tskips.set(anchor, slot - 1);\n\t\tanchor = slot;\n\t\twritten = match.index + match[0].length;\n\t}\n\tfor (; next < pending.length; next++) {\n\t\tdoc.dead.add(pending[next] as number);\n\t}\n\tparts[(tag.slot * 2) + 1] = '';\n\tparts[(tag.closeSlot * 2) + 1] = html.slice(written);\n\tskips.set(anchor, tag.closeSlot - 1);\n}\n\n// Replaces everything from a content tag's opening tag to its closing tag with what its definition returns\nfunction replaceContent(doc: Doc, tag: OpenTag, definition: ContentTag): void {\n\tconst { parts } = doc;\n\tconst { slot: openSlot, closeSlot } = tag;\n\t// A mis-nested content tag that dropped either end leaves this tag unmatched\n\tif (doc.dead?.has(openSlot) || doc.dead?.has(closeSlot)) {\n\t\treturn;\n\t}\n\n\tlet inner = '';\n\t// Slots still unresolved (e.g. an unmatched closing tag), which the definition sees as placeholders it may keep or drop\n\tlet pending: number[] | undefined;\n\t// The closing slot always ends the walk; the bound only stops a broken skip from running past it forever\n\tfor (let slot = (doc.skips?.get(openSlot) ?? openSlot) + 1; slot <= closeSlot; slot++) {\n\t\tinner += parts[slot * 2];\n\t\tparts[slot * 2] = '';\n\t\tif (slot === closeSlot) {\n\t\t\tbreak;\n\t\t}\n\t\tconst value = parts[(slot * 2) + 1];\n\t\tif (value === undefined) {\n\t\t\tinner += placeholder(slot);\n\t\t\tpending ??= [];\n\t\t\tpending.push(slot);\n\t\t} else {\n\t\t\tinner += value;\n\t\t}\n\t\tparts[(slot * 2) + 1] = '';\n\t\tslot = doc.skips?.get(slot) ?? slot;\n\t}\n\n\tlet replace = definition.replace;\n\tif (typeof(replace) === 'function') {\n\t\tconst attrs = attributesOf(tag);\n\t\treplace = replace(tag.attr, inner, attrs);\n\t}\n\tdoc.skips ??= new Map();\n\tif (pending !== undefined) {\n\t\treseat(doc, tag, String(replace), pending, doc.skips);\n\t\treturn;\n\t}\n\tparts[(openSlot * 2) + 1] = String(replace);\n\tparts[(closeSlot * 2) + 1] = '';\n\tdoc.skips.set(openSlot, doc.skips.get(closeSlot) ?? closeSlot);\n}\n\n// Runs after a tag's children, so content tags receive final HTML and a parent's closing slot is still free for autoClose\nfunction leave(doc: Doc, tag: OpenTag): void {\n\tconst definition = tag.kind.definition;\n\tif (!definition) {\n\t\tif (tag.closeSlot !== -1) {\n\t\t\tresolve(doc, tag.closeSlot, tagText(doc, tag.closeSlot));\n\t\t}\n\t\treturn;\n\t}\n\n\tif (definition.type === 'replace') {\n\t\tif (definition.autoClose && tag.closeSlot === -1) {\n\t\t\tautoClose(doc, tag, definition);\n\t\t}\n\t\tconst close = tagHtml(tag, definition.close);\n\t\tif (close && tag.closeSlot !== -1) {\n\t\t\tresolve(doc, tag.closeSlot, close);\n\t\t}\n\t} else if (definition.type === 'content' && tag.closeSlot !== -1) {\n\t\treplaceContent(doc, tag, definition);\n\t}\n}\n\n// Walks the tree without recursion, so deeply nested input cannot overflow the stack\nfunction renderTree(doc: Doc, root: OpenTag | undefined): void {\n\tlet tag = root;\n\twhile (tag !== undefined) {\n\t\tenter(doc, tag);\n\t\tif (tag.firstChild !== undefined) {\n\t\t\ttag = tag.firstChild;\n\t\t\tcontinue;\n\t\t}\n\t\tfor (;;) {\n\t\t\tleave(doc, tag);\n\t\t\tif (tag.nextSibling !== undefined) {\n\t\t\t\ttag = tag.nextSibling;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\ttag = tag.parent;\n\t\t\tif (tag === undefined) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtml(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? defaultAllowedSchemes) ?? '#')}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtml(this.#unsanitize(attr))}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'hr': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<hr/>',\n\t\t\tclose: null,\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: '</li>',\n\t\t\tautoClose: '</li>',\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\t// Leftover placeholders are resolved after this check, so a removed one could hide a scheme like `[/b]javascript:`\n\t\t\t\tlet url = content;\n\t\t\t\tif (url.includes(PLACEHOLDER_START)) {\n\t\t\t\t\turl = url.replaceAll(this.regex.placeholders, '');\n\t\t\t\t}\n\t\t\t\turl = this.#unsanitize(url);\n\t\t\t\tconst src = sanitizeUrl(url, this.config.allowedSchemes ?? defaultAllowedSchemes);\n\t\t\t\tif (src === undefined) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtml(src)}\" alt=\"${escapeHtml(this.#unsanitize(attr))}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\tplaceholders: /\\0(\\d+);/g,\n\t};\n\n\tconfig: Config;\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: [...defaultAllowedSchemes],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t// Built-in attribute tags escape for themselves, so undo parse()'s sanitizing first to avoid double-escaping `&`.\n\t#unsanitize(value: string): string {\n\t\tif (!this.config.sanitizeHtml) {\n\t\t\treturn value;\n\t\t}\n\t\treturn unescapeSanitized(value);\n\t}\n\n\t// What an unresolved slot becomes: nothing, or the original tag, escaped because it can land inside an attribute\n\t#leftover(doc: Doc, slot: number): string {\n\t\tif (this.config.cleanUnmatchable || slot * 2 >= doc.bounds.length) {\n\t\t\treturn '';\n\t\t}\n\t\t// With sanitizeHtml off the input was never escaped, so the tag is escaped here instead\n\t\treturn tagText(doc, slot, !this.config.sanitizeHtml);\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\tif (dangerousKeys.has(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 (input.includes(PLACEHOLDER_START)) {\n\t\t\tinput = input.replaceAll(PLACEHOLDER_START, '');\n\t\t}\n\t\tif (this.config.sanitizeHtml) {\n\t\t\tinput = escapeHtml(input);\n\t\t}\n\n\t\tlet lineBreak: string | undefined;\n\t\tif (this.config.newline) {\n\t\t\tlineBreak = '<br/>';\n\t\t\tif (this.config.paragraph) {\n\t\t\t\tlineBreak = PARAGRAPH_BREAK;\n\t\t\t}\n\t\t}\n\t\tconst doc: Doc = {\n\t\t\tinput,\n\t\t\tlineBreak,\n\t\t\tparts: [],\n\t\t\tbounds: [],\n\t\t\tnextLf: -1,\n\t\t\tnextCr: -1,\n\t\t\tskips: undefined,\n\t\t\tdead: undefined,\n\t\t};\n\t\tconst { parts } = doc;\n\n\t\tconst root = buildTree(doc, this.tags);\n\t\tif (this.config.paragraph) {\n\t\t\tparts[0] = '<p>' + parts[0];\n\t\t\tparts[parts.length - 1] += '</p>';\n\t\t}\n\t\trenderTree(doc, root);\n\n\t\t// Slots are internal markers, so unresolved ones become nothing or the original BBCode, never leak\n\t\tif (!this.config.cleanUnmatchable) {\n\t\t\tfor (let index = 1; index < parts.length; index += 2) {\n\t\t\t\tif (parts[index] === undefined) {\n\t\t\t\t\tparts[index] = this.#leftover(doc, (index - 1) / 2);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// join() writes unresolved slots as empty strings, and returns one flat string rather than a chain of pieces\n\t\tconst output = parts.join('');\n\t\t// Only a tag function can put a placeholder into the output, by repeating one it was given or by producing a NUL itself\n\t\tif (!output.includes(PLACEHOLDER_START)) {\n\t\t\treturn output;\n\t\t}\n\t\treturn output.replaceAll(placeholders, (_match: string, index: string) => this.#leftover(doc, Number(index)));\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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"}
|