/* global document */ /** * Link Favicon Module * * Displays favicons for external links using DuckDuckGo's favicon service. * Based on the linkfavicon plugin by Shao Yanmin. * * Note: DDG returns a grey placeholder icon for domains without favicons. * Detecting this placeholder client-side is not reliably possible due to * CORS restrictions preventing canvas pixel inspection. */ (function () { 'use strict'; var Luxtools = window.Luxtools || (window.Luxtools = {}); // Cache states for favicon URLs var ICON_NOT_FOUND = -1; var ICON_LOADING = 0; var ICON_LOADED = 1; var faviconCache = {}; /** * Preload an image to verify it loads. * @param {string} src - Image URL to load * @returns {Promise} Resolves on load, rejects on error */ function loadImage(src) { return new Promise(function (resolve, reject) { var image = new Image(); image.addEventListener('load', resolve); image.addEventListener('error', reject); image.src = src; }); } /** * Apply the favicon as background image to all matching links. * @param {string} faviconUrl - The favicon URL to apply */ function applyFavicon(faviconUrl) { if (faviconCache[faviconUrl] !== ICON_LOADED) return; var links = document.querySelectorAll('[data-linkfavicon="' + faviconUrl + '"]'); for (var i = 0; i < links.length; i++) { links[i].classList.add('linkfavicon'); links[i].style.backgroundImage = 'url(' + faviconUrl + ')'; } } /** * Get domain from URL. * @param {string} url - Full URL * @returns {string|null} Domain or null if invalid */ function getDomain(url) { try { var parsed = new URL(url); return parsed.hostname; } catch (e) { return null; } } /** * Initialize favicons for all external links on the page. */ function init() { // Find all external links (DokuWiki marks them with class 'urlextern') var links = document.querySelectorAll('a.urlextern'); for (var i = 0; i < links.length; i++) { var link = links[i]; var href = link.getAttribute('href'); if (!href) continue; var domain = getDomain(href); if (!domain) continue; // DuckDuckGo favicon service URL var faviconUrl = 'https://icons.duckduckgo.com/ip3/' + domain + '.ico'; // Mark the link with the favicon URL for later reference link.setAttribute('data-linkfavicon', faviconUrl); // Load and cache the favicon if not already processed if (faviconCache[faviconUrl] === undefined) { faviconCache[faviconUrl] = ICON_LOADING; (function (url) { loadImage(url) .then(function () { faviconCache[url] = ICON_LOADED; applyFavicon(url); }) .catch(function () { faviconCache[url] = ICON_NOT_FOUND; }); })(faviconUrl); } else if (faviconCache[faviconUrl] === ICON_LOADED) { // Already loaded, apply immediately link.classList.add('linkfavicon'); link.style.backgroundImage = 'url(' + faviconUrl + ')'; } } } // Export for potential external use Luxtools.LinkFavicon = { init: init }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', init, false); })();