import { MiniMessageComponent, ResolvedStyle } from './mini-message.types'; import { hoverEventToTitle, mergeStyle, ownText, styleToNgStyle } from './mini-message.util'; /** * Escapes text for safe insertion into HTML. Always used on raw text content — * never skip this when building the string yourself. */ function escapeHtml(input: string): string { return input .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function ngStyleToCss(style: Record): string { return Object.entries(style) .map(([key, value]) => `${key}:${value}`) .join(';'); } /** * Renders a component-JSON tree to an HTML string, e.g. for binding via * [innerHTML]="miniMessageToHtml(json) | ..." with Angular's DomSanitizer, or for use * outside Angular entirely. Prefer the component when you need click/hover * interactivity or the obfuscated animation — this string form is static (obfuscated text * renders as a fixed placeholder rather than an animated one). */ export function miniMessageToHtml( node: MiniMessageComponent, parentStyle?: ResolvedStyle ): string { const style = mergeStyle(parentStyle, node); const css = ngStyleToCss(styleToNgStyle(style)); const text = escapeHtml(ownText(node)); const title = hoverEventToTitle(node.hoverEvent); const titleAttr = title ? ` title="${escapeHtml(title)}"` : ''; const insertionAttr = node.insertion ? ` data-insertion="${escapeHtml(node.insertion)}"` : ''; let ownHtml: string; if (node.clickEvent?.action === 'open_url') { ownHtml = `${text}`; } else { const clickAttrs = node.clickEvent ? ` class="mm-node mm-clickable" data-click-action="${node.clickEvent.action}" data-click-value="${escapeHtml( node.clickEvent.value )}"` : ' class="mm-node"'; ownHtml = `${text}`; } const childrenHtml = (node.extra ?? []).map((child) => miniMessageToHtml(child, style)).join(''); return ownHtml + childrenHtml; }