diff --git a/tampermonkey/universal-url-highlighter.js b/tampermonkey/universal-url-highlighter.js new file mode 100644 index 0000000..5c57614 --- /dev/null +++ b/tampermonkey/universal-url-highlighter.js @@ -0,0 +1,333 @@ +// ==UserScript== +// @name Universal URL Highlighter with Conditional In-Page Highlighting +// @namespace http://tampermonkey.net/ +// @version 1.9 +// @description Highlights URLs on any webpage, lists them in a toggleable panel with citation counts, conditionally highlights frequent links in-page, and provides export functionality. Excludes links in header and footer, as well as parent URLs of the current page. Hover previews for links. +// @author MorganGeek +// @match *://*/* +// @exclude *zoemp.be* +// @exclude *duckduckgo* +// @exclude *search* +// @exclude *forum* +// @exclude *chatgpt* +// @grant GM_xmlhttpRequest +// @grant GM_setClipboard +// @connect * +// @run-at document-end +// ==/UserScript== + +(function () { + 'use strict'; + + // Configuration of colors based on frequency + const COLORS = { + low: '#d1e7dd', // Light Green + medium: '#fff3cd', // Light Yellow + high: '#f8d7da' // Light Red + }; + + // Create a toggle button to show/hide the panel + const toggleButton = document.createElement('button'); + toggleButton.textContent = 'Show URLs'; + toggleButton.style.position = 'fixed'; + toggleButton.style.bottom = '10px'; + toggleButton.style.right = '10px'; + toggleButton.style.zIndex = 1000; + toggleButton.style.padding = '10px 15px'; + toggleButton.style.backgroundColor = '#007bff'; + toggleButton.style.color = '#fff'; + toggleButton.style.border = 'none'; + toggleButton.style.borderRadius = '5px'; + toggleButton.style.cursor = 'pointer'; + document.body.appendChild(toggleButton); + + // Create a floating panel to display the URLs (hidden by default) + const panel = document.createElement('div'); + panel.id = 'url-panel'; + panel.style.position = 'fixed'; + panel.style.top = '10px'; + panel.style.right = '10px'; + panel.style.width = '350px'; + panel.style.backgroundColor = '#fff'; + panel.style.border = '1px solid #ccc'; + panel.style.borderRadius = '5px'; + panel.style.padding = '10px'; + panel.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)'; + panel.style.maxHeight = '80vh'; + panel.style.overflowY = 'auto'; + panel.style.zIndex = 1000; + panel.style.display = 'none'; // Hidden by default + panel.innerHTML = ` +

URLs

+
+ + +
+ +
+ + +
+ + `; + document.body.appendChild(panel); + + // Create a tooltip for previews + const tooltip = document.createElement('div'); + tooltip.id = 'url-tooltip'; + tooltip.style.position = 'absolute'; + tooltip.style.backgroundColor = '#fff'; + tooltip.style.border = '1px solid #ccc'; + tooltip.style.borderRadius = '5px'; + tooltip.style.padding = '10px'; + tooltip.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.2)'; + tooltip.style.maxWidth = '300px'; + tooltip.style.display = 'none'; + tooltip.style.zIndex = 1001; + document.body.appendChild(tooltip); + + // Data structure to store URLs with their metadata + const urlData = {}; + + // Store original background colors to restore later + const originalColors = new Map(); + + // Function to escape CSS selectors + function escapeSelector(selector) { + return CSS.escape(selector); + } + + // Helper function to check if an element is inside header or footer + function isInHeaderOrFooter(element) { + let parent = element.parentElement; + while (parent) { + if (parent.tagName.toLowerCase() === 'header' || parent.tagName.toLowerCase() === 'footer') { + return true; + } + parent = parent.parentElement; + } + return false; + } + + // Function to generate all parent URLs of the current page + function getParentUrls(currentUrl) { + const parents = []; + const urlObj = new URL(currentUrl); + const pathname = urlObj.pathname; + const pathSegments = pathname.split('/').filter(segment => segment.length > 0); + + while (pathSegments.length > 0) { + pathSegments.pop(); + const parentPath = '/' + pathSegments.join('/') + '/'; + parents.push(`${urlObj.origin}${parentPath}`); + } + + parents.push(`${urlObj.origin}/`); // Add the base origin + return parents; + } + + // Get current page's URL and its parent URLs + const currentPageUrl = window.location.href; + const parentUrls = new Set(getParentUrls(currentPageUrl)); + + // Extract URLs from the page and collect data without coloring + const links = document.querySelectorAll('a[href]'); + links.forEach(link => { + if (isInHeaderOrFooter(link)) { + // Ignore links inside header or footer + return; + } + + const url = link.href.trim(); + + // Exclude URLs that are parent URLs of the current page + if (parentUrls.has(url)) { + return; + } + + if (!urlData[url]) { + urlData[url] = { + url: url, + count: 1 + }; + + // Add the URL to the list in the panel + const listItem = document.createElement('li'); + listItem.innerHTML = `${url} 1`; + listItem.style.marginBottom = '5px'; + listItem.style.wordWrap = 'break-word'; + listItem.style.cursor = 'pointer'; + listItem.dataset.url = url; + panel.querySelector('#url-list').appendChild(listItem); + } else { + urlData[url].count += 1; + // Update the counter in the panel + const existingItem = panel.querySelector(`li[data-url="${escapeSelector(url)}"] span:last-child`); + if (existingItem) { + existingItem.textContent = urlData[url].count; + } + } + + // Add hover preview functionality to the links + link.addEventListener('mouseover', () => { + GM_xmlhttpRequest({ + method: 'GET', + url: url, + onload: response => { + tooltip.innerHTML = ''; + const parser = new DOMParser(); + const doc = parser.parseFromString(response.responseText, 'text/html'); + const title = doc.querySelector('title') ? doc.querySelector('title').innerText : 'No Title'; + const description = doc.querySelector('meta[name="description"]') ? doc.querySelector('meta[name="description"]').content : 'No Description'; + + tooltip.innerHTML = `${title}

${description}

`; + tooltip.style.display = 'block'; + }, + onerror: () => { + tooltip.innerHTML = 'Preview unavailable'; + tooltip.style.display = 'block'; + } + }); + + const rect = link.getBoundingClientRect(); + tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`; + tooltip.style.left = `${rect.left + window.scrollX}px`; + }); + + link.addEventListener('mouseout', () => { + tooltip.style.display = 'none'; + }); + }); + + // Function to update the panel display based on filters + function updatePanel() { + const sortBy = panel.querySelector('#sort-select').value; + const limit = panel.querySelector('#limit-select').value; + + // Convert the object to an array for sorting + let urlsArray = Object.values(urlData); + + // Sort based on the selected criteria + if (sortBy === 'frequency') { + urlsArray.sort((a, b) => b.count - a.count); + } else if (sortBy === 'name') { + urlsArray.sort((a, b) => a.url.localeCompare(b.url)); + } + + // Apply the limit if necessary + if (limit !== 'all') { + const limitNumber = parseInt(limit); + urlsArray = urlsArray.slice(0, limitNumber); + } + + const urlList = panel.querySelector('#url-list'); + urlList.innerHTML = ''; // Clear the current list + + // Determine frequencies for color coding + const counts = urlsArray.map(item => item.count); + const maxCount = Math.max(...counts); + const minCount = Math.min(...counts); + + urlsArray.forEach(item => { + const listItem = document.createElement('li'); + listItem.style.marginBottom = '5px'; + listItem.style.wordWrap = 'break-word'; + listItem.style.cursor = 'pointer'; + listItem.dataset.url = item.url; + + // Set color based on frequency + let color; + const ratio = (item.count - minCount) / (maxCount - minCount + 1); + if (ratio > 0.66) { + color = COLORS.high; + } else if (ratio > 0.33) { + color = COLORS.medium; + } else { + color = COLORS.low; + } + listItem.style.backgroundColor = color; + + // Create content with URL and citation count + listItem.innerHTML = ` + ${item.url} + ${item.count} + `; + + // Add click event to copy the URL + listItem.addEventListener('click', () => { + GM_setClipboard(item.url, 'text'); + alert('URL copied to clipboard!'); + }); + + urlList.appendChild(listItem); + }); + } + + // Function to highlight links in the page based on frequency + function highlightLinksInPage(urlsArray) { + // Apply color coding based on frequency + urlsArray.forEach(item => { + const linksToHighlight = document.querySelectorAll(`a[href="${escapeSelector(item.url)}"]:not(header a, footer a)`); + linksToHighlight.forEach(link => { + // Store original background color if not already stored + if (!originalColors.has(link)) { + originalColors.set(link, link.style.backgroundColor); + } + + let color; + const ratio = (item.count - Math.min(...urlsArray.map(u => u.count))) / (Math.max(...urlsArray.map(u => u.count)) - Math.min(...urlsArray.map(u => u.count)) + 1); + if (ratio > 0.66) { + color = COLORS.high; + } else if (ratio > 0.33) { + color = COLORS.medium; + } else { + color = COLORS.low; + } + link.style.backgroundColor = color; + }); + }); + } + + // Function to remove highlights from links in the page + function removeHighlights() { + originalColors.forEach((color, link) => { + link.style.backgroundColor = color || ''; + }); + originalColors.clear(); + } + + // Initialize the panel + updatePanel(); + + // Listen for changes in the sort and limit selectors + panel.querySelector('#sort-select').addEventListener('change', updatePanel); + panel.querySelector('#limit-select').addEventListener('change', updatePanel); + + // Handle the toggle button to show/hide the panel and manage in-page highlighting + toggleButton.addEventListener('click', () => { + if (panel.style.display === 'none') { + panel.style.display = 'block'; + toggleButton.textContent = 'Hide URLs'; + highlightLinksInPage(Object.values(urlData)); + } else { + panel.style.display = 'none'; + toggleButton.textContent = 'Show URLs'; + removeHighlights(); + } + }); + + // Handle the "Copy URLs" button + panel.querySelector('#copy-urls').addEventListener('click', () => { + const urlArray = Object.keys(urlData); + GM_setClipboard(urlArray.join('\n'), 'text'); + alert('URLs copied to clipboard!'); + }); + +})();