feat(content-curation): exclude parent URLs and toggle link highlighting

This commit is contained in:
SansGuidon 2024-12-31 14:59:19 +00:00
parent 3f142c0258
commit 32f613278e

View File

@ -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 = `
<h4>URLs</h4>
<div style="margin-bottom: 10px;">
<label for="sort-select">Sort by:</label>
<select id="sort-select" style="width: 100%; padding: 5px; margin-top: 5px;">
<option value="frequency">Frequency</option>
<option value="name">Name</option>
</select>
</div>
<ul id="url-list" style="margin: 0; padding: 0; list-style: none;"></ul>
<div style="margin-top: 10px;">
<label for="limit-select">Show:</label>
<select id="limit-select" style="width: 100%; padding: 5px; margin-top: 5px;">
<option value="all">All</option>
<option value="10">Top 10</option>
<option value="20">Top 20</option>
</select>
</div>
<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</button>
`;
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}</span>
<span style="float: right; font-weight: bold;">${item.count}</span>
`;
// 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!');
});
})();