Notion-Formula-Auto-Conversion-Tool

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

目前為 2025-02-05 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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('公式转换工具已加载');
})();