From 02cc92dda2ad9339475d0169ced155922419cc58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=AF=E5=98=89=E9=A2=96?= <568296125@qq.com> Date: Fri, 13 Mar 2026 23:04:03 +0800 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=E9=87=8D=E5=86=99=E5=85=AC?= =?UTF-8?q?=E5=BC=8F=E8=A7=A3=E6=9E=90=E4=B8=8E=E8=BD=AC=E6=8D=A2=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E5=8D=87=E7=BA=A7=E8=87=B3=20v3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公式解析 - 废弃正则整体匹配,改用字符级逐字扫描,解决歧义和越界问题 - 完整支持 $、$$、\(...\)、\[...\] 四种公式格式 - 新增转义字符检测(isEscaped)、行内公式有效性校验 - Token 携带 start/end/lineStart/lineEnd/standaloneBlock 等精确位置信息 行内公式转换 - 通过 getTextSegments + resolveTextPosition 精确映射跨文本节点的选区 - 新增 waitForInlineInput 机制,确认公式输入框已打开再写入, 避免误替换正文内容 - 修复行内公式在新版 Notion 界面下因输入框未就绪导致的失败问题 块公式转换 - 非独立行的块公式自动降级为行内公式(添加 \displaystyle) - 采用 waitForCondition 轮询替代固定 sleep,大幅缩短等待时间 - 移除表格内公式的特殊处理逻辑;getEditableEditors 直接过滤表格单元格 - 删除 retryFailedBlockEquations 重试逻辑,由前置检测保证可靠性 性能与稳定性 - 引入两阶段转换:先处理全部行内公式,再处理块公式,避免位置漂移 - 关键等待时间从数百 ms 降至 10-40 ms 量级 - simulateKey / simulateShortcut 新增 target 参数,事件派发更精准 - 新增 DEBUG_MODE 开关,便于调试时输出详细日志 --- .gitignore | 1 + Notion-Formula-Auto-Conversion-Tool.js | 1584 +++++++++++++++--------- 2 files changed, 1017 insertions(+), 568 deletions(-) diff --git a/.gitignore b/.gitignore index 0fd0354..1277a92 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .history/ +.DS_Store diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 7cfde5d..b34ed59 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1,9 +1,9 @@ // ==UserScript== // @name Notion-Formula-Auto-Conversion-Tool // @namespace http://tampermonkey.net/ -// @version 2.0 +// @version 3.0 // @description 自动公式转换工具 -// @author skyance +// @author skyance、0xstride // @match https://www.notion.so/* // @grant GM_addStyle // @github https://github.com/skyance/Notion-Formula-Auto-Conversion-Tool @@ -12,10 +12,10 @@ // @updateURL https://update.greasyfork.org/scripts/525730/Notion-Formula-Auto-Conversion-Tool.meta.js // ==/UserScript== -(function() { - 'use strict'; +(function () { + "use strict"; - GM_addStyle(` + GM_addStyle(` /* 基础样式 */ #formula-helper { position: fixed; @@ -239,19 +239,24 @@ } `); - // 缓存DOM元素 - let panel, statusText, convertBtn, progressBar, progressContainer, collapseBtn; - let isProcessing = false; - let shouldStop = false; - let formulaCount = 0; - let isCollapsed = true; - let hoverTimer = null; - - function createPanel() { - panel = document.createElement('div'); - panel.id = 'formula-helper'; - panel.classList.add('collapsed'); - panel.innerHTML = ` + // 缓存DOM元素 + let panel, + statusText, + convertBtn, + progressBar, + progressContainer, + collapseBtn; + let isProcessing = false; + let shouldStop = false; + let formulaCount = 0; + let isCollapsed = true; + let hoverTimer = null; + const DEBUG_MODE = false; + function createPanel() { + panel = document.createElement("div"); + panel.id = "formula-helper"; + panel.classList.add("collapsed"); + panel.innerHTML = ` -
- -
-
-
-
就绪
+ M +
+
+
- `; + 0 +
+ `; document.body.appendChild(panel); - statusText = panel.querySelector("#status-text"); - convertBtn = panel.querySelector("#convert-btn"); - progressBar = panel.querySelector("#progress-bar"); - progressContainer = panel.querySelector("#progress-container"); - collapseBtn = panel.querySelector("#collapse-btn"); + progressBar = panel.querySelector(".progress-bar-fill"); + progressText = panel.querySelector(".progress-text"); + + // 自动检测待处理个数 + let lastCount = -1; + const updateCount = () => { + if (isProcessing) return; + const formulas = findFormulas(document.body.textContent || ""); + const count = formulas.length; + if (count !== lastCount) { + lastCount = count; + progressText.textContent = count ? `${count}` : "0"; + } + }; + + // 初始检测 + updateCount(); - // 添加收起按钮事件 - collapseBtn.addEventListener("click", toggleCollapse); + // 监听页面变化(用户编辑、新增公式等) + const observer = new MutationObserver(() => { + updateCount(); + }); + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true + }); - // 添加鼠标悬停事件 + // Hover 逻辑 panel.addEventListener("mouseenter", () => { clearTimeout(hoverTimer); - if (isCollapsed) { - hoverTimer = setTimeout(() => { - panel.classList.remove("collapsed"); - isCollapsed = false; - }, 150); // 减少展开延迟时间 - } + updateCount(); + hoverTimer = setTimeout(() => { + if (!panel.classList.contains("hover")) { + panel.classList.add("hover"); + } + }, 150); }); panel.addEventListener("mouseleave", () => { clearTimeout(hoverTimer); - if (!isCollapsed && !isProcessing) { - // 添加处理中状态判断 - hoverTimer = setTimeout(() => { - panel.classList.add("collapsed"); - isCollapsed = true; - }, 800); // 适当减少收起延迟 - } + if (isProcessing) return; + hoverTimer = setTimeout(() => { + panel.classList.remove("hover"); + }, 800); }); - } - function toggleCollapse() { - isCollapsed = !isCollapsed; - panel.classList.toggle("collapsed"); - } - - function updateProgress(current, total) { - const percentage = total > 0 ? (current / total) * 100 : 0; - progressBar.style.width = `${percentage}%`; + // 点击处理 + panel.addEventListener("click", (e) => { + e.stopPropagation(); + if (isProcessing) { + shouldStop = true; + progressText.textContent = "Stopping…"; + } else { + convertFormulas(); + } + }); } - const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - async function waitForCondition( checkFn, { timeout = 240, interval = 12 } = {}, @@ -332,10 +191,6 @@ } function updateStatus(text, timeout = 0) { - statusText.textContent = text; - if (timeout) { - setTimeout(() => (statusText.textContent = "就绪"), timeout); - } console.log("[状态]", text); } @@ -1090,122 +945,54 @@ } } - // 优化的主转换函数 + // ---------- 核心转换 ---------- async function convertFormulas() { if (isProcessing) return; - isProcessing = true; - shouldStop = false; - convertBtn.classList.add("processing"); - convertBtn.textContent = "取消"; - + isProcessing = true; shouldStop = false; + panel.classList.add("processing"); try { - formulaCount = 0; - updateStatus("开始扫描文档... (按ESC取消)"); - + // 扫描并获取总数 const initialTasks = collectFormulaTasks(); - const totalFormulas = initialTasks.reduce( - (sum, item) => sum + item.formulas.length, - 0, - ); - + let totalFormulas = initialTasks.reduce((sum, item) => sum + item.formulas.length, 0); if (totalFormulas === 0) { - updateStatus("未找到需要转换的公式", 3000); - updateProgress(0, 0); - convertBtn.classList.remove("processing"); - isProcessing = false; + progressText.textContent = "0"; + progressBar.style.width = "0%"; return; } - updateStatus(`找到 ${totalFormulas} 个公式,开始转换...`); + let formulaCount = 0; + updateProgress(0, totalFormulas, "scanning"); const phases = [ - { - name: "行内公式", - getTasks: () => - initialTasks - .map(({ editor, formulas }) => ({ - editor, - formulas: formulas.filter( - (formula) => formula.type === "inline", - ), - })) - .filter((item) => item.formulas.length), - }, - { - name: "块公式", - getTasks: () => - collectFormulaTasks((formula) => formula.type === "block"), - }, + { name: "Inline", getTasks: () => initialTasks.map(({ editor, formulas }) => ({ editor, formulas: formulas.filter(f => f.type === "inline") })).filter(item => item.formulas.length) }, + { name: "Block", getTasks: () => collectFormulaTasks(f => f.type === "block") } ]; for (const phase of phases) { if (shouldStop) break; const phaseTasks = phase.getTasks(); - if (!phaseTasks.length) { - continue; - } - - updateStatus( - `开始转换${phase.name}... (${formulaCount}/${totalFormulas})`, - ); - - // 每个阶段独立处理,避免块级转换破坏后续行内公式定位 for (const { editor, formulas } of phaseTasks.slice().reverse()) { if (shouldStop) break; for (const formulaObj of formulas.slice().reverse()) { if (shouldStop) break; const result = await convertFormula(editor, formulaObj); - if (!result) { - continue; - } - const renderMode = result; - formulaCount++; - updateProgress(formulaCount, totalFormulas); - if ( - formulaCount === 1 || - formulaCount === totalFormulas || - formulaCount % 5 === 0 || - renderMode === "block" - ) { - updateStatus( - `正在转换... (${formulaCount}/${totalFormulas}) [${formulaObj.syntax} -> ${renderMode}]`, - ); - } - if (renderMode === "block") { - await sleep(24); + if (result) { + formulaCount++; + updateProgress(formulaCount, totalFormulas, `${formulaCount}/${totalFormulas}`); } } } } - if (shouldStop) { - updateStatus(`已取消。已完成: ${formulaCount}`, 3000); - } else { - updateStatus(`Done:${formulaCount}`, 3000); - } - - convertBtn.textContent = `🔄 (${formulaCount})`; - - // 转换完成后自动收起面板 - setTimeout(() => { - if (!panel.classList.contains("collapsed")) { - panel.classList.add("collapsed"); - isCollapsed = true; - } - }, 1000); - } catch (error) { - console.error("转换过程出错:", error); - updateStatus(`发生错误: ${error.message}`, 5000); - updateProgress(0, 0); + updateProgress(totalFormulas, totalFormulas, shouldStop ? "Stopped" : "Done"); } finally { isProcessing = false; - convertBtn.classList.remove("processing"); - - setTimeout(() => { - if (!isProcessing) { - updateProgress(0, 0); - } - }, 1000); + panel.classList.remove("processing"); + if (!shouldStop) { + setTimeout(() => { + panel.classList.remove("hover"); + }, 1200); + } } } @@ -1315,30 +1102,14 @@ // 初始化 createPanel(); - convertBtn.addEventListener("click", () => { - if (isProcessing) { - shouldStop = true; - updateStatus("正在取消..."); - } else { - convertFormulas(); - } - }); // 监听ESC键取消 document.addEventListener("keydown", (e) => { if (e.key === "Escape" && isProcessing) { shouldStop = true; - updateStatus("正在取消..."); + progressText.textContent = "Stopping…"; } }); - // 页面加载完成后检查公式数量 - setTimeout(() => { - const formulas = findFormulas(document.body.textContent); - if (formulas.length > 0) { - convertBtn.textContent = `🔄(${formulas.length})`; - } - }, 1000); - - console.log("公式转换工具已加载"); + console.log("Formula hover-to-convert tool loaded"); })(); From 4cf4b8a71968ace8e6ccf558c7fcf803c05a2caa Mon Sep 17 00:00:00 2001 From: Ckrvxr <85326108+Ckrvxr@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:45:51 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20=E6=9C=89=E4=B8=8A=E5=B1=82?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=97=B6=E8=87=AA=E5=8A=A8=E9=9A=90=E8=97=8F?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=94=B9=E5=9B=BE=E6=A0=87=E4=B8=8E=E5=AD=97?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 49 +++++++++++++++++++++----- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 3cb8973..1a7e0af 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -19,7 +19,7 @@ position: fixed; bottom: 82px; right: 16px; - z-index: 999; + z-index: 1; height: 40px; width: 40px; border-radius: 22px; @@ -34,7 +34,7 @@ cursor: pointer; transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); - font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif; + font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; user-select: none; } #formula-helper.hover, @@ -52,7 +52,7 @@ display: flex; align-items: center; justify-content: center; - font-family: 'Lyon-Text', 'Georgia', 'Times New Roman', serif; + font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; font-size: 19px; font-weight: 400; color: rgb(55, 53, 47); @@ -81,7 +81,7 @@ transition: width 0.3s ease; } .progress-text { - font-size: 12px; + font-size: 14px; color: rgba(55, 53, 47, 0.7); font-variant-numeric: tabular-nums; } @@ -204,11 +204,11 @@ const className = typeof element.className === "string" ? element.className - .trim() - .split(/\s+/) - .filter(Boolean) - .slice(0, 4) - .join(".") + .trim() + .split(/\s+/) + .filter(Boolean) + .slice(0, 4) + .join(".") : ""; const classes = className ? `.${className}` : ""; const role = element.getAttribute?.("role"); @@ -1103,6 +1103,37 @@ // 初始化 createPanel(); + // ===== 检测 Notion 侧边栏/设置面板,自动隐藏按钮 ===== + function shouldHide() { + return ( + document.querySelector('.chat_sidebar') || + document.querySelector('.notion-space-settings') || + document.querySelector('.notion-dialog') + ); + } + + const sidebarObserver = new MutationObserver(() => { + const helper = document.getElementById('formula-helper'); + if (!helper) return; + + if (shouldHide()) { + if (helper.style.display !== 'none') { + helper.style.display = 'none'; + } + } else { + if (helper.style.display !== '') { + helper.style.display = ''; + } + } + }); + + sidebarObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class'] + }); + // 监听ESC键取消 document.addEventListener("keydown", (e) => { if (e.key === "Escape" && isProcessing) { From 5688b4ea231a0183ebadd767ff863a896bd22f9f Mon Sep 17 00:00:00 2001 From: Ckrvxr <85326108+Ckrvxr@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:54:55 +0800 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=80=A7?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E5=8F=AA=E5=9C=A8=E9=BC=A0=E6=A0=87=E6=94=BE?= =?UTF-8?q?=E5=9C=A8=E6=8C=89=E9=92=AE=E4=B8=8A=E6=97=B6=E6=89=8D=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E5=85=AC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 1a7e0af..a0cd964 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -134,9 +134,8 @@ // 初始检测 updateCount(); - // 监听页面变化(用户编辑、新增公式等) const observer = new MutationObserver(() => { - updateCount(); + // 仅用于其他 UI 变化检测(如侧边栏显隐),不再触发公式扫描 }); observer.observe(document.body, { childList: true, From d8769459a9753e8d8b8aa3b39ecf9743e8372bf7 Mon Sep 17 00:00:00 2001 From: Ckrvxr <85326108+Ckrvxr@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:41:47 +0800 Subject: [PATCH 06/14] =?UTF-8?q?fix=EF=BC=9A=E4=BB=85=E5=9C=A8=E5=8F=AF?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E9=A1=B5=E9=9D=A2=E6=89=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index a0cd964..71f46d4 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1105,6 +1105,7 @@ // ===== 检测 Notion 侧边栏/设置面板,自动隐藏按钮 ===== function shouldHide() { return ( + !document.querySelector('.layout-content') || document.querySelector('.chat_sidebar') || document.querySelector('.notion-space-settings') || document.querySelector('.notion-dialog') From 7c52a6002547dd10556324e68b46cd998924a7eb Mon Sep 17 00:00:00 2001 From: Ckrvxr <85326108+Ckrvxr@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:52:34 +0800 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20=E6=9B=B4=E6=94=B9=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E9=9A=90=E8=97=8F=E6=A3=80=E6=B5=8B=E4=B8=BA=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E6=98=AF=E5=90=A6=E6=9C=89=E5=88=86=E4=BA=AB=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot --- Notion-Formula-Auto-Conversion-Tool.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 71f46d4..0d927e4 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1102,10 +1102,10 @@ // 初始化 createPanel(); - // ===== 检测 Notion 侧边栏/设置面板,自动隐藏按钮 ===== + // ===== 检测 Notion 侧边栏/设置面板/对话框/其他页面,自动隐藏按钮 ===== function shouldHide() { return ( - !document.querySelector('.layout-content') || + !document.querySelector('.notion-topbar-share-menu') || document.querySelector('.chat_sidebar') || document.querySelector('.notion-space-settings') || document.querySelector('.notion-dialog') From 41f8a158775f84a078f1d564acc31afe7424d203 Mon Sep 17 00:00:00 2001 From: Ckrvxr <85326108+Ckrvxr@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:31:30 +0800 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20=E7=BB=99=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B7=B1=E8=89=B2=E6=A8=A1=E5=BC=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 0d927e4..e2013d1 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Notion-Formula-Auto-Conversion-Tool // @namespace http://tampermonkey.net/ -// @version 3.2.0 +// @version 3.3.0 // @description Notion 自动公式转换工具 // @author skyance、0xstrid、fengjy73、Sparidae、ckrvxr // @match https://www.notion.so/* @@ -85,6 +85,11 @@ color: rgba(55, 53, 47, 0.7); font-variant-numeric: tabular-nums; } + @media (prefers-color-scheme: dark) { + #formula-helper { + background: rgb(211, 211, 211); + } + } `); let panel, progressBar, progressText; From 5cd5e6c73622f526070c4b79d8c2795c0765cf54 Mon Sep 17 00:00:00 2001 From: CocoaDuck Date: Wed, 6 May 2026 17:27:47 +0800 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=9A=84=E6=98=BE=E7=A4=BA=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 161 +++++++++++++------------ 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index e2013d1..5ee9e1a 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Notion-Formula-Auto-Conversion-Tool // @namespace http://tampermonkey.net/ -// @version 3.3.0 +// @version 3.3.1 // @description Notion 自动公式转换工具 // @author skyance、0xstrid、fengjy73、Sparidae、ckrvxr // @match https://www.notion.so/* @@ -15,82 +15,89 @@ "use strict"; GM_addStyle(` - #formula-helper { - position: fixed; - bottom: 82px; - right: 16px; - z-index: 1; - height: 40px; - width: 40px; - border-radius: 22px; - background: #ffffff; - box-shadow: 0px 6px 16px -4px rgba(0, 0, 0, 0.08), - 0px 8px 12px 0px rgba(25,25,25,.027), - 0px 2px 6px 0px rgba(25,25,25,.027), - 0px 0px 0px 1px rgba(42,28,0,.10); - display: flex; - align-items: center; - overflow: hidden; - cursor: pointer; - transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), - border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); - font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; - user-select: none; - } - #formula-helper.hover, - #formula-helper.processing { - width: 200px; - border-radius: 22px; - } - #formula-helper > * { - pointer-events: none; - } - .button-icon { - width: 40px; - height: 40px; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; - font-size: 19px; - font-weight: 400; - color: rgb(55, 53, 47); - line-height: 1; - } - .progress-wrapper { - display: flex; - align-items: center; - flex-grow: 1; - overflow: hidden; - padding-right: 12px; - white-space: nowrap; - } - .progress-bar-container { - flex-grow: 1; - height: 4px; - background: rgba(55, 53, 47, 0.09); - border-radius: 2px; - margin-right: 8px; - } - .progress-bar-fill { - width: 0%; - height: 100%; - background: rgb(35, 131, 226); - border-radius: 2px; - transition: width 0.3s ease; - } - .progress-text { - font-size: 14px; - color: rgba(55, 53, 47, 0.7); - font-variant-numeric: tabular-nums; - } - @media (prefers-color-scheme: dark) { - #formula-helper { - background: rgb(211, 211, 211); - } - } - `); + #formula-helper { + position: absolute; + bottom: 100px; + right: 30px; + z-index: 1; + height: 40px; + width: 40px; + border-radius: 22px; + background: #ffffff; + box-shadow: 0px 6px 16px -4px rgba(0, 0, 0, 0.08), + 0px 8px 12px 0px rgba(25,25,25,.027), + 0px 2px 6px 0px rgba(25,25,25,.027), + 0px 0px 0px 1px rgba(42,28,0,.10); + display: flex; + align-items: center; + overflow: hidden; + cursor: pointer; + transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1), + border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; + font-weight: 700; + user-select: none; + } + #formula-helper.hover, + #formula-helper.processing { + width: 200px; + border-radius: 22px; + transform: scale(1.08); + } + #formula-helper > * { + pointer-events: none; + } + .button-icon { + width: 40px; + height: 40px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; + font-size: 19px; + font-weight: 400; + color: rgb(55, 53, 47); + line-height: 1; + } + .progress-wrapper { + display: flex; + align-items: center; + flex-grow: 1; + overflow: hidden; + padding-right: 12px; + white-space: nowrap; + } + .progress-bar-container { + flex-grow: 1; + height: 4px; + background: rgba(55, 53, 47, 0.09); + border-radius: 2px; + margin-right: 8px; + } + .progress-bar-fill { + width: 0%; + height: 100%; + background: rgb(35, 131, 226); + border-radius: 2px; + transition: width 0.3s ease; + } + .progress-text { + font-size: 14px; + color: rgba(55, 53, 47, 0.7); + font-variant-numeric: tabular-nums; + } + @media (prefers-color-scheme: dark) { + #formula-helper { + background: rgb(211, 211, 211); + } + } + .notion-assistant-corner-origin-container > div[style*="display: flex"] { + inset-inline-end: unset !important; + right: 4px !important; + } + `); let panel, progressBar, progressText; let isProcessing = false; From dd020ea33c70a00c288342c752e21e725a62b2e3 Mon Sep 17 00:00:00 2001 From: CocoaDuck Date: Wed, 6 May 2026 18:31:07 +0800 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20=E6=8A=8A=E5=85=AC=E5=BC=8F?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E6=8D=A2=E4=B8=BA=E6=9B=B4=E5=8A=A0=E5=87=86?= =?UTF-8?q?=E7=A1=AE=E7=9A=84=E6=AD=A3=E5=88=99=E8=A1=A8=E8=BE=BE=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 167 +++---------------------- 1 file changed, 20 insertions(+), 147 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index 5ee9e1a..abcc6fd 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -264,157 +264,30 @@ }; } - function findClosingSingleDollar(text, startIndex) { - for (let i = startIndex; i < text.length; i++) { - if (text[i] === "\n") { - return -1; - } - if (text[i] !== "$" || isEscaped(text, i) || text[i + 1] === "$") { - continue; - } - return i; - } - return -1; - } - - function findClosingDoubleDollar(text, startIndex) { - for (let i = startIndex; i < text.length - 1; i++) { - if (text[i] === "$" && text[i + 1] === "$" && !isEscaped(text, i)) { - return i; - } - } - return -1; - } - - function findClosingLatex(text, startIndex, closeChar) { - for (let i = startIndex; i < text.length - 1; i++) { - if ( - text[i] === "\\" && - text[i + 1] === closeChar && - !isEscaped(text, i) - ) { - return i; - } - } - return -1; - } - - function isValidInlineDollar(text, start, end) { - const content = text.slice(start + 1, end); - if (!content.trim() || /\n/.test(content)) { - return false; - } - - const firstChar = text[start + 1]; - const lastChar = text[end - 1]; - if ( - !firstChar || - !lastChar || - /\s/.test(firstChar) || - /\s/.test(lastChar) - ) { - return false; - } - - return true; - } - - function buildFormulaToken( - text, - start, - end, - openLength, - closeLength, - type, - syntax, - ) { - const boundaries = findLineBoundaries(text, start, end); - return { - raw: text.slice(start, end), - start, - end, - type, - syntax, - content: text.slice(start + openLength, end - closeLength), - lineStart: boundaries.lineStart, - lineEnd: boundaries.lineEnd, - standaloneBlock: type === "block" ? boundaries.standaloneBlock : false, - }; - } - // 公式查找 function findFormulas(text) { const formulas = []; - - for (let i = 0; i < text.length; i++) { - if (text[i] === "$" && !isEscaped(text, i)) { - if (text[i + 1] === "$") { - const closeIndex = findClosingDoubleDollar(text, i + 2); - if (closeIndex !== -1) { - const end = closeIndex + 2; - const token = buildFormulaToken(text, i, end, 2, 2, "block", "$$"); - if (token.content.trim()) { - formulas.push(token); - } - i = end - 1; - continue; - } - } else { - const closeIndex = findClosingSingleDollar(text, i + 1); - if (closeIndex !== -1 && isValidInlineDollar(text, i, closeIndex)) { - const end = closeIndex + 1; - formulas.push(buildFormulaToken(text, i, end, 1, 1, "inline", "$")); - i = end - 1; - continue; - } - } - } - - if (text[i] === "\\" && !isEscaped(text, i)) { - if (text[i + 1] === "(") { - const closeIndex = findClosingLatex(text, i + 2, ")"); - if (closeIndex !== -1) { - const end = closeIndex + 2; - const token = buildFormulaToken( - text, - i, - end, - 2, - 2, - "inline", - "\\(\\)", - ); - if (token.content.trim()) { - formulas.push(token); - } - i = end - 1; - continue; - } - } - - if (text[i + 1] === "[") { - const closeIndex = findClosingLatex(text, i + 2, "]"); - if (closeIndex !== -1) { - const end = closeIndex + 2; - const token = buildFormulaToken( - text, - i, - end, - 2, - 2, - "block", - "\\[\\]", - ); - if (token.content.trim()) { - formulas.push(token); - } - i = end - 1; - continue; - } - } - } + const re = /(?:(\$\$)([\s\S]*?)\1)|(?:\\\[([\s\S]*?)\\\])|(?:\\\(([\s\S]*?)\\\))|(? Date: Wed, 6 May 2026 19:01:52 +0800 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20=E5=8A=A0=E5=85=A5=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=AE=A1=E7=90=86=E5=99=A8=E7=9A=84=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 88 +++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index abcc6fd..a0ab85c 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -6,6 +6,10 @@ // @author skyance、0xstrid、fengjy73、Sparidae、ckrvxr // @match https://www.notion.so/* // @grant GM_addStyle +// @grant GM_registerMenuCommand +// @grant GM_unregisterMenuCommand +// @grant GM_setValue +// @grant GM_getValue // @github https://github.com/skyance/Notion-Formula-Auto-Conversion-Tool // @downloadURL https://update.greasyfork.org/scripts/525730/Notion-Formula-Auto-Conversion-Tool.user.js // @updateURL https://update.greasyfork.org/scripts/525730/Notion-Formula-Auto-Conversion-Tool.meta.js @@ -105,8 +109,34 @@ let hoverTimer = null; const DEBUG_MODE = false; + // ---------- 速度配置 ---------- + const SPEED_PRESETS = { + slow: { label: "慢速", delay: 111 }, + normal: { label: "中速", delay: 11 }, + fast: { label: "快速", delay: 1 }, + custom: { label: "自定义", delay: null }, + }; + + const getDelay = () => { + const speed = GM_getValue("speed", "normal"); + return speed === "custom" ? GM_getValue("customDelay", 30) : SPEED_PRESETS[speed].delay; + }; + // ---------- 工具函数 ---------- - const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, getDelay())); + + // ---------- 菜单状态 ---------- + let totalConverted = GM_getValue("totalConverted", 0); + let totalMenuId = null; + let panelVisible = GM_getValue("panelVisible", true); + + function refreshTotalMenu() { + if (totalMenuId !== null) GM_unregisterMenuCommand(totalMenuId); + totalMenuId = GM_registerMenuCommand( + `📊 累计转换: ${totalConverted} 个公式`, + () => {} + ); + } function updateProgress(current, total, textOverride = null) { const percent = total > 0 ? (current / total) * 100 : 0; @@ -127,6 +157,7 @@
`; document.body.appendChild(panel); + if (!panelVisible) panel.style.display = "none"; progressBar = panel.querySelector(".progress-bar-fill"); progressText = panel.querySelector(".progress-text"); @@ -862,6 +893,8 @@ const result = await convertFormula(editor, formulaObj); if (result) { formulaCount++; + totalConverted++; + GM_setValue("totalConverted", totalConverted); updateProgress(formulaCount, totalFormulas, `${formulaCount}/${totalFormulas}`); } } @@ -869,6 +902,7 @@ } updateProgress(totalFormulas, totalFormulas, shouldStop ? "Stopped" : "Done"); + refreshTotalMenu(); } finally { isProcessing = false; panel.classList.remove("processing"); @@ -987,6 +1021,51 @@ // 初始化 createPanel(); + // ---------- 注册菜单 ---------- + const speedOrder = ["slow", "normal", "fast", "custom"]; + const menuIds = { toggle: null, speed: null }; + + function refreshToggleMenu() { + if (menuIds.toggle !== null) GM_unregisterMenuCommand(menuIds.toggle); + menuIds.toggle = GM_registerMenuCommand(`👀 悬浮按钮: ${panelVisible ? "隐藏" : "显示"}`, () => { + panelVisible = !panelVisible; + GM_setValue("panelVisible", panelVisible); + const helper = document.getElementById("formula-helper"); + if (helper) helper.style.display = panelVisible ? "" : "none"; + refreshToggleMenu(); + }); + } + + function refreshSpeedMenu() { + if (menuIds.speed !== null) GM_unregisterMenuCommand(menuIds.speed); + const speed = GM_getValue("speed", "normal"); + const label = speed === "custom" + ? `自定义(${GM_getValue("customDelay", 30)}ms)` + : SPEED_PRESETS[speed].label; + menuIds.speed = GM_registerMenuCommand(`⚡ 转换速度: ${label}`, () => { + const cur = GM_getValue("speed", "normal"); + const idx = speedOrder.indexOf(cur); + const next = speedOrder[(idx + 1) % speedOrder.length]; + if (next === "custom") { + const input = prompt("请输入自定义延迟(毫秒):", GM_getValue("customDelay", 30)); + const val = parseInt(input, 10); + if (!isNaN(val) && val >= 0) GM_setValue("customDelay", val); + else return; + } + GM_setValue("speed", next); + refreshSpeedMenu(); + }); + } + + GM_registerMenuCommand("🔄 执行公式转换", () => { if (!isProcessing) convertFormulas(); }); + + refreshToggleMenu(); + refreshSpeedMenu(); + + GM_registerMenuCommand("🔗 反馈问题", () => window.open("https://github.com/skyance/Notion-Formula-Auto-Conversion-Tool/issues")); + + refreshTotalMenu(); + // ===== 检测 Notion 侧边栏/设置面板/对话框/其他页面,自动隐藏按钮 ===== function shouldHide() { return ( @@ -1001,6 +1080,13 @@ const helper = document.getElementById('formula-helper'); if (!helper) return; + if (!panelVisible) { + if (helper.style.display !== 'none') { + helper.style.display = 'none'; + } + return; + } + if (shouldHide()) { if (helper.style.display !== 'none') { helper.style.display = 'none'; From 569b640ed368e72a20a1c0356ddb635c48add472 Mon Sep 17 00:00:00 2001 From: CocoaDuck Date: Wed, 6 May 2026 19:06:54 +0800 Subject: [PATCH 12/14] =?UTF-8?q?feat:=20=E6=8F=90=E9=AB=98=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E5=AD=97=E4=BD=93=E5=AD=97=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index a0ab85c..d0dbdc3 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -61,7 +61,7 @@ justify-content: center; font-family: 'Apple Chancery', 'Gabriola', 'Georgia', 'Times New Roman', serif; font-size: 19px; - font-weight: 400; + font-weight: 700; color: rgb(55, 53, 47); line-height: 1; } From dd407286e05759d1761ac9278fc7376e2623ea91 Mon Sep 17 00:00:00 2001 From: CocoaDuck Date: Wed, 6 May 2026 19:12:29 +0800 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E5=BE=85?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=85=AC=E5=BC=8F=E8=AE=A1=E7=AE=97=E7=9A=84?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Notion-Formula-Auto-Conversion-Tool.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Notion-Formula-Auto-Conversion-Tool.js b/Notion-Formula-Auto-Conversion-Tool.js index d0dbdc3..2441c64 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -166,8 +166,10 @@ let lastCount = -1; const updateCount = () => { if (isProcessing) return; - const formulas = findFormulas(document.body.textContent || ""); - const count = formulas.length; + let count = 0; + for (const editor of getEditableEditors()) { + count += findFormulas(editor.textContent).length; + } if (count !== lastCount) { lastCount = count; progressText.textContent = count ? `${count}` : "0"; From b29738b26a513979bd11055e8b6730389483cdfd Mon Sep 17 00:00:00 2001 From: Charles Cao <85326108+Ckrvxr@users.noreply.github.com> Date: Wed, 6 May 2026 20:09:20 +0800 Subject: [PATCH 14/14] =?UTF-8?q?chroe:=20=E6=9B=B4=E6=96=B0=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index e212eeb..fee8c63 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,15 @@ -# Notion公式自动转换工具 ✨ +# Notion公式自动转换工具 -## Demo +## 💻 演示 -![公式转换演示](https://github.com/user-attachments/assets/46c4177d-31cc-4c37-9a26-bbbff2195072) +https://github.com/user-attachments/assets/217f2e86-a4d6-4a98-8940-49f24ca90e32 -## 新版本UI: +## ✨ 特点 -
-
-

转换前:

- -
-
-

转换中:

- -
-
-

转换后:

- -
-
- -## ✨ 功能特点 - -- **智能表格处理**:自动检测表格环境,将块级公式转换为行内公式,避免表格排版错乱。 - **一键批量转换**:点击悬浮按钮即可扫描全文档并自动转换所有公式,解放双手。 - **实时进度反馈**:提供可视化的进度条和状态提示,实时显示转换进度和剩余数量。 -- **自动纠错机制**:内置重试逻辑,自动检测并修复转换失败的块级公式。 -- **优雅的交互体验**: - - 悬浮球设计,支持自动折叠,不遮挡内容。 - - 支持 `ESC` 键随时中断转换。 - - 适配 Mac/Windows 。 +- **悬浮球设计**:悬浮球设计,支持自动折叠,不遮挡内容。 +- **多平台适配**:适配多个平台 ,已测试 Mac/Windows。 ## 🛠️ 一键安装