// ==UserScript== // @name 论坛帖子文字自动放大器 // @namespace https://scriptcat.org/ // @version 0.2.6 // @description 安全放大论坛正文字号(白名单机制,默认不影响普通网站)。支持当前网站独立开启/关闭、字号增量定制及可视化点击拾取选择器。 // @author gymimc // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @license MIT // ==/UserScript== (function () { 'use strict'; // 在 CONFIG_KEYS 对象中【修改】存储 Key 前缀 const CONFIG_KEYS = { GLOBAL_DELTA: 'config_global_font_delta', DOMAIN_DELTA_PREFIX: 'font_delta_', DOMAIN_SELECTOR_PREFIX: 'selector_', DOMAIN_ENABLED_PREFIX: 'enabled_' // 【修改】替换原有的 DOMAIN_DISABLED_PREFIX,改为当前域名启用标志 }; // ========================================== // 【修改/新增】判断与设置当前域名是否在白名单(启用)的函数 // ========================================== /** * 【修改】判断当前域名是否已被用户显式启用(白名单机制,默认返回 false) * @returns {boolean} */ function isDomainEnabled() { return GM_getValue(`${CONFIG_KEYS.DOMAIN_ENABLED_PREFIX}${window.location.hostname}`, false) === true; } /** * 【修改】切换当前域名的启用状态 * @param {boolean} enabled */ function setDomainEnabled(enabled) { GM_setValue(`${CONFIG_KEYS.DOMAIN_ENABLED_PREFIX}${window.location.hostname}`, enabled); } const DEFAULT_CONFIG = { fontDelta: 3, // 默认全局增量 (像素/px) lineHeight: '1.6', // 默认自适应行高 }; // 1. 预设常用论坛选择器 const COMMON_SELECTORS = [ '.post-message', '.topic-content', '.markdown-body', '[class*="post-content"]', '[class*="thread-content"]', 'td.t_f', '.reply-content', '.c_post_content', '.article-content', '.entry-content', '.post-text' ]; /** * 读取全局通用字号增量 * @returns {number} */ function getGlobalFontDelta() { return parseInt(GM_getValue(CONFIG_KEYS.GLOBAL_DELTA, DEFAULT_CONFIG.fontDelta), 10); } /** * 读取当前域名的专属字号增量(若未设置,则返回 null) * @returns {number|null} */ function getDomainFontDelta() { const val = GM_getValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); return val !== null ? parseInt(val, 10) : null; } /** * 获取最终生效的字号增量(域名优先级 > 全局优先级) * @returns {number} */ function getEffectiveFontDelta() { const domainDelta = getDomainFontDelta(); if (domainDelta !== null && !isNaN(domainDelta)) { return domainDelta; } return getGlobalFontDelta(); } /** * 获取当前域名的自定义 CSS 选择器 * @returns {string} */ function getDomainSelector() { return GM_getValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, ''); } /** * 智能识别论坛正文节点 * @returns {Element[]} */ function findContentNodes() { // 策略 0:优先匹配当前域名的自定义选择器 const customSelector = getDomainSelector(); if (customSelector) { try { const customElements = document.querySelectorAll(customSelector); if (customElements.length > 0) { console.log(`[论坛字号放大器] 命中自定义规则 (${window.location.hostname}): ${customSelector}`); return Array.from(customElements); } } catch (e) { console.error('[论坛字号放大器] 自定义选择器语法错误:', e); } } // 策略 A:匹配内置预设规则 for (const selector of COMMON_SELECTORS) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { console.log(`[论坛字号放大器] 命中预设规则: ${selector}`); return Array.from(elements); } } // 策略 B:启发式降级分析(排除代码块与隐藏容器) console.log('[论坛字号放大器] 启动启发式分析...'); const candidates = Array.from(document.querySelectorAll('div, article, td')); return candidates.filter(el => { // 【新增】排除非展示性标签 const tag = el.tagName.toUpperCase(); if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA'].includes(tag)) return false; // 排除不可见的容器 if (el.offsetWidth === 0 && el.offsetHeight === 0) return false; // 排除内部包含 style 或 script 文本干预的情况 const clone = el.cloneNode(true); const scriptsAndStyles = clone.querySelectorAll('script, style, noscript'); scriptsAndStyles.forEach(node => node.remove()); const pureTextLength = clone.textContent.trim().length; const childContainers = el.querySelectorAll('div, section, article'); // 筛选条件:纯净文本长度 > 120 且没有太多深层块级子卡片 return pureTextLength > 120 && childContainers.length < 3; }); } /** * 安全放大字号(双层配置联动,防止死循环叠加与粗体渲染异常) * @param {Element[]} elements 目标节点数组 */ function applySafeEnlargement(elements) { if (!elements || elements.length === 0) return; // 计算当前生效的增量(域名优先,全局保底) const activeDelta = getEffectiveFontDelta(); elements.forEach(el => { // 1. 记录基准字号(防无限叠加) if (!el.dataset.originalFontSize) { const computedStyle = window.getComputedStyle(el); const baseSize = parseFloat(computedStyle.fontSize); if (!isNaN(baseSize) && baseSize > 0) { el.dataset.originalFontSize = baseSize; } } const baseFontSize = parseFloat(el.dataset.originalFontSize); if (!isNaN(baseFontSize) && baseFontSize > 0) { const targetFontSize = baseFontSize + activeDelta; // 应用字号与相对行高 el.style.setProperty('font-size', `${targetFontSize}px`, 'important'); el.style.setProperty('line-height', DEFAULT_CONFIG.lineHeight, 'important'); // 强制锁定字重并开启抗锯齿,修复部分论坛变粗问题 el.style.setProperty('font-weight', '400', 'important'); el.style.setProperty('-webkit-font-smoothing', 'antialiased', 'important'); // 解除容器限制,防止溢出截断 const computedStyle = window.getComputedStyle(el); if (computedStyle.maxHeight !== 'none') { el.style.setProperty('max-height', 'none', 'important'); } if (computedStyle.overflow === 'hidden') { el.style.setProperty('overflow', 'visible', 'important'); } el.dataset.fontEnlarged = 'true'; } }); } /** * 【修改】生成泛化 CSS 选择器(修复正则语法错误,过滤高亮类名) * @param {Element} el * @returns {string} */ function generateSelector(el) { const isGenericClass = (cls) => { return cls && typeof cls === 'string' && cls !== 'fe-picker-highlight' && // 排除拾取高亮类名 !/\d{3,}/.test(cls) && !cls.startsWith('js-') && !cls.includes(':'); }; // 辅助转义函数:彻底修复正则方括号闭合问题 const safeClass = (cls) => { if (window.CSS && typeof CSS.escape === 'function') { return CSS.escape(cls); } // 【修改】正确闭合方括号字符集 [\\\/:],安全转义反斜杠、斜杠和冒号 return cls.replace(/[\\\/:]/g, '\\$1'); }; if (el.className && typeof el.className === 'string') { const validClasses = el.className.split(/\s+/).filter(isGenericClass); if (validClasses.length > 0) { return `.${validClasses.map(safeClass).join('.')}`; } } let current = el; let depth = 0; while (current && current !== document.body && depth < 3) { if (current.className && typeof current.className === 'string') { const classes = current.className.split(/\s+/).filter(isGenericClass); if (classes.length > 0) { return `.${classes.map(safeClass).join('.')} ${el.tagName.toLowerCase()}`; } } current = current.parentElement; depth++; } return el.tagName.toLowerCase(); } /** * 可视化拾取模式 */ function startVisualPicker() { let hoveredEl = null; const styleEl = document.createElement('style'); styleEl.id = 'font-enlarger-picker-style'; styleEl.textContent = ` .fe-picker-highlight { outline: 2px dashed #ff4757 !important; outline-offset: -2px !important; background-color: rgba(255, 71, 87, 0.08) !important; cursor: crosshair !important; } `; document.head.appendChild(styleEl); const onMouseOver = (e) => { e.stopPropagation(); if (hoveredEl) hoveredEl.classList.remove('fe-picker-highlight'); hoveredEl = e.target; hoveredEl.classList.add('fe-picker-highlight'); }; const onClick = (e) => { e.preventDefault(); e.stopPropagation(); cleanup(); if (hoveredEl) { const selector = generateSelector(hoveredEl); const confirmed = confirm(`已选中目标区域,生成的通用选择器为:\n\n${selector}\n\n是否绑定到当前域名 (${window.location.hostname}) 并开启放大?`); if (confirmed) { GM_setValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, selector); setDomainEnabled(true); // 【新增】拾取成功后,自动将当前网站设为启用状态 alert('配置已保存并已开启当前网站功能!页面即将刷新。'); window.location.reload(); } } }; const onKeyDown = (e) => { if (e.key === 'Escape') { cleanup(); alert('已退出可视化选取模式。'); } }; function cleanup() { if (hoveredEl) hoveredEl.classList.remove('fe-picker-highlight'); document.removeEventListener('mouseover', onMouseOver, true); document.removeEventListener('click', onClick, true); document.removeEventListener('keydown', onKeyDown, true); const style = document.getElementById('font-enlarger-picker-style'); if (style) style.remove(); } document.addEventListener('mouseover', onMouseOver, true); document.addEventListener('click', onClick, true); document.addEventListener('keydown', onKeyDown, true); alert('【可视化元素拾取模式已激活】\n\n请将鼠标悬停在需要放大的论坛内容上(有红框高亮),然后【单击鼠标左键】完成选择。\n按 ESC 键退出。'); } /** * 主初始化逻辑 */ function init() { // 【修改】白名单拦截:只有当用户显式开启了当前网站,或者绑定了自定义选择器时才继续运行 const hasCustomSelector = !!getDomainSelector(); if (!isDomainEnabled() && !hasCustomSelector) { console.log(`[论坛字号放大器] 当前站点 (${window.location.hostname}) 未开启放大功能,跳过处理。`); return; } const targetNodes = findContentNodes(); if (targetNodes.length > 0) { applySafeEnlargement(targetNodes); console.log(`[论坛字号放大器] 成功处理 ${targetNodes.length} 个节点,当前生效字号增量: +${getEffectiveFontDelta()}px`); } else { console.warn('[论坛字号放大器] 未能自动识别到符合条件的内容区域。'); } } // 1. 运行初始化 init(); // 2. DOM 防抖变动监听(仅监听 DOM 树插入,忽略属性改变) let observerTimer = null; const observer = new MutationObserver((mutations) => { const hasNewNodes = mutations.some(m => m.addedNodes.length > 0); if (!hasNewNodes) return; if (observerTimer) clearTimeout(observerTimer); observerTimer = setTimeout(() => { init(); }, 500); }); observer.observe(document.body, { childList: true, subtree: true, attributes: false }); // 3. 注册插件菜单管理面板 const domainDelta = getDomainFontDelta(); const effectiveDelta = getEffectiveFontDelta(); const domainStatusText = domainDelta !== null ? `+${domainDelta}px (独立设置)` : `+${getGlobalFontDelta()}px (继承通用配置)`; // 菜单 1:设置全局通用放大倍率 GM_registerMenuCommand(`🌐 设置全局通用字号增量 (当前: +${getGlobalFontDelta()}px)`, () => { const input = prompt( `[论坛字号放大器 - 全局配置]\n\n设置未单独配置过论坛的默认放大字号 (单位: px):\n当前全局设置: +${getGlobalFontDelta()}px`, getGlobalFontDelta() ); if (input !== null) { const val = parseInt(input.trim(), 10); if (!isNaN(val) && val > 0) { GM_setValue(CONFIG_KEYS.GLOBAL_DELTA, val); alert(`全局设置成功!通用字号增量已调整为 +${val}px,页面即将刷新。`); window.location.reload(); } else { alert('请输入有效的正整数!'); } } }); // 菜单 2:设置当前网站独立倍率 GM_registerMenuCommand(`🏠 设置当前网站放大增量 (当前: ${domainStatusText})`, () => { const currentVal = domainDelta !== null ? domainDelta : getGlobalFontDelta(); const input = prompt( `[论坛字号放大器 - 站点配置]\n当前域名: ${window.location.hostname}\n\n请输入在此论坛专属的放大字号 (单位: px):\n(若要恢复继承全局配置,请输入 0 或留空)`, currentVal ); if (input !== null) { const trimmed = input.trim(); if (trimmed === '' || trimmed === '0') { // 清除独立设置,恢复继承全局 GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); alert(`已重置当前网站配置,恢复继承全局设置 (+${getGlobalFontDelta()}px)!`); window.location.reload(); } else { const val = parseInt(trimmed, 10); if (!isNaN(val) && val > 0) { GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, val); // 【新增】设置专属增量时,自动激活当前网站的放大功能,提升体验一致性 setDomainEnabled(true); alert(`绑定成功并已自动开启当前网站放大功能!域名 ${window.location.hostname} 专属字号增量已设置为 +${val}px。`); window.location.reload(); } else { alert('请输入有效的正整数(或输入 0 恢复全局设置)!'); } } } }); // 菜单 3:可视化拾取元素 GM_registerMenuCommand('🎯 开启可视化点击拾取区域', startVisualPicker); // 菜单 4:重置当前站点所有规则(选择器 + 专属增量 + 白名单状态) GM_registerMenuCommand('🗑️ 重置当前网站的所有自定义规则', () => { const selector = getDomainSelector(); const delta = getDomainFontDelta(); const enabled = isDomainEnabled(); // 【修改】读取启用状态 if (!selector && delta === null && !enabled) { alert('当前网站没有任何专属自定义设置。'); return; } if (confirm(`确定要重置域名 ${window.location.hostname} 的所有专属配置吗?\n\n已保存选择器: ${selector || '无'}\n已保存增量: ${delta !== null ? '+' + delta + 'px' : '无'}\n网站开启状态: ${enabled ? '已开启' : '未开启'}`)) { GM_setValue(`${CONFIG_KEYS.DOMAIN_SELECTOR_PREFIX}${window.location.hostname}`, ''); GM_setValue(`${CONFIG_KEYS.DOMAIN_DELTA_PREFIX}${window.location.hostname}`, null); setDomainEnabled(false); // 【修改】重置时恢复为默认禁用状态 alert('当前站点配置已彻底清空并已关闭该站点放大功能,页面即将刷新。'); window.location.reload(); } }); // ========================================== // 【修改】菜单项:切换当前网站的启用状态(白名单控制) // ========================================== const isEnabled = isDomainEnabled(); const switchMenuText = isEnabled ? `🔴 关闭当前网站的放大功能 (当前: 已开启)` : `🟢 在当前网站开启放大功能 (当前: 已关闭)`; GM_registerMenuCommand(switchMenuText, () => { if (isEnabled) { setDomainEnabled(false); alert(`已关闭当前网站 (${window.location.hostname}) 的放大功能!页面将恢复原生样式。`); } else { setDomainEnabled(true); alert(`已在当前网站 (${window.location.hostname}) 开启放大功能!`); } window.location.reload(); }); })();