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