Notion-Formula-Auto-Conversion-Tool

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

  1. // ==UserScript==
  2. // @name Notion-Formula-Auto-Conversion-Tool
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.6
  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 = true;
  244. let hoverTimer = null;
  245.  
  246. function createPanel() {
  247. panel = document.createElement('div');
  248. panel.id = 'formula-helper';
  249. panel.classList.add('collapsed');
  250. panel.innerHTML = `
  251. <button id="collapse-btn">
  252. <svg viewBox="0 0 24 24">
  253. <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"/>
  254. </svg>
  255. </button>
  256. <div class="content-wrapper">
  257. <button id="convert-btn">🔄 (0)</button>
  258. <div id="progress-container">
  259. <div id="progress-bar"></div>
  260. </div>
  261. <div id="status-text">就绪</div>
  262. </div>
  263. `;
  264. document.body.appendChild(panel);
  265.  
  266. statusText = panel.querySelector('#status-text');
  267. convertBtn = panel.querySelector('#convert-btn');
  268. progressBar = panel.querySelector('#progress-bar');
  269. progressContainer = panel.querySelector('#progress-container');
  270. collapseBtn = panel.querySelector('#collapse-btn');
  271.  
  272. // 添加收起按钮事件
  273. collapseBtn.addEventListener('click', toggleCollapse);
  274.  
  275. // 添加鼠标悬停事件
  276. panel.addEventListener('mouseenter', () => {
  277. clearTimeout(hoverTimer);
  278. if (isCollapsed) {
  279. hoverTimer = setTimeout(() => {
  280. panel.classList.remove('collapsed');
  281. isCollapsed = false;
  282. }, 150); // 减少展开延迟时间
  283. }
  284. });
  285.  
  286. panel.addEventListener('mouseleave', () => {
  287. clearTimeout(hoverTimer);
  288. if (!isCollapsed && !isProcessing) { // 添加处理中状态判断
  289. hoverTimer = setTimeout(() => {
  290. panel.classList.add('collapsed');
  291. isCollapsed = true;
  292. }, 800); // 适当减少收起延迟
  293. }
  294. });
  295. }
  296.  
  297. function toggleCollapse() {
  298. isCollapsed = !isCollapsed;
  299. panel.classList.toggle('collapsed');
  300. }
  301.  
  302. function updateProgress(current, total) {
  303. const percentage = total > 0 ? (current / total) * 100 : 0;
  304. progressBar.style.width = `${percentage}%`;
  305. }
  306.  
  307. const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
  308.  
  309. function updateStatus(text, timeout = 0) {
  310. statusText.textContent = text;
  311. if (timeout) {
  312. setTimeout(() => statusText.textContent = '就绪', timeout);
  313. }
  314. console.log('[状态]', text);
  315. }
  316.  
  317. // 公式查找
  318. function findFormulas(text) {
  319. const formulas = [];
  320. const combinedRegex = /\$\$(.*?)\$\$|\$([^\$\n]+?)\$|\\\((.*?)\\\)/gs;
  321.  
  322. let match;
  323. while ((match = combinedRegex.exec(text)) !== null) {
  324. const [fullMatch, blockFormula, inlineFormula, latexFormula] = match;
  325. const formula = fullMatch;
  326.  
  327. if (formula) {
  328. formulas.push({
  329. formula: fullMatch,
  330. index: match.index
  331. });
  332. }
  333. }
  334.  
  335. return formulas;
  336. }
  337.  
  338. // 操作区域查找
  339. async function findOperationArea() {
  340. const selector = '.notion-overlay-container';
  341. for (let i = 0; i < 5; i++) {
  342. const areas = document.querySelectorAll(selector);
  343. const area = Array.from(areas).find(a =>
  344. a.style.display !== 'none' && a.querySelector('[role="button"]')
  345. );
  346.  
  347. if (area) {
  348. console.log('找到操作区域');
  349. return area;
  350. }
  351. await sleep(50);
  352. }
  353. return null;
  354. }
  355.  
  356. // 按钮查找
  357. async function findButton(area, options = {}) {
  358. const {
  359. buttonText = [],
  360. hasSvg = false,
  361. attempts = 8
  362. } = options;
  363.  
  364. const buttons = area.querySelectorAll('[role="button"]');
  365. const cachedButtons = Array.from(buttons);
  366.  
  367. for (let i = 0; i < attempts; i++) {
  368. const button = cachedButtons.find(btn => {
  369. if (hasSvg && btn.querySelector('svg.equation')) return true;
  370. const text = btn.textContent.toLowerCase();
  371. return buttonText.some(t => text.includes(t));
  372. });
  373.  
  374. if (button) {
  375. return button;
  376. }
  377. await sleep(50);
  378. }
  379. return null;
  380. }
  381.  
  382. // 优化的公式转换
  383. async function convertFormula(editor, formula) {
  384. try {
  385. const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
  386. const textNodes = [];
  387. let node;
  388.  
  389. while (node = walker.nextNode()) {
  390. if (node.textContent.includes(formula)) {
  391. textNodes.unshift(node);
  392. }
  393. }
  394.  
  395. if (!textNodes.length) {
  396. console.warn('未找到匹配的文本');
  397. return;
  398. }
  399.  
  400. const targetNode = textNodes[0];
  401. const startOffset = targetNode.textContent.indexOf(formula);
  402. const range = document.createRange();
  403. range.setStart(targetNode, startOffset);
  404. range.setEnd(targetNode, startOffset + formula.length);
  405.  
  406. const selection = window.getSelection();
  407. selection.removeAllRanges();
  408. selection.addRange(range);
  409.  
  410. targetNode.parentElement.focus();
  411. document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
  412. await sleep(50);
  413.  
  414. const area = await findOperationArea();
  415. if (!area) throw new Error('未找到操作区域');
  416.  
  417. const formulaButton = await findButton(area, {
  418. hasSvg: true,
  419. buttonText: ['equation', '公式', 'math']
  420. });
  421. if (!formulaButton) throw new Error('未找到公式按钮');
  422.  
  423. await simulateClick(formulaButton);
  424. await sleep(50);
  425.  
  426. const doneButton = await findButton(document, {
  427. buttonText: ['done', '完成'],
  428. attempts: 10
  429. });
  430. if (!doneButton) throw new Error('未找到完成按钮');
  431.  
  432. await simulateClick(doneButton);
  433. await sleep(10);
  434.  
  435. return true;
  436. } catch (error) {
  437. console.error('转换公式时出错:', error);
  438. updateStatus(`错误: ${error.message}`);
  439. throw error;
  440. }
  441. }
  442.  
  443. // 优化的主转换函数
  444. async function convertFormulas() {
  445. if (isProcessing) return;
  446. isProcessing = true;
  447. convertBtn.classList.add('processing');
  448.  
  449. try {
  450. formulaCount = 0;
  451. updateStatus('开始扫描文档...');
  452.  
  453. const editors = document.querySelectorAll('[contenteditable="true"]');
  454. console.log('找到编辑区域数量:', editors.length);
  455.  
  456. // 预先收集所有公式
  457. const allFormulas = [];
  458. let totalFormulas = 0;
  459. for (const editor of editors) {
  460. const text = editor.textContent;
  461. const formulas = findFormulas(text);
  462. totalFormulas += formulas.length;
  463. allFormulas.push({ editor, formulas });
  464. }
  465.  
  466. if (totalFormulas === 0) {
  467. updateStatus('未找到需要转换的公式', 3000);
  468. updateProgress(0, 0);
  469. convertBtn.classList.remove('processing');
  470. isProcessing = false;
  471. return;
  472. }
  473.  
  474. updateStatus(`找到 ${totalFormulas} 个公式,开始转换...`);
  475.  
  476. // 从末尾开始处理公式
  477. for (const { editor, formulas } of allFormulas.reverse()) {
  478. for (const { formula } of formulas.reverse()) {
  479. await convertFormula(editor, formula);
  480. formulaCount++;
  481. updateProgress(formulaCount, totalFormulas);
  482. updateStatus(`正在转换... (${formulaCount}/${totalFormulas})`);
  483. }
  484. }
  485.  
  486. updateStatus(`Done:${formulaCount}`, 3000);
  487. convertBtn.textContent = `🔄 (${formulaCount})`;
  488.  
  489. } catch (error) {
  490. console.error('转换过程出错:', error);
  491. updateStatus(`发生错误: ${error.message}`, 5000);
  492. updateProgress(0, 0);
  493. } finally {
  494. isProcessing = false;
  495. convertBtn.classList.remove('processing');
  496.  
  497. setTimeout(() => {
  498. if (!isProcessing) {
  499. updateProgress(0, 0);
  500. }
  501. }, 1000);
  502. }
  503. }
  504.  
  505. // 点击事件模拟
  506. async function simulateClick(element) {
  507. const rect = element.getBoundingClientRect();
  508. const centerX = rect.left + rect.width / 2;
  509. const centerY = rect.top + rect.height / 2;
  510.  
  511. const events = [
  512. new MouseEvent('mousemove', { bubbles: true, clientX: centerX, clientY: centerY }),
  513. new MouseEvent('mouseenter', { bubbles: true, clientX: centerX, clientY: centerY }),
  514. new MouseEvent('mousedown', { bubbles: true, clientX: centerX, clientY: centerY }),
  515. new MouseEvent('mouseup', { bubbles: true, clientX: centerX, clientY: centerY }),
  516. new MouseEvent('click', { bubbles: true, clientX: centerX, clientY: centerY })
  517. ];
  518.  
  519. for (const event of events) {
  520. element.dispatchEvent(event);
  521. await sleep(20);
  522. }
  523. }
  524.  
  525. // 初始化
  526. createPanel();
  527. convertBtn.addEventListener('click', convertFormulas);
  528.  
  529. // 页面加载完成后检查公式数量
  530. setTimeout(() => {
  531. const formulas = findFormulas(document.body.textContent);
  532. if (formulas.length > 0) {
  533. convertBtn.textContent = `🔄(${formulas.length})`;
  534. }
  535. }, 1000);
  536.  
  537. console.log('公式转换工具已加载');
  538. })();

QingJ © 2025

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