'use strict'; (function attachIconUtils(globalScope) { const MULTI_PART_SUFFIXES = new Set([ 'ac.cn', 'ac.jp', 'ac.uk', 'co.jp', 'co.nz', 'co.uk', 'com.au', 'com.br', 'com.cn', 'com.hk', 'com.sg', 'edu.cn', 'gov.cn', 'gov.uk', 'net.au', 'net.cn', 'org.au', 'org.cn', 'org.uk', ]); const MULTI_TENANT_HOST_SUFFIXES = [ 'blogspot.com', 'github.io', 'notion.site', 'substack.com', 'vercel.app', 'wordpress.com', ]; function getHostname(url) { if (!url) return ''; try { return new URL(url).hostname; } catch { return ''; } } function getPageOriginFaviconUrl(url = '') { if (!url) return ''; try { const parsed = new URL(url); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return ''; return `${parsed.origin}/favicon.ico`; } catch { return ''; } } function isIpAddress(hostname = '') { return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(String(hostname || '')); } function getPrimaryDomain(hostname = '') { const cleanHostname = String(hostname || '').trim().replace(/^www\./, '').toLowerCase(); if (!cleanHostname || cleanHostname === 'localhost' || isIpAddress(cleanHostname)) return cleanHostname; if (MULTI_TENANT_HOST_SUFFIXES.some(suffix => cleanHostname.endsWith(`.${suffix}`))) { return cleanHostname; } const parts = cleanHostname.split('.').filter(Boolean); if (parts.length <= 2) return cleanHostname; const trailingPair = parts.slice(-2).join('.'); if (MULTI_PART_SUFFIXES.has(trailingPair) && parts.length >= 3) { return parts.slice(-3).join('.'); } return parts.slice(-2).join('.'); } function getGoogleFaviconUrl(hostname, size = 16) { if (!hostname) return ''; return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=${size}`; } function getFaviconUrl(input) { const rawUrl = typeof input === 'string' ? input : (input?.domain ?? ''); if (!rawUrl) return { url: '', source: '', fallback: '' }; const size = typeof input === 'string' ? 128 : (input?.size ?? 128); let protocol, hostname, pageUrl, fallbackOrigin; try { const parsed = new URL(rawUrl); protocol = parsed.protocol; hostname = parsed.hostname; pageUrl = rawUrl; fallbackOrigin = parsed.origin; } catch { if (/^[a-zA-Z0-9.-]+$/.test(rawUrl) && rawUrl.includes('.')) { protocol = 'https:'; hostname = rawUrl; pageUrl = `https://${rawUrl}`; fallbackOrigin = pageUrl; } else { return { url: '', source: '', fallback: '' }; } } if (protocol !== 'http:' && protocol !== 'https:') { return { url: '', source: '', fallback: '' }; } const faviconBase = (typeof chrome !== 'undefined' && chrome.runtime?.getURL) ? chrome.runtime.getURL('_favicon/') : ''; const chromeUrl = faviconBase ? `${faviconBase}?pageUrl=${encodeURIComponent(pageUrl)}&size=${size}` : ''; const fallbackUrl = fallbackOrigin ? `${fallbackOrigin}/favicon.ico` : ''; return { url: chromeUrl, source: chromeUrl ? 'chrome' : '', fallback: fallbackUrl, }; } function dedupeIconSources(sources = []) { const seen = new Set(); return sources.filter(source => { const normalized = String(source || '').trim(); if (!normalized || seen.has(normalized)) return false; seen.add(normalized); return true; }); } function isStableIconUrl(url = '') { if (!url) return false; try { const parsed = new URL(url); return parsed.protocol === 'http:' || parsed.protocol === 'https:' || parsed.protocol === 'data:'; } catch { return false; } } function escapeHtml(value = '') { return String(value) .replace(/&/g, '&') .replace(//g, '>'); } function escapeHtmlAttribute(value = '') { return String(value) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function getFallbackLabel(label, hostname = '') { const cleanLabel = (label || '').trim(); if (cleanLabel) { const tokens = cleanLabel .split(/[\s./:_-]+/) .filter(Boolean) .slice(0, 2) .map(token => token[0]?.toUpperCase() || ''); const joined = tokens.join(''); if (joined) return joined; } const cleanHost = hostname.replace(/^www\./, ''); return (cleanHost.slice(0, 2) || '?').toUpperCase(); } function getIconSources({ favIconUrl = '', url = '' } = {}, size = 16) { const hostname = getHostname(url); const primaryDomain = getPrimaryDomain(hostname); const faviconData = getFaviconUrl({ domain: url, size }); const originFaviconUrl = faviconData.fallback || getPageOriginFaviconUrl(url); const sources = dedupeIconSources([ isStableIconUrl(favIconUrl) ? favIconUrl : '', faviconData.url, originFaviconUrl, primaryDomain ? getGoogleFaviconUrl(primaryDomain, size) : '', hostname && hostname !== primaryDomain ? getGoogleFaviconUrl(hostname, size) : '', ]); return { hostname, primaryDomain, originFaviconUrl, sources, }; } function getGroupIcon(group, label, size = 32) { const tabs = group?.tabs || []; const preferredTab = tabs.find(tab => isStableIconUrl(tab?.favIconUrl)) || tabs.find(tab => tab?.url) || tabs[0] || {}; const { hostname, sources } = getIconSources(preferredTab, size); return { hostname, src: sources[0] || '', fallbackSrc: sources[1] || '', fallbackSources: sources.slice(1), fallbackLabel: getFallbackLabel(label, hostname), }; } const api = { escapeHtml, escapeHtmlAttribute, getFallbackLabel, getPageOriginFaviconUrl, getGoogleFaviconUrl, getGroupIcon, getHostname, getPrimaryDomain, getFaviconUrl, getIconSources, }; if (typeof module !== 'undefined' && module.exports) { module.exports = api; } globalScope.TabOutIconUtils = api; })(typeof globalThis !== 'undefined' ? globalThis : window);