Notion 公式自动转换工具

从llm复制到notion的公式默认情况下需要自己一个一个转换成公式,用这个脚本可实现自动转换。然而有很多bug。

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

// ==UserScript==
// @name         Notion 公式自动转换工具
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  从llm复制到notion的公式默认情况下需要自己一个一个转换成公式,用这个脚本可实现自动转换。然而有很多bug。
// @author       Huii
// @match        https://www.notion.so/*
// @grant        GM_addStyle
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/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;
        }
    `);

    const 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);

    let isProcessing = false;
    let formulaCount = 0;

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function updateStatus(text, timeout = 0) {
        const status = document.querySelector('#status-text');
        status.textContent = text;
        if (timeout) {
            setTimeout(() => status.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;

        console.log('模拟点击元素:', {
            text: element.textContent,
            class: element.className,
            position: {x: centerX, y: centerY}
        });

        // 移动到元素
        element.dispatchEvent(new MouseEvent('mousemove', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
        await sleep(50);

        // 模拟悬停
        element.dispatchEvent(new MouseEvent('mouseover', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
        element.dispatchEvent(new MouseEvent('mouseenter', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
        await sleep(50);

        // 模拟点击
        element.dispatchEvent(new MouseEvent('mousedown', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
        await sleep(50);

        element.dispatchEvent(new MouseEvent('mouseup', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
        element.dispatchEvent(new MouseEvent('click', {
            bubbles: true,
            clientX: centerX,
            clientY: centerY
        }));
    }

    // 获取所有公式
function findFormulas(text) {
    const formulas = [];
    let startIndex = 0;

    while (startIndex < text.length) {
        // 使用正则表达式查找公式开始位置
        const formulaStarts = [
            {pattern: /\$/, type: 'dollar'},
            {pattern: /\\\(|\(/, type: 'backslash'}
        ];

        let index = -1;
        let type = null;

        for (const {pattern, type: t} of formulaStarts) {
            const match = pattern.exec(text.slice(startIndex));
            if (match) {
                const pos = startIndex + match.index;
                if (index === -1 || pos < index) {
                    index = pos;
                    type = t;
                }
            }
        }

        if (index === -1) break;

        // 检查是否是公式开始
        if (text[index] === '$') {
            // 判断是块级公式还是内联公式
            if (text[index + 1] === '$') {
                // 块级公式,$$...$$
                let endIndex = text.indexOf('$$', index + 2);
                if (endIndex > -1) {
                    const formula = text.substring(index + 2, endIndex);  // 去掉 $$ 符号
                    formulas.push({
                        formula: '$$' + formula + '$$',  // 以 $$ 包裹起来
                        index
                    });
                    startIndex = endIndex + 2;
                    continue;
                }
            } else {
                // 内联公式,$...$
                let endIndex = index + 1;
                while (true) {
                    endIndex = text.indexOf('$', endIndex + 1);
                    if (endIndex === -1) break;

                    // 确保这不是块级公式的开始
                    if (text[endIndex + 1] !== '$') {
                        const formula = text.substring(index + 1, endIndex);  // 去掉 $
                        formulas.push({
                            formula: '$' + formula + '$',  // 转回 $...$ 形式
                            index
                        });
                        startIndex = endIndex + 1;
                        break;
                    }
                }
            }
        } else if (type === 'backslash') {
            let endIndex = -1;
            const formula = text.substring(index);
            const endMatch = /\\?\)/.exec(formula.slice(2));

            if (endMatch) {
                endIndex = index + 2 + endMatch.index;
            }

            if (endIndex > -1) {
                // 将各种格式转换为 $ $ 格式
                let formula = text.substring(index, endIndex + 2);
                formula = formula.replace(/\\?\(/, '$');
                formula = formula.replace(/\\?\)/, '$');
                formulas.push({
                    formula: formula,
                    index
                });
                startIndex = endIndex + 2;
                continue;
            }
        }

        startIndex = index + 1;
    }

    if (formulas.length > 0) {
        console.log('找到公式:', formulas);
    }
    return formulas;
}

    // 查找操作区域(工具栏或弹窗)
    async function findOperationArea() {
        for (let i = 0; i < 10; i++) {
            const areas = Array.from(document.querySelectorAll('.notion-overlay-container'));
            for (const area of areas) {
                if (area.style.display !== 'none' && area.querySelector('[role="button"]')) {
                    console.log('找到操作区域');
                    return area;
                }
            }
            await sleep(100);
        }
        return null;
    }

    // 查找按钮
    async function findButton(area, options = {}) {
        const {
            buttonText = [],
            hasSvg = false,
            attempts = 15
        } = options;

        console.log('开始查找按钮:', {buttonText, hasSvg});

        for (let i = 0; i < attempts; i++) {
            const buttons = Array.from(area.querySelectorAll('[role="button"]'));

            for (const button of buttons) {
                // 检查SVG
                if (hasSvg && button.querySelector('svg.equation')) {
                    console.log('找到带SVG的按钮');
                    return button;
                }

                // 检查文本
                const text = button.textContent.toLowerCase();
                if (buttonText.some(t => text.includes(t))) {
                    console.log('找到文本匹配的按钮:', text);
                    return button;
                }
            }

            await sleep(100);
        }

        return null;
    }

    // 转换单个公式
    async function convertFormula(editor, formula) {
        try {
            console.log('开始处理公式:', formula);

            const walker = document.createTreeWalker(
                editor,
                NodeFilter.SHOW_TEXT,
                null,
                false
            );

            let textNode;
            let targetNode = null;

            while (textNode = walker.nextNode()) {
                if (textNode.textContent.includes(formula)) {
                    targetNode = textNode;
                    break;
                }
            }

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

            // 设置选区
            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(300);

            // 查找操作区域
            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(300);

            // 点击Done按钮
            const doneButton = await findButton(document, {
                buttonText: ['done', '完成'],
                attempts: 20
            });

            if (!doneButton) {
                throw new Error('未找到完成按钮');
            }

            await simulateClick(doneButton);
            await sleep(200);

            console.log('公式转换完成:', formula);
            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);

            for (const editor of editors) {
                const text = editor.textContent;
                console.log('处理编辑区域,文本长度:', text.length);

                const formulas = findFormulas(text);
                console.log('找到公式数量:', formulas.length);

                for (const {formula} of formulas) {
                    await convertFormula(editor, formula);
                    formulaCount++;
                    await sleep(200);
                    updateStatus(`已转换 ${formulaCount} 个公式`);
                }
            }

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

    // 绑定事件
    document.querySelector('#convert-btn').addEventListener('click', convertFormulas);

    // 监听页面变化
    const observer = new MutationObserver(() => {
        if (!isProcessing) {
            document.querySelector('#convert-btn').textContent = '转换公式 (!)';
        }
    });
    observer.observe(document.body, { subtree: true, childList: true });
    console.log('公式转换工具已加载');
})();

QingJ © 2025

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