ProseFlow Optimizer

自动将网页中的段落按句子进行物理切分,完美兼容沉浸式翻译等插件。移除悬浮球,改为油猴菜单控制。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();