Google Scholar Author Highlighter Script

Highlights first-author, second-author, co-first, corresponding and last-author papers on Google Scholar profile pages and provides simple filters and metrics.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
    }
  })();
})();