// ==UserScript== // @name ByteByteGo Reference Linker // @namespace https://github.com/abd3lraouf // @version 1.7.5 // @description Converts [n] reference markers into clickable links on ByteByteGo courses. Click the reference to open the URL, or click the arrow to scroll to the References section. // @author abd3lraouf // @license MIT // @match https://bytebytego.com/* // @match https://*.bytebytego.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=bytebytego.com // @grant GM_xmlhttpRequest // @grant GM_openInTab // @connect * // @run-at document-idle // @homepage https://github.com/abd3lraouf/bytebytego-reference-linker // @supportURL https://github.com/abd3lraouf/bytebytego-reference-linker/issues // @updateURL https://raw.githubusercontent.com/abd3lraouf/bytebytego-reference-linker/main/bytebytego-references.user.js // @downloadURL https://raw.githubusercontent.com/abd3lraouf/bytebytego-reference-linker/main/bytebytego-references.user.js // ==/UserScript== (function() { 'use strict'; // Store parsed references const references = new Map(); // Store reference link elements in article for up-arrow navigation const referenceLinkLocations = new Map(); // Script version for update notifications const SCRIPT_VERSION = '1.7.5'; const VERSION_KEY = 'bytebytego-refs-version'; const LAST_UPDATE_CHECK_KEY = 'bytebytego-refs-last-update-check'; const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const SCRIPT_UPDATE_URL = 'https://raw.githubusercontent.com/abd3lraouf/bytebytego-reference-linker/main/bytebytego-references.user.js'; const CHANGELOG_URL = 'https://raw.githubusercontent.com/abd3lraouf/bytebytego-reference-linker/main/changelog.json'; // Hover card state let hoverCard = null; let hoverTimeout = null; let hideTimeout = null; let currentHoveredLink = null; let isHoveringCard = false; const CLASS_REF_WRAPPER = 'bbg-ref-wrapper'; const CLASS_UP_ARROW = 'bbg-ref-up-arrow'; const WRAPPER_PROCESSED_ATTR = 'data-bbg-ref-processed'; const debouncedProcessPage = debounce(processPage, 400); function debounce(fn, delay) { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } function clearUpArrows() { document.querySelectorAll(`.${CLASS_UP_ARROW}`).forEach(el => el.remove()); } function scrollToRefLink(num, allowRetry = true) { const refLinkWrapper = document.querySelector(`[data-ref-link="${num}"]`); if (refLinkWrapper) { refLinkWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }); const link = refLinkWrapper.querySelector('.ref-link'); if (link) { const originalBg = link.style.backgroundColor; link.style.backgroundColor = 'rgba(99, 102, 241, 0.2)'; link.style.transition = 'background-color 0.3s'; setTimeout(() => { link.style.backgroundColor = originalBg; }, 1500); } return true; } if (allowRetry) { processPage(); setTimeout(() => scrollToRefLink(num, false), 150); } else { console.warn(`[ByteByteGo Refs] Could not find reference [${num}] in article after retry`); } return false; } function findNextHeaderAfter(element) { let sibling = element.nextElementSibling; while (sibling) { if (sibling.tagName && sibling.tagName.match(/^H[1-6]$/)) { return sibling; } sibling = sibling.nextElementSibling; } return null; } function isAfter(target, marker) { return !!(marker.compareDocumentPosition(target) & Node.DOCUMENT_POSITION_FOLLOWING); } function isBefore(target, marker) { return !!(target.compareDocumentPosition(marker) & Node.DOCUMENT_POSITION_FOLLOWING); } // Inject styles for hover card function injectStyles() { if (document.getElementById('bytebytego-ref-styles')) return; const style = document.createElement('style'); style.id = 'bytebytego-ref-styles'; style.textContent = ` .ref-hover-card { position: fixed; z-index: 10000; background: #ffffff; border: 1px solid #e1e4e8; border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); overflow: hidden; max-width: 400px; opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s; pointer-events: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .ref-hover-card.visible { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; } .ref-hover-card.above { transform: translateY(-4px); } .ref-hover-card.above.visible { transform: translateY(0); } /* Card with image */ .ref-card-image-container { position: relative; width: 100%; height: 200px; background: #f6f8fa; border-bottom: 1px solid #e1e4e8; display: none; } .ref-card-image-container.has-image { display: block; } .ref-card-image { width: 100%; height: 100%; object-fit: cover; display: block; } .ref-card-badge { position: absolute; top: 8px; right: 8px; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(4px); color: white; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 4px; } .ref-card-content { padding: 12px; } .ref-hover-card.has-image .ref-card-content { padding: 10px 12px 12px; } .ref-card-header { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; } .ref-card-icon { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; object-fit: cover; background: #f6f8fa; } .ref-card-header-text { flex: 1; min-width: 0; } .ref-card-domain { font-size: 11px; font-weight: 500; color: #6b7280; margin-bottom: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ref-card-badge-inline { display: inline-block; background: #6366f1; color: white; font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 3px; margin-left: 6px; vertical-align: middle; } .ref-card-title { font-size: 13px; font-weight: 600; color: #1f2937; line-height: 1.4; margin-bottom: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .ref-card-url { font-size: 11px; font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', monospace; color: #6366f1; background: #f6f8fa; padding: 6px 8px; border-radius: 4px; margin-bottom: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ref-card-actions { display: flex; gap: 6px; } .ref-card-btn { flex: 1; padding: 7px 12px; border-radius: 5px; font-size: 11px; font-weight: 600; cursor: pointer; transition: all 0.12s ease; text-align: center; text-decoration: none; border: none; display: flex; align-items: center; justify-content: center; gap: 4px; } .ref-card-btn-primary { background: #6366f1; color: white; } .ref-card-btn-primary:hover { background: #4f46e5; } .ref-card-btn-secondary { background: #f3f4f6; color: #4b5563; border: 1px solid #e5e7eb; } .ref-card-btn-secondary:hover { background: #e5e7eb; } /* Dark mode */ @media (prefers-color-scheme: dark) { .ref-hover-card { background: #1f2937; border-color: #374151; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); } .ref-card-image-container { background: #111827; border-bottom-color: #374151; } .ref-card-icon { background: #111827; } .ref-card-domain { color: #9ca3af; } .ref-card-title { color: #f9fafb; } .ref-card-url { background: #374151; color: #818cf8; } .ref-card-btn-secondary { background: #374151; color: #d1d5db; border-color: #4b5563; } .ref-card-btn-secondary:hover { background: #4b5563; } } /* Update notification toast */ .bytebytego-update-toast { position: fixed; bottom: 24px; right: 24px; z-index: 100000; background: #ffffff; border: 1px solid #e1e4e8; border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); padding: 16px 20px; max-width: 420px; opacity: 0; transform: translateY(20px); transition: opacity 0.3s ease, transform 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .bytebytego-update-toast.show { opacity: 1; transform: translateY(0); } .update-toast-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .update-toast-title { font-size: 14px; font-weight: 600; color: #1f2937; display: flex; align-items: center; gap: 8px; } .update-toast-badge { background: #10b981; color: white; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 4px; } .update-toast-close { background: none; border: none; color: #6b7280; cursor: pointer; font-size: 20px; line-height: 1; padding: 0; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; transition: background 0.15s; } .update-toast-close:hover { background: #f3f4f6; } .update-toast-content { font-size: 12px; color: #4b5563; line-height: 1.6; } .update-toast-content ul { margin: 8px 0; padding-left: 20px; } .update-toast-content li { margin: 4px 0; } .update-toast-actions { display: flex; gap: 8px; margin-top: 12px; } .update-toast-btn { flex: 1; border: none; padding: 9px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.15s, color 0.15s, box-shadow 0.15s; } .update-toast-btn.primary { background: #10b981; color: #ffffff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 1px 8px rgba(16, 185, 129, 0.3); } .update-toast-btn.primary:hover { background: #0ea371; } .update-toast-btn.secondary { background: #f3f4f6; color: #374151; border: 1px solid #e5e7eb; } .update-toast-btn.secondary:hover { background: #e5e7eb; } .update-toast-footer { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb; font-size: 11px; color: #6b7280; } @media (prefers-color-scheme: dark) { .bytebytego-update-toast { background: #1f2937; border-color: #374151; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); } .update-toast-title { color: #f9fafb; } .update-toast-close { color: #9ca3af; } .update-toast-close:hover { background: #374151; } .update-toast-content { color: #d1d5db; } .update-toast-btn.secondary { background: #374151; border-color: #4b5563; color: #e5e7eb; } .update-toast-btn.secondary:hover { background: #4b5563; } .update-toast-footer { border-top-color: #374151; color: #9ca3af; } } `; document.head.appendChild(style); } // Fetch OG image from URL async function fetchOGImage(url) { try { // Check if GM_xmlhttpRequest is available if (typeof GM_xmlhttpRequest === 'undefined') { return null; } return new Promise((resolve) => { // Set timeout to avoid hanging const timeout = setTimeout(() => resolve(null), 3000); GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 3000, onload: (response) => { clearTimeout(timeout); const html = response.responseText; // Try to find og:image meta tag const ogImageMatch = html.match(/ { clearTimeout(timeout); resolve(null); }, ontimeout: () => { clearTimeout(timeout); resolve(null); } }); }); } catch (error) { return null; } } // Get better quality icon function getIconUrl(url) { try { const urlObj = new URL(url); // Use icon.horse for better quality icons return `https://icon.horse/icon/${urlObj.hostname}`; } catch { return ''; } } // Create hover card element function createHoverCard() { if (hoverCard) return hoverCard; hoverCard = document.createElement('div'); hoverCard.className = 'ref-hover-card'; hoverCard.innerHTML = `
Tap update to install the latest improvements without leaving the page.