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 +21 -21
- package/README.md +4 -2
- package/dist/ya-bbcode.cjs +164 -85
- package/dist/ya-bbcode.cjs.map +1 -1
- package/dist/ya-bbcode.d.cts +6 -4
- package/dist/ya-bbcode.d.mts +6 -4
- package/dist/ya-bbcode.mjs +153 -73
- package/dist/ya-bbcode.mjs.map +1 -1
- package/package.json +18 -17
package/dist/ya-bbcode.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ya-bbcode.cjs","names":["attrEntities: { [key: string]: string; }","attrs: TagAttributes","module","#ignoreLoop","#contentLoopOptimized","#contentLoop","#tagLoop","tagsMap: TagItem[]","moduleName: string","parsedAttrs: { attr: string; attrs: TagAttributes; }","item: TagItem"],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n/**\n * Escapes HTML attribute values to prevent XSS\n * Encodes quotes, angle brackets, and other special characters\n */\nfunction escapeHtmlAttr(value: string): string {\n\tconst attrEntities: { [key: string]: string; } = {\n\t\t'&': '&',\n\t\t'<': '<',\n\t\t'>': '>',\n\t\t'\"': '"',\n\t\t'\\'': ''',\n\t};\n\treturn String(value).replaceAll(/[\"&'<>]/g, (char: string) => attrEntities[char] || char);\n}\n\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string {\n\tconst trimmed = String(url).trim();\n\tif (!trimmed) { return '#'; }\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn '#';\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) { continue; }\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? ['http', 'https', 'mailto', 'ftp', 'ftps']))}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(attr)}\">`,\n\t\t\tclose: '</blockquote>',\n\t\t},\n\t\t'b': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<strong>',\n\t\t\tclose: '</strong>',\n\t\t},\n\t\t'u': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<u>',\n\t\t\tclose: '</u>',\n\t\t},\n\t\t'i': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<i>',\n\t\t\tclose: '</i>',\n\t\t},\n\t\t'h1': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h1>',\n\t\t\tclose: '</h1>',\n\t\t},\n\t\t'h2': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h2>',\n\t\t\tclose: '</h2>',\n\t\t},\n\t\t'h3': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h3>',\n\t\t\tclose: '</h3>',\n\t\t},\n\t\t'h4': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h4>',\n\t\t\tclose: '</h4>',\n\t\t},\n\t\t'h5': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h5>',\n\t\t\tclose: '</h5>',\n\t\t},\n\t\t'h6': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<h6>',\n\t\t\tclose: '</h6>',\n\t\t},\n\t\t'code': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<code>',\n\t\t\tclose: '</code>',\n\t\t},\n\t\t'strike': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-strike\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'spoiler': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<span class=\"yabbcode-spoiler\">',\n\t\t\tclose: '</span>',\n\t\t},\n\t\t'list': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ul>',\n\t\t\tclose: '</ul>',\n\t\t},\n\t\t'olist': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<ol>',\n\t\t\tclose: '</ol>',\n\t\t},\n\t\t'*': {\n\t\t\ttype: 'replace',\n\t\t\topen: '<li>',\n\t\t\tclose: null,\n\t\t},\n\t\t'img': {\n\t\t\ttype: 'content',\n\t\t\treplace: (attr, content) => {\n\t\t\t\tif (!content) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(content)}\" alt=\"${escapeHtmlAttr(attr)}\"/>`;\n\t\t\t},\n\t\t},\n\t\t'noparse': {\n\t\t\ttype: 'ignore',\n\t\t},\n\t};\n\n\tregex = {\n\t\ttags: /(\\[[^\\]^]+])/g,\n\t\tnewline: /\\r\\n|\\r|\\n/g,\n\t\tplaceholders: /\\[TAG-[1-9]+]/g,\n\t};\n\n\tconfig: Config;\n\n\tcontentModules = {\n\t\treplace: (tag: TagItem, module: ReplaceTag, content: string): string => {\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\t// do the replace\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\t//content = content\n\t\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\t\t\treturn content;\n\t\t},\n\t\tcontent: (tag: TagItem, module: ContentTag, content: string): string => {\n\t\t\tif (!tag.closing) { return content; }\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst closeTag = '[TAG-' + tag.closing.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tconst end = content.indexOf(closeTag);\n\t\t\tlet replace = module.replace;\n\n\t\t\tconst innerContent = content.slice(start + openTag.length, end);\n\t\t\tif (typeof(replace) === 'function') {\n\t\t\t\treplace = replace(tag.attr, innerContent, tag.attrs);\n\t\t\t}\n\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\n\t\t\treturn contentStart + replace + contentEnd;\n\t\t},\n\t\tignore: (tag: TagItem, _module: IgnoreTag, content: string): string => {\n\t\t\tconst openTag = '[TAG-' + tag.index + ']';\n\t\t\tconst start = content.indexOf(openTag);\n\t\t\tlet closeTag = '';\n\t\t\tlet end = content.length;\n\t\t\tif (tag.closing) {\n\t\t\t\tcloseTag = '[TAG-' + tag.closing.index + ']';\n\t\t\t\tend = content.indexOf(closeTag);\n\t\t\t}\n\t\t\tlet innerContent = content.slice(start + openTag.length, end);\n\t\t\tinnerContent = this.#ignoreLoop(tag.children, innerContent);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: ['http', 'https', 'mailto', 'ftp', 'ftps'],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t#ignoreLoop(tagsMap: TagItem[], content: string): string {\n\t\tfor (const tag of tagsMap) {\n\t\t\tcontent = content.replace('[TAG-' + tag.index + ']', tag.raw);\n\t\t\tif (tag.closing) {\n\t\t\t\tcontent = content.replace('[TAG-' + tag.closing.index + ']', tag.closing.raw);\n\t\t\t}\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\tcontent = this.#ignoreLoop(tag.children, content);\n\t\t\t}\n\t\t}\n\t\treturn content;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type (most common case for large docs)\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content or ignore type tags\n\t\t\tconst hasSpecialTypes = tagsMap.some((tag) => {\n\t\t\t\tconst module = this.tags[tag.module];\n\t\t\t\treturn module && (module.type === 'content' || module.type === 'ignore');\n\t\t\t});\n\n\t\t\t// If only replace-type tags, use optimized single-pass approach\n\t\t\tif (!hasSpecialTypes) {\n\t\t\t\treturn this.#contentLoopOptimized(tagsMap, content);\n\t\t\t}\n\t\t}\n\n\t\tfor (const tag of tagsMap) {\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\t// ignore invalid BBCode\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (!this.contentModules[module.type]) {\n\t\t\t\tthrow new Error('Cannot parse content block. Invalid block type [' + module.type + '] provided for tag [' + tag.module + ']');\n\t\t\t}\n\t\t\tif (module.type === 'replace') {\n\t\t\t\tcontent = this.contentModules.replace(tag, module, content);\n\t\t\t} else if (module.type === 'content') {\n\t\t\t\tcontent = this.contentModules.content(tag, module, content);\n\t\t\t} else if (module.type === 'ignore') {\n\t\t\t\tcontent = this.contentModules.ignore(tag, module, content);\n\t\t\t}\n\t\t\tif (tag.children.length > 0 && module.type !== 'ignore') {\n\t\t\t\tcontent = this.#contentLoop(tag.children, content);\n\t\t\t}\n\t\t}\n\n\t\treturn content;\n\t}\n\n\t#contentLoopOptimized(tagsMap: TagItem[], content: string): string {\n\t\t// Optimized single-pass replacement for replace-type tags only\n\t\t// Build replacement map for all placeholders\n\t\tconst replacements = new Map<string, string>();\n\n\t\tconst processTag = (tag: TagItem) => {\n\t\t\tlet module = this.tags[tag.module];\n\t\t\tif (!module) {\n\t\t\t\tmodule = {\n\t\t\t\t\ttype: 'replace',\n\t\t\t\t\topen: tag.raw,\n\t\t\t\t\tclose: tag.closing ? tag.closing.raw : '',\n\t\t\t\t} as ReplaceTag;\n\t\t\t}\n\n\t\t\t// Only handle replace type (this function is only called when all tags are replace type)\n\t\t\tif (module.type !== 'replace') { return; }\n\n\t\t\tlet open = module.open;\n\t\t\tlet close = module.close;\n\t\t\tif (typeof(open) === 'function') {\n\t\t\t\topen = open(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (typeof(close) === 'function') {\n\t\t\t\tclose = close(tag.attr, tag.attrs);\n\t\t\t}\n\t\t\tif (open && !tag.isClosing) {\n\t\t\t\treplacements.set('[TAG-' + tag.index + ']', open);\n\t\t\t}\n\t\t\tif (close && tag.closing) {\n\t\t\t\treplacements.set('[TAG-' + tag.closing.index + ']', close);\n\t\t\t}\n\n\t\t\t// Process children recursively\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\tfor (const child of tag.children) {\n\t\t\t\t\tprocessTag(child);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t// Build replacement map for all tags\n\t\tfor (const tag of tagsMap) {\n\t\t\tprocessTag(tag);\n\t\t}\n\n\t\t// Single-pass replacement using regex\n\t\tconst placeholderRegex = /\\[TAG-(\\d+)]/g;\n\t\treturn content.replaceAll(placeholderRegex, (match: string) => replacements.get(match) || '');\n\t}\n\n\t#tagLoop(tagsMap: TagItem[], parent?: number): TagItem[] {\n\t\t// Use stack-based algorithm - O(n) instead of O(n²)\n\t\tconst stack = [];\n\t\tconst processed = new Set();\n\n\t\tfor (let i = 0; i < tagsMap.length; i++) {\n\t\t\tconst tag = tagsMap[i];\n\t\t\tif (!tag) { continue; }\n\n\t\t\tif (tag.isClosing) {\n\t\t\t\t// Find matching opening tag on stack (search backwards for proper nesting)\n\t\t\t\tfor (let stackIndex = stack.length - 1; stackIndex >= 0; stackIndex--) {\n\t\t\t\t\tconst openTag = stack[stackIndex];\n\t\t\t\t\tif (!openTag) { continue; }\n\t\t\t\t\tif (openTag.module === tag.module && !processed.has(openTag.index)) {\n\t\t\t\t\t\t// Match found\n\t\t\t\t\t\topenTag.matchTag = tag.index;\n\t\t\t\t\t\topenTag.closing = tag;\n\t\t\t\t\t\ttag.matchTag = openTag.index;\n\t\t\t\t\t\tprocessed.add(openTag.index);\n\n\t\t\t\t\t\t// Set children for the opening tag\n\t\t\t\t\t\tconst childStart = (openTag.stackIndex ?? 0) + 1;\n\t\t\t\t\t\tif (childStart < i) {\n\t\t\t\t\t\t\topenTag.children = [];\n\t\t\t\t\t\t\tfor (let childIndex = childStart; childIndex < i; childIndex++) {\n\t\t\t\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\t\t\t\tif (childTag && !childTag.isClosing && childTag.parent === null) {\n\t\t\t\t\t\t\t\t\tchildTag.parent = openTag.index;\n\t\t\t\t\t\t\t\t\topenTag.children.push(childTag);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Remove matched opening tag from stack\n\t\t\t\t\t\tstack.splice(stackIndex, 1);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Opening tag - add to stack\n\t\t\t\ttag.stackIndex = i;\n\t\t\t\tstack.push(tag);\n\t\t\t}\n\t\t}\n\n\t\t// Handle unmatched opening tags - assign all remaining tags as children\n\t\tfor (const tag of stack) {\n\t\t\ttag.matchTag = false;\n\t\t\tconst childStart = (tag.stackIndex ?? 0) + 1;\n\t\t\tif (childStart < tagsMap.length) {\n\t\t\t\ttag.children = [];\n\t\t\t\tfor (let childIndex = childStart; childIndex < tagsMap.length; childIndex++) {\n\t\t\t\t\tconst childTag = tagsMap[childIndex];\n\t\t\t\t\tif (childTag && !childTag.isClosing && childTag.parent === null) {\n\t\t\t\t\t\tchildTag.parent = tag.index;\n\t\t\t\t\t\ttag.children.push(childTag);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Filter and process recursively\n\t\tconst result = [];\n\t\tfor (const tag of tagsMap) {\n\t\t\tif (!tag.isClosing) {\n\t\t\t\tconst shouldInclude = (parent === undefined && tag.parent === null) ||\n\t\t\t\t\t(parent !== undefined && tag.parent === parent);\n\t\t\t\tif (shouldInclude) {\n\t\t\t\t\tif (tag.children.length > 0) {\n\t\t\t\t\t\ttag.children = this.#tagLoop(tag.children, tag.index);\n\t\t\t\t\t}\n\t\t\t\t\tresult.push(tag);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tclearTags(): this {\n\t\tthis.tags = {};\n\t\treturn this;\n\t}\n\n\tregisterTag(tag: string, options: TagDefinition): this {\n\t\tconst tagName = String(tag).toLowerCase();\n\n\t\t// Prevent prototype pollution\n\t\tconst dangerousKeys = ['__proto__', 'constructor', 'prototype'];\n\t\tif (dangerousKeys.includes(tagName)) {\n\t\t\tthrow new Error(`Cannot register tag with dangerous name: ${tagName}`);\n\t\t}\n\n\t\tthis.tags[tagName] = options;\n\t\treturn this;\n\t}\n\n\tparse(bbcInput: string | number | boolean): string {\n\t\tif (\n\t\t\ttypeof(bbcInput) === 'boolean'\n\t\t\t|| (typeof(bbcInput) !== 'string' && Number.isNaN(Number(bbcInput)))\n\t\t) { return ''; }\n\t\tlet input = String(bbcInput);\n\t\tif (this.config.sanitizeHtml) {\n\t\t\t// Batch HTML entity replacement for better performance\n\t\t\tconst htmlEntities = { '&': '&', '<': '<', '>': '>', '\"': '"', '\\'': ''' };\n\t\t\tinput = input.replaceAll(/[\"&'<>]/g, (char: string) => htmlEntities[char as keyof typeof htmlEntities]);\n\t\t}\n\n\t\t// reset\n\t\tlet tagsMap: TagItem[] = [];\n\t\t// split input into tags by index\n\t\tconst tags = String(input).match(this.regex.tags);\n\n\t\tif (this.config.newline) {\n\t\t\tif (this.config.paragraph) {\n\t\t\t\tinput = input.replace(this.regex.newline, '</p><p>');\n\t\t\t} else {\n\t\t\t\tinput = input.replace(this.regex.newline, '<br/>');\n\t\t\t}\n\t\t}\n\t\tif (this.config.paragraph) {\n\t\t\tinput = '<p>' + input + '</p>';\n\t\t}\n\n\t\t// handle when no tags are present\n\t\tif (!tags || tags.length === 0) {\n\t\t\treturn input;\n\t\t}\n\t\t// Build tag map and replace with placeholders\n\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\tconst tagContent = tag.slice(1, -1); // Remove [ ]\n\t\t\tconst isClosing = tagContent.startsWith('/');\n\t\t\tconst contentToParse = isClosing ? tagContent.slice(1) : tagContent;\n\n\t\t\t// Parse module name and attributes\n\t\t\tconst spaceIndex = contentToParse.indexOf(' ');\n\t\t\tconst equalsIndex = contentToParse.indexOf('=');\n\t\t\tlet moduleName: string;\n\t\t\tlet parsedAttrs: { attr: string; attrs: TagAttributes; };\n\n\t\t\t// Extract module name (before space or =)\n\t\t\tif (spaceIndex === -1 && equalsIndex === -1) {\n\t\t\t\t// No attributes: [tag]\n\t\t\t\tmoduleName = contentToParse.toLowerCase();\n\t\t\t\tparsedAttrs = { attr: '', attrs: {} };\n\t\t\t} else if (spaceIndex === -1 || (equalsIndex !== -1 && equalsIndex < spaceIndex)) {\n\t\t\t\t// Old format: [tag=value]\n\t\t\t\tmoduleName = (contentToParse.split('=')[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t} else {\n\t\t\t\t// New format: [tag attr1 key=value]\n\t\t\t\tmoduleName = (contentToParse.split(/\\s+/)[0] ?? '').toLowerCase();\n\t\t\t\tparsedAttrs = parseTagAttributes(contentToParse);\n\t\t\t}\n\n\t\t\tconst item: TagItem = {\n\t\t\t\tindex: i,\n\t\t\t\tmodule: moduleName,\n\t\t\t\tisClosing,\n\t\t\t\traw: tag,\n\t\t\t\tattr: parsedAttrs.attr,\n\t\t\t\tattrs: parsedAttrs.attrs,\n\t\t\t\tchildren: [],\n\t\t\t\tparent: null,\n\t\t\t\tmatchTag: null,\n\t\t\t};\n\n\t\t\ttagsMap.push(item);\n\t\t}\n\n\t\t// Replace tags with placeholders - optimized for different input sizes\n\t\tif (tags.length <= 3) {\n\t\t\t// Fast path for few tags - direct replacement is faster\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tinput = input.replace(tag, '[TAG-' + i + ']');\n\t\t\t}\n\t\t} else {\n\t\t\t// Batch replacement for many tags\n\t\t\tconst tagPositions = [];\n\t\t\tlet lastIndex = 0;\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tconst pos = input.indexOf(tag, lastIndex);\n\t\t\t\tif (pos !== -1) {\n\t\t\t\t\ttagPositions.push({ pos, tag: tag, placeholder: '[TAG-' + i + ']' });\n\t\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (tagPositions.length > 0) {\n\t\t\t\tconst parts = [];\n\t\t\t\tlet lastPos = 0;\n\t\t\t\tfor (const { pos, tag, placeholder } of tagPositions) {\n\t\t\t\t\tparts.push(input.slice(lastPos, pos));\n\t\t\t\t\tparts.push(placeholder);\n\t\t\t\t\tlastPos = pos + tag.length;\n\t\t\t\t}\n\t\t\t\tparts.push(input.slice(lastPos));\n\t\t\t\tinput = parts.join('');\n\t\t\t}\n\t\t}\n\t\t// loop through each tag to create nested elements\n\t\ttagsMap = this.#tagLoop(tagsMap);\n\t\t// put back all non-found matches?\n\t\tinput = this.#contentLoop(tagsMap, input);\n\t\tif (this.config.cleanUnmatchable) {\n\t\t\tinput = input.replace(this.regex.placeholders, '');\n\t\t}\n\t\treturn input;\n\t}\n}\n\n// Export types for users\nexport type { Config, TagAttributes, ReplaceTag, ContentTag, IgnoreTag, TagDefinition };\n\n// Default export for both CJS and ESM\nexport default yabbcode;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ya-bbcode.cjs","names":[],"sources":["../src/ya-bbcode.ts"],"sourcesContent":["// Type definitions\ninterface Config {\n\tnewline?: boolean;\n\tparagraph?: boolean;\n\tcleanUnmatchable?: boolean;\n\tsanitizeHtml?: boolean;\n\tallowedSchemes?: string[];\n}\n\n// Parsed attributes from tags like [code lang=javascript skip-lint]\ninterface TagAttributes {\n\t[key: string]: string | boolean;\n}\n\ninterface ReplaceTag {\n\ttype: 'replace';\n\topen: ((attr: string, attrs: TagAttributes) => string) | string;\n\tclose: ((attr: string, attrs: TagAttributes) => string) | string | null;\n\t/**\n\t * HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).\n\t * Useful for tags like [*] that don't have explicit closing tags in BBCode.\n\t */\n\tautoClose?: ((attr: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface ContentTag {\n\ttype: 'content';\n\treplace: ((attr: string, content: string, attrs: TagAttributes) => string) | string;\n}\n\ninterface IgnoreTag {\n\ttype: 'ignore';\n}\n\ntype TagDefinition = ReplaceTag | ContentTag | IgnoreTag;\n\ninterface TagsMap {\n\t[key: string]: TagDefinition;\n}\n\ninterface TagItem {\n\tindex: number;\n\tmodule: string;\n\traw: string;\n\tattr: string; // Backward compat: first value or empty\n\tattrs: TagAttributes; // Parsed attributes\n\tisClosing: boolean;\n\tmatchTag: number | false | null;\n\tclosing?: TagItem;\n\tchildren: TagItem[];\n\tparent: number | null;\n\tstackIndex?: number;\n}\n\n// 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\n/**\n * Validates and sanitizes URLs to prevent javascript: and data: URL attacks\n * Only allows safe URL schemes based on the allowedSchemes list\n */\nfunction sanitizeUrl(url: string, allowedSchemes: string[]): string {\n\tconst trimmed = normalizeUrl(String(url));\n\tif (!trimmed) {\n\t\treturn '#';\n\t}\n\n\t// Allow relative URLs (starting with /, ./, or ../)\n\tif (/^\\.{0,2}\\//.test(trimmed)) {\n\t\treturn trimmed;\n\t}\n\n\t// Allow fragment identifiers\n\tif (trimmed.startsWith('#')) {\n\t\treturn trimmed;\n\t}\n\n\t// Check for scheme\n\tconst schemeMatch = /^([a-z][\\d+.a-z-]*):/.exec(trimmed.toLowerCase());\n\tif (schemeMatch) {\n\t\tconst scheme = schemeMatch[1] ?? '';\n\t\tif (!allowedSchemes.includes(scheme)) {\n\t\t\t// Dangerous scheme detected (javascript:, data:, vbscript:, etc.)\n\t\t\treturn '#';\n\t\t}\n\t}\n\n\treturn trimmed;\n}\n\n/**\n * Parse tag attributes from a tag string\n * Supports both formats:\n * - [tag=value] → { attr: 'value', attrs: {} }\n * - [tag key=value flag] → { attr: 'value', attrs: { key: 'value', flag: true } }\n */\nfunction parseTagAttributes(tagContent: string): { attr: string; attrs: TagAttributes; } {\n\t// Check if tag has spaces (space-separated) or = (simple attribute)\n\tconst firstSpace = tagContent.indexOf(' ');\n\tconst firstEquals = tagContent.indexOf('=');\n\n\t// Simple attribute format: [tag=value] or [tag] (no attributes)\n\tif (firstSpace === -1 || (firstEquals !== -1 && firstEquals < firstSpace)) {\n\t\tconst parts = tagContent.split('=');\n\t\treturn {\n\t\t\tattr: parts.slice(1).join('='), // Everything after first =\n\t\t\tattrs: {},\n\t\t};\n\t}\n\n\t// Space-separated attributes format: [tag attr1 key=value attr2]\n\tconst parts = tagContent.split(/\\s+/);\n\t// Use Object.create(null) to prevent prototype pollution\n\tconst attrs: TagAttributes = Object.create(null) as TagAttributes;\n\tlet firstValue = '';\n\n\t// Dangerous keys that could lead to prototype pollution\n\tconst dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);\n\n\tfor (let i = 1; i < parts.length; i++) {\n\t\tconst part = parts[i];\n\t\tif (!part) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (part.includes('=')) {\n\t\t\tconst eqIndex = part.indexOf('=');\n\t\t\tconst key = part.slice(0, eqIndex);\n\t\t\tconst value = part.slice(eqIndex + 1);\n\n\t\t\t// Prevent prototype pollution\n\t\t\tif (dangerousKeys.has(key.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tattrs[key] = value;\n\t\t\t// First key=value becomes the attr for backward compat\n\t\t\tif (!firstValue && value) {\n\t\t\t\tfirstValue = value;\n\t\t\t}\n\t\t} else {\n\t\t\t// Boolean flag - also check for dangerous keys\n\t\t\tif (dangerousKeys.has(part.toLowerCase())) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tattrs[part] = true;\n\t\t}\n\t}\n\n\treturn { attr: firstValue, attrs };\n}\n\nclass yabbcode {\n\ttags: TagsMap = {\n\t\t'url': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<a href=\"${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? ['http', 'https', 'mailto', 'ftp', 'ftps']))}\">`,\n\t\t\tclose: '</a>',\n\t\t},\n\t\t'quote': {\n\t\t\ttype: 'replace',\n\t\t\topen: attr => `<blockquote author=\"${escapeHtmlAttr(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\tif (!content) {\n\t\t\t\t\treturn '';\n\t\t\t\t}\n\t\t\t\treturn `<img src=\"${escapeHtmlAttr(this.#unsanitize(content))}\" 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\tlet innerContent = content.slice(start + openTag.length, end);\n\t\t\tinnerContent = this.#ignoreLoop(tag.children, innerContent);\n\t\t\tconst contentStart = content.slice(0, Math.max(0, start));\n\t\t\tconst contentEnd = content.slice(end + closeTag.length);\n\t\t\treturn contentStart + innerContent + contentEnd;\n\t\t},\n\t};\n\n\tconstructor(config: Config = {}) {\n\t\tthis.config = {\n\t\t\tnewline: true,\n\t\t\tparagraph: false,\n\t\t\tcleanUnmatchable: true,\n\t\t\tsanitizeHtml: true,\n\t\t\tallowedSchemes: ['http', 'https', 'mailto', 'ftp', 'ftps'],\n\t\t};\n\t\tif (config.newline !== undefined) {\n\t\t\tthis.config.newline = config.newline;\n\t\t}\n\t\tif (config.paragraph !== undefined) {\n\t\t\tthis.config.paragraph = config.paragraph;\n\t\t}\n\t\tif (config.cleanUnmatchable !== undefined) {\n\t\t\tthis.config.cleanUnmatchable = config.cleanUnmatchable;\n\t\t}\n\t\tif (config.sanitizeHtml !== undefined) {\n\t\t\tthis.config.sanitizeHtml = config.sanitizeHtml;\n\t\t}\n\t\tif (config.allowedSchemes !== undefined) {\n\t\t\tthis.config.allowedSchemes = config.allowedSchemes;\n\t\t}\n\t}\n\n\t// 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#ignoreLoop(tagsMap: TagItem[], content: string): string {\n\t\tfor (const tag of tagsMap) {\n\t\t\tcontent = replaceFirst(content, placeholder(tag.index), tag.raw);\n\t\t\tif (tag.closing) {\n\t\t\t\tcontent = replaceFirst(content, placeholder(tag.closing.index), tag.closing.raw);\n\t\t\t}\n\t\t\tif (tag.children.length > 0) {\n\t\t\t\tcontent = this.#ignoreLoop(tag.children, content);\n\t\t\t}\n\t\t}\n\t\treturn content;\n\t}\n\n\t#contentLoop(tagsMap: TagItem[], content: string, parentClosingTagIndex?: number): string {\n\t\t// Adaptive threshold: use optimized path for large documents (>50 tags)\n\t\t// Only optimize when all tags are replace-type and none have autoClose\n\t\tif (tagsMap.length > 50) {\n\t\t\t// Check if we have any content, ignore type, or autoClose tags.\n\t\t\t// Must recurse into children: special tags like [*] (autoClose) or [img]\n\t\t\t// (content) are usually nested, so a top-level-only check would miss them\n\t\t\t// and incorrectly take the optimized path that ignores those behaviors.\n\t\t\tconst hasSpecialType = (tag: TagItem): boolean => {\n\t\t\t\tconst module = this.#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\t// Replace tags with placeholders - optimized for different input sizes\n\t\tif (tags.length <= 3) {\n\t\t\t// Fast path for few tags - direct replacement is faster\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tinput = replaceFirst(input, tag, placeholder(i));\n\t\t\t}\n\t\t} else {\n\t\t\t// Batch replacement for many tags\n\t\t\tconst tagPositions = [];\n\t\t\tlet lastIndex = 0;\n\t\t\tfor (const [i, tag] of tags.entries()) {\n\t\t\t\tconst pos = input.indexOf(tag, lastIndex);\n\t\t\t\tif (pos === -1) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttagPositions.push({ pos, tag: tag, placeholder: placeholder(i) });\n\t\t\t\tlastIndex = pos + tag.length;\n\t\t\t}\n\n\t\t\tif (tagPositions.length > 0) {\n\t\t\t\tconst parts = [];\n\t\t\t\tlet lastPos = 0;\n\t\t\t\tfor (const { pos, tag, placeholder } of tagPositions) {\n\t\t\t\t\tparts.push(input.slice(lastPos, pos));\n\t\t\t\t\tparts.push(placeholder);\n\t\t\t\t\tlastPos = pos + tag.length;\n\t\t\t\t}\n\t\t\t\tparts.push(input.slice(lastPos));\n\t\t\t\tinput = parts.join('');\n\t\t\t}\n\t\t}\n\t\t// 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":";AAuDA,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AAExB,SAAS,YAAY,OAAuB;CAC3C,OAAO,oBAAoB,QAAQ;AACpC;AAGA,SAAS,aAAa,SAAiB,QAAgB,aAA6B;CACnF,MAAM,QAAQ,QAAQ,QAAQ,MAAM;CACpC,IAAI,UAAU,IACb,OAAO;CAER,OAAO,QAAQ,MAAM,GAAG,KAAK,IAAI,cAAc,QAAQ,MAAM,QAAQ,OAAO,MAAM;AACnF;AAEA,MAAM,oBAAoB;CACzB,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,SAAS;AACV;AAEA,SAAS,kBAAkB,OAAuB;CACjD,OAAO,MAAM,WAAW,8BAA8B,WAAmB,kBAAkB,OAAyC;AACrI;;;;;AAMA,SAAS,eAAe,OAAuB;CAC9C,MAAM,eAA2C;EAChD,KAAK;EACL,KAAK;EACL,KAAK;EACL,MAAK;EACL,KAAM;CACP;CACA,OAAO,OAAO,KAAK,CAAC,CAAC,WAAW,aAAa,SAAiB,aAAa,SAAS,IAAI;AACzF;AAGA,SAAS,aAAa,KAAqB;CAC1C,IAAI,QAAQ;CACZ,IAAI,MAAM,IAAI;CAEd,OAAO,QAAQ,OAAO,IAAI,OAAO,KAAK,KAAK,KAC1C;CAED,OAAO,MAAM,SAAS,IAAI,OAAO,MAAM,CAAC,KAAK,KAC5C;CAED,OAAO,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,aAAa,EAAE;AACxD;;;;;AAMA,SAAS,YAAY,KAAa,gBAAkC;CACnE,MAAM,UAAU,aAAa,OAAO,GAAG,CAAC;CACxC,IAAI,CAAC,SACJ,OAAO;CAIR,IAAI,aAAa,KAAK,OAAO,GAC5B,OAAO;CAIR,IAAI,QAAQ,WAAW,GAAG,GACzB,OAAO;CAIR,MAAM,cAAc,uBAAuB,KAAK,QAAQ,YAAY,CAAC;CACrE,IAAI,aAAa;EAChB,MAAM,SAAS,YAAY,MAAM;EACjC,IAAI,CAAC,eAAe,SAAS,MAAM,GAElC,OAAO;CAET;CAEA,OAAO;AACR;;;;;;;AAQA,SAAS,mBAAmB,YAA6D;CAExF,MAAM,aAAa,WAAW,QAAQ,GAAG;CACzC,MAAM,cAAc,WAAW,QAAQ,GAAG;CAG1C,IAAI,eAAe,MAAO,gBAAgB,MAAM,cAAc,YAE7D,OAAO;EACN,MAFa,WAAW,MAAM,GAEpB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;EAC7B,OAAO,CAAC;CACT;CAID,MAAM,QAAQ,WAAW,MAAM,KAAK;CAEpC,MAAM,QAAuB,OAAO,OAAO,IAAI;CAC/C,IAAI,aAAa;CAGjB,MAAM,gCAAgB,IAAI,IAAI;EAAC;EAAa;EAAe;CAAW,CAAC;CAEvE,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACtC,MAAM,OAAO,MAAM;EACnB,IAAI,CAAC,MACJ;EAGD,IAAI,KAAK,SAAS,GAAG,GAAG;GACvB,MAAM,UAAU,KAAK,QAAQ,GAAG;GAChC,MAAM,MAAM,KAAK,MAAM,GAAG,OAAO;GACjC,MAAM,QAAQ,KAAK,MAAM,UAAU,CAAC;GAGpC,IAAI,cAAc,IAAI,IAAI,YAAY,CAAC,GACtC;GAGD,MAAM,OAAO;GAEb,IAAI,CAAC,cAAc,OAClB,aAAa;EAEf,OAAO;GAEN,IAAI,cAAc,IAAI,KAAK,YAAY,CAAC,GACvC;GAED,MAAM,QAAQ;EACf;CACD;CAEA,OAAO;EAAE,MAAM;EAAY;CAAM;AAClC;AAEA,IAAM,WAAN,MAAe;CACd,OAAgB;EACf,OAAO;GACN,MAAM;GACN,OAAM,SAAQ,YAAY,eAAe,YAAY,KAAK,YAAY,IAAI,GAAG,KAAK,OAAO,kBAAkB;IAAC;IAAQ;IAAS;IAAU;IAAO;GAAM,CAAC,CAAC,EAAE;GACxJ,OAAO;EACR;EACA,SAAS;GACR,MAAM;GACN,OAAM,SAAQ,uBAAuB,eAAe,KAAK,YAAY,IAAI,CAAC,EAAE;GAC5E,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,QAAQ;GACP,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,MAAM;GACL,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,UAAU;GACT,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,WAAW;GACV,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,QAAQ;GACP,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,SAAS;GACR,MAAM;GACN,MAAM;GACN,OAAO;EACR;EACA,KAAK;GACJ,MAAM;GACN,MAAM;GACN,OAAO;GACP,WAAW;EACZ;EACA,OAAO;GACN,MAAM;GACN,UAAU,MAAM,YAAY;IAC3B,IAAI,CAAC,SACJ,OAAO;IAER,OAAO,aAAa,eAAe,KAAK,YAAY,OAAO,CAAC,EAAE,SAAS,eAAe,KAAK,YAAY,IAAI,CAAC,EAAE;GAC/G;EACD;EACA,WAAW,EACV,MAAM,SACP;CACD;CAEA,QAAQ;EACP,MAAM;EACN,SAAS;EACT,cAAc;CACf;CAEA;CAEA,iBAAiB;EAChB,UAAU,KAAc,QAAoB,YAA4B;GACvE,IAAI,OAAO,OAAO;GAClB,IAAI,QAAQ,OAAO;GACnB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;GAEhC,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;GAGlC,IAAI,QAAQ,CAAC,IAAI,WAChB,UAAU,aAAa,SAAS,YAAY,IAAI,KAAK,GAAG,IAAI;GAE7D,IAAI,SAAS,IAAI,SAChB,UAAU,aAAa,SAAS,YAAY,IAAI,QAAQ,KAAK,GAAG,KAAK;GAEtE,OAAO;EACR;EACA,UAAU,KAAc,QAAoB,YAA4B;GACvE,IAAI,CAAC,IAAI,SACR,OAAO;GAER,MAAM,UAAU,YAAY,IAAI,KAAK;GACrC,MAAM,WAAW,YAAY,IAAI,QAAQ,KAAK;GAC9C,MAAM,QAAQ,QAAQ,QAAQ,OAAO;GACrC,MAAM,MAAM,QAAQ,QAAQ,QAAQ;GACpC,IAAI,UAAU,OAAO;GAErB,MAAM,eAAe,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG;GAC9D,IAAI,OAAO,YAAa,YACvB,UAAU,QAAQ,IAAI,MAAM,cAAc,IAAI,KAAK;GAGpD,MAAM,eAAe,QAAQ,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;GACxD,MAAM,aAAa,QAAQ,MAAM,MAAM,SAAS,MAAM;GAEtD,OAAO,eAAe,UAAU;EACjC;EACA,SAAS,KAAc,SAAoB,YAA4B;GACtE,MAAM,UAAU,YAAY,IAAI,KAAK;GACrC,MAAM,QAAQ,QAAQ,QAAQ,OAAO;GACrC,IAAI,WAAW;GACf,IAAI,MAAM,QAAQ;GAClB,IAAI,IAAI,SAAS;IAChB,WAAW,YAAY,IAAI,QAAQ,KAAK;IACxC,MAAM,QAAQ,QAAQ,QAAQ;GAC/B;GACA,IAAI,eAAe,QAAQ,MAAM,QAAQ,QAAQ,QAAQ,GAAG;GAC5D,eAAe,KAAK,YAAY,IAAI,UAAU,YAAY;GAC1D,MAAM,eAAe,QAAQ,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,CAAC;GACxD,MAAM,aAAa,QAAQ,MAAM,MAAM,SAAS,MAAM;GACtD,OAAO,eAAe,eAAe;EACtC;CACD;CAEA,YAAY,SAAiB,CAAC,GAAG;EAChC,KAAK,SAAS;GACb,SAAS;GACT,WAAW;GACX,kBAAkB;GAClB,cAAc;GACd,gBAAgB;IAAC;IAAQ;IAAS;IAAU;IAAO;GAAM;EAC1D;EACA,IAAI,OAAO,YAAY,QACtB,KAAK,OAAO,UAAU,OAAO;EAE9B,IAAI,OAAO,cAAc,QACxB,KAAK,OAAO,YAAY,OAAO;EAEhC,IAAI,OAAO,qBAAqB,QAC/B,KAAK,OAAO,mBAAmB,OAAO;EAEvC,IAAI,OAAO,iBAAiB,QAC3B,KAAK,OAAO,eAAe,OAAO;EAEnC,IAAI,OAAO,mBAAmB,QAC7B,KAAK,OAAO,iBAAiB,OAAO;CAEtC;CAGA,QAAQ,MAAyC;EAChD,IAAI,CAAC,OAAO,OAAO,KAAK,MAAM,IAAI,GACjC;EAED,OAAO,KAAK,KAAK;CAClB;CAEA,iBAAiB,OAAuB;EACvC,IAAI,CAAC,KAAK,OAAO,SAChB,OAAO;EAER,IAAI,KAAK,OAAO,WACf,OAAO,MAAM,QAAQ,KAAK,MAAM,SAAS,SAAS;EAEnD,OAAO,MAAM,QAAQ,KAAK,MAAM,SAAS,OAAO;CACjD;CAEA,aAAa,OAAuB;EACnC,QAAQ,KAAK,iBAAiB,KAAK;EACnC,IAAI,KAAK,OAAO,WACf,QAAQ,QAAQ,QAAQ;EAEzB,OAAO;CACR;CAGA,YAAY,OAAuB;EAClC,IAAI,CAAC,KAAK,OAAO,cAChB,OAAO;EAER,OAAO,kBAAkB,KAAK;CAC/B;CAEA,YAAY,SAAoB,SAAyB;EACxD,KAAK,MAAM,OAAO,SAAS;GAC1B,UAAU,aAAa,SAAS,YAAY,IAAI,KAAK,GAAG,IAAI,GAAG;GAC/D,IAAI,IAAI,SACP,UAAU,aAAa,SAAS,YAAY,IAAI,QAAQ,KAAK,GAAG,IAAI,QAAQ,GAAG;GAEhF,IAAI,IAAI,SAAS,SAAS,GACzB,UAAU,KAAK,YAAY,IAAI,UAAU,OAAO;EAElD;EACA,OAAO;CACR;CAEA,aAAa,SAAoB,SAAiB,uBAAwC;EAGzF,IAAI,QAAQ,SAAS,IAAI;GAKxB,MAAM,kBAAkB,QAA0B;IACjD,MAAM,SAAS,KAAK,QAAQ,IAAI,MAAM;IACtC,IAAI,QAAQ;KACX,IAAI,OAAO,SAAS,aAAa,OAAO,SAAS,UAChD,OAAO;KAER,IAAI,OAAO,SAAS,aAAc,OAAsB,WACvD,OAAO;IAET;IACA,OAAO,IAAI,SAAS,MAAK,UAAS,eAAe,KAAK,CAAC;GACxD;GAIA,IAAI,CAHoB,QAAQ,MAAK,QAAO,eAAe,GAAG,CAG3C,GAClB,OAAO,KAAK,sBAAsB,SAAS,OAAO;EAEpD;EAEA,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACxC,MAAM,MAAM,QAAQ;GACpB,IAAI,CAAC,KACJ;GAGD,IAAI,SAAS,KAAK,QAAQ,IAAI,MAAM;GACpC,IAAI,CAAC,QAEJ,SAAS;IACR,MAAM;IACN,MAAM,IAAI;IACV,OAAO,IAAI,UAAU,IAAI,QAAQ,MAAM;GACxC;GAED,IAAI,CAAC,KAAK,eAAe,OAAO,OAC/B,MAAM,IAAI,MAAM,qDAAqD,OAAO,OAAO,yBAAyB,IAAI,SAAS,GAAG;GAG7H,IAAI,OAAO,SAAS,WAAW;IAC9B,MAAM,gBAAgB;IAGtB,IAAI,OAAO,cAAc;IACzB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;IAEhC,IAAI,QAAQ,CAAC,IAAI,WAChB,UAAU,aAAa,SAAS,YAAY,IAAI,KAAK,GAAG,IAAI;IAI7D,IAAI,IAAI,SAAS,SAAS,GAAG;KAC5B,MAAM,eAAe,IAAI,SAAS;KAClC,UAAU,KAAK,aAAa,IAAI,UAAU,SAAS,YAAY;IAChE;IAGA,IAAI,cAAc,aAAa,CAAC,IAAI,SAAS;KAC5C,IAAI,YAAY,cAAc;KAC9B,IAAI,OAAO,cAAe,YACzB,YAAY,UAAU,IAAI,MAAM,IAAI,KAAK;KAI1C,IAAI;KACJ,KAAK,IAAI,eAAe,IAAI,GAAG,eAAe,QAAQ,QAAQ,gBAAgB;MAC7E,MAAM,UAAU,QAAQ;MACxB,IAAI,SAAS,WAAW,IAAI,QAAQ;OACnC,eAAe,YAAY,QAAQ,KAAK;OACxC;MACD;KACD;KACA,IAAI,iBAAiB,UAAa,0BAA0B,QAC3D,eAAe,YAAY,qBAAqB;KAGjD,IAAI,gBAAgB,WACnB,UAAU,aAAa,SAAS,cAAc,YAAY,YAAY;IAExE;IAGA,IAAI,QAAQ,cAAc;IAC1B,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;IAElC,IAAI,SAAS,IAAI,SAChB,UAAU,aAAa,SAAS,YAAY,IAAI,QAAQ,KAAK,GAAG,KAAK;GAEvE,OAAO,IAAI,OAAO,SAAS,WAAW;IAErC,IAAI,IAAI,SAAS,SAAS,GACzB,UAAU,KAAK,aAAa,IAAI,UAAU,SAAS,IAAI,SAAS,KAAK;IAEtE,UAAU,KAAK,eAAe,QAAQ,KAAK,QAAsB,OAAO;GACzE,OAAO,IAAI,OAAO,SAAS,UAC1B,UAAU,KAAK,eAAe,OAAO,KAAK,QAAqB,OAAO;EAExE;EAEA,OAAO;CACR;CAEA,sBAAsB,SAAoB,SAAyB;EAGlE,MAAM,+BAAe,IAAI,IAAoB;EAE7C,MAAM,cAAc,QAAiB;GACpC,IAAI,SAAS,KAAK,QAAQ,IAAI,MAAM;GACpC,IAAI,CAAC,QACJ,SAAS;IACR,MAAM;IACN,MAAM,IAAI;IACV,OAAO,IAAI,UAAU,IAAI,QAAQ,MAAM;GACxC;GAID,IAAI,OAAO,SAAS,WACnB;GAGD,IAAI,OAAO,OAAO;GAClB,IAAI,QAAQ,OAAO;GACnB,IAAI,OAAO,SAAU,YACpB,OAAO,KAAK,IAAI,MAAM,IAAI,KAAK;GAEhC,IAAI,OAAO,UAAW,YACrB,QAAQ,MAAM,IAAI,MAAM,IAAI,KAAK;GAElC,IAAI,QAAQ,CAAC,IAAI,WAChB,aAAa,IAAI,YAAY,IAAI,KAAK,GAAG,IAAI;GAE9C,IAAI,SAAS,IAAI,SAChB,aAAa,IAAI,YAAY,IAAI,QAAQ,KAAK,GAAG,KAAK;GAIvD,KAAK,MAAM,SAAS,IAAI,UACvB,WAAW,KAAK;EAElB;EAGA,KAAK,MAAM,OAAO,SACjB,WAAW,GAAG;EAIf,OAAO,QAAQ,WAAW,KAAK,MAAM,eAAe,UAAkB,aAAa,IAAI,KAAK,KAAK,KAAK;CACvG;CAEA,SAAS,SAAoB,QAA4B;EAExD,MAAM,QAAQ,CAAC;EACf,MAAM,4BAAY,IAAI,IAAI;EAE1B,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACxC,MAAM,MAAM,QAAQ;GACpB,IAAI,CAAC,KACJ;GAGD,IAAI,IAAI,WAEP,KAAK,IAAI,aAAa,MAAM,SAAS,GAAG,cAAc,GAAG,cAAc;IACtE,MAAM,UAAU,MAAM;IACtB,IAAI,CAAC,SACJ;IAED,IAAI,QAAQ,WAAW,IAAI,UAAU,CAAC,UAAU,IAAI,QAAQ,KAAK,GAAG;KAEnE,QAAQ,WAAW,IAAI;KACvB,QAAQ,UAAU;KAClB,IAAI,WAAW,QAAQ;KACvB,UAAU,IAAI,QAAQ,KAAK;KAG3B,MAAM,cAAc,QAAQ,cAAc,KAAK;KAC/C,IAAI,aAAa,GAAG;MACnB,QAAQ,WAAW,CAAC;MACpB,KAAK,IAAI,aAAa,YAAY,aAAa,GAAG,cAAc;OAC/D,MAAM,WAAW,QAAQ;OACzB,IAAI,EAAE,YAAY,CAAC,SAAS,aAAa,SAAS,WAAW,OAC5D;OAGD,SAAS,SAAS,QAAQ;OAC1B,QAAQ,SAAS,KAAK,QAAQ;MAC/B;KACD;KAGA,MAAM,OAAO,YAAY,CAAC;KAC1B;IACD;GACD;QACM;IAEN,IAAI,aAAa;IACjB,MAAM,KAAK,GAAG;GACf;EACD;EAGA,KAAK,MAAM,OAAO,OAAO;GACxB,IAAI,WAAW;GACf,MAAM,cAAc,IAAI,cAAc,KAAK;GAC3C,IAAI,EAAE,aAAa,QAAQ,SAC1B;GAGD,IAAI,WAAW,CAAC;GAChB,KAAK,IAAI,aAAa,YAAY,aAAa,QAAQ,QAAQ,cAAc;IAC5E,MAAM,WAAW,QAAQ;IACzB,IAAI,EAAE,YAAY,CAAC,SAAS,aAAa,SAAS,WAAW,OAC5D;IAGD,SAAS,SAAS,IAAI;IACtB,IAAI,SAAS,KAAK,QAAQ;GAC3B;EACD;EAGA,MAAM,SAAS,CAAC;EAChB,KAAK,MAAM,OAAO,SAAS;GAC1B,IAAI,IAAI,WACP;GAKD,IAAI,EAFmB,WAAW,UAAa,IAAI,WAAW,QAC5D,WAAW,UAAa,IAAI,WAAW,SAExC;GAGD,IAAI,IAAI,SAAS,SAAS,GACzB,IAAI,WAAW,KAAK,SAAS,IAAI,UAAU,IAAI,KAAK;GAErD,OAAO,KAAK,GAAG;EAChB;EAEA,OAAO;CACR;CAEA,YAAkB;EACjB,KAAK,OAAO,CAAC;EACb,OAAO;CACR;CAEA,YAAY,KAAa,SAA8B;EACtD,MAAM,UAAU,OAAO,GAAG,CAAC,CAAC,YAAY;EAIxC,IAAI;GADmB;GAAa;GAAe;EACnC,CAAC,CAAC,SAAS,OAAO,GACjC,MAAM,IAAI,MAAM,4CAA4C,SAAS;EAGtE,KAAK,KAAK,WAAW;EACrB,OAAO;CACR;CAEA,MAAM,UAA6C;EAClD,IACC,OAAO,aAAc,aACjB,OAAO,aAAc,YAAY,OAAO,MAAM,OAAO,QAAQ,CAAC,GAElE,OAAO;EAER,IAAI,QAAQ,OAAO,QAAQ,CAAC,CAAC,WAAW,mBAAmB,EAAE;EAC7D,IAAI,KAAK,OAAO,cAAc;GAE7B,MAAM,eAAe;IAAE,KAAK;IAAS,KAAK;IAAQ,KAAK;IAAQ,MAAK;IAAU,KAAM;GAAQ;GAC5F,QAAQ,MAAM,WAAW,aAAa,SAAiB,aAAa,KAAkC;EACvG;EAGA,IAAI,UAAqB,CAAC;EAE1B,MAAM,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI;EAGhD,IAAI,CAAC,QAAQ,KAAK,WAAW,GAC5B,OAAO,KAAK,aAAa,KAAK;EAG/B,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GAAG;GACtC,MAAM,aAAa,IAAI,MAAM,GAAG,EAAE;GAClC,MAAM,YAAY,WAAW,WAAW,GAAG;GAC3C,MAAM,iBAAiB,YAAY,WAAW,MAAM,CAAC,IAAI;GAGzD,MAAM,aAAa,eAAe,QAAQ,GAAG;GAC7C,MAAM,cAAc,eAAe,QAAQ,GAAG;GAC9C,IAAI;GACJ,IAAI;GAGJ,IAAI,eAAe,MAAM,gBAAgB,IAAI;IAE5C,aAAa,eAAe,YAAY;IACxC,cAAc;KAAE,MAAM;KAAI,OAAO,CAAC;IAAE;GACrC,OAAO,IAAI,eAAe,MAAO,gBAAgB,MAAM,cAAc,YAAa;IAEjF,cAAc,eAAe,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,GAAE,CAAE,YAAY;IACjE,cAAc,mBAAmB,cAAc;GAChD,OAAO;IAEN,cAAc,eAAe,MAAM,OAAO,CAAC,CAAC,CAAC,MAAM,GAAE,CAAE,YAAY;IACnE,cAAc,mBAAmB,cAAc;GAChD;GAEA,MAAM,OAAgB;IACrB,OAAO;IACP,QAAQ;IACR;IAEA,KAAK,KAAK,iBAAiB,GAAG;IAC9B,MAAM,YAAY;IAClB,OAAO,YAAY;IACnB,UAAU,CAAC;IACX,QAAQ;IACR,UAAU;GACX;GAEA,QAAQ,KAAK,IAAI;EAClB;EAGA,IAAI,KAAK,UAAU,GAElB,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GACnC,QAAQ,aAAa,OAAO,KAAK,YAAY,CAAC,CAAC;OAE1C;GAEN,MAAM,eAAe,CAAC;GACtB,IAAI,YAAY;GAChB,KAAK,MAAM,CAAC,GAAG,QAAQ,KAAK,QAAQ,GAAG;IACtC,MAAM,MAAM,MAAM,QAAQ,KAAK,SAAS;IACxC,IAAI,QAAQ,IACX;IAGD,aAAa,KAAK;KAAE;KAAU;KAAK,aAAa,YAAY,CAAC;IAAE,CAAC;IAChE,YAAY,MAAM,IAAI;GACvB;GAEA,IAAI,aAAa,SAAS,GAAG;IAC5B,MAAM,QAAQ,CAAC;IACf,IAAI,UAAU;IACd,KAAK,MAAM,EAAE,KAAK,KAAK,iBAAiB,cAAc;KACrD,MAAM,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;KACpC,MAAM,KAAK,WAAW;KACtB,UAAU,MAAM,IAAI;IACrB;IACA,MAAM,KAAK,MAAM,MAAM,OAAO,CAAC;IAC/B,QAAQ,MAAM,KAAK,EAAE;GACtB;EACD;EAEA,QAAQ,KAAK,aAAa,KAAK;EAE/B,UAAU,KAAK,SAAS,OAAO;EAC/B,QAAQ,KAAK,aAAa,SAAS,KAAK;EAExC,IAAI,KAAK,OAAO,kBACf,OAAO,MAAM,WAAW,KAAK,MAAM,cAAc,EAAE;EAGpD,OAAO,MAAM,WAAW,KAAK,MAAM,eAAe,QAAgB,UAAkB;GACnF,IAAI,MAAM,KAAK,OAAO,KAAK,MAAM;GACjC,IAAI,CAAC,KAAK,OAAO,cAChB,MAAM,eAAe,GAAG;GAEzB,OAAO,KAAK,iBAAiB,GAAG;EACjC,CAAC;CACF;AACD"}
|
package/dist/ya-bbcode.d.cts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/ya-bbcode.d.ts
|
|
2
1
|
interface Config {
|
|
3
2
|
newline?: boolean;
|
|
4
3
|
paragraph?: boolean;
|
|
@@ -13,6 +12,11 @@ interface ReplaceTag {
|
|
|
13
12
|
type: 'replace';
|
|
14
13
|
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
15
14
|
close: ((attr: string, attrs: TagAttributes) => string) | string | null;
|
|
15
|
+
/**
|
|
16
|
+
* HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).
|
|
17
|
+
* Useful for tags like [*] that don't have explicit closing tags in BBCode.
|
|
18
|
+
*/
|
|
19
|
+
autoClose?: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
16
20
|
}
|
|
17
21
|
interface ContentTag {
|
|
18
22
|
type: 'content';
|
|
@@ -57,6 +61,4 @@ declare class yabbcode {
|
|
|
57
61
|
registerTag(tag: string, options: TagDefinition): this;
|
|
58
62
|
parse(bbcInput: string | number | boolean): string;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
62
|
-
//# sourceMappingURL=ya-bbcode.d.cts.map
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
package/dist/ya-bbcode.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/ya-bbcode.d.ts
|
|
2
1
|
interface Config {
|
|
3
2
|
newline?: boolean;
|
|
4
3
|
paragraph?: boolean;
|
|
@@ -13,6 +12,11 @@ interface ReplaceTag {
|
|
|
13
12
|
type: 'replace';
|
|
14
13
|
open: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
15
14
|
close: ((attr: string, attrs: TagAttributes) => string) | string | null;
|
|
15
|
+
/**
|
|
16
|
+
* HTML to insert when the tag implicitly closes (e.g., before next sibling or parent close).
|
|
17
|
+
* Useful for tags like [*] that don't have explicit closing tags in BBCode.
|
|
18
|
+
*/
|
|
19
|
+
autoClose?: ((attr: string, attrs: TagAttributes) => string) | string;
|
|
16
20
|
}
|
|
17
21
|
interface ContentTag {
|
|
18
22
|
type: 'content';
|
|
@@ -57,6 +61,4 @@ declare class yabbcode {
|
|
|
57
61
|
registerTag(tag: string, options: TagDefinition): this;
|
|
58
62
|
parse(bbcInput: string | number | boolean): string;
|
|
59
63
|
}
|
|
60
|
-
|
|
61
|
-
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
|
62
|
-
//# sourceMappingURL=ya-bbcode.d.mts.map
|
|
64
|
+
export { type Config, type ContentTag, type IgnoreTag, type ReplaceTag, type TagAttributes, type TagDefinition, yabbcode as default };
|
package/dist/ya-bbcode.mjs
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
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
|
+
"<": "<",
|
|
15
|
+
">": ">",
|
|
16
|
+
""": "\"",
|
|
17
|
+
"'": "'"
|
|
18
|
+
};
|
|
19
|
+
function unescapeSanitized(value) {
|
|
20
|
+
return value.replaceAll(/&(?:amp|lt|gt|quot|#39);/g, (entity) => sanitizedEntities[entity]);
|
|
21
|
+
}
|
|
2
22
|
/**
|
|
3
23
|
* Escapes HTML attribute values to prevent XSS
|
|
4
24
|
* Encodes quotes, angle brackets, and other special characters
|
|
@@ -13,12 +33,19 @@ function escapeHtmlAttr(value) {
|
|
|
13
33
|
};
|
|
14
34
|
return String(value).replaceAll(/["&'<>]/g, (char) => attrEntities[char] || char);
|
|
15
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
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* Validates and sanitizes URLs to prevent javascript: and data: URL attacks
|
|
18
45
|
* Only allows safe URL schemes based on the allowedSchemes list
|
|
19
46
|
*/
|
|
20
47
|
function sanitizeUrl(url, allowedSchemes) {
|
|
21
|
-
const trimmed = String(url)
|
|
48
|
+
const trimmed = normalizeUrl(String(url));
|
|
22
49
|
if (!trimmed) return "#";
|
|
23
50
|
if (/^\.{0,2}\//.test(trimmed)) return trimmed;
|
|
24
51
|
if (trimmed.startsWith("#")) return trimmed;
|
|
@@ -45,7 +72,7 @@ function parseTagAttributes(tagContent) {
|
|
|
45
72
|
const parts = tagContent.split(/\s+/);
|
|
46
73
|
const attrs = Object.create(null);
|
|
47
74
|
let firstValue = "";
|
|
48
|
-
const dangerousKeys = new Set([
|
|
75
|
+
const dangerousKeys = /* @__PURE__ */ new Set([
|
|
49
76
|
"__proto__",
|
|
50
77
|
"constructor",
|
|
51
78
|
"prototype"
|
|
@@ -74,7 +101,7 @@ var yabbcode = class {
|
|
|
74
101
|
tags = {
|
|
75
102
|
"url": {
|
|
76
103
|
type: "replace",
|
|
77
|
-
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(attr, this.config.allowedSchemes ?? [
|
|
104
|
+
open: (attr) => `<a href="${escapeHtmlAttr(sanitizeUrl(this.#unsanitize(attr), this.config.allowedSchemes ?? [
|
|
78
105
|
"http",
|
|
79
106
|
"https",
|
|
80
107
|
"mailto",
|
|
@@ -85,7 +112,7 @@ var yabbcode = class {
|
|
|
85
112
|
},
|
|
86
113
|
"quote": {
|
|
87
114
|
type: "replace",
|
|
88
|
-
open: (attr) => `<blockquote author="${escapeHtmlAttr(attr)}">`,
|
|
115
|
+
open: (attr) => `<blockquote author="${escapeHtmlAttr(this.#unsanitize(attr))}">`,
|
|
89
116
|
close: "</blockquote>"
|
|
90
117
|
},
|
|
91
118
|
"b": {
|
|
@@ -138,6 +165,11 @@ var yabbcode = class {
|
|
|
138
165
|
open: "<code>",
|
|
139
166
|
close: "</code>"
|
|
140
167
|
},
|
|
168
|
+
"hr": {
|
|
169
|
+
type: "replace",
|
|
170
|
+
open: "<hr/>",
|
|
171
|
+
close: null
|
|
172
|
+
},
|
|
141
173
|
"strike": {
|
|
142
174
|
type: "replace",
|
|
143
175
|
open: "<span class=\"yabbcode-strike\">",
|
|
@@ -161,21 +193,22 @@ var yabbcode = class {
|
|
|
161
193
|
"*": {
|
|
162
194
|
type: "replace",
|
|
163
195
|
open: "<li>",
|
|
164
|
-
close:
|
|
196
|
+
close: "</li>",
|
|
197
|
+
autoClose: "</li>"
|
|
165
198
|
},
|
|
166
199
|
"img": {
|
|
167
200
|
type: "content",
|
|
168
201
|
replace: (attr, content) => {
|
|
169
202
|
if (!content) return "";
|
|
170
|
-
return `<img src="${escapeHtmlAttr(content)}" alt="${escapeHtmlAttr(attr)}"/>`;
|
|
203
|
+
return `<img src="${escapeHtmlAttr(this.#unsanitize(content))}" alt="${escapeHtmlAttr(this.#unsanitize(attr))}"/>`;
|
|
171
204
|
}
|
|
172
205
|
},
|
|
173
206
|
"noparse": { type: "ignore" }
|
|
174
207
|
};
|
|
175
208
|
regex = {
|
|
176
|
-
tags: /(\[[^\]^]
|
|
209
|
+
tags: /(\[[^\]^]+\])/g,
|
|
177
210
|
newline: /\r\n|\r|\n/g,
|
|
178
|
-
placeholders: /\
|
|
211
|
+
placeholders: /\0(\d+);/g
|
|
179
212
|
};
|
|
180
213
|
config;
|
|
181
214
|
contentModules = {
|
|
@@ -184,14 +217,14 @@ var yabbcode = class {
|
|
|
184
217
|
let close = module.close;
|
|
185
218
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
186
219
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
187
|
-
if (open && !tag.isClosing) content = content
|
|
188
|
-
if (close && tag.closing) content = content
|
|
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);
|
|
189
222
|
return content;
|
|
190
223
|
},
|
|
191
224
|
content: (tag, module, content) => {
|
|
192
225
|
if (!tag.closing) return content;
|
|
193
|
-
const openTag =
|
|
194
|
-
const closeTag =
|
|
226
|
+
const openTag = placeholder(tag.index);
|
|
227
|
+
const closeTag = placeholder(tag.closing.index);
|
|
195
228
|
const start = content.indexOf(openTag);
|
|
196
229
|
const end = content.indexOf(closeTag);
|
|
197
230
|
let replace = module.replace;
|
|
@@ -202,12 +235,12 @@ var yabbcode = class {
|
|
|
202
235
|
return contentStart + replace + contentEnd;
|
|
203
236
|
},
|
|
204
237
|
ignore: (tag, _module, content) => {
|
|
205
|
-
const openTag =
|
|
238
|
+
const openTag = placeholder(tag.index);
|
|
206
239
|
const start = content.indexOf(openTag);
|
|
207
240
|
let closeTag = "";
|
|
208
241
|
let end = content.length;
|
|
209
242
|
if (tag.closing) {
|
|
210
|
-
closeTag =
|
|
243
|
+
closeTag = placeholder(tag.closing.index);
|
|
211
244
|
end = content.indexOf(closeTag);
|
|
212
245
|
}
|
|
213
246
|
let innerContent = content.slice(start + openTag.length, end);
|
|
@@ -237,40 +270,91 @@ var yabbcode = class {
|
|
|
237
270
|
if (config.sanitizeHtml !== void 0) this.config.sanitizeHtml = config.sanitizeHtml;
|
|
238
271
|
if (config.allowedSchemes !== void 0) this.config.allowedSchemes = config.allowedSchemes;
|
|
239
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
|
+
}
|
|
240
291
|
#ignoreLoop(tagsMap, content) {
|
|
241
292
|
for (const tag of tagsMap) {
|
|
242
|
-
content = content
|
|
243
|
-
if (tag.closing) content = content
|
|
293
|
+
content = replaceFirst(content, placeholder(tag.index), tag.raw);
|
|
294
|
+
if (tag.closing) content = replaceFirst(content, placeholder(tag.closing.index), tag.closing.raw);
|
|
244
295
|
if (tag.children.length > 0) content = this.#ignoreLoop(tag.children, content);
|
|
245
296
|
}
|
|
246
297
|
return content;
|
|
247
298
|
}
|
|
248
|
-
#contentLoop(tagsMap, content) {
|
|
299
|
+
#contentLoop(tagsMap, content, parentClosingTagIndex) {
|
|
249
300
|
if (tagsMap.length > 50) {
|
|
250
|
-
|
|
251
|
-
const module = this
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
310
|
}
|
|
255
|
-
for (
|
|
256
|
-
|
|
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);
|
|
257
315
|
if (!module) module = {
|
|
258
316
|
type: "replace",
|
|
259
317
|
open: tag.raw,
|
|
260
318
|
close: tag.closing ? tag.closing.raw : ""
|
|
261
319
|
};
|
|
262
320
|
if (!this.contentModules[module.type]) throw new Error("Cannot parse content block. Invalid block type [" + module.type + "] provided for tag [" + tag.module + "]");
|
|
263
|
-
if (module.type === "replace")
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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);
|
|
267
351
|
}
|
|
268
352
|
return content;
|
|
269
353
|
}
|
|
270
354
|
#contentLoopOptimized(tagsMap, content) {
|
|
271
355
|
const replacements = /* @__PURE__ */ new Map();
|
|
272
356
|
const processTag = (tag) => {
|
|
273
|
-
let module = this
|
|
357
|
+
let module = this.#getTag(tag.module);
|
|
274
358
|
if (!module) module = {
|
|
275
359
|
type: "replace",
|
|
276
360
|
open: tag.raw,
|
|
@@ -281,12 +365,12 @@ var yabbcode = class {
|
|
|
281
365
|
let close = module.close;
|
|
282
366
|
if (typeof open === "function") open = open(tag.attr, tag.attrs);
|
|
283
367
|
if (typeof close === "function") close = close(tag.attr, tag.attrs);
|
|
284
|
-
if (open && !tag.isClosing) replacements.set(
|
|
285
|
-
if (close && tag.closing) replacements.set(
|
|
286
|
-
|
|
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);
|
|
287
371
|
};
|
|
288
372
|
for (const tag of tagsMap) processTag(tag);
|
|
289
|
-
return content.replaceAll(
|
|
373
|
+
return content.replaceAll(this.regex.placeholders, (match) => replacements.get(match) ?? match);
|
|
290
374
|
}
|
|
291
375
|
#tagLoop(tagsMap, parent) {
|
|
292
376
|
const stack = [];
|
|
@@ -307,10 +391,9 @@ var yabbcode = class {
|
|
|
307
391
|
openTag.children = [];
|
|
308
392
|
for (let childIndex = childStart; childIndex < i; childIndex++) {
|
|
309
393
|
const childTag = tagsMap[childIndex];
|
|
310
|
-
if (childTag && !childTag.isClosing && childTag.parent === null)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
394
|
+
if (!(childTag && !childTag.isClosing && childTag.parent === null)) continue;
|
|
395
|
+
childTag.parent = openTag.index;
|
|
396
|
+
openTag.children.push(childTag);
|
|
314
397
|
}
|
|
315
398
|
}
|
|
316
399
|
stack.splice(stackIndex, 1);
|
|
@@ -325,23 +408,21 @@ var yabbcode = class {
|
|
|
325
408
|
for (const tag of stack) {
|
|
326
409
|
tag.matchTag = false;
|
|
327
410
|
const childStart = (tag.stackIndex ?? 0) + 1;
|
|
328
|
-
if (childStart < tagsMap.length)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
}
|
|
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);
|
|
337
418
|
}
|
|
338
419
|
}
|
|
339
420
|
const result = [];
|
|
340
|
-
for (const tag of tagsMap)
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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);
|
|
345
426
|
}
|
|
346
427
|
return result;
|
|
347
428
|
}
|
|
@@ -361,7 +442,7 @@ var yabbcode = class {
|
|
|
361
442
|
}
|
|
362
443
|
parse(bbcInput) {
|
|
363
444
|
if (typeof bbcInput === "boolean" || typeof bbcInput !== "string" && Number.isNaN(Number(bbcInput))) return "";
|
|
364
|
-
let input = String(bbcInput);
|
|
445
|
+
let input = String(bbcInput).replaceAll(PLACEHOLDER_START, "");
|
|
365
446
|
if (this.config.sanitizeHtml) {
|
|
366
447
|
const htmlEntities = {
|
|
367
448
|
"&": "&",
|
|
@@ -374,10 +455,7 @@ var yabbcode = class {
|
|
|
374
455
|
}
|
|
375
456
|
let tagsMap = [];
|
|
376
457
|
const tags = String(input).match(this.regex.tags);
|
|
377
|
-
if (
|
|
378
|
-
else input = input.replace(this.regex.newline, "<br/>");
|
|
379
|
-
if (this.config.paragraph) input = "<p>" + input + "</p>";
|
|
380
|
-
if (!tags || tags.length === 0) return input;
|
|
458
|
+
if (!tags || tags.length === 0) return this.#applyLayout(input);
|
|
381
459
|
for (const [i, tag] of tags.entries()) {
|
|
382
460
|
const tagContent = tag.slice(1, -1);
|
|
383
461
|
const isClosing = tagContent.startsWith("/");
|
|
@@ -393,17 +471,17 @@ var yabbcode = class {
|
|
|
393
471
|
attrs: {}
|
|
394
472
|
};
|
|
395
473
|
} else if (spaceIndex === -1 || equalsIndex !== -1 && equalsIndex < spaceIndex) {
|
|
396
|
-
moduleName = (contentToParse.split("=")[0] ?? "").toLowerCase();
|
|
474
|
+
moduleName = (contentToParse.split("=", 1)[0] ?? "").toLowerCase();
|
|
397
475
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
398
476
|
} else {
|
|
399
|
-
moduleName = (contentToParse.split(/\s
|
|
477
|
+
moduleName = (contentToParse.split(/\s+/, 1)[0] ?? "").toLowerCase();
|
|
400
478
|
parsedAttrs = parseTagAttributes(contentToParse);
|
|
401
479
|
}
|
|
402
480
|
const item = {
|
|
403
481
|
index: i,
|
|
404
482
|
module: moduleName,
|
|
405
483
|
isClosing,
|
|
406
|
-
raw: tag,
|
|
484
|
+
raw: this.#convertNewlines(tag),
|
|
407
485
|
attr: parsedAttrs.attr,
|
|
408
486
|
attrs: parsedAttrs.attrs,
|
|
409
487
|
children: [],
|
|
@@ -412,20 +490,19 @@ var yabbcode = class {
|
|
|
412
490
|
};
|
|
413
491
|
tagsMap.push(item);
|
|
414
492
|
}
|
|
415
|
-
if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = input
|
|
493
|
+
if (tags.length <= 3) for (const [i, tag] of tags.entries()) input = replaceFirst(input, tag, placeholder(i));
|
|
416
494
|
else {
|
|
417
495
|
const tagPositions = [];
|
|
418
496
|
let lastIndex = 0;
|
|
419
497
|
for (const [i, tag] of tags.entries()) {
|
|
420
498
|
const pos = input.indexOf(tag, lastIndex);
|
|
421
|
-
if (pos
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
499
|
+
if (pos === -1) continue;
|
|
500
|
+
tagPositions.push({
|
|
501
|
+
pos,
|
|
502
|
+
tag,
|
|
503
|
+
placeholder: placeholder(i)
|
|
504
|
+
});
|
|
505
|
+
lastIndex = pos + tag.length;
|
|
429
506
|
}
|
|
430
507
|
if (tagPositions.length > 0) {
|
|
431
508
|
const parts = [];
|
|
@@ -439,14 +516,17 @@ var yabbcode = class {
|
|
|
439
516
|
input = parts.join("");
|
|
440
517
|
}
|
|
441
518
|
}
|
|
519
|
+
input = this.#applyLayout(input);
|
|
442
520
|
tagsMap = this.#tagLoop(tagsMap);
|
|
443
521
|
input = this.#contentLoop(tagsMap, input);
|
|
444
|
-
if (this.config.cleanUnmatchable)
|
|
445
|
-
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
|
+
});
|
|
446
528
|
}
|
|
447
529
|
};
|
|
448
|
-
var ya_bbcode_default = yabbcode;
|
|
449
530
|
|
|
450
|
-
|
|
451
|
-
export { ya_bbcode_default as default };
|
|
531
|
+
export { yabbcode as default };
|
|
452
532
|
//# sourceMappingURL=ya-bbcode.mjs.map
|