// ==UserScript== // @name GitHub Collapse Markdown // @version 3.2.3 // @description 🚀 简洁高效的GitHub Markdown标题折叠脚本:智能嵌套🧠+快捷键⌨️+目录📑+搜索🔍+状态记忆💾+简约GUI🔘 // @license MIT // @author Xyea // @namespace https://github.com/Mottie // @match https://github.com/* // @match https://gist.github.com/* // @match https://help.github.com/* // @run-at document-idle // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @icon https://github.githubassets.com/pinned-octocat.svg // @downloadURL https://update.greasyfork.icu/scripts/541407/GitHub%20Collapse%20Markdown.user.js // @updateURL https://update.greasyfork.icu/scripts/541407/GitHub%20Collapse%20Markdown.meta.js // ==/UserScript== (() => { "use strict"; // 配置常量 const CONFIG = { debug: GM_getValue("ghcm-debug-mode", false), // 调试模式开关 colors: GM_getValue("ghcm-colors", [ "#6778d0", "#ac9c3d", "#b94a73", "#56ae6c", "#9750a1", "#ba543d" ]), animation: { duration: 200, easing: "cubic-bezier(0.4, 0, 0.2, 1)", maxAnimatedElements: GM_getValue("ghcm-performance-mode", false) ? 0 : 20, // 根据用户设置 batchSize: 10 // 批量处理大小 }, selectors: { markdownContainers: [ ".markdown-body", ".markdown-format", ".comment-body" ], headers: ["H1", "H2", "H3", "H4", "H5", "H6"], excludeClicks: [".anchor", ".octicon-link", "a", "img"] }, classes: { collapsed: "ghcm-collapsed", hidden: "ghcm-hidden", hiddenByParent: "ghcm-hidden-by-parent", noContent: "ghcm-no-content", tocContainer: "ghcm-toc-container", searchContainer: "ghcm-search-container", menuContainer: "ghcm-menu-container", menuButton: "ghcm-menu-button", bookmarked: "ghcm-bookmarked" }, hotkeys: { enabled: GM_getValue("ghcm-hotkeys-enabled", true), toggleAll: "ctrl+shift+a", // 切换所有折叠 collapseAll: "ctrl+shift+c", // 折叠所有 expandAll: "ctrl+shift+e", // 展开所有 showToc: "ctrl+shift+l", // 显示目录 search: "ctrl+shift+f", // 搜索 menu: "ctrl+shift+m" // 显示菜单 }, memory: { enabled: GM_getValue("ghcm-memory-enabled", true), key: "ghcm-page-states" } }; // 日志控制函数 const Logger = { log: (...args) => { if (CONFIG.debug) { console.log(...args); } }, warn: (...args) => { console.warn(...args); }, error: (...args) => { console.error(...args); } }; // GUI菜单管理器 class MenuManager { constructor(app) { this.app = app; this.isVisible = false; this.menuContainer = null; this.menuButton = null; this.init(); } init() { this.createMenuButton(); this.addMenuStyles(); } addMenuStyles() { GM_addStyle(` /* 菜单按钮 */ .${CONFIG.classes.menuButton} { position: fixed; bottom: 20px; right: 20px; width: 50px; height: 50px; background: #6b7280; border: none; border-radius: 50%; cursor: pointer; z-index: 9999; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; font-size: 18px; color: white; user-select: none; } .${CONFIG.classes.menuButton}:hover { background: #4b5563; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .${CONFIG.classes.menuButton}:active { transform: translateY(0) scale(0.95); } .${CONFIG.classes.menuButton}.menu-open { background: #374151; transform: rotate(45deg); } /* 菜单容器 */ .${CONFIG.classes.menuContainer} { position: fixed; bottom: 80px; right: 20px; width: 300px; background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(10px); border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); z-index: 9998; opacity: 0; transform: translateY(10px) scale(0.95); transition: all 0.25s ease; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .${CONFIG.classes.menuContainer}.show { opacity: 1; transform: translateY(0) scale(1); } /* 菜单头部 */ .ghcm-menu-header { padding: 16px 20px 12px; background: #f9fafb; color: #374151; text-align: center; border-bottom: 1px solid #e5e7eb; } .ghcm-menu-title { font-size: 16px; font-weight: 600; margin: 0 0 4px; } .ghcm-menu-subtitle { font-size: 11px; opacity: 0.7; margin: 0; } /* 菜单内容 */ .ghcm-menu-content { padding: 0; max-height: 400px; overflow-y: auto; } /* 菜单分组 */ .ghcm-menu-group { padding: 12px 0; border-bottom: 1px solid #f3f4f6; } .ghcm-menu-group:last-child { border-bottom: none; } .ghcm-menu-group-title { font-size: 10px; font-weight: 600; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; margin: 0 20px 8px; } /* 菜单项 */ .ghcm-menu-item { display: flex; align-items: center; padding: 10px 20px; cursor: pointer; transition: background-color 0.15s ease; color: #374151; text-decoration: none; font-size: 13px; line-height: 1.4; } .ghcm-menu-item:hover { background: #f3f4f6; color: #1f2937; } .ghcm-menu-item:active { background: #e5e7eb; } .ghcm-menu-item-icon { width: 20px; height: 20px; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; } .ghcm-menu-item-text { flex: 1; font-weight: 500; } .ghcm-menu-item-shortcut { font-size: 10px; color: #9ca3af; background: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-family: Monaco, 'Courier New', monospace; } .ghcm-menu-item-badge { background: #6b7280; color: white; font-size: 10px; padding: 2px 6px; border-radius: 6px; font-weight: 500; } /* 切换开关 */ .ghcm-menu-toggle { position: relative; width: 36px; height: 18px; background: #d1d5db; border-radius: 9px; transition: background 0.2s ease; cursor: pointer; } .ghcm-menu-toggle.active { background: #6b7280; } .ghcm-menu-toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; background: white; border-radius: 50%; transition: transform 0.2s ease; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .ghcm-menu-toggle.active::after { transform: translateX(18px); } /* 统计信息 */ .ghcm-menu-stats { padding: 12px 20px; background: #f9fafb; font-size: 11px; color: #6b7280; line-height: 1.5; } .ghcm-menu-stats-item { display: flex; justify-content: space-between; margin-bottom: 3px; } .ghcm-menu-stats-item:last-child { margin-bottom: 0; } .ghcm-menu-stats-value { font-weight: 600; color: #374151; } /* 深色主题适配 */ @media (prefers-color-scheme: dark) { .${CONFIG.classes.menuContainer} { background: rgba(31, 41, 55, 0.98); border-color: #374151; } .ghcm-menu-header { background: #1f2937; color: #f9fafb; border-bottom-color: #374151; } .ghcm-menu-item { color: #e5e7eb; } .ghcm-menu-item:hover { background: #374151; color: #f9fafb; } .ghcm-menu-group { border-bottom-color: #374151; } .ghcm-menu-group-title { color: #9ca3af; } .ghcm-menu-item-shortcut { background: #374151; color: #9ca3af; } .ghcm-menu-stats { background: #1f2937; color: #9ca3af; } .ghcm-menu-stats-value { color: #e5e7eb; } } /* 响应式设计 */ @media (max-width: 480px) { .${CONFIG.classes.menuContainer} { right: 15px; width: calc(100vw - 30px); max-width: 320px; } .${CONFIG.classes.menuButton} { right: 15px; bottom: 15px; } } `); } createMenuButton() { this.menuButton = document.createElement('button'); this.menuButton.className = CONFIG.classes.menuButton; this.menuButton.innerHTML = '⚙️'; this.menuButton.title = 'GitHub Collapse Markdown 设置'; this.menuButton.addEventListener('click', (e) => { e.stopPropagation(); this.toggle(); }); document.body.appendChild(this.menuButton); } createMenuContainer() { const container = document.createElement('div'); container.className = CONFIG.classes.menuContainer; container.innerHTML = `

📝 Collapse Markdown

智能标题折叠工具

${this.generateMenuContent()}
`; this.setupMenuEvents(container); return container; } generateMenuContent() { const stats = this.getStatistics(); return `
总标题数 ${stats.total}
已折叠 ${stats.collapsed}
可见 ${stats.visible}
基础操作
📁
折叠所有
${CONFIG.hotkeys.collapseAll}
📂
展开所有
${CONFIG.hotkeys.expandAll}
🔄
智能切换
${CONFIG.hotkeys.toggleAll}
工具功能
📑
目录导航
${CONFIG.hotkeys.showToc}
🔍
搜索标题
${CONFIG.hotkeys.search}
设置选项
性能模式
💾
状态记忆
⌨️
快捷键
🐛
调试模式
重置功能
🔄
重置状态
🗑️
清除记忆
帮助信息
使用说明
`; } setupMenuEvents(container) { // 点击菜单项事件 container.addEventListener('click', (e) => { const item = e.target.closest('.ghcm-menu-item'); if (!item) return; const action = item.getAttribute('data-action'); const toggle = e.target.closest('.ghcm-menu-toggle'); if (toggle) { this.handleToggle(toggle); return; } if (action) { this.handleAction(action); this.hide(); } }); // 阻止菜单容器内的点击事件冒泡 container.addEventListener('click', (e) => { e.stopPropagation(); }); } handleAction(action) { switch (action) { case 'collapseAll': this.app.collapseManager.collapseAll(); break; case 'expandAll': this.app.collapseManager.expandAll(); break; case 'toggleAll': this.app.collapseManager.toggleAll(); break; case 'showToc': this.app.tocGenerator.toggle(); break; case 'showSearch': this.app.searchManager.toggle(); break; case 'togglePerformance': this.app.togglePerformanceMode(); this.refreshMenu(); break; case 'toggleMemory': this.app.toggleMemory(); this.refreshMenu(); break; case 'toggleHotkeys': this.app.toggleHotkeys(); this.refreshMenu(); break; case 'toggleDebug': this.app.toggleDebug(); this.refreshMenu(); break; case 'resetStates': if (confirm('确定要重置当前页面的所有折叠状态吗?')) { this.app.resetAllStates(); this.refreshMenu(); } break; case 'clearMemory': if (confirm('确定要清除所有页面的记忆数据吗?')) { this.app.clearAllMemory(); this.refreshMenu(); } break; case 'showHelp': this.app.showHotkeyHelp(); break; } } handleToggle(toggle) { const toggleType = toggle.getAttribute('data-toggle'); const isActive = toggle.classList.contains('active'); toggle.classList.toggle('active', !isActive); switch (toggleType) { case 'performance': this.app.togglePerformanceMode(); break; case 'memory': this.app.toggleMemory(); break; case 'hotkeys': this.app.toggleHotkeys(); break; case 'debug': this.app.toggleDebug(); break; } } getStatistics() { const headers = this.app.collapseManager.getAllHeaders(); const collapsed = headers.filter(h => h.classList.contains(CONFIG.classes.collapsed)); const visible = headers.filter(h => !h.classList.contains(CONFIG.classes.collapsed) && !h.classList.contains(CONFIG.classes.noContent) ); return { total: headers.length, collapsed: collapsed.length, visible: visible.length }; } refreshMenu() { if (this.menuContainer && this.isVisible) { const content = this.menuContainer.querySelector('.ghcm-menu-content'); if (content) { content.innerHTML = this.generateMenuContent(); this.setupMenuEvents(this.menuContainer); } } } show() { if (this.isVisible) return; if (this.menuContainer) { this.menuContainer.remove(); } this.menuContainer = this.createMenuContainer(); document.body.appendChild(this.menuContainer); // 动画显示 requestAnimationFrame(() => { this.menuContainer.classList.add('show'); }); this.menuButton.classList.add('menu-open'); this.isVisible = true; // 点击外部关闭 setTimeout(() => { document.addEventListener('click', this.hideOnClickOutside); }, 100); } hide() { if (!this.isVisible || !this.menuContainer) return; this.menuContainer.classList.remove('show'); this.menuButton.classList.remove('menu-open'); setTimeout(() => { if (this.menuContainer) { this.menuContainer.remove(); this.menuContainer = null; } }, 300); this.isVisible = false; document.removeEventListener('click', this.hideOnClickOutside); } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } hideOnClickOutside = (e) => { if (!this.menuContainer?.contains(e.target) && !this.menuButton?.contains(e.target)) { this.hide(); } } } // 状态管理 class StateManager { constructor() { this.headerStates = new Map(); this.observers = []; this.pageUrl = window.location.href; } setHeaderState(headerKey, state) { this.headerStates.set(headerKey, state); this.saveToMemory(); } getHeaderState(headerKey) { return this.headerStates.get(headerKey); } generateHeaderKey(element) { const level = this.getHeaderLevel(element); const text = element.textContent?.trim() || ""; const position = Array.from(element.parentElement?.children || []).indexOf(element); return `${level}-${text}-${position}`; } getHeaderLevel(element) { return parseInt(element.nodeName.replace(/[^\d]/, ""), 10); } clear() { this.headerStates.clear(); this.saveToMemory(); } // 状态记忆功能 saveToMemory() { if (!CONFIG.memory.enabled) return; try { const pageStates = GM_getValue(CONFIG.memory.key, {}); const currentStates = {}; this.headerStates.forEach((state, key) => { currentStates[key] = state.isCollapsed; }); pageStates[this.pageUrl] = currentStates; GM_setValue(CONFIG.memory.key, pageStates); } catch (e) { Logger.warn("[GHCM] 保存状态失败:", e); } } loadFromMemory() { if (!CONFIG.memory.enabled) return; try { const pageStates = GM_getValue(CONFIG.memory.key, {}); const currentStates = pageStates[this.pageUrl]; if (currentStates) { Object.entries(currentStates).forEach(([key, isCollapsed]) => { this.headerStates.set(key, { isCollapsed }); }); Logger.log(`[GHCM] 已加载 ${Object.keys(currentStates).length} 个已保存的状态`); } } catch (e) { Logger.warn("[GHCM] 加载状态失败:", e); } } clearMemory() { try { const pageStates = GM_getValue(CONFIG.memory.key, {}); delete pageStates[this.pageUrl]; GM_setValue(CONFIG.memory.key, pageStates); Logger.log("[GHCM] 已清除当前页面的记忆状态"); } catch (e) { Logger.warn("[GHCM] 清除状态失败:", e); } } } // 快捷键管理器 class HotkeyManager { constructor(collapseManager) { this.collapseManager = collapseManager; this.setupHotkeys(); } setupHotkeys() { if (!CONFIG.hotkeys.enabled) return; document.addEventListener('keydown', this.handleKeyDown.bind(this)); Logger.log("[GHCM] 快捷键已启用:", Object.entries(CONFIG.hotkeys) .filter(([k, v]) => k !== 'enabled') .map(([k, v]) => `${k}: ${v}`) .join(', ')); } handleKeyDown(event) { if (!CONFIG.hotkeys.enabled) return; const combo = this.getKeyCombo(event); switch (combo) { case CONFIG.hotkeys.collapseAll: event.preventDefault(); this.collapseManager.collapseAll(); break; case CONFIG.hotkeys.expandAll: event.preventDefault(); this.collapseManager.expandAll(); break; case CONFIG.hotkeys.toggleAll: event.preventDefault(); this.collapseManager.toggleAll(); break; case CONFIG.hotkeys.showToc: event.preventDefault(); this.collapseManager.toggleToc(); break; case CONFIG.hotkeys.search: event.preventDefault(); this.collapseManager.toggleSearch(); break; case CONFIG.hotkeys.menu: event.preventDefault(); if (this.collapseManager.menuManager) { this.collapseManager.menuManager.toggle(); } break; } } getKeyCombo(event) { const keys = []; if (event.ctrlKey) keys.push('ctrl'); if (event.shiftKey) keys.push('shift'); if (event.altKey) keys.push('alt'); if (event.metaKey) keys.push('meta'); const key = event.key.toLowerCase(); if (key !== 'control' && key !== 'shift' && key !== 'alt' && key !== 'meta') { keys.push(key); } return keys.join('+'); } } // 目录生成器 class TocGenerator { constructor() { this.tocContainer = null; this.isVisible = false; } generateToc() { const headers = this.getAllHeaders(); if (headers.length === 0) return null; const toc = document.createElement('div'); toc.className = CONFIG.classes.tocContainer; toc.innerHTML = `

📑 目录导航

${this.generateTocItems(headers)}
`; this.setupTocEvents(toc); return toc; } getAllHeaders() { const headers = []; CONFIG.selectors.markdownContainers.forEach(container => { if (container) { CONFIG.selectors.headers.forEach(headerTag => { const elements = DOMUtils.$$(`${container} ${headerTag.toLowerCase()}`); elements.forEach(el => { headers.push({ element: el, level: parseInt(headerTag.replace('H', ''), 10), text: el.textContent.trim(), id: this.getHeaderId(el) }); }); }); } }); return headers.sort((a, b) => this.getElementPosition(a.element) - this.getElementPosition(b.element)); } generateTocItems(headers) { return headers.map(header => { const indent = (header.level - 1) * 20; const isCollapsed = header.element.classList.contains(CONFIG.classes.collapsed); const collapseIcon = isCollapsed ? '▶' : '▼'; return `
${collapseIcon} ${header.text}
`; }).join(''); } getHeaderId(element) { // 尝试获取已有的ID const anchor = element.querySelector('.anchor'); if (anchor) return anchor.getAttribute('href')?.slice(1) || ''; const id = element.id || element.getAttribute('id'); if (id) return id; // 生成ID return 'header-' + element.textContent.trim().toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-'); } getElementPosition(element) { let position = 0; let current = element; while (current && current.parentNode) { const siblings = Array.from(current.parentNode.children); position += siblings.indexOf(current); current = current.parentNode; } return position; } setupTocEvents(toc) { // 关闭按钮 toc.querySelector('.ghcm-toc-close').addEventListener('click', () => { this.hideToc(); }); // 点击目录项 toc.querySelectorAll('.ghcm-toc-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const headerId = link.getAttribute('data-header-id'); this.scrollToHeader(headerId); }); }); } scrollToHeader(headerId) { const element = document.getElementById(headerId) || document.querySelector(`[id="${headerId}"]`) || document.querySelector(`#user-content-${headerId}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); // 如果标题被折叠,自动展开其父级 this.expandParentHeaders(element); // 更新目录显示状态 setTimeout(() => { this.refreshTocStates(); }, 500); } } // 刷新目录中的折叠状态显示 refreshTocStates() { if (!this.tocContainer) return; const tocItems = this.tocContainer.querySelectorAll('.ghcm-toc-item'); tocItems.forEach(item => { const link = item.querySelector('.ghcm-toc-link'); const headerId = link.getAttribute('data-header-id'); const icon = item.querySelector('.ghcm-toc-collapse-icon'); // 查找对应的标题元素 const headerElement = document.getElementById(headerId) || document.querySelector(`[id="${headerId}"]`) || document.querySelector(`#user-content-${headerId}`); if (headerElement && icon) { const isCollapsed = headerElement.classList.contains('ghcm-collapsed'); icon.textContent = isCollapsed ? '▶' : '▼'; } }); } expandParentHeaders(targetElement) { // 找到对应的collapseManager实例并展开到该标题 if (window.ghcmInstance && window.ghcmInstance.collapseManager) { window.ghcmInstance.collapseManager.expandToHeader(targetElement); } } showToc() { if (this.tocContainer) { this.tocContainer.remove(); } this.tocContainer = this.generateToc(); if (this.tocContainer) { document.body.appendChild(this.tocContainer); this.isVisible = true; // 确保状态正确显示 setTimeout(() => { this.refreshTocStates(); }, 100); } } hideToc() { if (this.tocContainer) { this.tocContainer.remove(); this.tocContainer = null; this.isVisible = false; } } toggle() { if (this.isVisible) { this.hideToc(); } else { this.showToc(); } } } // 搜索功能 class SearchManager { constructor(collapseManager) { this.collapseManager = collapseManager; this.searchContainer = null; this.isVisible = false; } createSearchUI() { const container = document.createElement('div'); container.className = CONFIG.classes.searchContainer; container.innerHTML = `

🔍 搜索标题

`; this.setupSearchEvents(container); return container; } setupSearchEvents(container) { const input = container.querySelector('.ghcm-search-input'); const results = container.querySelector('.ghcm-search-results'); const closeBtn = container.querySelector('.ghcm-search-close'); // 实时搜索 let searchTimeout; input.addEventListener('input', () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.performSearch(input.value.trim(), results); }, 300); }); // 关闭搜索 closeBtn.addEventListener('click', () => { this.hideSearch(); }); // 自动聚焦 setTimeout(() => input.focus(), 100); } performSearch(query, resultsContainer) { if (!query) { resultsContainer.innerHTML = '
请输入搜索关键词
'; return; } const headers = this.getAllSearchableHeaders(); const matches = headers.filter(header => header.text.toLowerCase().includes(query.toLowerCase()) ); if (matches.length === 0) { resultsContainer.innerHTML = '
未找到匹配的标题
'; return; } const resultHtml = matches.map(header => `
H${header.level} ${this.highlightMatch(header.text, query)}
`).join(''); resultsContainer.innerHTML = resultHtml; // 添加点击事件 resultsContainer.querySelectorAll('.ghcm-search-result').forEach(result => { result.addEventListener('click', () => { const headerId = result.getAttribute('data-header-element'); this.jumpToHeader(headerId); }); }); } getAllSearchableHeaders() { const headers = []; let index = 0; CONFIG.selectors.markdownContainers.forEach(container => { if (container) { CONFIG.selectors.headers.forEach(headerTag => { const elements = DOMUtils.$$(`${container} ${headerTag.toLowerCase()}`); elements.forEach(el => { headers.push({ element: el, level: parseInt(headerTag.replace('H', ''), 10), text: el.textContent.trim(), id: `search-header-${index++}` }); el.setAttribute('data-search-id', `search-header-${index - 1}`); }); }); } }); return headers; } highlightMatch(text, query) { const regex = new RegExp(`(${query})`, 'gi'); return text.replace(regex, '$1'); } jumpToHeader(headerId) { const element = document.querySelector(`[data-search-id="${headerId}"]`); if (element) { // 展开到该标题 this.collapseManager.expandToHeader(element); // 隐藏搜索界面 this.hideSearch(); } } showSearch() { if (this.searchContainer) { this.searchContainer.remove(); } this.searchContainer = this.createSearchUI(); document.body.appendChild(this.searchContainer); this.isVisible = true; } hideSearch() { if (this.searchContainer) { this.searchContainer.remove(); this.searchContainer = null; this.isVisible = false; } } toggle() { if (this.isVisible) { this.hideSearch(); } else { this.showSearch(); } } } // DOM 工具类 class DOMUtils { static $(selector, parent = document) { return parent.querySelector(selector); } static $$(selector, parent = document) { return Array.from(parent.querySelectorAll(selector)); } static isHeader(element) { return CONFIG.selectors.headers.includes(element.nodeName); } static isInMarkdown(element) { return CONFIG.selectors.markdownContainers.some(selector => element.closest(selector) ); } static getHeaderContainer(header) { return header.closest('.markdown-heading') || header; } static clearSelection() { const selection = window.getSelection?.() || document.selection; if (selection) { if (selection.removeAllRanges) { selection.removeAllRanges(); } else if (selection.empty) { selection.empty(); } } } } // 样式管理器 class StyleManager { constructor() { this.arrowColors = document.createElement("style"); this.init(); } init() { this.addBaseStyles(); this.addColorStyles(); document.head.appendChild(this.arrowColors); } addBaseStyles() { const headerSelectors = this.generateHeaderSelectors(); GM_addStyle(` /* 基础样式 */ ${headerSelectors.base} { position: relative; padding-right: 3em; cursor: pointer; transition: all ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}; } /* 箭头指示器 */ ${headerSelectors.after} { display: inline-block; position: absolute; right: 0.5em; top: 50%; transform: translateY(-50%); font-size: 0.8em; font-weight: bold; pointer-events: none; transition: transform ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}; } /* 各级标题的箭头内容 */ ${this.generateArrowContent()} /* 折叠状态的箭头旋转 */ .${CONFIG.classes.collapsed}:after { transform: translateY(-50%) rotate(-90deg); } /* 隐藏元素 */ .${CONFIG.classes.hidden}, .${CONFIG.classes.hiddenByParent} { display: none !important; opacity: 0 !important; } /* 无内容标题 */ .${CONFIG.classes.noContent}:after { display: none !important; } /* 禁用链接事件 */ .octicon-link, .octicon-link > * { pointer-events: none; } /* 平滑动画 */ .ghcm-transitioning { transition: opacity ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}, transform ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}; } /* 目录容器样式 */ .${CONFIG.classes.tocContainer} { position: fixed; top: 20px; right: 20px; width: 300px; max-height: 70vh; background: var(--color-canvas-default, #ffffff); border: 1px solid var(--color-border-default, #d0d7de); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.12); z-index: 10000; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .ghcm-toc-header { padding: 8px 12px; background: var(--color-canvas-subtle, #f6f8fa); border-bottom: 1px solid var(--color-border-default, #d0d7de); display: flex; justify-content: space-between; align-items: center; min-height: 36px; } .ghcm-toc-header h3 { margin: 0; font-size: 13px; font-weight: 600; color: var(--color-fg-default, #24292f); line-height: 1.2; } .ghcm-toc-close { background: none; border: none; font-size: 14px; cursor: pointer; padding: 2px 4px; border-radius: 3px; color: var(--color-fg-muted, #656d76); line-height: 1; } .ghcm-toc-close:hover { background: var(--color-danger-subtle, #ffebe9); color: var(--color-danger-fg, #cf222e); } .ghcm-toc-content { max-height: calc(70vh - 44px); overflow-y: auto; padding: 6px 0; } .ghcm-toc-item { display: flex; align-items: center; padding: 4px 16px; border-radius: 4px; margin: 1px 8px; cursor: pointer; } .ghcm-toc-item:hover { background: var(--color-neutral-subtle, #f6f8fa); } .ghcm-toc-collapse-icon { font-size: 10px; margin-right: 8px; color: var(--color-fg-muted, #656d76); min-width: 12px; } .ghcm-toc-link { text-decoration: none; color: var(--color-fg-default, #24292f); font-size: 13px; line-height: 1.4; flex: 1; } .ghcm-toc-link:hover { color: var(--color-accent-fg, #0969da); } /* 搜索容器样式 */ .${CONFIG.classes.searchContainer} { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 480px; max-width: 90vw; max-height: 80vh; background: var(--color-canvas-default, #ffffff); border: 1px solid var(--color-border-default, #d0d7de); border-radius: 12px; box-shadow: 0 16px 32px rgba(0,0,0,0.24); z-index: 10001; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .ghcm-search-header { padding: 16px 20px; background: var(--color-canvas-subtle, #f6f8fa); border-bottom: 1px solid var(--color-border-default, #d0d7de); display: flex; justify-content: space-between; align-items: center; } .ghcm-search-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: var(--color-fg-default, #24292f); } .ghcm-search-close { background: none; border: none; font-size: 18px; cursor: pointer; padding: 6px; border-radius: 6px; color: var(--color-fg-muted, #656d76); } .ghcm-search-close:hover { background: var(--color-danger-subtle, #ffebe9); color: var(--color-danger-fg, #cf222e); } .ghcm-search-content { padding: 20px; } .ghcm-search-input { width: 100%; padding: 12px 16px; border: 2px solid var(--color-border-default, #d0d7de); border-radius: 8px; font-size: 16px; background: var(--color-canvas-default, #ffffff); color: var(--color-fg-default, #24292f); outline: none; transition: border-color 0.2s; } .ghcm-search-input:focus { border-color: var(--color-accent-emphasis, #0969da); } .ghcm-search-results { margin-top: 16px; max-height: 400px; overflow-y: auto; } .ghcm-search-result { display: flex; align-items: center; padding: 12px 16px; border-radius: 8px; cursor: pointer; margin: 4px 0; border: 1px solid transparent; } .ghcm-search-result:hover { background: var(--color-neutral-subtle, #f6f8fa); border-color: var(--color-border-default, #d0d7de); } .ghcm-search-level { background: var(--color-accent-subtle, #ddf4ff); color: var(--color-accent-fg, #0969da); padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-right: 12px; min-width: 24px; text-align: center; } .ghcm-search-text { flex: 1; font-size: 14px; color: var(--color-fg-default, #24292f); } .ghcm-search-text mark { background: var(--color-attention-subtle, #fff8c5); color: var(--color-attention-fg, #9a6700); padding: 1px 2px; border-radius: 2px; } .ghcm-search-hint, .ghcm-search-no-results { text-align: center; padding: 40px 20px; color: var(--color-fg-muted, #656d76); font-style: italic; } /* 深色主题适配 */ @media (prefers-color-scheme: dark) { .${CONFIG.classes.tocContainer}, .${CONFIG.classes.searchContainer} { background: var(--color-canvas-default, #0d1117); border-color: var(--color-border-default, #30363d); } } `); } generateHeaderSelectors() { const containers = CONFIG.selectors.markdownContainers; const headers = CONFIG.selectors.headers.map(h => h.toLowerCase()); const baseSelectors = []; const afterSelectors = []; containers.forEach(container => { if (container) { headers.forEach(header => { baseSelectors.push(`${container} ${header}`); baseSelectors.push(`${container} ${header}.heading-element`); afterSelectors.push(`${container} ${header}:after`); afterSelectors.push(`${container} ${header}.heading-element:after`); }); } }); return { base: baseSelectors.join(", "), after: afterSelectors.join(", ") }; } generateArrowContent() { return CONFIG.selectors.headers.map((header, index) => { const level = index + 1; const containers = CONFIG.selectors.markdownContainers; const selectors = []; containers.forEach(container => { if (container) { selectors.push(`${container} ${header.toLowerCase()}:after`); selectors.push(`${container} ${header.toLowerCase()}.heading-element:after`); } }); return `${selectors.join(", ")} { content: "${level}▼"; }`; }).join("\n"); } addColorStyles() { const styles = CONFIG.selectors.headers.map((header, index) => { const containers = CONFIG.selectors.markdownContainers; const selectors = []; containers.forEach(container => { if (container) { selectors.push(`${container} ${header.toLowerCase()}:after`); selectors.push(`${container} ${header.toLowerCase()}.heading-element:after`); } }); return `${selectors.join(", ")} { color: ${CONFIG.colors[index]}; }`; }).join("\n"); this.arrowColors.textContent = styles; } updateColors(newColors) { CONFIG.colors = newColors; GM_setValue("ghcm-colors", newColors); this.addColorStyles(); } } // 折叠功能核心类 class CollapseManager { constructor(stateManager) { this.stateManager = stateManager; this.animationQueue = new Map(); } toggle(header, isShiftClicked = false) { if (!header || header.classList.contains(CONFIG.classes.noContent)) { return; } const startTime = performance.now(); const level = this.stateManager.getHeaderLevel(header); const isCollapsed = !header.classList.contains(CONFIG.classes.collapsed); Logger.log("[GHCM] Toggle:", header, "Level:", level, "Will collapse:", isCollapsed); if (isShiftClicked) { this.toggleAllSameLevel(level, isCollapsed); } else { this.toggleSingle(header, isCollapsed); } // 性能监控 const endTime = performance.now(); const duration = endTime - startTime; if (duration > 100 && CONFIG.animation.maxAnimatedElements > 0) { Logger.warn(`[GHCM] 检测到性能问题 (${duration.toFixed(1)}ms),建议启用性能模式`); // 自动降级性能设置 if (!GM_getValue("ghcm-auto-performance-warned", false)) { CONFIG.animation.maxAnimatedElements = Math.max(5, CONFIG.animation.maxAnimatedElements / 2); Logger.log(`[GHCM] 自动调整动画阈值为: ${CONFIG.animation.maxAnimatedElements}`); GM_setValue("ghcm-auto-performance-warned", true); } } DOMUtils.clearSelection(); this.dispatchToggleEvent(header, level, isCollapsed); } toggleSingle(header, isCollapsed) { header.classList.toggle(CONFIG.classes.collapsed, isCollapsed); this.updateContent(header, isCollapsed); } toggleAllSameLevel(level, isCollapsed) { const headerName = CONFIG.selectors.headers[level - 1].toLowerCase(); const selectors = CONFIG.selectors.markdownContainers .filter(container => container) .map(container => `${container} ${headerName}, ${container} ${headerName}.heading-element`) .join(", "); DOMUtils.$$(selectors).forEach(header => { if (DOMUtils.isHeader(header)) { header.classList.toggle(CONFIG.classes.collapsed, isCollapsed); this.updateContent(header, isCollapsed); } }); } updateContent(header, isCollapsed) { const level = this.stateManager.getHeaderLevel(header); const headerKey = this.stateManager.generateHeaderKey(header); const elements = this.getContentElements(header, level); // 分析元素:区分普通内容和子标题 const analyzedElements = elements.map(el => { const childHeader = DOMUtils.isHeader(el) ? el : el.querySelector(CONFIG.selectors.headers.join(",")); return { element: el, isHeader: !!childHeader, childHeader: childHeader, childHeaderCollapsed: childHeader ? childHeader.classList.contains(CONFIG.classes.collapsed) : false }; }); // 更新状态 this.stateManager.setHeaderState(headerKey, { isCollapsed, elements: analyzedElements }); // 执行智能动画(考虑子标题状态) this.animateElementsIntelligent(analyzedElements, isCollapsed, headerKey); } getContentElements(header, level) { const container = DOMUtils.getHeaderContainer(header); const elements = []; let nextElement = container.nextElementSibling; // 构建同级和更高级别的选择器 const higherLevelSelectors = CONFIG.selectors.headers .slice(0, level) .map(h => h.toLowerCase()) .join(","); while (nextElement) { // 如果遇到同级或更高级别的标题,停止 if (nextElement.matches(higherLevelSelectors) || (nextElement.classList?.contains('markdown-heading') && nextElement.querySelector(higherLevelSelectors))) { break; } elements.push(nextElement); nextElement = nextElement.nextElementSibling; } return elements; } animateElements(elements, isCollapsed, headerKey) { // 取消之前的动画 if (this.animationQueue.has(headerKey)) { clearTimeout(this.animationQueue.get(headerKey)); this.animationQueue.delete(headerKey); } // 性能优化:如果元素太多,直接切换而不做动画 if (elements.length > CONFIG.animation.maxAnimatedElements) { this.toggleElementsInstantly(elements, isCollapsed); return; } // 对于适量元素,使用优化的批量动画 this.animateElementsBatch(elements, isCollapsed, headerKey); } // 新的智能动画方法,考虑子标题状态 animateElementsIntelligent(analyzedElements, isCollapsed, headerKey) { // 取消之前的动画 if (this.animationQueue.has(headerKey)) { clearTimeout(this.animationQueue.get(headerKey)); this.animationQueue.delete(headerKey); } Logger.log(`[GHCM] 智能动画: ${analyzedElements.length} 个元素, 阈值: ${CONFIG.animation.maxAnimatedElements}`); // 性能优化:如果元素太多,直接切换 if (analyzedElements.length > CONFIG.animation.maxAnimatedElements) { Logger.log(`[GHCM] 元素过多,使用即时切换模式`); this.toggleElementsIntelligentInstantly(analyzedElements, isCollapsed); return; } // 使用智能批量动画 Logger.log(`[GHCM] 使用批量动画模式`); this.animateElementsIntelligentBatch(analyzedElements, isCollapsed, headerKey); } // 智能即时切换(性能模式) toggleElementsIntelligentInstantly(analyzedElements, isCollapsed) { Logger.log(`[GHCM] 性能模式:即时切换 ${analyzedElements.length} 个元素`); analyzedElements.forEach(({ element, isHeader, childHeader, childHeaderCollapsed }) => { if (isCollapsed) { // 折叠:隐藏所有内容 element.classList.add(CONFIG.classes.hiddenByParent); element.style.display = 'none'; } else { // 展开:根据子标题状态决定是否显示 element.classList.remove(CONFIG.classes.hiddenByParent); element.style.display = 'block'; // 如果是子标题且原本是折叠的,需要保持其内容隐藏 if (isHeader && childHeaderCollapsed) { setTimeout(() => { this.ensureChildHeaderContentHidden(childHeader); }, 10); } // 清理动画样式 element.style.removeProperty('opacity'); element.style.removeProperty('transform'); element.style.removeProperty('transition'); element.classList.remove('ghcm-transitioning'); } }); } // 智能批量动画 animateElementsIntelligentBatch(analyzedElements, isCollapsed, headerKey) { // 检查是否应该使用动画 if (CONFIG.animation.maxAnimatedElements === 0) { this.toggleElementsIntelligentInstantly(analyzedElements, isCollapsed); return; } const batches = this.createIntelligentBatches(analyzedElements, CONFIG.animation.batchSize); const processBatch = (batchIndex) => { if (batchIndex >= batches.length) return; const batch = batches[batchIndex]; if (isCollapsed) { this.collapseIntelligentBatch(batch); } else { this.expandIntelligentBatch(batch); } // 处理下一个批次 if (batchIndex < batches.length - 1) { const timeout = setTimeout(() => { processBatch(batchIndex + 1); }, 30); // 减少延迟,让动画更流畅 this.animationQueue.set(`${headerKey}-batch-${batchIndex}`, timeout); } }; processBatch(0); } createIntelligentBatches(analyzedElements, batchSize) { const batches = []; for (let i = 0; i < analyzedElements.length; i += batchSize) { batches.push(analyzedElements.slice(i, i + batchSize)); } return batches; } collapseIntelligentBatch(batch) { Logger.log(`[GHCM] 折叠动画批次: ${batch.length} 个元素`); // 折叠批次:先设置初始状态和过渡效果 batch.forEach(({ element }) => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; element.style.transition = `opacity ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}, transform ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}`; }); // 使用requestAnimationFrame确保样式已应用 requestAnimationFrame(() => { batch.forEach(({ element }) => { element.style.opacity = '0'; element.style.transform = 'translateY(-8px)'; }); // 动画完成后隐藏元素 setTimeout(() => { batch.forEach(({ element }) => { element.classList.add(CONFIG.classes.hiddenByParent); element.style.display = 'none'; element.style.removeProperty('opacity'); element.style.removeProperty('transform'); element.style.removeProperty('transition'); }); Logger.log(`[GHCM] 折叠动画批次完成`); }, CONFIG.animation.duration); }); } expandIntelligentBatch(batch) { Logger.log(`[GHCM] 展开动画批次: ${batch.length} 个元素`); // 展开批次:先显示元素但设为初始动画状态 batch.forEach(({ element, isHeader, childHeader, childHeaderCollapsed }) => { element.classList.remove(CONFIG.classes.hiddenByParent); element.style.display = 'block'; element.style.opacity = '0'; element.style.transform = 'translateY(-8px)'; element.style.transition = `opacity ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}, transform ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}`; }); // 使用requestAnimationFrame确保DOM更新完成 requestAnimationFrame(() => { batch.forEach(({ element, isHeader, childHeader, childHeaderCollapsed }) => { element.style.opacity = '1'; element.style.transform = 'translateY(0)'; // 如果是子标题且原本是折叠的,确保其内容保持隐藏 if (isHeader && childHeaderCollapsed) { // 延迟执行,确保动画和DOM更新完成 setTimeout(() => { this.ensureChildHeaderContentHidden(childHeader); }, CONFIG.animation.duration + 50); } }); // 清理样式 setTimeout(() => { batch.forEach(({ element }) => { element.style.removeProperty('opacity'); element.style.removeProperty('transform'); element.style.removeProperty('transition'); }); Logger.log(`[GHCM] 展开动画批次完成`); }, CONFIG.animation.duration); }); } // 确保子标题的内容保持隐藏状态 ensureChildHeaderContentHidden(childHeader) { if (!childHeader || !childHeader.classList.contains(CONFIG.classes.collapsed)) { return; } const childLevel = this.stateManager.getHeaderLevel(childHeader); const childElements = this.getContentElements(childHeader, childLevel); // 立即隐藏子标题的内容,不使用动画 childElements.forEach(element => { element.classList.add(CONFIG.classes.hiddenByParent); element.style.display = 'none'; element.style.removeProperty('opacity'); element.style.removeProperty('transform'); element.classList.remove('ghcm-transitioning'); }); Logger.log(`[GHCM] 已恢复子标题的折叠状态:`, childHeader.textContent.trim()); } // 即时切换,无动画 toggleElementsInstantly(elements, isCollapsed) { // 批量DOM操作,减少重排 const fragment = document.createDocumentFragment(); elements.forEach(element => { if (isCollapsed) { element.classList.add(CONFIG.classes.hiddenByParent); element.style.display = 'none'; } else { element.classList.remove(CONFIG.classes.hiddenByParent); element.style.display = 'block'; // 清理可能存在的动画样式 element.style.removeProperty('opacity'); element.style.removeProperty('transform'); element.classList.remove('ghcm-transitioning'); } }); } // 批量动画处理 animateElementsBatch(elements, isCollapsed, headerKey) { const batches = this.createBatches(elements, CONFIG.animation.batchSize); let completedBatches = 0; const processBatch = (batchIndex) => { if (batchIndex >= batches.length) return; const batch = batches[batchIndex]; // 为每个批次准备DOM变更 if (isCollapsed) { this.collapseBatch(batch); } else { this.expandBatch(batch); } completedBatches++; // 处理下一个批次 if (batchIndex < batches.length - 1) { const timeout = setTimeout(() => { processBatch(batchIndex + 1); }, 50); // 批次间短暂延迟 this.animationQueue.set(`${headerKey}-batch-${batchIndex}`, timeout); } }; processBatch(0); } createBatches(elements, batchSize) { const batches = []; for (let i = 0; i < elements.length; i += batchSize) { batches.push(elements.slice(i, i + batchSize)); } return batches; } collapseBatch(batch) { // 先设置初始状态 batch.forEach(element => { element.style.transition = `opacity ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}`; element.style.opacity = '1'; }); // 触发动画 requestAnimationFrame(() => { batch.forEach(element => { element.style.opacity = '0'; }); // 动画完成后隐藏 setTimeout(() => { batch.forEach(element => { element.classList.add(CONFIG.classes.hiddenByParent); element.style.display = 'none'; element.style.removeProperty('opacity'); element.style.removeProperty('transition'); }); }, CONFIG.animation.duration); }); } expandBatch(batch) { // 先显示元素但设为透明 batch.forEach(element => { element.classList.remove(CONFIG.classes.hiddenByParent); element.style.display = 'block'; element.style.opacity = '0'; element.style.transition = `opacity ${CONFIG.animation.duration}ms ${CONFIG.animation.easing}`; }); // 触发淡入动画 requestAnimationFrame(() => { batch.forEach(element => { element.style.opacity = '1'; }); // 清理样式 setTimeout(() => { batch.forEach(element => { element.style.removeProperty('opacity'); element.style.removeProperty('transition'); }); }, CONFIG.animation.duration); }); } // 展开到指定标题(用于hash导航) expandToHeader(targetHeader) { if (!targetHeader) return; const level = this.stateManager.getHeaderLevel(targetHeader); let current = targetHeader; // 向上查找所有父级标题并展开 while (current) { const container = DOMUtils.getHeaderContainer(current); let previous = container.previousElementSibling; let foundParent = false; // 查找更高级别的父标题 while (previous) { const parentHeader = this.findHeaderInElement(previous, level - 1); if (parentHeader) { if (parentHeader.classList.contains(CONFIG.classes.collapsed)) { this.toggleSingle(parentHeader, false); } current = parentHeader; foundParent = true; break; } previous = previous.previousElementSibling; } if (!foundParent) break; } // 滚动到目标位置 this.scrollToElement(targetHeader); } findHeaderInElement(element, maxLevel) { if (DOMUtils.isHeader(element)) { const elementLevel = this.stateManager.getHeaderLevel(element); if (elementLevel < maxLevel) return element; } // 查找容器内的标题 for (let i = 1; i < maxLevel; i++) { const headerName = CONFIG.selectors.headers[i - 1].toLowerCase(); const header = element.querySelector(headerName) || element.querySelector(`${headerName}.heading-element`); if (header) return header; } return null; } scrollToElement(element) { if (!element) return; const targetPosition = element.offsetTop - 100; // 留出一些顶部空间 // 平滑滚动 window.scrollTo({ top: targetPosition, behavior: 'smooth' }); // 延迟再次确保位置正确 setTimeout(() => { if (Math.abs(window.scrollY - targetPosition) > 50) { window.scrollTo({ top: targetPosition, behavior: 'smooth' }); } }, 500); } dispatchToggleEvent(header, level, isCollapsed) { document.dispatchEvent(new CustomEvent("ghcm:toggle-complete", { detail: { header, level, isCollapsed } })); // 如果是展开操作,检查并恢复子标题状态 if (!isCollapsed) { setTimeout(() => { this.checkAndRestoreChildHeaderStates(header, level); }, CONFIG.animation.duration + 100); } } // 检查并恢复子标题的折叠状态 checkAndRestoreChildHeaderStates(parentHeader, parentLevel) { const container = DOMUtils.getHeaderContainer(parentHeader); let nextElement = container.nextElementSibling; // 查找所有子标题并恢复其状态 while (nextElement) { // 停止条件:遇到同级或更高级别的标题 const higherLevelSelectors = CONFIG.selectors.headers .slice(0, parentLevel) .map(h => h.toLowerCase()) .join(","); if (nextElement.matches(higherLevelSelectors) || (nextElement.classList?.contains('markdown-heading') && nextElement.querySelector(higherLevelSelectors))) { break; } // 检查是否是子标题 const childHeader = DOMUtils.isHeader(nextElement) ? nextElement : nextElement.querySelector(CONFIG.selectors.headers.join(",")); if (childHeader && childHeader.classList.contains(CONFIG.classes.collapsed)) { // 确保这个子标题的内容保持隐藏 this.ensureChildHeaderContentHidden(childHeader); } nextElement = nextElement.nextElementSibling; } } // 批量操作方法 getAllHeaders() { const headers = []; CONFIG.selectors.markdownContainers.forEach(container => { if (container) { CONFIG.selectors.headers.forEach(headerTag => { const elements = DOMUtils.$$(`${container} ${headerTag.toLowerCase()}`); headers.push(...elements.filter(el => DOMUtils.isHeader(el))); }); } }); return headers; } collapseAll() { const headers = this.getAllHeaders(); let count = 0; headers.forEach(header => { if (!header.classList.contains(CONFIG.classes.collapsed) && !header.classList.contains(CONFIG.classes.noContent)) { header.classList.add(CONFIG.classes.collapsed); this.updateContent(header, true); count++; } }); Logger.log(`[GHCM] 已折叠 ${count} 个标题`); this.showNotification(`📁 已折叠 ${count} 个标题`); } expandAll() { const headers = this.getAllHeaders(); let count = 0; headers.forEach(header => { if (header.classList.contains(CONFIG.classes.collapsed)) { header.classList.remove(CONFIG.classes.collapsed); this.updateContent(header, false); count++; } }); Logger.log(`[GHCM] 已展开 ${count} 个标题`); this.showNotification(`📂 已展开 ${count} 个标题`); } toggleAll() { const headers = this.getAllHeaders(); const collapsedCount = headers.filter(h => h.classList.contains(CONFIG.classes.collapsed) ).length; const totalCount = headers.filter(h => !h.classList.contains(CONFIG.classes.noContent) ).length; // 如果超过一半已折叠,则全部展开;否则全部折叠 if (collapsedCount > totalCount / 2) { this.expandAll(); } else { this.collapseAll(); } } // 按级别批量操作 collapseLevel(level) { const headerTag = CONFIG.selectors.headers[level - 1]; if (!headerTag) return; const headers = []; CONFIG.selectors.markdownContainers.forEach(container => { if (container) { const elements = DOMUtils.$$(`${container} ${headerTag.toLowerCase()}`); headers.push(...elements.filter(el => DOMUtils.isHeader(el))); } }); let count = 0; headers.forEach(header => { if (!header.classList.contains(CONFIG.classes.collapsed) && !header.classList.contains(CONFIG.classes.noContent)) { header.classList.add(CONFIG.classes.collapsed); this.updateContent(header, true); count++; } }); Logger.log(`[GHCM] 已折叠 ${count} 个 H${level} 标题`); this.showNotification(`📁 已折叠 ${count} 个 H${level} 标题`); } expandLevel(level) { const headerTag = CONFIG.selectors.headers[level - 1]; if (!headerTag) return; const headers = []; CONFIG.selectors.markdownContainers.forEach(container => { if (container) { const elements = DOMUtils.$$(`${container} ${headerTag.toLowerCase()}`); headers.push(...elements.filter(el => DOMUtils.isHeader(el))); } }); let count = 0; headers.forEach(header => { if (header.classList.contains(CONFIG.classes.collapsed)) { header.classList.remove(CONFIG.classes.collapsed); this.updateContent(header, false); count++; } }); Logger.log(`[GHCM] 已展开 ${count} 个 H${level} 标题`); this.showNotification(`📂 已展开 ${count} 个 H${level} 标题`); } // 通知功能 showNotification(message) { // 创建通知元素 const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: var(--color-canvas-default, #ffffff); border: 1px solid var(--color-border-default, #d0d7de); border-radius: 8px; padding: 12px 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10002; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; color: var(--color-fg-default, #24292f); opacity: 0; transition: opacity 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); // 显示动画 requestAnimationFrame(() => { notification.style.opacity = '1'; }); // 自动消失 setTimeout(() => { notification.style.opacity = '0'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 2000); } // 加载已保存的状态 loadSavedStates() { this.stateManager.loadFromMemory(); // 分层应用已保存的状态(从高级别到低级别) for (let level = 1; level <= 6; level++) { this.applyStatesForLevel(level); } } applyStatesForLevel(level) { const headers = this.getAllHeaders().filter(h => this.stateManager.getHeaderLevel(h) === level ); headers.forEach(header => { const headerKey = this.stateManager.generateHeaderKey(header); const savedState = this.stateManager.getHeaderState(headerKey); if (savedState && savedState.isCollapsed) { Logger.log(`[GHCM] 恢复 H${level} 标题状态:`, header.textContent.trim()); header.classList.add(CONFIG.classes.collapsed); this.updateContent(header, true); } }); } applyStateToElement(headerKey, state) { // 保留原方法作为备用 const headers = this.getAllHeaders(); headers.forEach(header => { const currentKey = this.stateManager.generateHeaderKey(header); if (currentKey === headerKey && state.isCollapsed) { header.classList.add(CONFIG.classes.collapsed); this.updateContent(header, true); } }); } // 代理目录和搜索功能 toggleToc() { if (this.tocGenerator) { this.tocGenerator.toggle(); } } toggleSearch() { if (this.searchManager) { this.searchManager.toggle(); } } // 检查标题是否有内容 markEmptyHeaders() { CONFIG.selectors.markdownContainers.forEach(containerSelector => { if (!containerSelector) return; CONFIG.selectors.headers.forEach(headerName => { const headerSelector = `${containerSelector} ${headerName.toLowerCase()}`; DOMUtils.$$(headerSelector).concat( DOMUtils.$$(`${headerSelector}.heading-element`) ).forEach(header => { const level = this.stateManager.getHeaderLevel(header); const elements = this.getContentElements(header, level); if (elements.length === 0) { header.classList.add(CONFIG.classes.noContent); } else { header.classList.remove(CONFIG.classes.noContent); } }); }); }); } } // 事件管理器 class EventManager { constructor(collapseManager) { this.collapseManager = collapseManager; this.setupEventListeners(); } setupEventListeners() { // 点击事件 document.addEventListener("click", this.handleClick.bind(this), true); // Hash 变化事件 window.addEventListener("hashchange", this.handleHashChange.bind(this)); // DOM 变化监听(如果有其他脚本修改DOM) if (window.ghmo) { window.addEventListener("ghmo:dom", this.handleDOMChange.bind(this)); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', this.handleDOMChange.bind(this)); } else { setTimeout(() => this.handleDOMChange(), 200); } } handleClick(event) { let target = event.target; // 处理SVG点击 if (target.nodeName === "path") { target = target.closest("svg"); } // 跳过排除的元素 if (!target || this.shouldSkipElement(target)) { return; } // 查找最近的标题元素 const header = target.closest(CONFIG.selectors.headers.map(h => h.toLowerCase()).join(",")); if (header && DOMUtils.isHeader(header) && DOMUtils.isInMarkdown(header)) { event.preventDefault(); Logger.log("[GHCM] Header clicked:", header); this.collapseManager.toggle(header, event.shiftKey); } } shouldSkipElement(element) { const nodeName = element.nodeName?.toLowerCase(); return CONFIG.selectors.excludeClicks.some(selector => { if (selector.startsWith('.')) { return element.classList.contains(selector.slice(1)); } return nodeName === selector; }); } handleHashChange() { const hash = window.location.hash.replace(/#/, ""); if (hash) { this.openHashTarget(hash); } } handleDOMChange() { // 重新标记空标题 this.collapseManager.markEmptyHeaders(); // 处理当前hash this.handleHashChange(); } openHashTarget(id) { // 尝试多种ID格式 const possibleSelectors = [ `#user-content-${id}`, `#${id}`, `[id="${id}"]` ]; let targetElement = null; for (const selector of possibleSelectors) { targetElement = DOMUtils.$(selector); if (targetElement) break; } if (!targetElement) return; // 查找对应的标题 let header = targetElement; if (!DOMUtils.isHeader(header)) { header = targetElement.closest(CONFIG.selectors.headers.map(h => h.toLowerCase()).join(",")); } if (header && DOMUtils.isHeader(header)) { this.collapseManager.expandToHeader(header); } } } // 主应用类 class GitHubCollapseMarkdown { constructor() { this.stateManager = new StateManager(); this.styleManager = new StyleManager(); this.collapseManager = new CollapseManager(this.stateManager); this.tocGenerator = new TocGenerator(); this.searchManager = new SearchManager(this.collapseManager); this.menuManager = new MenuManager(this); this.hotkeyManager = new HotkeyManager(this.collapseManager); this.eventManager = new EventManager(this.collapseManager); // 将附加功能关联到折叠管理器 this.collapseManager.tocGenerator = this.tocGenerator; this.collapseManager.searchManager = this.searchManager; this.collapseManager.menuManager = this.menuManager; this.init(); } init() { const performanceMode = GM_getValue("ghcm-performance-mode", false); const memoryEnabled = CONFIG.memory.enabled; const hotkeysEnabled = CONFIG.hotkeys.enabled; const animationStatus = performanceMode ? "性能模式 (无动画)" : "标准模式 (有动画)"; Logger.log(`[GHCM] Initializing GitHub Collapse Markdown (Optimized v3.2.3) - ${animationStatus}`); Logger.log(`[GHCM] 🧠 智能嵌套状态管理: 启用`); Logger.log(`[GHCM] 🎨 现代GUI界面: 启用`); Logger.log(`[GHCM] 动画阈值: ${CONFIG.animation.maxAnimatedElements} 个元素`); Logger.log(`[GHCM] 状态记忆: ${memoryEnabled ? "启用" : "禁用"}`); Logger.log(`[GHCM] 快捷键: ${hotkeysEnabled ? "启用" : "禁用"}`); // 添加菜单命令 this.setupMenuCommands(); // 初始检查和状态加载 setTimeout(() => { this.collapseManager.markEmptyHeaders(); // 加载已保存的折叠状态 if (memoryEnabled) { this.collapseManager.loadSavedStates(); } }, 500); // 监听折叠状态变化,更新目录显示和菜单统计 document.addEventListener('ghcm:toggle-complete', () => { if (this.tocGenerator.isVisible) { setTimeout(() => { this.tocGenerator.refreshTocStates(); }, CONFIG.animation.duration + 150); } // 如果菜单打开,刷新统计信息 if (this.menuManager.isVisible) { setTimeout(() => { this.menuManager.refreshMenu(); }, CONFIG.animation.duration + 150); } }); } setupMenuCommands() { try { // === 基础操作 === GM_registerMenuCommand("📁 折叠所有标题", () => { this.collapseManager.collapseAll(); }); GM_registerMenuCommand("📂 展开所有标题", () => { this.collapseManager.expandAll(); }); GM_registerMenuCommand("🔄 智能切换", () => { this.collapseManager.toggleAll(); }); // === 工具功能 === GM_registerMenuCommand("📑 目录导航", () => { this.tocGenerator.toggle(); }); GM_registerMenuCommand("🔍 搜索标题", () => { this.searchManager.toggle(); }); // === 设置选项 === GM_registerMenuCommand("⚡ 性能模式", () => { this.togglePerformanceMode(); }); GM_registerMenuCommand("💾 状态记忆", () => { this.toggleMemory(); }); GM_registerMenuCommand("⌨️ 快捷键", () => { this.toggleHotkeys(); }); GM_registerMenuCommand("🐛 调试模式", () => { this.toggleDebug(); }); // === 重置功能 === GM_registerMenuCommand("🔄 重置折叠状态", () => { this.resetAllStates(); }); GM_registerMenuCommand("🗑️ 清除记忆数据", () => { this.clearAllMemory(); }); // === 信息帮助 === GM_registerMenuCommand("📊 当前统计", () => { this.showStatistics(); }); GM_registerMenuCommand("❓ 快捷键说明", () => { this.showHotkeyHelp(); }); } catch (e) { Logger.warn("[GHCM] 菜单功能不可用:", e); } } toggleMemory() { const newState = !CONFIG.memory.enabled; CONFIG.memory.enabled = newState; GM_setValue("ghcm-memory-enabled", newState); const status = newState ? "启用" : "禁用"; Logger.log(`[GHCM] 状态记忆已${status}`); this.collapseManager.showNotification(`💾 状态记忆已${status}`); } toggleHotkeys() { const newState = !CONFIG.hotkeys.enabled; CONFIG.hotkeys.enabled = newState; GM_setValue("ghcm-hotkeys-enabled", newState); const status = newState ? "启用" : "禁用"; Logger.log(`[GHCM] 快捷键已${status}`); this.collapseManager.showNotification(`⌨️ 快捷键已${status}`); if (newState) { // 重新绑定快捷键 this.hotkeyManager.setupHotkeys(); } } toggleDebug() { const newState = !CONFIG.debug; CONFIG.debug = newState; GM_setValue("ghcm-debug-mode", newState); const status = newState ? "启用" : "禁用"; Logger.log(`[GHCM] 调试模式已${status}`); this.collapseManager.showNotification(`🐛 调试模式已${status}`); } togglePerformanceMode() { const isPerformanceMode = CONFIG.animation.maxAnimatedElements === 0; const newState = !isPerformanceMode; if (newState) { // 启用性能模式(禁用动画) CONFIG.animation.maxAnimatedElements = 0; GM_setValue("ghcm-performance-mode", true); Logger.log("[GHCM] 已启用性能模式 - 动画已禁用"); this.collapseManager.showNotification("⚡ 性能模式已启用"); } else { // 禁用性能模式(启用动画) CONFIG.animation.maxAnimatedElements = 20; GM_setValue("ghcm-performance-mode", false); Logger.log("[GHCM] 已禁用性能模式 - 动画已启用"); this.collapseManager.showNotification("🎬 动画效果已启用"); } } clearAllMemory() { if (confirm("确定要清除所有页面的折叠状态记忆吗?")) { GM_setValue(CONFIG.memory.key, {}); this.stateManager.clear(); Logger.log("[GHCM] 已清除所有记忆数据"); this.collapseManager.showNotification("🗑️ 已清除所有记忆数据"); } } showHotkeyHelp() { const helpContent = ` GitHub Collapse Markdown - 使用说明 ✨ 快捷键: • ${CONFIG.hotkeys.collapseAll} - 折叠所有标题 • ${CONFIG.hotkeys.expandAll} - 展开所有标题 • ${CONFIG.hotkeys.toggleAll} - 智能切换 • ${CONFIG.hotkeys.showToc} - 目录导航 • ${CONFIG.hotkeys.search} - 搜索标题 • ${CONFIG.hotkeys.menu} - 显示/隐藏菜单 🖱️ 鼠标操作: • 点击标题 - 折叠/展开该标题 • Shift + 点击 - 折叠/展开同级别所有标题 • 点击右下角设置按钮 - 打开GUI菜单 🔥 核心功能: • 🧠 智能嵌套状态 - 展开父标题时保持子标题折叠状态 • 💾 状态记忆 - 自动记住每个页面的折叠状态 • ⚡ 性能优化 - 大量内容时自动优化动画 • 📑 目录导航 - 快速跳转到任意标题 • 🔍 实时搜索 - 搜索标题内容并快速定位 • 🎨 现代GUI界面 - 美观简约的设置面板 ⚙️ 设置说明: • 点击右下角浮动按钮打开现代化GUI菜单 • 所有功能都支持实时切换,无需刷新页面 • 性能模式可在长文档中提高响应速度 • 状态记忆和快捷键都支持自由切换 `.trim(); alert(helpContent); } showStatistics() { const headers = this.collapseManager.getAllHeaders(); const collapsed = headers.filter(h => h.classList.contains(CONFIG.classes.collapsed)); const visible = headers.filter(h => !h.classList.contains(CONFIG.classes.collapsed) && !h.classList.contains(CONFIG.classes.noContent) ); const levelStats = {}; for (let i = 1; i <= 6; i++) { const levelHeaders = headers.filter(h => this.stateManager.getHeaderLevel(h) === i ); if (levelHeaders.length > 0) { levelStats[`H${i}`] = { total: levelHeaders.length, collapsed: levelHeaders.filter(h => h.classList.contains(CONFIG.classes.collapsed)).length }; } } const levelStatsText = Object.entries(levelStats) .map(([level, stats]) => `${level}: ${stats.total}个 (${stats.collapsed}个已折叠)` ).join(', '); const statsContent = ` 📊 当前页面统计 📝 标题概况: • 总计:${headers.length} 个标题 • 已折叠:${collapsed.length} 个 • 可见:${visible.length} 个 📋 级别分布:${levelStatsText || '无标题'} ⚙️ 功能状态: • 性能模式:${CONFIG.animation.maxAnimatedElements === 0 ? '🟢 启用' : '🔴 禁用'} • 状态记忆:${CONFIG.memory.enabled ? '🟢 启用' : '🔴 禁用'} • 快捷键:${CONFIG.hotkeys.enabled ? '🟢 启用' : '🔴 禁用'} `.trim(); alert(statsContent); } resetAllStates() { // 移除所有折叠状态 DOMUtils.$$(".ghcm-collapsed").forEach(element => { element.classList.remove(CONFIG.classes.collapsed); }); // 显示所有隐藏的内容 DOMUtils.$$(".ghcm-hidden-by-parent").forEach(element => { element.classList.remove(CONFIG.classes.hiddenByParent); element.style.display = ''; element.style.opacity = ''; element.style.transform = ''; }); // 清空状态 this.stateManager.clear(); Logger.log("[GHCM] 已重置所有折叠状态"); } } // 启动应用 window.ghcmInstance = new GitHubCollapseMarkdown(); })();