自动将网页中的段落按句子进行物理切分,完美兼容沉浸式翻译等插件。移除悬浮球,改为油猴菜单控制。
// ==UserScript==
// @name ProseFlow Optimizer
// @namespace http://tampermonkey.net/
// @version 4.0
// @description 自动将网页中的段落按句子进行物理切分,完美兼容沉浸式翻译等插件。移除悬浮球,改为油猴菜单控制。
// @author Gemini
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// ==/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);
});
})();