diff --git a/README.md b/README.md index e5c5715..c20b80c 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,19 @@ Behaviour: Scratchpads render the referenced file as wikitext and (when you have edit rights on the host page) provide an inline editor that saves directly to the backing file. +### 7) Link Favicons (automatic) + +External links automatically display the favicon of the linked website. This feature: + +- Uses DuckDuckGo's favicon service (`icons.duckduckgo.com`) +- Works on all external links (class `urlextern`) +- Shows grayscale icons that become colored on hover +- Browser handles caching; no server-side storage needed + +No configuration required. The feature is enabled by default for all external links. + +Based on the [linkfavicon plugin](https://github.com/shaoyanmin/linkfavicon) by Shao Yanmin. + ## Inline options reference (directory/images) diff --git a/action.php b/action.php index 055a4fa..6a80d2e 100644 --- a/action.php +++ b/action.php @@ -48,6 +48,7 @@ class action_plugin_luxtools extends ActionPlugin "gallery-thumbnails.js", "open-service.js", "scratchpads.js", + "linkfavicon.js", "main.js", ]; diff --git a/js/linkfavicon.js b/js/linkfavicon.js new file mode 100644 index 0000000..247f3c1 --- /dev/null +++ b/js/linkfavicon.js @@ -0,0 +1,116 @@ +/* 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); +})(); diff --git a/style.css b/style.css index 7b064bc..92fc7ea 100644 --- a/style.css +++ b/style.css @@ -470,3 +470,27 @@ html.luxtools-noscroll body { padding: 3px; text-align: left; } + +/* ============================================================ + * Link Favicon + * Display favicons for external links. + * ============================================================ */ + +/* External links with favicon loaded */ +.dokuwiki a.urlextern.linkfavicon { + /* Reserve space for 16x16 favicon + small gap */ + padding-left: 20px; + background-repeat: no-repeat; + background-position: left center; + background-size: 16px 16px; + /* Muted grayscale effect until hover */ + background-color: #fff; + background-blend-mode: luminosity; +} + +/* Show full color on hover */ +.dokuwiki a.urlextern.linkfavicon:hover { + background-color: transparent; + background-blend-mode: normal; + transition: all 0.2s ease-in-out; +}