Notion-Formula-Auto-Conversion-Tool

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

当前为 2025-02-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Notion-Formula-Auto-Conversion-Tool
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description 自动公式转换工具(支持持久化)
  6. // @author YourName
  7. // @match https://www.notion.so/*
  8. // @grant GM_addStyle
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. GM_addStyle(`
  16. /* 基础样式 */
  17. #formula-helper {
  18. position: fixed;
  19. bottom: 90px;
  20. right: 20px;
  21. z-index: 9999;
  22. background: white;
  23. padding: 0;
  24. border-radius: 12px;
  25. box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 30px,
  26. rgba(0, 0, 0, 0.1) 0px 1px 8px;
  27. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  28. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  29. min-width: 200px;
  30. transform-origin: center;
  31. will-change: transform;
  32. overflow: hidden;
  33. }
  34.  
  35. .content-wrapper {
  36. padding: 16px;
  37. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  38. transform-origin: center;
  39. }
  40.  
  41. /* 收起状态 */
  42. #formula-helper.collapsed {
  43. width: 48px;
  44. min-width: 48px;
  45. height: 48px;
  46. padding: 12px;
  47. opacity: 0.9;
  48. transform: scale(0.98);
  49. border-radius: 50%;
  50. }
  51.  
  52. #formula-helper.collapsed .content-wrapper {
  53. opacity: 0;
  54. transform: scale(0.8);
  55. pointer-events: none;
  56. transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  57. }
  58.  
  59. #formula-helper #convert-btn,
  60. #formula-helper #progress-container,
  61. #formula-helper #status-text {
  62. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  63. opacity: 1;
  64. transform: translateY(0);
  65. transform-origin: center;
  66. }
  67.  
  68. /* 收起按钮样式 */
  69. #collapse-btn {
  70. position: absolute;
  71. top: 8px;
  72. right: 8px;
  73. width: 24px;
  74. height: 24px;
  75. border: none;
  76. background: transparent;
  77. cursor: pointer;
  78. padding: 0;
  79. display: flex;
  80. align-items: center;
  81. justify-content: center;
  82. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  83. transform-origin: center;
  84. z-index: 2;
  85. }
  86.  
  87. #collapse-btn:hover {
  88. transform: scale(1.1);
  89. }
  90.  
  91. #collapse-btn:active {
  92. transform: scale(0.95);
  93. }
  94.  
  95. #collapse-btn svg {
  96. width: 16px;
  97. height: 16px;
  98. fill: #4b5563;
  99. transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  100. }
  101.  
  102. #formula-helper.collapsed #collapse-btn {
  103. position: static;
  104. width: 100%;
  105. height: 100%;
  106. }
  107.  
  108. #formula-helper.collapsed #collapse-btn svg {
  109. transform: rotate(180deg);
  110. }
  111.  
  112. @media (hover: hover) {
  113. #formula-helper:not(.collapsed):hover {
  114. transform: translateY(-2px);
  115. box-shadow: rgba(0, 0, 0, 0.15) 0px 15px 35px,
  116. rgba(0, 0, 0, 0.12) 0px 3px 10px;
  117. }
  118.  
  119. #formula-helper.collapsed:hover {
  120. opacity: 1;
  121. transform: scale(1.05);
  122. }
  123. }
  124.  
  125. /* 按钮样式 */
  126. #convert-btn {
  127. background: #2563eb;
  128. color: white;
  129. border: none;
  130. padding: 10px 20px;
  131. border-radius: 6px;
  132. cursor: pointer;
  133. margin-top: 20px;
  134. margin-bottom: 12px;
  135. width: 100%;
  136. font-weight: 500;
  137. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  138. display: flex;
  139. align-items: center;
  140. justify-content: center;
  141. gap: 8px;
  142. position: relative;
  143. overflow: hidden;
  144. }
  145.  
  146. #convert-btn::after {
  147. content: '';
  148. position: absolute;
  149. top: 0;
  150. left: 0;
  151. right: 0;
  152. bottom: 0;
  153. background: rgba(255, 255, 255, 0.1);
  154. opacity: 0;
  155. transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  156. }
  157.  
  158. #convert-btn:hover {
  159. background: #1d4ed8;
  160. transform: translateY(-2px);
  161. box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
  162. }
  163.  
  164. #convert-btn:hover::after {
  165. opacity: 1;
  166. }
  167.  
  168. #convert-btn:active {
  169. transform: translateY(1px);
  170. box-shadow: 0 2px 6px rgba(37, 99, 235, 0.15);
  171. }
  172.  
  173. #convert-btn.processing {
  174. background: #9ca3af;
  175. pointer-events: none;
  176. transform: scale(0.98);
  177. box-shadow: none;
  178. }
  179.  
  180. /* 状态和进度显示 */
  181. #status-text {
  182. font-size: 13px;
  183. color: #4b5563;
  184. margin-bottom: 10px;
  185. line-height: 1.5;
  186. }
  187.  
  188. #progress-container {
  189. background: #e5e7eb;
  190. height: 4px;
  191. border-radius: 2px;
  192. overflow: hidden;
  193. margin-bottom: 15px;
  194. transform-origin: center;
  195. }
  196.  
  197. #progress-bar {
  198. background: #2563eb;
  199. height: 100%;
  200. width: 0%;
  201. transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  202. position: relative;
  203. overflow: hidden;
  204. }
  205.  
  206. #progress-bar::after {
  207. content: '';
  208. position: absolute;
  209. top: 0;
  210. left: 0;
  211. right: 0;
  212. bottom: 0;
  213. background: linear-gradient(
  214. 90deg,
  215. transparent,
  216. rgba(255, 255, 255, 0.3),
  217. transparent
  218. );
  219. animation: progress-shine 1.5s linear infinite;
  220. }
  221.  
  222. @keyframes progress-shine {
  223. 0% { transform: translateX(-100%); }
  224. 100% { transform: translateX(100%); }
  225. }
  226.  
  227. /* 动画效果 */
  228. @keyframes pulse {
  229. 0% { opacity: 1; transform: scale(1); }
  230. 50% { opacity: 0.7; transform: scale(0.98); }
  231. 100% { opacity: 1; transform: scale(1); }
  232. }
  233.  
  234. .processing #status-text {
  235. animation: pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  236. }
  237. `);
  238.  
  239. // 缓存DOM元素
  240. let panel, statusText, convertBtn, progressBar, progressContainer, collapseBtn;
  241. let isProcessing = false;
  242. let formulaCount = 0;
  243. let isCollapsed = false;
  244. let hoverTimer = null;
  245.  
  246. function createPanel() {
  247. panel = document.createElement('div');
  248. panel.id = 'formula-helper';
  249. panel.innerHTML = `
  250. <button id="collapse-btn">
  251. <svg viewBox="0 0 24 24">
  252. <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
  253. </svg>
  254. </button>
  255. <div class="content-wrapper">
  256. <button id="convert-btn">🔄 (0)</button>
  257. <div id="progress-container">
  258. <div id="progress-bar"></div>
  259. </div>
  260. <div id="status-text">就绪</div>
  261. </div>
  262. `;
  263. document.body.appendChild(panel);
  264.  
  265. statusText = panel.querySelector('#status-text');
  266. convertBtn = panel.querySelector('#convert-btn');
  267. progressBar = panel.querySelector('#progress-bar');
  268. progressContainer = panel.querySelector('#progress-container');
  269. collapseBtn = panel.querySelector('#collapse-btn');
  270.  
  271. // 添加收起按钮事件
  272. collapseBtn.addEventListener('click', toggleCollapse);
  273.  
  274. // 添加鼠标悬停事件
  275. panel.addEventListener('mouseenter', () => {
  276. clearTimeout(hoverTimer);
  277. if (isCollapsed) {
  278. hoverTimer = setTimeout(() => {
  279. panel.classList.remove('collapsed');
  280. isCollapsed = false;
  281. }, 150); // 减少展开延迟时间
  282. }
  283. });
  284.  
  285. panel.addEventListener('mouseleave', () => {
  286. clearTimeout(hoverTimer);
  287. if (!isCollapsed && !isProcessing) { // 添加处理中状态判断
  288. hoverTimer = setTimeout(() => {
  289. panel.classList.add('collapsed');
  290. isCollapsed = true;
  291. }, 800); // 适当减少收起延迟
  292. }
  293. });
  294. }
  295.  
  296. function toggleCollapse() {
  297. isCollapsed = !isCollapsed;
  298. panel.classList.toggle('collapsed');
  299. }
  300.  
  301. function updateProgress(current, total) {
  302. const percentage = total > 0 ? (current / total) * 100 : 0;
  303. progressBar.style.width = `${percentage}%`;
  304. }
  305.  
  306. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  307.  
  308. function updateStatus(text, timeout = 0) {
  309. statusText.textContent = text;
  310. if (timeout) {
  311. setTimeout(() => statusText.textContent = '就绪', timeout);
  312. }
  313. console.log('[状态]', text);
  314. }
  315.  
  316. // 公式查找
  317. function findFormulas(text) {
  318. const formulas = [];
  319. const combinedRegex = /\$\$(.*?)\$\$|\$([^\$\n]+?)\$|\\\((.*?)\\\)/gs;
  320.  
  321. let match;
  322. while ((match = combinedRegex.exec(text)) !== null) {
  323. const [fullMatch, blockFormula, inlineFormula, latexFormula] = match;
  324. const formula = fullMatch;
  325.  
  326. if (formula) {
  327. formulas.push({
  328. formula: fullMatch,
  329. index: match.index
  330. });
  331. }
  332. }
  333.  
  334. return formulas;
  335. }
  336.  
  337. // 操作区域查找
  338. async function findOperationArea() {
  339. const selector = '.notion-overlay-container';
  340. for (let i = 0; i < 5; i++) {
  341. const areas = document.querySelectorAll(selector);
  342. const area = Array.from(areas).find(a =>
  343. a.style.display !== 'none' && a.querySelector('[role="button"]')
  344. );
  345.  
  346. if (area) {
  347. console.log('找到操作区域');
  348. return area;
  349. }
  350. await sleep(50);
  351. }
  352. return null;
  353. }
  354.  
  355. // 按钮查找
  356. async function findButton(area, options = {}) {
  357. const {
  358. buttonText = [],
  359. hasSvg = false,
  360. attempts = 8
  361. } = options;
  362.  
  363. const buttons = area.querySelectorAll('[role="button"]');
  364. const cachedButtons = Array.from(buttons);
  365.  
  366. for (let i = 0; i < attempts; i++) {
  367. const button = cachedButtons.find(btn => {
  368. if (hasSvg && btn.querySelector('svg.equation')) return true;
  369. const text = btn.textContent.toLowerCase();
  370. return buttonText.some(t => text.includes(t));
  371. });
  372.  
  373. if (button) {
  374. return button;
  375. }
  376. await sleep(50);
  377. }
  378. return null;
  379. }
  380.  
  381. // 优化的公式转换
  382. async function convertFormula(editor, formula) {
  383. try {
  384. const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
  385. const textNodes = [];
  386. let node;
  387.  
  388. while (node = walker.nextNode()) {
  389. if (node.textContent.includes(formula)) {
  390. textNodes.unshift(node);
  391. }
  392. }
  393.  
  394. if (!textNodes.length) {
  395. console.warn('未找到匹配的文本');
  396. return;
  397. }
  398.  
  399. const targetNode = textNodes[0];
  400. const startOffset = targetNode.textContent.indexOf(formula);
  401. const range = document.createRange();
  402. range.setStart(targetNode, startOffset);
  403. range.setEnd(targetNode, startOffset + formula.length);
  404.  
  405. const selection = window.getSelection();
  406. selection.removeAllRanges();
  407. selection.addRange(range);
  408.  
  409. targetNode.parentElement.focus();
  410. document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
  411. await sleep(50);
  412.  
  413. const area = await findOperationArea();
  414. if (!area) throw new Error('未找到操作区域');
  415.  
  416. const formulaButton = await findButton(area, {
  417. hasSvg: true,
  418. buttonText: ['equation', '公式', 'math']
  419. });
  420. if (!formulaButton) throw new Error('未找到公式按钮');
  421.  
  422. await simulateClick(formulaButton);
  423. await sleep(50);
  424.  
  425. const doneButton = await findButton(document, {
  426. buttonText: ['done', '完成'],
  427. attempts: 10
  428. });
  429. if (!doneButton) throw new Error('未找到完成按钮');
  430.  
  431. await simulateClick(doneButton);
  432. await sleep(10);
  433.  
  434. return true;
  435. } catch (error) {
  436. console.error('转换公式时出错:', error);
  437. updateStatus(`错误: ${error.message}`);
  438. throw error;
  439. }
  440. }
  441.  
  442. // 优化的主转换函数
  443. async function convertFormulas() {
  444. if (isProcessing) return;
  445. isProcessing = true;
  446. convertBtn.classList.add('processing');
  447.  
  448. try {
  449. formulaCount = 0;
  450. updateStatus('开始扫描文档...');
  451.  
  452. const editors = document.querySelectorAll('[contenteditable="true"]');
  453. console.log('找到编辑区域数量:', editors.length);
  454.  
  455. // 预先收集所有公式
  456. const allFormulas = [];
  457. let totalFormulas = 0;
  458. for (const editor of editors) {
  459. const text = editor.textContent;
  460. const formulas = findFormulas(text);
  461. totalFormulas += formulas.length;
  462. allFormulas.push({ editor, formulas });
  463. }
  464.  
  465. if (totalFormulas === 0) {
  466. updateStatus('未找到需要转换的公式', 3000);
  467. updateProgress(0, 0);
  468. convertBtn.classList.remove('processing');
  469. isProcessing = false;
  470. return;
  471. }
  472.  
  473. updateStatus(`找到 ${totalFormulas} 个公式,开始转换...`);
  474.  
  475. // 从末尾开始处理公式
  476. for (const { editor, formulas } of allFormulas.reverse()) {
  477. for (const { formula } of formulas.reverse()) {
  478. await convertFormula(editor, formula);
  479. formulaCount++;
  480. updateProgress(formulaCount, totalFormulas);
  481. updateStatus(`正在转换... (${formulaCount}/${totalFormulas})`);
  482. }
  483. }
  484.  
  485. updateStatus(`Done:${formulaCount}`, 3000);
  486. convertBtn.textContent = `🔄 (${formulaCount})`;
  487.  
  488. } catch (error) {
  489. console.error('转换过程出错:', error);
  490. updateStatus(`发生错误: ${error.message}`, 5000);
  491. updateProgress(0, 0);
  492. } finally {
  493. isProcessing = false;
  494. convertBtn.classList.remove('processing');
  495.  
  496. setTimeout(() => {
  497. if (!isProcessing) {
  498. updateProgress(0, 0);
  499. }
  500. }, 1000);
  501. }
  502. }
  503.  
  504. // 点击事件模拟
  505. async function simulateClick(element) {
  506. const rect = element.getBoundingClientRect();
  507. const centerX = rect.left + rect.width / 2;
  508. const centerY = rect.top + rect.height / 2;
  509.  
  510. const events = [
  511. new MouseEvent('mousemove', { bubbles: true, clientX: centerX, clientY: centerY }),
  512. new MouseEvent('mouseenter', { bubbles: true, clientX: centerX, clientY: centerY }),
  513. new MouseEvent('mousedown', { bubbles: true, clientX: centerX, clientY: centerY }),
  514. new MouseEvent('mouseup', { bubbles: true, clientX: centerX, clientY: centerY }),
  515. new MouseEvent('click', { bubbles: true, clientX: centerX, clientY: centerY })
  516. ];
  517.  
  518. for (const event of events) {
  519. element.dispatchEvent(event);
  520. await sleep(20);
  521. }
  522. }
  523.  
  524. // 初始化
  525. createPanel();
  526. convertBtn.addEventListener('click', convertFormulas);
  527.  
  528. // 页面加载完成后检查公式数量
  529. setTimeout(() => {
  530. const formulas = findFormulas(document.body.textContent);
  531. if (formulas.length > 0) {
  532. convertBtn.textContent = `🔄(${formulas.length})`;
  533. }
  534. }, 1000);
  535.  
  536. console.log('公式转换工具已加载');
  537. })();

QingJ © 2025

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