const ICON_PATHS = { arrowLeft: [ '', '', '', ], arrowRight: [ '', '', '', ], bell: [ '', '', ], calendar: [ '', '', '', '', ], check: [''], chevronDown: [''], chevronLeft: [''], chevronRight: [''], chevronUp: [''], dots: [ '', '', '', ], edit: [ '', '', ], lock: [ '', '', '', ], mail: [ '', '', ], plus: ['', ''], search: [ '', '', ], send: [ '', '', ], settings: [ '', '', ], user: [ '', '', ], x: ['', ''], } as const; const ICON_ALIASES: Record = { add: "plus", back: "arrowLeft", close: "x", collapse: "chevronUp", caret: "chevronDown", chevron: "chevronDown", dropdown: "chevronDown", down: "chevronDown", email: "mail", expand: "chevronDown", forward: "arrowRight", gear: "settings", menu: "dots", more: "dots", next: "chevronRight", password: "lock", previous: "chevronLeft", profile: "user", right: "chevronRight", submit: "send", up: "chevronUp", }; const ICON_MARKER_RE = /<(span|i)\b([^>]*\s)?data-icon\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))([^>]*)>(?:\s*)<\/\1>|<(span|i)\b([^>]*\s)?data-icon\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))([^>]*)\/>/gi; function normalizeIconName(value: string): keyof typeof ICON_PATHS | null { const normalized = value .trim() .replace(/^icon/i, "") .replace(/[^a-z0-9]+/gi, " ") .trim() .replace(/\s+([a-z0-9])/gi, (_match, char: string) => char.toUpperCase()) .replace(/^./, (char) => char.toLowerCase()); return normalized in ICON_PATHS ? (normalized as keyof typeof ICON_PATHS) : (ICON_ALIASES[normalized] ?? null); } function readAttribute(attrs: string, name: string): string | null { const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const match = attrs.match( new RegExp( `\\b${escaped}\\s*=\\s*("[^"]*"|'[^']*'|[^\\s"'=<>` + "`" + `]+)`, "i", ), ); if (!match) return null; const raw = match[1] ?? ""; return decodeAttrEntities(raw.replace(/^["']|["']$/g, "")); } function decodeAttrEntities(value: string): string { return value.replace( /&(#x[0-9a-f]+|#\d+|amp|apos|quot|lt|gt);/gi, (entity, body: string) => { const normalized = body.toLowerCase(); if (normalized === "amp") return "&"; if (normalized === "apos") return "'"; if (normalized === "quot") return '"'; if (normalized === "lt") return "<"; if (normalized === "gt") return ">"; const isHex = normalized.startsWith("#x"); const digits = normalized.slice(isHex ? 2 : 1); const codePoint = Number.parseInt(digits, isHex ? 16 : 10); if ( !Number.isFinite(codePoint) || codePoint < 0 || codePoint > 0x10ffff ) { return entity; } try { return String.fromCodePoint(codePoint); } catch { return entity; } }, ); } function escapeAttr(value: string): string { return value .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } function iconAccessibility(label: string | null): string { const accessibleLabel = label?.trim(); return accessibleLabel ? `role="img" aria-label="${escapeAttr(accessibleLabel)}"` : 'aria-hidden="true"'; } function iconSvg(name: keyof typeof ICON_PATHS, label: string | null): string { return `${ICON_PATHS[name].join("")}`; } function iconFallback(rawName: string, label: string | null): string { const iconName = rawName.trim() || "unknown"; const accessibleLabel = label?.trim() || `Unsupported icon: ${iconName}`; return `?`; } export function renderWireframeIconHtml(html: string): string { return html.replace( ICON_MARKER_RE, ( _match, _tagA, beforeA, quotedA, doubleA, singleA, bareA, afterA, _tagB, beforeB, quotedB, doubleB, singleB, bareB, afterB, ) => { const rawName = doubleA ?? singleA ?? bareA ?? doubleB ?? singleB ?? bareB ?? quotedA ?? quotedB ?? ""; const name = normalizeIconName(rawName); const attrs = `${beforeA ?? ""}${afterA ?? ""}${beforeB ?? ""}${afterB ?? ""}`; const label = readAttribute(attrs, "aria-label") ?? readAttribute(attrs, "title"); return name ? iconSvg(name, label) : iconFallback(rawName, label); }, ); }