"use client"; import { useMemo, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; /** * Apple Color Emoji PNGs served from /emoji/apple/64 (vendored from * emoji-datasource-apple). External CDNs are blocked in many networks. */ const APPLE_BASE = "/emoji/apple/64"; /** Convert an emoji grapheme to Apple datasource filename stem (e.g. ❤️ → 2764-fe0f). */ export function emojiToAppleCode(emoji: string): string | null { const cps: string[] = []; for (const ch of emoji) { const cp = ch.codePointAt(0); if (cp == null) continue; // Text-presentation selector only — keep FE0F (required for ❤️ etc.) if (cp === 0xfe0e) continue; cps.push(cp.toString(16)); } if (cps.length === 0) return null; return cps.join("-"); } /** Candidate filenames: with FE0F as typed, then without FE0F segments. */ export function appleEmojiCandidates(emoji: string): string[] { const primary = emojiToAppleCode(emoji); if (!primary) return []; const out = [`${APPLE_BASE}/${primary}.png`]; if (primary.includes("fe0f")) { const stripped = primary .split("-") .filter((p) => p !== "fe0f") .join("-"); if (stripped && stripped !== primary) { out.push(`${APPLE_BASE}/${stripped}.png`); } } else { // Some keycaps / symbols are stored with -fe0f even when the string lacks it out.push(`${APPLE_BASE}/${primary}-fe0f.png`); } return out; } export function appleEmojiSrc(emoji: string): string | null { return appleEmojiCandidates(emoji)[0] ?? null; } export function AppleEmoji({ emoji, size = 20, className, title, }: { emoji: string; size?: number; className?: string; title?: string; }) { const candidates = useMemo(() => appleEmojiCandidates(emoji), [emoji]); const [idx, setIdx] = useState(0); const [failed, setFailed] = useState(false); const src = candidates[idx]; if (!src || failed) { return ( {emoji} ); } return ( // eslint-disable-next-line @next/next/no-img-element {emoji} { if (idx + 1 < candidates.length) { setIdx((i) => i + 1); } else { setFailed(true); } }} /> ); } const EMOJI_RE = /(?:\p{Extended_Pictographic}(?:\uFE0F)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F)?)*)|\p{Regional_Indicator}{2}/gu; /** Render plain text with Apple emoji images inline. */ export function AppleEmojiText({ text, size = 18, className, }: { text: string; size?: number; className?: string; }) { if (!text) return null; const parts: ReactNode[] = []; let last = 0; let i = 0; for (const match of text.matchAll(EMOJI_RE)) { const idx = match.index ?? 0; if (idx > last) { parts.push({text.slice(last, idx)}); } parts.push(); last = idx + match[0].length; } if (last < text.length) { parts.push({text.slice(last)}); } return {parts}; }