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