Last active
January 21, 2026 20:22
-
-
Save liuzhenqi77/64b5eb22ab4c9c2f12e769f6d7105d73 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Google Scholar Researcher Page Enhance | |
| // @version 0.1.1 | |
| // @description Fetching some useful metadata to save some clicks | |
| // @author Zhen-Qi Liu | |
| // @match https://scholar.google.com/citations?*user=* | |
| // @exclude https://scholar.google.com/citations?*view_op=view_citation* | |
| // @match https://scholar.google.ca/citations?*user=* | |
| // @exclude https://scholar.google.ca/citations?*view_op=view_citation* | |
| // @match https://scholar.google.co.uk/citations?*user=* | |
| // @exclude https://scholar.google.co.uk/citations?*view_op=view_citation* | |
| // @note 2026/01/21 Added first author detection and researcher name highlighting | |
| // @note 2025/08/08 Added borders to last author papers | |
| // @note 2025/08/07 Added the basics: full author list, abstract, links | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_addStyle | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| // Add CSS styles for the abstract display | |
| GM_addStyle(` | |
| #gsc_bdy { | |
| max-width: 1800px; | |
| } | |
| .load-metadata-panel { | |
| position: fixed; | |
| left: 2px; | |
| top: 95%; | |
| transform: translateY(-50%); | |
| color: white; | |
| padding: 8px 10px; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| z-index: 1000; | |
| box-shadow: 0 2px 2px rgba(0,0,0,0.2); | |
| } | |
| .load-metadata-btn { | |
| color: #1a0dab; | |
| cursor: pointer; | |
| } | |
| .load-metadata-btn:hover:not(:disabled) { | |
| text-decoration: underline; | |
| } | |
| .load-metadata-btn:disabled { | |
| color: #777777 !important; | |
| cursor: not-allowed !important; | |
| text-decoration: none !important; | |
| } | |
| .progress-indicator { | |
| text-align: center; | |
| margin: 10px 0; | |
| font-size: 10px; | |
| color: #666; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 4px; | |
| background-color: #e0e0e0; | |
| border-radius: 2px; | |
| overflow: hidden; | |
| margin: 10px 0; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background-color: #1a73e8; | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| } | |
| .metadata-container { | |
| margin: 8px 0; | |
| padding-left: 4px; | |
| background-color: #f8f9fa; | |
| border-left: 2px solid #4285f4; | |
| font-size: 13px; | |
| } | |
| .metadata-content { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| } | |
| .metadata-field { | |
| display: flex; | |
| flex-wrap: wrap; | |
| } | |
| .metadata-description { | |
| text-align: justify; | |
| font-style: italic; | |
| color: #555; | |
| width: 100%; | |
| } | |
| .metadata-loading { | |
| color: #666; | |
| font-style: italic; | |
| } | |
| .metadata-error { | |
| color: #d93025; | |
| font-style: italic; | |
| } | |
| .metadata-links-container { | |
| font-weight:normal; | |
| padding:16px 16px 0 16px; | |
| vertical-align:top; | |
| text-align:right; | |
| } | |
| #gsc_a_t th.metadata-links-container { | |
| box-sizing:border-box; | |
| text-transform:uppercase; | |
| vertical-align:middle; | |
| padding-top:0; | |
| padding-bottom:0; | |
| } | |
| th.metadata-links-container { | |
| width: 88px; | |
| white-space: nowrap; | |
| } | |
| .metadata-links { | |
| margin-top: 3px; | |
| } | |
| .lastAuthor { | |
| border: 1px solid darkblue; | |
| } | |
| .firstAuthor { | |
| border: 1px solid darkred; | |
| } | |
| .researcher-name-highlight { | |
| background-color: yellow; | |
| font-weight: bold; | |
| } | |
| `); | |
| let isLoading = false; | |
| let loadPanel = null; | |
| let loadButton = null; | |
| let progressIndicator = null; | |
| let progressBar = null; | |
| const scholarName = document.querySelector("#gsc_prf_in").textContent; | |
| function createLoadPanel() { | |
| const container = document.createElement("div"); | |
| container.className = "load-metadata-panel"; | |
| container.innerHTML = ` | |
| <button class="load-metadata-btn">Load All Metadata</button> | |
| <div class="progress-indicator">Fetching...</div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill"></div> | |
| </div> | |
| `; | |
| return container; | |
| } | |
| // Function to create abstract container for a paper | |
| function createMetadataContainer() { | |
| const container = document.createElement("div"); | |
| container.className = "metadata-container"; | |
| container.innerHTML = '<div class="metadata-loading">Loading...</div>'; | |
| return container; | |
| } | |
| function createMetadataLinksContainer() { | |
| const container = document.createElement("td"); | |
| container.className = "metadata-links-container"; | |
| return container; | |
| } | |
| function createMetadataLinksHeader() { | |
| const tr0 = document.createElement("th"); | |
| tr0.className = "metadata-links-container"; | |
| const trh = document.createElement("th"); | |
| trh.className = "metadata-links-container"; | |
| trh.scope = "col"; | |
| trh.innerHTML = ` | |
| <div>Links</div> | |
| `; | |
| return { tr0, trh }; | |
| } | |
| function isLastAuthor(authorString, targetName) { | |
| const authors = authorString | |
| .split(",") | |
| .map((author) => author.trim()) | |
| .filter((author) => author.length > 0); | |
| if (authors.length === 0) return false; | |
| const lastAuthor = authors[authors.length - 1]; | |
| const lastAuthorParts = lastAuthor.trim().split(/\s+/); | |
| const lastAuthorLastName = lastAuthorParts[lastAuthorParts.length - 1]; | |
| const targetNameParts = targetName.trim().split(/\s+/); | |
| const targetNameLastName = targetNameParts[targetNameParts.length - 1]; | |
| return ( | |
| lastAuthorLastName.toLowerCase() === targetNameLastName.toLowerCase() | |
| ); | |
| } | |
| function isFirstAuthor(authorString, targetName) { | |
| const authors = authorString | |
| .split(",") | |
| .map((author) => author.trim()) | |
| .filter((author) => author.length > 0); | |
| if (authors.length === 0) return false; | |
| const firstAuthor = authors[0]; | |
| const firstAuthorParts = firstAuthor.trim().split(/\s+/); | |
| const firstAuthorLastName = firstAuthorParts[firstAuthorParts.length - 1]; | |
| const targetNameParts = targetName.trim().split(/\s+/); | |
| const targetNameLastName = targetNameParts[targetNameParts.length - 1]; | |
| return ( | |
| firstAuthorLastName.toLowerCase() === targetNameLastName.toLowerCase() | |
| ); | |
| } | |
| function highlightResearcherName(authorString, targetName) { | |
| const targetNameParts = targetName.trim().split(/\s+/); | |
| const targetNameLastName = targetNameParts[targetNameParts.length - 1]; | |
| // Use a regex to find and wrap the researcher's last name | |
| // This will match word boundaries to avoid partial matches | |
| const regex = new RegExp(`\\b(${targetNameLastName})\\b`, 'gi'); | |
| return authorString.replace(regex, '<span class="researcher-name-highlight">$1</span>'); | |
| } | |
| // Function to fetch metadata from paper URL | |
| function fetchMetadata(container, paperUrl) { | |
| return new Promise((resolve) => { | |
| GM_xmlhttpRequest({ | |
| method: "GET", | |
| url: paperUrl, | |
| onload: function (response) { | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString( | |
| response.responseText, | |
| "text/html" | |
| ); | |
| // Extract all metadata fields | |
| const metadataFields = doc.querySelectorAll(".gs_scl"); | |
| const metadata = {}; | |
| metadataFields.forEach((field) => { | |
| const fieldLabel = field.querySelector(".gsc_oci_field"); | |
| const fieldValue = field.querySelector(".gsc_oci_value"); | |
| if (fieldLabel && fieldValue) { | |
| const label = fieldLabel.textContent.trim(); | |
| let value = fieldValue.textContent.trim(); | |
| if (label === "Authors") { | |
| metadata[label] = value; | |
| } | |
| // Special handling for description (abstract) | |
| if (label === "Description") { | |
| metadata[label] = fieldValue.textContent.trim(); | |
| } | |
| } | |
| }); | |
| const metadataLinkFields = doc.querySelectorAll(".gsc_oci_title_ggi"); | |
| const metadataLinks = []; | |
| metadataLinkFields.forEach((metadataLinkField) => { | |
| const linkObject = metadataLinkField.querySelector("a"); | |
| let href = linkObject.href; | |
| const trimToFormat = (text) => | |
| text.includes("[HTML]") | |
| ? "[HTML]" | |
| : text.includes("[PDF]") | |
| ? "[PDF]" | |
| : text; | |
| const text = trimToFormat(linkObject.text); | |
| metadataLinks.push({ href, text }); | |
| }); | |
| // Build HTML display | |
| if (Object.keys(metadata).length > 0) { | |
| if (metadata["Authors"]) { | |
| const highlightedAuthors = highlightResearcherName(metadata["Authors"], scholarName); | |
| container.parentElement.querySelector(".gs_gray").innerHTML = | |
| highlightedAuthors; | |
| const block = container.parentElement.parentElement; | |
| if (isLastAuthor(metadata["Authors"], scholarName)) { | |
| block.className += " lastAuthor"; | |
| } else if (isFirstAuthor(metadata["Authors"], scholarName)) { | |
| block.className += " firstAuthor"; | |
| } | |
| } | |
| if (metadataLinks.length > 0) { | |
| let html_links = "<div>"; | |
| for (let i = 0; i < metadataLinks.length; i++) { | |
| html_links += `<div><a href=${metadataLinks[i]["href"]}>${metadataLinks[i]["text"]}</a></div>`; | |
| } | |
| html_links += "</div>"; | |
| container.parentElement.parentElement.querySelector( | |
| ".metadata-links-container" | |
| ).innerHTML = html_links; | |
| } | |
| let html = '<div class="metadata-content">'; | |
| if (metadata["Description"]) { | |
| html += `<div class="metadata-field"> | |
| <div class="metadata-description">${metadata["Description"]}</div> | |
| </div>`; | |
| html += "</div>"; | |
| container.innerHTML = html; | |
| } | |
| } else { | |
| container.innerHTML = | |
| '<div class="metadata-error">Metadata not available</div>'; | |
| console.log(response); | |
| } | |
| resolve(); | |
| }, | |
| onerror: function () { | |
| container.innerHTML = | |
| '<div class="metadata-error">Failed to load metadata</div>'; | |
| resolve(); | |
| }, | |
| }); | |
| }); | |
| } | |
| // Function to update progress | |
| function updateProgress(current, total) { | |
| const percentage = (current / total) * 100; | |
| const progressFill = document.querySelector(".progress-fill"); | |
| const progressText = document.querySelector(".progress-indicator"); | |
| progressFill.style.width = `${percentage}%`; | |
| progressText.textContent = `Fetching... ${current}/${total}`; | |
| } | |
| // Main function to load all metadata | |
| async function loadAllMetadata() { | |
| if (isLoading) return; | |
| isLoading = true; | |
| loadButton.disabled = true; | |
| // loadButton.textContent = 'Loading...'; | |
| // Show progress indicator | |
| progressIndicator.style.display = "block"; | |
| const paperElements = document.querySelectorAll(".gsc_a_tr"); | |
| const validPapers = []; | |
| // Add table header for metadata-links-container | |
| const { tr0, trh } = createMetadataLinksHeader(); | |
| const tableHead = document.querySelector("#gsc_a_tw table thead"); | |
| tableHead.querySelector("#gsc_a_tr0").appendChild(tr0); | |
| tableHead.querySelector("#gsc_a_trh").appendChild(trh); | |
| // Prepare papers and add containers | |
| paperElements.forEach((paperElement) => { | |
| const titleLink = paperElement.querySelector(".gsc_a_at"); | |
| if (!titleLink) return; | |
| const paperUrl = | |
| "https://scholar.google.com" + titleLink.getAttribute("href"); | |
| const titleCell = paperElement.querySelector(".gsc_a_t"); | |
| if (titleCell && !titleCell.querySelector(".metadata-container")) { | |
| const container = createMetadataContainer(); | |
| titleCell.appendChild(container); | |
| validPapers.push({ container, paperUrl }); | |
| } | |
| if (!paperElement.querySelector(".metadata-links-container")) { | |
| const links = createMetadataLinksContainer(); | |
| paperElement.appendChild(links); | |
| } | |
| }); | |
| // Load metadata with delay to avoid rate limiting | |
| for (let i = 0; i < validPapers.length; i++) { | |
| const { container, paperUrl } = validPapers[i]; | |
| updateProgress(i + 1, validPapers.length); | |
| await fetchMetadata(container, paperUrl); | |
| // Add delay between requests to be respectful to the server | |
| if (i < validPapers.length - 1) { | |
| await new Promise((resolve) => setTimeout(resolve, 500)); | |
| } | |
| } | |
| // Hide progress and update button | |
| progressIndicator.style.display = "none"; | |
| loadButton.textContent = "Metadata Loaded"; | |
| loadButton.disabled = true; | |
| isLoading = false; | |
| } | |
| function insertLoadPanel() { | |
| if (loadPanel) return; | |
| loadPanel = createLoadPanel(); | |
| loadButton = loadPanel.querySelector(".load-metadata-btn"); | |
| loadButton.onclick = loadAllMetadata; | |
| progressIndicator = loadPanel.querySelector(".progress-indicator"); | |
| progressIndicator.style.display = "none"; | |
| // Insert floating button directly to body | |
| document.body.appendChild(loadPanel); | |
| } | |
| // Function to handle dynamic content loading | |
| function observeChanges() { | |
| const observer = new MutationObserver(function (mutations) { | |
| mutations.forEach(function (mutation) { | |
| if (mutation.addedNodes.length > 0) { | |
| // Check if new paper entries were added | |
| const hasNewPapers = Array.from(mutation.addedNodes).some( | |
| (node) => | |
| node.nodeType === 1 && | |
| (node.matches(".gsc_a_tr") || node.querySelector(".gsc_a_tr")) | |
| ); | |
| if (hasNewPapers && loadButton && !isLoading) { | |
| // Re-enable button if new papers are loaded | |
| loadButton.disabled = false; | |
| loadButton.textContent = "Load All Metadata"; | |
| } | |
| } | |
| }); | |
| }); | |
| const targetNode = document.querySelector("#gsc_a_b") || document.body; | |
| observer.observe(targetNode, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| // Initialize the script | |
| function init() { | |
| // Wait for page to load | |
| if (document.readyState === "loading") { | |
| document.addEventListener("DOMContentLoaded", init); | |
| return; | |
| } | |
| // Remove language parameters from the Google Scholar URL and reload the page if necessary. | |
| const currentUrl = window.location.href; | |
| const url = new URL(currentUrl); | |
| url.searchParams.delete('hl'); | |
| if (url.toString() !== currentUrl) { | |
| window.location.href = url.toString(); | |
| } | |
| // Insert load button | |
| setTimeout(insertLoadPanel, 1000); | |
| // Set up observer for dynamic loading | |
| observeChanges(); | |
| // Handle "Show more" button clicks | |
| const showMoreButton = document.querySelector("#gsc_bpf_more"); | |
| if (showMoreButton) { | |
| showMoreButton.addEventListener("click", function () { | |
| setTimeout(() => { | |
| if (loadButton && !isLoading) { | |
| loadButton.disabled = false; | |
| loadButton.textContent = "Load All Metadata"; | |
| } | |
| }, 2000); | |
| }); | |
| } | |
| } | |
| // Start the script | |
| init(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment