// ==UserScript== // @name ProseFlow Optimizer // @namespace http://tampermonkey.net/ // @version 4.0 // @description 自动将网页中的段落按句子进行物理切分,完美兼容沉浸式翻译等插件。移除悬浮球,改为油猴菜单控制。 // @author Gemini // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @downloadURL https://update.greasyfork.icu/scripts/573407/ProseFlow%20Optimizer.user.js // @updateURL https://update.greasyfork.icu/scripts/573407/ProseFlow%20Optimizer.meta.js // ==/UserScript== (function() { 'use strict'; // 从油猴存储中读取开关状态,默认值为 true (开启) let isEnabled = GM_getValue('isSplitEnabled', true); // 注册油猴脚本菜单 function setupMenu() { const menuText = isEnabled ? "✅ 自动分段: 已开启 (点击关闭)" : "❌ 自动分段: 已关闭 (点击开启)"; GM_registerMenuCommand(menuText, () => { // 切换状态并保存 isEnabled = !isEnabled; GM_setValue('isSplitEnabled', isEnabled); // 因为进行了深度的物理 DOM 替换,直接刷新页面是应用或恢复原状最稳妥的方式 location.reload(); }); } // 核心排版函数:进行 DOM 级别的逐句物理切分 function processParagraphs() { // 如果当前状态是关闭,则不执行任何操作 if (!isEnabled) return; // 选取可能的正文容器或段落 const selectors = 'p, .content, .article-content, .read-content, article div, [id*="content"], [class*="content"]'; const paragraphs = document.querySelectorAll(selectors); paragraphs.forEach(p => { // 安全机制:如果该元素内部还包含其他复杂的块级结构,则跳过 if (p.querySelector('p, div, article, section, table, ul, ol')) return; // 防止重复处理 if (p.getAttribute('data-gp-formatted') === 'true') return; let rawHtml = p.innerHTML; // 正则匹配句子:任意非贪婪字符 + 常见结束标点 + 可选的右侧引号 const sentenceRegex = /([\s\S]*?(?:[。!?!?]+|\.\s|\.$)['"”’]?)/g; // 直接在每个句子匹配项后插入分割标记 let processedHtml = rawHtml.replace(sentenceRegex, function(match) { // 如果匹配到的只是一堆空白或 HTML 标签而没有实质文字,则不分割 let pureText = match.replace(/<[^>]+>/g, '').trim(); if (pureText.length === 0) { return match; } return match + '|||GEMINI_SPLIT_MARKER|||'; }); // 根据标记将 HTML 字符串切分成数组 let htmlChunks = processedHtml.split('|||GEMINI_SPLIT_MARKER|||').filter(chunk => chunk.trim() !== ''); // 如果确实需要切分(数组长度大于1) if (htmlChunks.length > 1) { let fragment = document.createDocumentFragment(); htmlChunks.forEach((chunk, index) => { // 浅拷贝原节点,保留 class 等样式属性 let newNode = p.cloneNode(false); // 防止同一页面出现多个相同的 ID 导致网页原有脚本报错 if (index > 0) { newNode.removeAttribute('id'); } newNode.innerHTML = chunk; newNode.setAttribute('data-gp-formatted', 'true'); // 增加段落间距 newNode.style.marginBottom = '1em'; fragment.appendChild(newNode); }); // 用新的多个节点替换掉原来的单一巨大节点 if (p.parentNode) { p.parentNode.replaceChild(fragment, p); } } else { // 如果没有切分,也打上标记避免重复计算 p.setAttribute('data-gp-formatted', 'true'); } }); } // 初始化脚本 function init() { // 第一步:注册菜单 setupMenu(); // 如果处于关闭状态,直接退出,不监听 DOM 也不处理段落 if (!isEnabled) return; processParagraphs(); // 监听 DOM 变动,支持移动端常见的无限下拉加载 const observer = new MutationObserver((mutations) => { let shouldProcess = false; for (let mutation of mutations) { if (mutation.addedNodes.length > 0) { shouldProcess = true; break; } } if (shouldProcess) { clearTimeout(window.geminiProcessTimer); window.geminiProcessTimer = setTimeout(processParagraphs, 600); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 延迟执行,确保页面加载完毕 window.addEventListener('load', () => { setTimeout(init, 800); }); })();