2024-12-31 14:59:19 +00:00
// ==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*
2025-01-02 11:03:08 +00:00
// @exclude *google.com*
// @exclude *tampermonkey*
2024-12-31 14:59:19 +00:00
// @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 = `
< h4 > URLs < / h 4 >
< div style = "margin-bottom: 10px;" >
< label for = "sort-select" > Sort by : < / l a b e l >
< select id = "sort-select" style = "width: 100%; padding: 5px; margin-top: 5px;" >
< option value = "frequency" > Frequency < / o p t i o n >
< option value = "name" > Name < / o p t i o n >
< / s e l e c t >
< / d i v >
< ul id = "url-list" style = "margin: 0; padding: 0; list-style: none;" > < / u l >
< div style = "margin-top: 10px;" >
< label for = "limit-select" > Show : < / l a b e l >
< select id = "limit-select" style = "width: 100%; padding: 5px; margin-top: 5px;" >
< option value = "all" > All < / o p t i o n >
< option value = "10" > Top 10 < / o p t i o n >
< option value = "20" > Top 20 < / o p t i o n >
< / s e l e c t >
< / d i v >
< button id = "copy-urls" style = "margin-top: 10px; display: block; width: 100%; padding: 10px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer;" > Copy URLs < / b u t t o n >
` ;
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 = ` <span> ${ url } </span> <span style="float: right; font-weight: bold;">1</span> ` ;
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 = ` <strong> ${ title } </strong><p> ${ description } </p> ` ;
tooltip . style . display = 'block' ;
} ,
onerror : ( ) => {
tooltip . innerHTML = '<strong>Preview unavailable</strong>' ;
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 = `
< span > $ { item . url } < / s p a n >
< span style = "float: right; font-weight: bold;" > $ { item . count } < / s p a n >
` ;
// 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!' ) ;
} ) ;
} ) ( ) ;