// ==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 // @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; } `); const panel = document.createElement('div'); panel.id = 'formula-helper'; panel.innerHTML = `
就绪
`; 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('公式转换工具已加载'); })();