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 ``;
}
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);
},
);
}