Notion-Formula-Auto-Conversion-Tool

自动公式转换工具(支持持久化)

目前为 2025-02-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         Notion-Formula-Auto-Conversion-Tool
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  自动公式转换工具(支持持久化)
// @author       YourName
// @match        https://www.notion.so/*
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        #formula-helper {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            background: white;
            padding: 10px;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        }
        #convert-btn {
            background: #37352f;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            margin-bottom: 8px;
        }
        #status-text {
            font-size: 12px;
            color: #666;
            max-width: 200px;
            word-break: break-word;
        }
    `);

    // 缓存DOM元素
    let panel, statusText, convertBtn;

    function createPanel() {
        panel = document.createElement('div');
        panel.id = 'formula-helper';
        panel.innerHTML = `
            <button id="convert-btn">转换公式 (0)</button>
            <div id="status-text">就绪</div>
        `;
        document.body.appendChild(panel);

        statusText = panel.querySelector('#status-text');
        convertBtn = panel.querySelector('#convert-btn');
    }

    let isProcessing = false;
    let formulaCount = 0;

    const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

    function updateStatus(text, timeout = 0) {
        statusText.textContent = text;
        if (timeout) {
            setTimeout(() => statusText.textContent = '就绪', timeout);
        }
        console.log('[状态]', text);
    }

    // 优化的点击事件模拟
    async function simulateClick(element) {
        const rect = element.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        const events = [
            new MouseEvent('mousemove', { bubbles: true, clientX: centerX, clientY: centerY }),
            new MouseEvent('mouseenter', { bubbles: true, clientX: centerX, clientY: centerY }),
            new MouseEvent('mousedown', { bubbles: true, clientX: centerX, clientY: centerY }),
            new MouseEvent('mouseup', { bubbles: true, clientX: centerX, clientY: centerY }),
            new MouseEvent('click', { bubbles: true, clientX: centerX, clientY: centerY })
        ];

        for (const event of events) {
            element.dispatchEvent(event);
            await sleep(20); // 减少延迟时间
        }
    }

    // 优化的公式查找
    function findFormulas(text) {
        const formulas = [];
        const combinedRegex = /(\$\$([\s\S]*?)\$\$|\$([^\$\n]+?)\$|\\\((.*?)\\\))/g;

        let match;
        while ((match = combinedRegex.exec(text)) !== null) {
            const [fullMatch, , blockFormula, inlineFormula, latexFormula] = match;
            const formula = (blockFormula || inlineFormula || latexFormula || '').trim();

            if (formula) {
                formulas.push({
                    formula: fullMatch, // 保持原始格式
                    index: match.index
                });
            }
        }

        return formulas;
    }

    // 优化的操作区域查找
    async function findOperationArea() {
        const selector = '.notion-overlay-container';
        for (let i = 0; i < 5; i++) { // 减少尝试次数
            const areas = document.querySelectorAll(selector);
            const area = Array.from(areas).find(a =>
                a.style.display !== 'none' && a.querySelector('[role="button"]')
            );

            if (area) {
                console.log('找到操作区域');
                return area;
            }
            await sleep(50); // 减少延迟时间
        }
        return null;
    }

    // 优化的按钮查找
    async function findButton(area, options = {}) {
        const {
            buttonText = [],
            hasSvg = false,
            attempts = 8 // 减少尝试次数
        } = options;

        const buttons = area.querySelectorAll('[role="button"]');
        const cachedButtons = Array.from(buttons);

        for (let i = 0; i < attempts; i++) {
            const button = cachedButtons.find(btn => {
                if (hasSvg && btn.querySelector('svg.equation')) return true;
                const text = btn.textContent.toLowerCase();
                return buttonText.some(t => text.includes(t));
            });

            if (button) {
                return button;
            }
            await sleep(50); // 减少延迟时间
        }
        return null;
    }

    // 优化的公式转换
    async function convertFormula(editor, formula) {
        try {
            // 从末尾开始收集文本节点
            const textNodes = [];
            const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
            let node;

            // 先收集所有包含公式的文本节点
            while (node = walker.nextNode()) {
                if (node.textContent.includes(formula)) {
                    textNodes.unshift(node); // 使用 unshift 而不是 push,这样最后的节点会在数组前面
                }
            }

            if (!textNodes.length) {
                console.warn('未找到匹配的文本');
                return;
            }

            // 获取最后添加的文本节点(数组中的第一个)
            const targetNode = textNodes[0];
            const startOffset = targetNode.textContent.indexOf(formula);
            const range = document.createRange();
            range.setStart(targetNode, startOffset);
            range.setEnd(targetNode, startOffset + formula.length);

            const selection = window.getSelection();
            selection.removeAllRanges();
            selection.addRange(range);

            targetNode.parentElement.focus();
            document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
            await sleep(50); // 减少延迟时间

            const area = await findOperationArea();
            if (!area) throw new Error('未找到操作区域');

            const formulaButton = await findButton(area, {
                hasSvg: true,
                buttonText: ['equation', '公式', 'math']
            });
            if (!formulaButton) throw new Error('未找到公式按钮');

            await simulateClick(formulaButton);
            await sleep(50); // 减少延迟时间

            const doneButton = await findButton(document, {
                buttonText: ['done', '完成'],
                attempts: 10
            });
            if (!doneButton) throw new Error('未找到完成按钮');

            await simulateClick(doneButton);
            await sleep(10); // 减少延迟时间

            return true;
        } catch (error) {
            console.error('转换公式时出错:', error);
            updateStatus(`错误: ${error.message}`);
            throw error;
        }
    }

    // 优化的主转换函数
    async function convertFormulas() {
        if (isProcessing) return;
        isProcessing = true;

        try {
            formulaCount = 0;
            updateStatus('开始扫描文档...');

            const editors = document.querySelectorAll('[contenteditable="true"]');
            console.log('找到编辑区域数量:', editors.length);

            // 预先收集所有公式
            const allFormulas = [];
            for (const editor of editors) {
                const text = editor.textContent;
                const formulas = findFormulas(text);
                allFormulas.push({ editor, formulas });
            }

            // 从末尾开始处理公式
            for (const { editor, formulas } of allFormulas.reverse()) {
                // 对每个编辑区域内的公式也从末尾开始处理
                for (const { formula } of formulas.reverse()) {
                    await convertFormula(editor, formula);
                    formulaCount++;
                    updateStatus(`已转换 ${formulaCount} 个公式`);
                }
            }

            updateStatus(`转换完成!共处理 ${formulaCount} 个公式`, 3000);
            convertBtn.textContent = `转换公式 (${formulaCount})`;
        } catch (error) {
            console.error('转换过程出错:', error);
            updateStatus(`发生错误: ${error.message}`, 5000);
        } finally {
            isProcessing = false;
        }
    }

    // 初始化
    createPanel();
    convertBtn.addEventListener('click', convertFormulas);

    // 优化的页面变化监听
    const observer = new MutationObserver(() => {
        if (!isProcessing) {
            convertBtn.textContent = '转换公式 (!)';
        }
    });

    observer.observe(document.body, {
        subtree: true,
        childList: true,
        characterData: true
    });

    console.log('公式转换工具已加载');
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址