/** * Picks the colour and the letter of `ThumbnailPlaceholder`. The CSS mixes the * tint into the theme's surface (`--thumb-tint`) rather than painting it alone, * so the text keeps its contrast whatever the site's colour and the theme. * * @module */ /** * Checks whether `value` is a lowercase `#rrggbb` colour. An `accentColor` * comes from a dumped page's `theme-color`, which anyone controls, and a custom * property is substituted verbatim wherever it is used, unparsed. So the form * is checked here before it reaches a style. */ function isSafeHex(value: string | undefined): value is string { return !!value && /^#[0-9a-f]{6}$/.test(value); } /** * Returns the FNV-1a hash of `seed`, which is small, stable, and well spread * across the hue circle. */ function hashString(seed: string): number { let hash = 0x811c9dc5; for (let i = 0; i < seed.length; i++) { hash ^= seed.charCodeAt(i); hash = Math.imul(hash, 0x01000193); } return hash >>> 0; } /** * Returns a colour of a hue drawn from `seed`, so that the same site always * gets the same tint. */ function hueFromString(seed: string): string { return `hsl(${hashString(seed) % 360} 55% 55%)`; } function hostnameOf(url: string | undefined): string | undefined { if (!url) return undefined; try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return undefined; } } /** * Returns the page's own colour when it declared a valid one, or else a hue * drawn from `seed` or the hostname of `url`, so that every card gets a tint, * even for a dump saved before colours were extracted. */ export function tintFor( { accentColor, url, seed }: { accentColor?: string; url?: string; seed?: string; }, ): string { if (isSafeHex(accentColor)) return accentColor; return hueFromString(seed ?? hostnameOf(url) ?? url ?? ""); } /** * Returns the letter drawn when there's no favicon to show, the first letter or * digit of `siteName`, or else of the hostname of `url`, or "?". */ export function initialsFor( siteName: string | undefined, url: string | undefined, ): string { const source = siteName?.trim() || hostnameOf(url) || url || ""; const letter = [...source].find((ch) => /\p{L}|\p{N}/u.test(ch)); return (letter ?? "?").toUpperCase(); }