Highlights first-author, second-author, co-first, corresponding and last-author papers on Google Scholar profile pages and provides simple filters and metrics.
// ==UserScript==
// @name Google Scholar Author Highlighter Script
// @namespace https://controlnet.space/
// @version 2.1.1
// @description Highlights first-author, second-author, co-first, corresponding and last-author papers on Google Scholar profile pages and provides simple filters and metrics.
// @author ControlNet
// @include https://scholar.google.*/citations?*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// @license AGPL-3.0
// ==/UserScript==
/*
* This userscript is adapted from the original Chrome extension
* "Google Scholar Author Highlighter".
*
* It refactors the extension into a Tampermonkey userscript while preserving
* the main highlighting, filtering, and metrics features.
*
* Credit for the original plugin and core implementation belongs to the
* original author.
*/
(function () {
'use strict';
// Prevent double injection on dynamic navigations
if (window.googleScholarAuthorHighlighterLoaded) {
return;
}
window.googleScholarAuthorHighlighterLoaded = true;
/**
* ---------------------------------------------------------------------------
* Inject styles
*
* The original extension ships a standalone CSS file that defines the
* colour scheme for each author position as well as styles for the control
* panel, histogram and modal. We inline the contents here via GM_addStyle.
*/
GM_addStyle(`
.first-author { background-color: #ffffd9 !important; border-left: 4px solid #ffd700 !important; }
.second-author { background-color: #fff0e6 !important; border-left: 4px solid #ff8c00 !important; }
.co-first-author { background-color: #e6ffe6 !important; border-left: 4px solid #00cc00 !important; }
.corresponding-author { background-color: #f3e5f5 !important; border-left: 4px solid #9c27b0 !important; }
.last-author { background-color: #e6f3ff !important; border-right: 4px solid #4285f4 !important; }
.first-author.last-author { background: linear-gradient(to right, #ffffd9 50%, #e6f3ff 50%) !important; border-left: 4px solid #ffd700 !important; border-right: 4px solid #4285f4 !important; }
.second-author.last-author { background: linear-gradient(to right, #fff0e6 50%, #e6f3ff 50%) !important; border-left: 4px solid #ff8c00 !important; border-right: 4px solid #4285f4 !important; }
.co-first-author.last-author { background: linear-gradient(to right, #e6ffe6 50%, #e6f3ff 50%) !important; border-left: 4px solid #00cc00 !important; border-right: 4px solid #4285f4 !important; }
.corresponding-author.last-author { background: linear-gradient(to right, #f3e5f5 50%, #e6f3ff 50%) !important; border-left: 4px solid #9c27b0 !important; border-right: 4px solid #4285f4 !important; }
.second-author.corresponding-author { background: linear-gradient(to right, #fff0e6 50%, #f3e5f5 50%) !important; border-left: 4px solid #9c27b0 !important; }
.first-author.corresponding-author { background: linear-gradient(to right, #ffffd9 50%, #f3e5f5 50%) !important; border-left: 4px solid #9c27b0 !important; }
.author-highlighter-controls { background-color: #f8f9fa; border: 1px solid #dadce0; border-radius: 8px; padding: 10px; margin-bottom: 16px; position: relative; z-index: 1000; font-size: 14px; line-height: 1.3; }
.author-highlighter-controls h3 { margin: 0 0 6px 0; color: #1a73e8; font-size: 16px; display: flex; align-items: center; justify-content: space-between; }
.title-container { display: flex; align-items: center; }
.toggle-container, .filter-container { margin-top: 6px; }
.toggle-container label, .filter-container>label { display: inline-block; margin-right: 6px; font-weight: bold; font-size: 13px; }
.filter-container { display: flex; align-items: center; justify-content: space-between; }
.filter-options { display: grid; grid-template-columns: max-content max-content max-content; gap: 8px 12px; align-items: center; margin-left: 6px; margin-right: 8px; }
.filter-options div { display: flex; align-items: center; }
.filter-options label { margin-left: 3px; font-size: 13px; white-space: nowrap; }
#visibility-toggle, .filter-options input[type="checkbox"] { transform: scale(1.1); margin-right: 3px; }
#gsc_prf_w { position: relative; }
#gsc_prf_w>div:first-child { margin-bottom: 16px; }
.highlighted-cited-by { background-color: #f8f9fa; border: 1px solid #dadce0; border-radius: 8px; padding: 10px; margin-top: 10px; font-size: 14px; }
.highlighted-cited-by h4 { margin: 0 0 6px 0; color: #1a73e8; font-size: 16px; }
.highlighted-cited-by p { margin: 4px 0; }
.highlighted-metrics { margin-top: 10px; font-size: 14px; }
.highlighted-metrics h4 { margin: 10px 0 6px 0; color: #1a73e8; font-size: 16px; }
.metrics-grid { display: flex; flex-direction: column; gap: 4px; }
.metric-item { display: flex; justify-content: space-between; align-items: center; }
.metric-label { font-weight: bold; color: #5f6368; }
.metric-value { font-weight: bold; color: #202124; margin-right: 8px; }
.metrics-divider { border: none; border-top: 1px solid #dadce0; margin: 10px 0; }
.highlighted-author-name { background-color: #ffff00 !important; font-weight: bold !important; }
.info-icon { display: inline-block; margin-left: 5px; font-size: 13px; color: #4285f4; cursor: help; position: relative; vertical-align: text-bottom; font-style: normal; font-weight: normal; }
.info-icon:hover { color: #1a73e8; }
.info-icon::before { content: attr(data-tooltip); position: absolute; bottom: 150%; left: 50%; transform: translateX(-50%); background-color: #333; color: #fff; padding: 8px 14px; border-radius: 6px; white-space: pre; font-size: 13px; font-weight: normal; line-height: 1.5; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; z-index: 10000; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); text-align: left; min-width: 300px; }
.info-icon:hover::before { opacity: 1; }
.time-filter-container { margin-top: 6px; display: flex; align-items: center; }
.time-filter-container label { margin-right: 6px; font-weight: bold; font-size: 13px; }
.year-input { padding: 2px 4px; border: 1px solid #dadce0; border-radius: 4px; font-size: 13px; margin: 0 4px; height: 24px; background-color: white; }
.year-input:focus { outline: none; border-color: #1a73e8; }
.metric-total { font-size: 0.9em; color: #5f6368; font-weight: normal; margin-left: 4px; }
/* Histogram styles */
.metrics-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.metrics-header h4 { margin: 0; display: flex; align-items: center; flex: 1; }
.histogram-emoji-btn { cursor: pointer; margin-left: auto; margin-right: 8px; color: #1a73e8; opacity: 0.7; transition: color 0.2s, opacity 0.2s; user-select: none; display: flex; align-items: center; }
.histogram-emoji-btn:hover { color: #1558b0; opacity: 1; }
.histogram-wrapper { margin-top: 10px; padding-top: 10px; padding-left: 15px; padding-right: 8px; border-top: 1px dotted #dadce0; }
.histogram { display: flex; align-items: flex-end; height: 60px; gap: 2px; padding-bottom: 5px; border-bottom: 1px solid #dadce0; position: relative; }
.histogram-bar-container { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; position: relative; }
.histogram-bar { display: flex !important; flex-direction: column; background-color: transparent; border-radius: 1px 1px 0 0; min-width: 2px; width: 100%; overflow: hidden; }
.histogram-segment { width: 100%; transition: background-color 0.1s; }
.histogram-segment.normal { background-color: #dadce0; }
.histogram-segment.highlighted { background-color: #bdc1c6; }
.histogram-bar-container:hover .histogram-segment.normal { background-color: #bdc1c6; }
.histogram-bar-container:hover .histogram-segment.highlighted { background-color: #9aa0a6; }
.histogram-bar-container::after { content: attr(data-label); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background-color: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.1s ease; z-index: 10; margin-bottom: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.histogram-bar-container:hover::after { opacity: 1; }
.histogram-axis { position: relative; height: 14px; width: 100%; margin-top: 4px; }
.histogram-axis span { position: absolute; transform: translateX(-50%); font-size: 10px; color: #5f6368; white-space: nowrap; }
.histogram-y-axis-line { position: absolute; left: 0; right: 0; border-top: 1px dashed #dadce0; pointer-events: none; z-index: 1; }
.histogram-y-axis-label { position: absolute; left: -12px; transform: translateY(50%); font-size: 10px; color: #5f6368; line-height: 1; }
.histogram-year-label { position: absolute; left: 50%; transform: translateX(-50%); font-size: 10px; color: #5f6368; white-space: nowrap; }
`);
/**
* ---------------------------------------------------------------------------
* Storage Helper
*
* The original extension uses chrome.storage.sync to persist settings. In
* Tampermonkey we use GM_getValue/GM_setValue instead. These functions are
* synchronous so we wrap them in the same asynchronous signature that
* chrome.storage exposed (callback with (result, error)). Keys may be
* provided as a single string or an array of strings.
*/
window.storageHelper = (function () {
function get(keys, callback) {
try {
const result = {};
if (Array.isArray(keys)) {
keys.forEach((k) => {
result[k] = GM_getValue(k);
});
} else if (typeof keys === 'object' && keys !== null) {
// Tampermonkey does not support default objects the way chrome.storage does
Object.keys(keys).forEach((k) => {
result[k] = GM_getValue(k, keys[k]);
});
} else {
result[keys] = GM_getValue(keys);
}
if (callback) callback(result, null);
} catch (e) {
console.warn('storageHelper.get error:', e);
if (callback) callback(null, e);
}
}
function set(items, callback) {
try {
Object.keys(items).forEach((k) => {
GM_setValue(k, items[k]);
});
if (callback) callback(null);
} catch (e) {
console.warn('storageHelper.set error:', e);
if (callback) callback(e);
}
}
return { get, set };
})();
/**
* ---------------------------------------------------------------------------
* Author Matcher
*
* Utilities to normalise names, compute Levenshtein distance and find the
* best matching author position within a paper’s author list. Copied
* directly from the original extension.
*/
(function () {
function normalizeProfileName(profileName) {
const temp = profileName
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[\-\.,()()]/g, ' ')
.replace(/[\'’]/g, '')
.replace(/[\*†‡§#✉]/g, '');
return temp.replace(/\s+/g, '');
}
function levenshteinDistance(s1, s2) {
if (s1.length < s2.length) return levenshteinDistance(s2, s1);
if (s2.length === 0) return s1.length;
const previousRow = Array.from({ length: s2.length + 1 }, (_, i) => i);
for (let i = 0; i < s1.length; i++) {
const currentRow = [i + 1];
for (let j = 0; j < s2.length; j++) {
const insertions = previousRow[j + 1] + 1;
const deletions = currentRow[j] + 1;
const substitutions = previousRow[j] + (s1[i] !== s2[j] ? 1 : 0);
currentRow.push(Math.min(insertions, deletions, substitutions));
}
previousRow.splice(0, previousRow.length, ...currentRow);
}
return previousRow[s2.length];
}
function getAuthorSymbol(author) {
const symbols = '*†‡§#✉';
for (const symbol of symbols) {
if (author.includes(symbol)) return symbol;
}
return '';
}
const isCoFirstAuthor = getAuthorSymbol;
function isCharSubset(candidate, profile) {
if (!candidate) return true;
if (!profile && candidate) return false;
if (!profile && !candidate) return true;
const counts = {};
for (const ch of profile) counts[ch] = (counts[ch] || 0) + 1;
for (const ch of candidate) {
if (!counts[ch] || counts[ch] === 0) return false;
counts[ch]--;
}
return true;
}
function extractFamilyNamePart(authorString) {
const cleaned = authorString.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[\-\.,()()]/g, ' ')
.replace(/[\'’]/g, '')
.replace(/[\*†‡§#✉]/g, '')
.trim();
const parts = cleaned.split(/\s+/).filter(p => p.length > 0);
if (parts.length === 0) return '';
return parts[parts.length - 1];
}
function extractProfileNameParts(profileName) {
return profileName
.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[\-\.,()()]/g, ' ')
.replace(/[\'’]/g, '')
.replace(/[\*†‡§#✉]/g, '')
.split(/\s+/)
.filter(p => p.length > 0);
}
function getMatchScore(authorStringFromList, localFullyCleanedProfileName, profileName) {
const tempAuthor = authorStringFromList.toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[\-\.,()()]/g, ' ')
.replace(/[\'’]/g, '')
.replace(/[\*†‡§#✉]/g, '');
const candidate = tempAuthor.replace(/\s+/g, '');
if (candidate === '') return Infinity;
if (!isCharSubset(candidate, localFullyCleanedProfileName)) return Infinity;
const familyNamePart = extractFamilyNamePart(authorStringFromList);
let hasContiguousFamilyName = false;
if (familyNamePart.length > 1) {
if (profileName) {
const profileParts = extractProfileNameParts(profileName);
if (!profileParts.includes(familyNamePart)) return Infinity;
hasContiguousFamilyName = true;
} else {
hasContiguousFamilyName = localFullyCleanedProfileName.includes(familyNamePart);
if (!hasContiguousFamilyName) return Infinity;
}
}
let score = levenshteinDistance(candidate, localFullyCleanedProfileName);
if (candidate.length > 0 && localFullyCleanedProfileName.length > 0) {
if (candidate[0] === localFullyCleanedProfileName[0]) score -= 0.5;
}
if (hasContiguousFamilyName && candidate.length > familyNamePart.length) {
const index = localFullyCleanedProfileName.indexOf(familyNamePart);
const positionAfterFamilyName = index + familyNamePart.length;
const initialsInCandidate = candidate.substring(0, candidate.length - familyNamePart.length);
if (initialsInCandidate.length > 0 && positionAfterFamilyName < localFullyCleanedProfileName.length && initialsInCandidate[0] === localFullyCleanedProfileName[positionAfterFamilyName]) {
score -= 1.0;
}
}
return score;
}
function findBestMatch(authorList, fullyCleanedProfileName, profileName, otherNames = []) {
let bestMatchIndex = -1;
let minScore = Infinity;
let actualMatchedAuthorString = '';
authorList.forEach((currentAuthorName, index) => {
let score = getMatchScore(currentAuthorName, fullyCleanedProfileName, profileName);
if (otherNames && otherNames.length > 0) {
otherNames.forEach((alias) => {
const aliasCleaned = normalizeProfileName(alias);
const aliasScore = getMatchScore(currentAuthorName, aliasCleaned, alias);
if (aliasScore < score) score = aliasScore;
});
}
if (score < minScore) {
minScore = score;
bestMatchIndex = index;
actualMatchedAuthorString = currentAuthorName;
}
});
return {
matchIndex: bestMatchIndex,
matchedAuthor: actualMatchedAuthorString,
score: minScore
};
}
window.authorMatcher = {
normalizeProfileName,
levenshteinDistance,
isCoFirstAuthor,
getAuthorSymbol,
isCharSubset,
getMatchScore,
findBestMatch
};
})();
/**
* ---------------------------------------------------------------------------
* Highlighter Module
*
* This module tags each paper row with data attributes describing whether the
* logged‑in user is first author, second author, co‑first author, corresponding
* author or last author. It then invokes a callback so filters can be
* applied. Taken directly from the original extension.
*/
(function () {
function highlightAuthors(papers, onComplete) {
const fullNameElement = document.querySelector('#gsc_prf_in');
if (!fullNameElement) {
console.error('Google Scholar Author Highlighter: Could not find author name element');
return;
}
const profileNameOriginal = fullNameElement.textContent;
const fullyCleanedProfileName = window.authorMatcher.normalizeProfileName(profileNameOriginal);
// helper to extract alternative names
function extractOtherNames() {
const aliases = [];
const infoRows = document.querySelectorAll('.gsc_prf_il');
infoRows.forEach((row) => {
const text = row.textContent.trim();
const labelMatch = text.match(/^(also published as|also known as|alternative names|别名|曾用名)[:\s]+(.*)/i);
if (labelMatch && labelMatch[2]) {
const names = labelMatch[2].split(',');
names.forEach((name) => {
const cleanName = name.trim();
if (cleanName) aliases.push(cleanName);
});
}
});
const otherNamesElement = document.querySelector('#gs_prf_ion_txt');
if (otherNamesElement) {
const text = otherNamesElement.textContent.trim();
if (text) {
const names = text.split(',');
names.forEach((name) => {
const cleanName = name.trim();
if (cleanName) aliases.push(cleanName);
});
}
}
return aliases;
}
const otherNames = extractOtherNames();
papers.forEach((paper) => {
if (paper.dataset.processed) return;
const authorsTextNode = paper.querySelector('.gs_gray');
const authorsText = authorsTextNode ? authorsTextNode.textContent : '';
const authorList = authorsText.split(',').map((a) => a.trim()).filter((a) => a.length > 0 && a !== '...');
let isFirst = false;
let isSecond = false;
let isCoFirst = false;
let isLast = false;
let isCorresponding = false;
if (authorList.length > 0) {
const matchResult = window.authorMatcher.findBestMatch(authorList, fullyCleanedProfileName, profileNameOriginal, otherNames);
const bestIndex = matchResult.matchIndex;
const actualMatchedAuthorString = matchResult.matchedAuthor;
const minScore = matchResult.score;
const includesEllipsis = authorsText.includes('...');
if (minScore !== Infinity) {
const authorSymbol = window.authorMatcher.getAuthorSymbol(actualMatchedAuthorString);
let qualifiesCoFirst = false;
if (authorSymbol) {
let allPrecedingHaveSameSymbol = true;
if (bestIndex > 0) {
for (let j = 0; j < bestIndex; j++) {
if (!authorList[j].includes(authorSymbol)) {
allPrecedingHaveSameSymbol = false;
break;
}
}
}
if (allPrecedingHaveSameSymbol) {
qualifiesCoFirst = true;
} else {
isCorresponding = true;
}
}
if (qualifiesCoFirst) {
isCoFirst = true;
} else {
if (bestIndex === 0) isFirst = true;
if (bestIndex === 1) isSecond = true;
if (!includesEllipsis && bestIndex === authorList.length - 1) isLast = true;
}
}
}
paper.dataset.isFirstAuthor = isFirst;
paper.dataset.isSecondAuthor = isSecond;
paper.dataset.isCoFirstAuthor = isCoFirst;
paper.dataset.isLastAuthor = isLast;
paper.dataset.isCorrespondingAuthor = isCorresponding;
paper.dataset.processed = 'true';
});
if (onComplete) onComplete();
}
window.highlighterModule = { highlightAuthors };
})();
/**
* ---------------------------------------------------------------------------
* Filter Manager
*
* Applies filters to the paper list based on which author positions should be
* highlighted and whether to show only highlighted papers. It also handles
* year filtering. Adapted from the original extension.
*/
(function () {
function getYearFromPaper(paper) {
const yearEl = paper.querySelector('.gsc_a_y');
if (!yearEl) return null;
const text = yearEl.textContent.trim();
if (!text) return null;
const year = parseInt(text, 10);
return isNaN(year) ? null : year;
}
function applyFilters(filters, yearFilter) {
if (!yearFilter) {
const startEl = document.getElementById('start-year-input');
const endEl = document.getElementById('end-year-input');
yearFilter = {
start: startEl ? startEl.value : '',
end: endEl ? endEl.value : ''
};
}
const papers = document.querySelectorAll('.gsc_a_tr');
papers.forEach((paper) => {
paper.classList.remove('highlighted-author', 'first-author', 'second-author', 'co-first-author', 'corresponding-author', 'last-author');
if (filters.first && paper.dataset.isFirstAuthor === 'true') {
paper.classList.add('first-author', 'highlighted-author');
}
if (filters.second && paper.dataset.isSecondAuthor === 'true') {
paper.classList.add('second-author', 'highlighted-author');
}
if (filters.coFirst && paper.dataset.isCoFirstAuthor === 'true') {
paper.classList.add('co-first-author', 'highlighted-author');
}
if (filters.corresponding && paper.dataset.isCorrespondingAuthor === 'true') {
paper.classList.add('corresponding-author', 'highlighted-author');
}
if (filters.last && paper.dataset.isLastAuthor === 'true') {
paper.classList.add('last-author', 'highlighted-author');
}
});
const visibilityToggle = document.getElementById('visibility-toggle');
if (visibilityToggle) {
togglePaperVisibility(visibilityToggle.checked, yearFilter);
} else {
window.storageHelper.get(['showOnlyHighlighted'], function (result, error) {
const showOnlyHighlighted = (error || !result.showOnlyHighlighted) ? false : result.showOnlyHighlighted;
togglePaperVisibility(showOnlyHighlighted, yearFilter);
});
}
window.metricsCalculator.displayHighlightedMetrics();
}
function togglePaperVisibility(showOnlyHighlighted, yearFilter) {
if (!yearFilter) {
const startEl = document.getElementById('start-year-input');
const endEl = document.getElementById('end-year-input');
yearFilter = {
start: startEl ? startEl.value : '',
end: endEl ? endEl.value : ''
};
}
const startYear = yearFilter.start ? parseInt(yearFilter.start, 10) : null;
const endYear = yearFilter.end ? parseInt(yearFilter.end, 10) : null;
const papers = document.querySelectorAll('.gsc_a_tr');
papers.forEach((paper) => {
let visible = true;
if (showOnlyHighlighted && !paper.classList.contains('highlighted-author')) visible = false;
if (visible) {
const paperYear = getYearFromPaper(paper);
if (startYear !== null || endYear !== null) {
if (paperYear === null) {
visible = false;
} else {
if (startYear !== null && paperYear < startYear) visible = false;
if (endYear !== null && paperYear > endYear) visible = false;
}
}
}
paper.style.display = visible ? '' : 'none';
});
window.metricsCalculator.displayHighlightedMetrics();
}
function updateFilters() {
const filters = {
first: document.getElementById('first-author-filter').checked,
second: document.getElementById('second-author-filter').checked,
coFirst: document.getElementById('co-first-author-filter').checked,
corresponding: document.getElementById('corresponding-author-filter').checked,
last: document.getElementById('last-author-filter').checked
};
const yearFilter = {
start: document.getElementById('start-year-input').value,
end: document.getElementById('end-year-input').value
};
try {
sessionStorage.setItem('authorHighlighterYearFilter', JSON.stringify(yearFilter));
} catch (e) {
console.warn('Error saving year filter to sessionStorage:', e);
}
window.storageHelper.set({ filters: filters }, function (error) {
applyFilters(filters, yearFilter);
});
}
window.filterManager = { applyFilters, togglePaperVisibility, updateFilters };
})();
/**
* ---------------------------------------------------------------------------
* Name Highlighter
*
* Adds an additional highlight to the user’s name within the author list of
* each paper. The state of this toggle is stored via storageHelper.
*/
(function () {
function removeHighlighting(authorsTextNode) {
const existingHighlight = authorsTextNode.querySelector('.highlighted-author-name');
if (existingHighlight) authorsTextNode.innerHTML = authorsTextNode.textContent;
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function applyHighlighting(authorsTextNode, fullyCleanedProfileName, profileNameOriginal, otherNames) {
const authorsText = authorsTextNode.textContent;
const authorList = authorsText.split(',').map((a) => a.trim()).filter((a) => a.length > 0 && a !== '...');
const matchResult = window.authorMatcher.findBestMatch(authorList, fullyCleanedProfileName, profileNameOriginal, otherNames);
if (matchResult.score !== Infinity && matchResult.matchedAuthor) {
const matchedAuthor = matchResult.matchedAuthor;
const escapedAuthor = escapeRegex(matchedAuthor);
const regex = new RegExp(`(${escapedAuthor})`, 'gi');
const highlightedHTML = authorsText.replace(regex, '<span class="highlighted-author-name">$1</span>');
authorsTextNode.innerHTML = highlightedHTML;
}
}
function highlightAuthorNames(enabled) {
const fullNameElement = document.querySelector('#gsc_prf_in');
if (!fullNameElement) {
console.error('Google Scholar Author Highlighter: Could not find author name element');
return;
}
const profileNameOriginal = fullNameElement.textContent;
const fullyCleanedProfileName = window.authorMatcher.normalizeProfileName(profileNameOriginal);
function extractOtherNames() {
const aliases = [];
const infoRows = document.querySelectorAll('.gsc_prf_il');
infoRows.forEach((row) => {
const text = row.textContent.trim();
const labelMatch = text.match(/^(also published as|also known as|alternative names|别名|曾用名)[:\s]+(.*)/i);
if (labelMatch && labelMatch[2]) {
const names = labelMatch[2].split(',');
names.forEach((name) => {
const cleanName = name.trim();
if (cleanName) aliases.push(cleanName);
});
}
});
const otherNamesElement = document.querySelector('#gs_prf_ion_txt');
if (otherNamesElement) {
const text = otherNamesElement.textContent.trim();
if (text) {
const names = text.split(',');
names.forEach((name) => {
const cleanName = name.trim();
if (cleanName) aliases.push(cleanName);
});
}
}
return aliases;
}
const otherNames = extractOtherNames();
const papers = document.querySelectorAll('.gsc_a_tr');
papers.forEach((paper) => {
const authorsTextNode = paper.querySelector('.gs_gray');
if (!authorsTextNode) return;
removeHighlighting(authorsTextNode);
if (!enabled) return;
applyHighlighting(authorsTextNode, fullyCleanedProfileName, profileNameOriginal, otherNames);
});
}
window.nameHighlighter = { highlightAuthorNames };
})();
/**
* ---------------------------------------------------------------------------
* DOM Observer
*
* Watches the page for the profile container and newly added papers. When
* additional papers load (for example when clicking “Show more”), it invokes
* the highlighter again. Derived from the original extension.
*/
(function () {
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function observeInitialLoad(onProfileFound) {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
const profileContainer = document.querySelector('#gsc_prf_w');
if (profileContainer && !document.getElementById('author-highlighter-controls')) {
onProfileFound();
observer.disconnect();
break;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
}
function observePaperChanges(onPapersChanged) {
const paperContainer = document.querySelector('#gsc_a_b');
if (paperContainer) {
const debouncedHandler = debounce(onPapersChanged, 150);
const paperObserver = new MutationObserver((mutations) => {
const addedNodes = [];
for (const mutation of mutations) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE && node.matches('.gsc_a_tr')) {
addedNodes.push(node);
}
});
}
}
if (addedNodes.length > 0) debouncedHandler(addedNodes);
});
paperObserver.observe(paperContainer, { childList: true });
return paperObserver;
}
return null;
}
window.domObserver = { observeInitialLoad, observePaperChanges };
})();
/**
* ---------------------------------------------------------------------------
* Metrics Calculator
*
* Computes summary metrics (count, citations, h‑index and i10‑index) for
* highlighted papers versus the total set and optionally draws a histogram
* of highlighted vs non‑highlighted papers per year. Adapted from the
* original extension.
*/
(function () {
let isHistogramVisible = false;
function calculateMetrics() {
const papers = document.querySelectorAll('.gsc_a_tr');
const startEl = document.getElementById('start-year-input');
const endEl = document.getElementById('end-year-input');
const startYear = startEl && startEl.value ? parseInt(startEl.value, 10) : null;
const endYear = endEl && endEl.value ? parseInt(endEl.value, 10) : null;
const stats = {
total: { count: 0, citations: 0, citationCounts: [], years: {} },
highlighted: { count: 0, citations: 0, citationCounts: [], years: {} }
};
papers.forEach((paper) => {
let paperYear = null;
const yearEl = paper.querySelector('.gsc_a_y');
if (yearEl) {
const yearText = yearEl.textContent.trim();
if (yearText) {
const parsed = parseInt(yearText, 10);
if (!isNaN(parsed)) paperYear = parsed;
}
}
let inYearRange = true;
if (startYear !== null || endYear !== null) {
if (paperYear === null) {
inYearRange = false;
} else {
if (startYear !== null && paperYear < startYear) inYearRange = false;
if (endYear !== null && paperYear > endYear) inYearRange = false;
}
}
if (!inYearRange) return;
let citationCount = 0;
const citationElement = paper.querySelector('.gsc_a_ac');
if (citationElement) {
const text = citationElement.textContent.trim();
const parsed = parseInt(text, 10);
if (!isNaN(parsed)) citationCount = parsed;
}
stats.total.count++;
stats.total.citations += citationCount;
stats.total.citationCounts.push(citationCount);
if (paperYear !== null) {
stats.total.years[paperYear] = (stats.total.years[paperYear] || 0) + 1;
}
if (paper.classList.contains('highlighted-author')) {
stats.highlighted.count++;
stats.highlighted.citations += citationCount;
stats.highlighted.citationCounts.push(citationCount);
if (paperYear !== null) {
stats.highlighted.years[paperYear] = (stats.highlighted.years[paperYear] || 0) + 1;
}
}
});
const calcIndices = (citations) => {
citations.sort((a, b) => b - a);
const hIndex = citations.filter((c, i) => c > i).length;
const i10index = citations.filter((c) => c >= 10).length;
return { hIndex, i10index };
};
const totalIndices = calcIndices(stats.total.citationCounts);
const highlightedIndices = calcIndices(stats.highlighted.citationCounts);
return {
total: {
count: stats.total.count,
citations: stats.total.citations,
hIndex: totalIndices.hIndex,
i10index: totalIndices.i10index,
years: stats.total.years
},
highlighted: {
count: stats.highlighted.count,
citations: stats.highlighted.citations,
hIndex: highlightedIndices.hIndex,
i10index: highlightedIndices.i10index,
years: stats.highlighted.years
}
};
}
function displayHighlightedMetrics() {
const metrics = calculateMetrics();
let metricsComponent = document.getElementById('highlighted-metrics');
if (!metricsComponent) {
metricsComponent = document.createElement('div');
metricsComponent.id = 'highlighted-metrics';
metricsComponent.className = 'highlighted-metrics';
const controlsContainer = document.getElementById('author-highlighter-controls');
if (controlsContainer) controlsContainer.appendChild(metricsComponent);
}
// prepare histogram
const totalYearCounts = metrics.total.years;
const highlightedYearCounts = metrics.highlighted.years;
const years = Object.keys(totalYearCounts).map((y) => parseInt(y, 10)).filter((y) => y >= 1980).sort((a, b) => a - b);
let histogramHTML = '';
if (years.length > 0) {
const minYear = years[0];
const maxYear = years[years.length - 1];
const maxCount = Math.max(...years.map((y) => totalYearCounts[y] || 0));
const safeMaxCount = maxCount > 0 ? maxCount : 1;
let barsHTML = '';
for (let y = minYear; y <= maxYear; y++) {
const totalCount = totalYearCounts[y] || 0;
const highlightedCount = highlightedYearCounts[y] || 0;
const normalCount = totalCount - highlightedCount;
const totalHeightPercent = (totalCount / safeMaxCount) * 100;
const highlightedHeightPercent = totalCount > 0 ? (highlightedCount / totalCount) * 100 : 0;
const normalHeightPercent = totalCount > 0 ? (normalCount / totalCount) * 100 : 0;
barsHTML += `
<div class="histogram-bar-container" data-label="${highlightedCount} of ${totalCount} (${y})">
<div class="histogram-bar" style="height: ${totalHeightPercent}%">
<div class="histogram-segment normal" style="height: ${normalHeightPercent}%"></div>
<div class="histogram-segment highlighted" style="height: ${highlightedHeightPercent}%"></div>
</div>
</div>`;
}
const displayStyle = isHistogramVisible ? 'block' : 'none';
const totalYears = maxYear - minYear + 1;
const getPosPercent = (year) => {
const index = year - minYear;
return (((index + 0.5) / totalYears) * 100).toFixed(2);
};
let axisHTML = '';
const range = maxYear - minYear;
let numLabels = 5;
if (range <= 8) numLabels = 3; else if (range <= 16) numLabels = 4;
const labelYears = new Set();
labelYears.add(minYear);
labelYears.add(maxYear);
if (maxYear - minYear > 1) {
const step = (maxYear - minYear) / (numLabels - 1);
for (let i = 1; i < numLabels - 1; i++) {
labelYears.add(Math.round(minYear + step * i));
}
}
const sortedLabelYears = Array.from(labelYears).sort((a, b) => a - b);
sortedLabelYears.forEach((year) => {
axisHTML += `<span style="left: ${getPosPercent(year)}%">${year}</span>`;
});
histogramHTML = `
<div class="histogram-wrapper" id="histogram-wrapper" style="display: ${displayStyle}">
<div class="histogram">
${barsHTML}
${maxCount >= 1 ? (() => { const midValue = Math.max(1, Math.round(maxCount / 2)); return `
<div class="histogram-y-axis-line" style="bottom: ${(midValue / safeMaxCount) * 100}%"></div>
<div class="histogram-y-axis-label" style="bottom: ${(midValue / safeMaxCount) * 100}%">${midValue}</div>
`; })() : ''}
</div>
<div class="histogram-axis">
${axisHTML}
</div>
</div>`;
}
metricsComponent.innerHTML = `
<hr class="metrics-divider">
<div class="metrics-header">
<h4>Highlighted Papers Metrics <span id="histogram-toggle-icon" class="histogram-emoji-btn" title="Show papers by year"><svg xmlns="http://www.w3.org/2000/svg" height="20" viewBox="0 0 24 24" width="20" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM7 10h2v7H7zm4-3h2v10h-2zm4 6h2v4h-2z"/></svg></span></h4>
</div>
<div class="metrics-grid">
<div class="metric-item">
<span class="metric-label">Papers:</span>
<span class="metric-value">${metrics.highlighted.count} <span class="metric-total">of ${metrics.total.count}</span></span>
</div>
<div class="metric-item">
<span class="metric-label">Citations:</span>
<span class="metric-value">${metrics.highlighted.citations} <span class="metric-total">of ${metrics.total.citations}</span></span>
</div>
<div class="metric-item">
<span class="metric-label">h-index:</span>
<span class="metric-value">${metrics.highlighted.hIndex} <span class="metric-total">of ${metrics.total.hIndex}</span></span>
</div>
<div class="metric-item">
<span class="metric-label">i10-index:</span>
<span class="metric-value">${metrics.highlighted.i10index} <span class="metric-total">of ${metrics.total.i10index}</span></span>
</div>
</div>
${histogramHTML}
`;
const toggleBtn = document.getElementById('histogram-toggle-icon');
if (toggleBtn) {
toggleBtn.addEventListener('click', function () {
const wrapper = document.getElementById('histogram-wrapper');
isHistogramVisible = !isHistogramVisible;
if (wrapper) wrapper.style.display = isHistogramVisible ? 'block' : 'none';
});
}
}
window.metricsCalculator = { displayHighlightedMetrics };
})();
/**
* ---------------------------------------------------------------------------
* UI Manager
*
* Injects the control panel into the profile page and wires up event
* listeners for all toggles and filters. It also populates the year
* dropdowns based on the available papers. Adapted from the original
* extension with minor adjustments for Tampermonkey storage.
*/
(function () {
function populateYearDropdowns() {
const startSelect = document.getElementById('start-year-input');
const endSelect = document.getElementById('end-year-input');
if (!startSelect || !endSelect) return;
const currentStart = startSelect.value;
const currentEnd = endSelect.value;
while (startSelect.options.length > 1) startSelect.remove(1);
while (endSelect.options.length > 1) endSelect.remove(1);
const papers = document.querySelectorAll('.gsc_a_tr');
const years = new Set();
if (currentStart) years.add(parseInt(currentStart, 10));
if (currentEnd) years.add(parseInt(currentEnd, 10));
papers.forEach((paper) => {
const yearEl = paper.querySelector('.gsc_a_y');
if (yearEl) {
const yearText = yearEl.textContent.trim();
const year = parseInt(yearText, 10);
if (!isNaN(year)) years.add(year);
}
});
const sortedYears = Array.from(years).sort((a, b) => b - a);
const createOption = (val) => {
const opt = document.createElement('option');
opt.value = val;
opt.textContent = val;
return opt;
};
sortedYears.forEach((year) => {
startSelect.appendChild(createOption(year));
endSelect.appendChild(createOption(year));
});
if (currentStart) startSelect.value = currentStart;
if (currentEnd) endSelect.value = currentEnd;
}
function ensureOption(selectElement, value) {
if (!value) return;
let exists = false;
for (let i = 0; i < selectElement.options.length; i++) {
if (selectElement.options[i].value === value.toString()) {
exists = true;
break;
}
}
if (!exists) {
const opt = document.createElement('option');
opt.value = value;
opt.textContent = value;
selectElement.appendChild(opt);
}
}
function setupEventListeners(initialHighlightNameState, highlightNameCallback) {
const visibilityToggle = document.getElementById('visibility-toggle');
const highlightNameToggle = document.getElementById('highlight-name-toggle');
const firstAuthorFilter = document.getElementById('first-author-filter');
const secondAuthorFilter = document.getElementById('second-author-filter');
const coFirstAuthorFilter = document.getElementById('co-first-author-filter');
const correspondingAuthorFilter = document.getElementById('corresponding-author-filter');
const lastAuthorFilter = document.getElementById('last-author-filter');
const startYearInput = document.getElementById('start-year-input');
const endYearInput = document.getElementById('end-year-input');
visibilityToggle.addEventListener('change', function () {
const showOnlyHighlighted = this.checked;
window.storageHelper.set({ showOnlyHighlighted: showOnlyHighlighted });
window.filterManager.togglePaperVisibility(showOnlyHighlighted);
});
startYearInput.addEventListener('change', window.filterManager.updateFilters);
endYearInput.addEventListener('change', window.filterManager.updateFilters);
highlightNameToggle.addEventListener('change', function () {
const highlightName = this.checked;
if (highlightNameCallback) highlightNameCallback(highlightName);
});
[firstAuthorFilter, secondAuthorFilter, coFirstAuthorFilter, correspondingAuthorFilter, lastAuthorFilter].forEach((filter) => {
filter.addEventListener('change', window.filterManager.updateFilters);
});
window.storageHelper.get(['showOnlyHighlighted', 'filters'], function (result, error) {
if (error) {
visibilityToggle.checked = false;
const filters = { first: true, second: true, coFirst: true, corresponding: true, last: true };
firstAuthorFilter.checked = filters.first;
secondAuthorFilter.checked = filters.second;
coFirstAuthorFilter.checked = filters.coFirst;
correspondingAuthorFilter.checked = filters.corresponding;
lastAuthorFilter.checked = filters.last;
window.filterManager.applyFilters(filters);
} else {
visibilityToggle.checked = result.showOnlyHighlighted || false;
const filters = result.filters || { first: true, second: true, coFirst: true, corresponding: true, last: true };
firstAuthorFilter.checked = filters.first;
secondAuthorFilter.checked = filters.second;
coFirstAuthorFilter.checked = filters.coFirst;
correspondingAuthorFilter.checked = filters.corresponding !== undefined ? filters.corresponding : true;
lastAuthorFilter.checked = filters.last;
window.filterManager.applyFilters(filters);
try {
const storedYearFilter = sessionStorage.getItem('authorHighlighterYearFilter');
if (storedYearFilter) {
const yearFilter = JSON.parse(storedYearFilter);
if (yearFilter.start) ensureOption(startYearInput, yearFilter.start);
if (yearFilter.end) ensureOption(endYearInput, yearFilter.end);
startYearInput.value = yearFilter.start || '';
endYearInput.value = yearFilter.end || '';
window.filterManager.applyFilters(filters, yearFilter);
}
} catch (e) {
console.warn('Error loading year filter from sessionStorage:', e);
}
}
highlightNameToggle.checked = initialHighlightNameState;
window.nameHighlighter.highlightAuthorNames(initialHighlightNameState);
});
}
function injectFilterUI(initialHighlightNameState, highlightNameCallback) {
if (document.getElementById('author-highlighter-controls')) return;
const filterHTML = `
<div id="author-highlighter-controls" class="author-highlighter-controls">
<h3><span class="title-container">Author Highlighter<span class="info-icon" data-tooltip="Co-first: Beginning authors marked with symbols (* † ‡ § # ✉).
Corresponding: Non-beginning authors marked with symbols (* † ‡ § # ✉)."><svg focusable="false" width="16" height="16" viewBox="0 0 24 24" style="fill: currentColor"><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"></path></svg></span></span></h3>
<div class="filter-container">
<label>Highlight:</label>
<div class="filter-options">
<div>
<input type="checkbox" id="first-author-filter" checked>
<label for="first-author-filter">First</label>
</div>
<div>
<input type="checkbox" id="co-first-author-filter" checked>
<label for="co-first-author-filter">Co-First</label>
</div>
<div>
<input type="checkbox" id="second-author-filter" checked>
<label for="second-author-filter">Second</label>
</div>
<div>
<input type="checkbox" id="corresponding-author-filter" checked>
<label for="corresponding-author-filter">Corr.</label>
</div>
<div>
<input type="checkbox" id="last-author-filter" checked>
<label for="last-author-filter">Last</label>
</div>
</div>
</div>
<div class="toggle-container">
<label for="visibility-toggle">Show only highlighted:</label>
<input type="checkbox" id="visibility-toggle">
</div>
<div class="toggle-container">
<label for="highlight-name-toggle">Highlight author name:</label>
<input type="checkbox" id="highlight-name-toggle">
</div>
<div class="time-filter-container">
<label>Year:</label>
<select id="start-year-input" class="year-input">
<option value="">Start</option>
</select>
<span>-</span>
<select id="end-year-input" class="year-input">
<option value="">End</option>
</select>
</div>
</div>`;
const citedBySection = document.querySelector('#gsc_rsb_cit');
if (citedBySection) {
const controlsContainer = document.createElement('div');
controlsContainer.innerHTML = filterHTML;
citedBySection.parentNode.insertBefore(controlsContainer, citedBySection);
populateYearDropdowns();
setupEventListeners(initialHighlightNameState, highlightNameCallback);
}
}
window.uiManager = { injectFilterUI, populateYearDropdowns };
})();
/**
* ---------------------------------------------------------------------------
* Show More Clicker
*
* Google Scholar paginates long author lists behind a “Show more” button. To
* ensure all papers are processed we simulate clicking this button until
* everything is loaded. Adapted from the original extension but simplified
* slightly. Debug logging has been removed for clarity.
*/
(function () {
let retryCount = 0;
const MAX_RETRIES = 10;
function clickShowMoreUntilAllLoaded() {
const possibleSelectors = [
'#gsc_bpf_more', '.gsc_pgn_pnx', 'button[onclick*="showMore"]', 'button[onclick*="bpf_more"]', '.gs_btnPR[onclick*="showMore"]', '[data-i18n="show_more"]', '.gsc_pgn button', '#gsc_pgn_pnx'
];
let showMoreButton = null;
let foundSelector = null;
for (const selector of possibleSelectors) {
if (selector.includes(':contains(')) {
const text = selector.match(/:contains\("(.+)"\)/)?.[1];
if (text) {
const buttons = document.querySelectorAll('button');
for (const btn of buttons) {
if (btn.textContent.trim().toLowerCase().includes(text.toLowerCase())) {
showMoreButton = btn;
foundSelector = `button containing "${text}"`;
break;
}
}
}
} else {
showMoreButton = document.querySelector(selector);
if (showMoreButton) {
foundSelector = selector;
break;
}
}
}
if (showMoreButton) {
const style = window.getComputedStyle(showMoreButton);
const isClickable = !showMoreButton.disabled && style.display !== 'none' && style.visibility !== 'hidden';
if (isClickable) {
try { showMoreButton.click(); } catch (e) {
try { showMoreButton.dispatchEvent(new MouseEvent('click', { bubbles: true })); } catch (e2) {
if (showMoreButton.onclick) showMoreButton.onclick.call(showMoreButton);
}
}
retryCount++;
setTimeout(() => { clickShowMoreUntilAllLoaded(); }, 2000);
} else {
finishLoading();
}
} else {
const paperCount = document.querySelectorAll('.gsc_a_tr').length;
if (retryCount < MAX_RETRIES && paperCount < 20) {
retryCount++;
setTimeout(() => { clickShowMoreUntilAllLoaded(); }, 1000);
} else {
finishLoading();
}
}
}
function finishLoading() {
retryCount = 0;
if (typeof window.highlightAuthors === 'function') {
window.highlightAuthors();
}
}
function waitForPageReady() {
const paperContainer = document.querySelector('#gsc_a_b, .gsc_a_tr');
if (paperContainer) {
setTimeout(() => { clickShowMoreUntilAllLoaded(); }, 1000);
} else {
setTimeout(waitForPageReady, 500);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForPageReady);
} else {
waitForPageReady();
}
window.addEventListener('load', () => {
setTimeout(waitForPageReady, 1000);
});
})();
/**
* ---------------------------------------------------------------------------
* Main Orchestrator
*
* Coordinates all modules: injects the UI, applies highlights and filters
* when the page is ready, observes newly loaded papers and keeps state in
* sync with storage. Derived from the original extension.
*/
(function () {
let highlightNameStateForThisTab = false;
function setHighlightNameState(isEnabled) {
highlightNameStateForThisTab = isEnabled;
window.storageHelper.set({ highlightName: isEnabled });
window.nameHighlighter.highlightAuthorNames(isEnabled);
}
function highlightAuthors(papers) {
const papersToProcess = papers || document.querySelectorAll('.gsc_a_tr');
window.highlighterModule.highlightAuthors(papersToProcess, function () {
window.storageHelper.get(['filters'], function (result, error) {
const filters = (error || !result.filters) ? { first: true, second: true, coFirst: true, corresponding: true, last: true } : result.filters;
window.uiManager.populateYearDropdowns();
window.filterManager.applyFilters(filters);
window.nameHighlighter.highlightAuthorNames(highlightNameStateForThisTab);
});
});
}
window.highlightAuthors = highlightAuthors; // expose for showMoreClicker
function initialize() {
const start = () => {
window.uiManager.injectFilterUI(highlightNameStateForThisTab, setHighlightNameState);
highlightAuthors();
window.domObserver.observePaperChanges(highlightAuthors);
};
const profileContainer = document.querySelector('#gsc_prf_w');
window.storageHelper.get('highlightName', function (result, error) {
highlightNameStateForThisTab = (error || typeof result.highlightName === 'undefined') ? false : result.highlightName;
if (profileContainer) {
start();
} else {
window.domObserver.observeInitialLoad(start);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
})();