Notion-Formula-Auto-Conversion-Tool

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

Version vom 05.02.2025. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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