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..2441c64 100644 --- a/Notion-Formula-Auto-Conversion-Tool.js +++ b/Notion-Formula-Auto-Conversion-Tool.js @@ -1,866 +1,1119 @@ // ==UserScript== // @name Notion-Formula-Auto-Conversion-Tool // @namespace http://tampermonkey.net/ -// @version 2.0 -// @description 自动公式转换工具 -// @author skyance +// @version 3.3.1 +// @description Notion 自动公式转换工具 +// @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 -// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @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 // ==/UserScript== -(function() { - 'use strict'; - - GM_addStyle(` - /* 基础样式 */ - #formula-helper { - position: fixed; - bottom: 90px; - right: 20px; - z-index: 9999; - background: white; - padding: 0; - border-radius: 12px; - box-shadow: rgba(0, 0, 0, 0.1) 0px 10px 30px, - rgba(0, 0, 0, 0.1) 0px 1px 8px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - min-width: 200px; - transform-origin: center; - will-change: transform; - overflow: hidden; - } - - .content-wrapper { - padding: 16px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - transform-origin: center; - } - - /* 收起状态 */ - #formula-helper.collapsed { - width: 48px; - min-width: 48px; - height: 48px; - padding: 12px; - opacity: 0.9; - transform: scale(0.98); - border-radius: 50%; - } - - #formula-helper.collapsed .content-wrapper { - opacity: 0; - transform: scale(0.8); - pointer-events: none; - transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); - } - - #formula-helper #convert-btn, - #formula-helper #progress-container, - #formula-helper #status-text { - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - opacity: 1; - transform: translateY(0); - transform-origin: center; - } - - /* 收起按钮样式 */ - #collapse-btn { - position: absolute; - top: 8px; - right: 8px; - width: 24px; - height: 24px; - border: none; - background: transparent; - cursor: pointer; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - transform-origin: center; - z-index: 2; - } - - #collapse-btn:hover { - transform: scale(1.1); - } - - #collapse-btn:active { - transform: scale(0.95); - } - - #collapse-btn svg { - width: 16px; - height: 16px; - fill: #4b5563; - transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); - } +(function () { + "use strict"; + + GM_addStyle(` + #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: 700; + 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; + let shouldStop = false; + 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, 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; + progressBar.style.width = `${percent}%`; + progressText.textContent = textOverride || `${current}/${total}`; + } + + function createPanel() { + panel = document.createElement("div"); + panel.id = "formula-helper"; + panel.innerHTML = ` + M +
+
+
+
+ 0 +
+ `; + document.body.appendChild(panel); + if (!panelVisible) panel.style.display = "none"; + + progressBar = panel.querySelector(".progress-bar-fill"); + progressText = panel.querySelector(".progress-text"); + + // 自动检测待处理个数 + let lastCount = -1; + const updateCount = () => { + if (isProcessing) return; + let count = 0; + for (const editor of getEditableEditors()) { + count += findFormulas(editor.textContent).length; + } + if (count !== lastCount) { + lastCount = count; + progressText.textContent = count ? `${count}` : "0"; + } + }; + + // 初始检测 + updateCount(); + + const observer = new MutationObserver(() => { + // 仅用于其他 UI 变化检测(如侧边栏显隐),不再触发公式扫描 + }); + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true + }); - #formula-helper.collapsed #collapse-btn { - position: static; - width: 100%; - height: 100%; + // Hover 逻辑 + panel.addEventListener("mouseenter", () => { + clearTimeout(hoverTimer); + updateCount(); + hoverTimer = setTimeout(() => { + if (!panel.classList.contains("hover")) { + panel.classList.add("hover"); } + }, 150); + }); - #formula-helper.collapsed #collapse-btn svg { - transform: rotate(180deg); - } + panel.addEventListener("mouseleave", () => { + clearTimeout(hoverTimer); + if (isProcessing) return; + hoverTimer = setTimeout(() => { + panel.classList.remove("hover"); + }, 800); + }); - @media (hover: hover) { - #formula-helper:not(.collapsed):hover { - transform: translateY(-2px); - box-shadow: rgba(0, 0, 0, 0.15) 0px 15px 35px, - rgba(0, 0, 0, 0.12) 0px 3px 10px; - } + // 点击处理 + panel.addEventListener("click", (e) => { + e.stopPropagation(); + if (isProcessing) { + shouldStop = true; + progressText.textContent = "Stopping…"; + } else { + convertFormulas(); + } + }); + } + + async function waitForCondition( + checkFn, + { timeout = 240, interval = 12 } = {}, + ) { + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + const result = checkFn(); + if (result) { + return result; + } + await sleep(interval); + } + return null; + } - #formula-helper.collapsed:hover { - opacity: 1; - transform: scale(1.05); - } - } + function updateStatus(text, timeout = 0) { + console.log("[状态]", text); + } - /* 按钮样式 */ - #convert-btn { - background: #2563eb; - color: white; - border: none; - padding: 10px 20px; - border-radius: 6px; - cursor: pointer; - margin-top: 20px; - margin-bottom: 12px; - width: 100%; - font-weight: 500; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - position: relative; - overflow: hidden; - } + function describeElement(element) { + if (!element) { + return "[null]"; + } - #convert-btn::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.1); - opacity: 0; - transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); - } + const tag = element.tagName ? element.tagName.toLowerCase() : "unknown"; + const id = element.id ? `#${element.id}` : ""; + const className = + typeof element.className === "string" + ? element.className + .trim() + .split(/\s+/) + .filter(Boolean) + .slice(0, 4) + .join(".") + : ""; + const classes = className ? `.${className}` : ""; + const role = element.getAttribute?.("role"); + const roleText = role ? `[role="${role}"]` : ""; + const aria = + element.getAttribute?.("aria-roledescription") || + element.getAttribute?.("aria-label") || + ""; + const text = (element.textContent || "") + .replace(/\s+/g, " ") + .trim() + .slice(0, 40); + return `${tag}${id}${classes}${roleText}${aria ? `("${aria}")` : ""}${text ? ` text="${text}"` : ""}`; + } + + function debugLog(...args) { + if (!DEBUG_MODE) { + return; + } + console.log("[FormulaDebug]", ...args); + } - #convert-btn:hover { - background: #1d4ed8; - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); - } + function isEscaped(text, index) { + let slashCount = 0; + for (let i = index - 1; i >= 0 && text[i] === "\\"; i--) { + slashCount++; + } + return slashCount % 2 === 1; + } + + function findLineBoundaries(text, start, end) { + const lineStart = text.lastIndexOf("\n", start - 1) + 1; + const lineEndIndex = text.indexOf("\n", end); + const lineEnd = lineEndIndex === -1 ? text.length : lineEndIndex; + const before = text.slice(lineStart, start).trim(); + const after = text.slice(end, lineEnd).trim(); + + return { + lineStart, + lineEnd, + standaloneBlock: before === "" && after === "", + }; + } + + // 公式查找 + function findFormulas(text) { + const formulas = []; + const re = /(?:(\$\$)([\s\S]*?)\1)|(?:\\\[([\s\S]*?)\\\])|(?:\\\(([\s\S]*?)\\\))|(? segment.start && offset <= segment.end + : offset >= segment.start && offset < segment.end; + + if (inSegment) { + return { + node: segment.node, + offset: Math.min( + segment.node.textContent.length, + offset - segment.start, + ), + }; + } - #progress-container { - background: #e5e7eb; - height: 4px; - border-radius: 2px; - overflow: hidden; - margin-bottom: 15px; - transform-origin: center; - } + if (preferEnd && offset === segment.end) { + return { node: segment.node, offset: segment.node.textContent.length }; + } + } - #progress-bar { - background: #2563eb; - height: 100%; - width: 0%; - transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; - } + const lastSegment = segments[segments.length - 1]; + return { + node: lastSegment.node, + offset: lastSegment.node.textContent.length, + }; + } + + function createRangeFromOffsets(editor, start, end) { + const segments = getTextSegments(editor); + if (!segments.length) { + return null; + } - #progress-bar::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.3), - transparent - ); - animation: progress-shine 1.5s linear infinite; - } + const startPos = resolveTextPosition(segments, start, false); + const endPos = resolveTextPosition(segments, end, true); + if (!startPos || !endPos) { + return null; + } - @keyframes progress-shine { - 0% { transform: translateX(-100%); } - 100% { transform: translateX(100%); } - } + const range = document.createRange(); + range.setStart(startPos.node, startPos.offset); + range.setEnd(endPos.node, endPos.offset); + return range; + } + + async function focusEditor(editor) { + if (!editor) return; + await ensureFocus(editor); + await sleep(10); + } + + async function selectRange(editor, start, end) { + const range = createRangeFromOffsets(editor, start, end); + if (!range) { + return null; + } - /* 动画效果 */ - @keyframes pulse { - 0% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.7; transform: scale(0.98); } - 100% { opacity: 1; transform: scale(1); } - } + await focusEditor(editor); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + await sleep(10); + return range; + } + + function normalizeFormulaContent(content, { useDisplayStyle = false } = {}) { + let normalized = content.trim(); + if ( + useDisplayStyle && + normalized && + !/^\\displaystyle\b/.test(normalized) + ) { + normalized = `\\displaystyle ${normalized}`; + } + return normalized; + } - .processing #status-text { - animation: pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; - } - `); - - // 缓存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 = ` - -
- -
-
-
-
就绪
-
- `; - 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'); - - // 添加收起按钮事件 - collapseBtn.addEventListener('click', toggleCollapse); - - // 添加鼠标悬停事件 - panel.addEventListener('mouseenter', () => { - clearTimeout(hoverTimer); - if (isCollapsed) { - hoverTimer = setTimeout(() => { - panel.classList.remove('collapsed'); - isCollapsed = false; - }, 150); // 减少展开延迟时间 - } - }); + function isSafeInlineInputElement(element, editor) { + if (!element || element === editor) { + return false; + } - panel.addEventListener('mouseleave', () => { - clearTimeout(hoverTimer); - if (!isCollapsed && !isProcessing) { // 添加处理中状态判断 - hoverTimer = setTimeout(() => { - panel.classList.add('collapsed'); - isCollapsed = true; - }, 800); // 适当减少收起延迟 - } - }); + if (element.closest("#formula-helper")) { + return false; } - function toggleCollapse() { - isCollapsed = !isCollapsed; - panel.classList.toggle('collapsed'); + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { + return true; } - function updateProgress(current, total) { - const percentage = total > 0 ? (current / total) * 100 : 0; - progressBar.style.width = `${percentage}%`; + if (!element.isContentEditable) { + return false; } - const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + if (editor.contains(element)) { + return true; + } - function updateStatus(text, timeout = 0) { - statusText.textContent = text; - if (timeout) { - setTimeout(() => statusText.textContent = '就绪', timeout); - } - console.log('[状态]', text); - } - - // 公式查找 - function findFormulas(text) { - const formulas = []; - const combinedRegex = /\$\$(.*?)\$\$|\$([^\$\n]+?)\$|\\\((.*?)\\\)|\\\[(.*?)\\\]/gs; - - let match; - while ((match = combinedRegex.exec(text)) !== null) { - const [fullMatch, blockFormula, inlineFormula, latexFormula, latexBlockFormula] = match; - const formula = fullMatch; - - if (formula) { - // 判断公式类型:块公式(行间)或行内公式 - const isBlockFormula = fullMatch.startsWith('$$') || fullMatch.startsWith('\\['); - formulas.push({ - formula: fullMatch, - index: match.index, - type: isBlockFormula ? 'block' : 'inline', - content: blockFormula || inlineFormula || latexFormula || latexBlockFormula - }); - } - } + return !!element.closest( + '.notion-overlay-container, [role="dialog"], .notion-modal', + ); + } - return formulas; + function selectElementContents(element) { + if (!element) { + return false; } - // 操作区域查找 - 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"]') - ); + element.focus(); - if (area) { - console.log('找到操作区域'); - return area; - } - await sleep(50); - } - return null; + if (typeof element.select === "function") { + element.select(); + return true; } - // 按钮查找 - async function findButton(area, options = {}) { - const { - buttonText = [], - hasSvg = false, - attempts = 8 - } = options; - - const buttons = area.querySelectorAll('[role="button"]'); - const cachedButtons = Array.from(buttons); + if (element.isContentEditable) { + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + return true; + } - 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)); - }); + return false; + } - if (button) { - return button; - } - await sleep(50); - } - return null; - } - - // 文本输入模拟 - async function simulateTyping(text, quick = false) { - const activeElement = document.activeElement; - if (activeElement) { - if (quick) { - // 快速模式:直接插入整段文本 (模拟粘贴) - const inputEvent = new InputEvent('beforeinput', { - bubbles: true, - cancelable: true, - inputType: 'insertText', - data: text - }); - activeElement.dispatchEvent(inputEvent); - - document.execCommand('insertText', false, text); - - const inputEventAfter = new InputEvent('input', { - bubbles: true, - cancelable: false, - inputType: 'insertText', - data: text - }); - activeElement.dispatchEvent(inputEventAfter); - } else { - // 普通模式:逐字输入 (用于触发命令菜单等) - for (const char of text) { - const inputEvent = new InputEvent('beforeinput', { - bubbles: true, - cancelable: true, - inputType: 'insertText', - data: char - }); - activeElement.dispatchEvent(inputEvent); - - document.execCommand('insertText', false, char); - - const inputEventAfter = new InputEvent('input', { - bubbles: true, - cancelable: false, - inputType: 'insertText', - data: char - }); - activeElement.dispatchEvent(inputEventAfter); - - await sleep(5); - } - } - } + function isVisibleElement(element) { + if (!element) { + return false; + } + const style = window.getComputedStyle(element); + if (style.display === "none" || style.visibility === "hidden") { + return false; + } + return element.getClientRects().length > 0; + } + + function findInlineInputCandidate(editor) { + const localSelectors = 'input, textarea, [contenteditable="true"]'; + const globalSelectors = [ + ".notion-overlay-container input", + ".notion-overlay-container textarea", + '.notion-overlay-container [contenteditable="true"]', + '[role="dialog"] input', + '[role="dialog"] textarea', + '[role="dialog"] [contenteditable="true"]', + ".notion-modal input", + ".notion-modal textarea", + '.notion-modal [contenteditable="true"]', + ]; + + const candidates = [ + ...Array.from(editor.querySelectorAll(localSelectors)), + ...Array.from(document.querySelectorAll(globalSelectors.join(","))), + ]; + debugLog("inline input candidates", candidates.map(describeElement)); + return ( + candidates.find( + (candidate) => + isVisibleElement(candidate) && + isSafeInlineInputElement(candidate, editor), + ) || null + ); + } + + async function waitForInlineInput(editor, attempts = 8, interval = 12) { + for (let i = 0; i < attempts; i++) { + const activeElement = document.activeElement; + debugLog(`waitForInlineInput attempt ${i + 1}`, { + activeElement: describeElement(activeElement), + editor: describeElement(editor), + }); + if (isSafeInlineInputElement(activeElement, editor)) { + return activeElement; + } + + const candidate = findInlineInputCandidate(editor); + if (candidate) { + return candidate; + } + + await sleep(interval); + } + return null; + } + + function isSimpleTableCellEditor(editor) { + return ( + !!editor && + !!editor.closest("td, th") && + editor.matches( + '.notion-table-cell-text[contenteditable="true"], [data-content-editable-leaf="true"][contenteditable="true"]', + ) + ); + } + + function getEditableEditors() { + return Array.from( + document.querySelectorAll('[contenteditable="true"]'), + ).filter((editor) => { + const simpleTableCell = isSimpleTableCellEditor(editor); + if (editor.closest("#formula-helper")) { + return false; + } + if (editor.closest('.notion-table-view, [role="gridcell"], [role="cell"]')) { + return false; + } + if (!simpleTableCell && editor.closest('.notion-simple-table-block, td, th')) { + return false; + } + if (!editor.textContent || !editor.textContent.trim()) { + return false; + } + if (editor.getClientRects().length === 0) { + return false; + } + return !editor.querySelector('[contenteditable="true"]'); + }); + } + + function collectFormulaTasks(filterFn = () => true) { + const tasks = []; + + for (const editor of getEditableEditors()) { + const simpleTableCell = isSimpleTableCellEditor(editor); + const formulas = findFormulas(editor.textContent).filter( + (formula) => filterFn(formula) && (!simpleTableCell || formula.type === "inline"), + ); + if (!formulas.length) { + continue; + } + tasks.push({ editor, formulas }); } - // 单个按键模拟 - async function simulateKey(keyName) { - const keyInfo = getKeyCode(keyName); - const keydownEvent = new KeyboardEvent('keydown', { - key: keyInfo.key, - code: keyInfo.code, - keyCode: keyInfo.keyCode, - bubbles: true + return tasks; + } + + function restoreSelection(range) { + if (!range) { + return; + } + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + debugLog("selection restored", { + text: selection.toString(), + anchorNode: selection.anchorNode?.textContent?.slice(0, 60) || null, + }); + } + + async function openInlineEquationEditor(editor, selectedRange = null) { + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + await ensureFocus(editor); + await sleep(isSimpleTableCellEditor(editor) ? 16 : 8); + restoreSelection(selectedRange); + await sleep(isSimpleTableCellEditor(editor) ? 16 : 8); + debugLog("openInlineEquationEditor shortcut mode", { + editor: describeElement(editor), + selectedText: selectedRange?.toString() || "", + activeElement: describeElement(document.activeElement), + }); + await simulateShortcut(isMac ? "Meta+Shift+E" : "Ctrl+Shift+E", document.activeElement || editor); + await sleep(isSimpleTableCellEditor(editor) ? 20 : 10); + return true; + } + + // 文本输入模拟 + async function simulateTyping(text, quick = false) { + const activeElement = document.activeElement; + if (activeElement) { + if (quick) { + // 快速模式:直接插入整段文本 (模拟粘贴) + const inputEvent = new InputEvent("beforeinput", { + bubbles: true, + cancelable: true, + inputType: "insertText", + data: text, }); - const keyupEvent = new KeyboardEvent('keyup', { - key: keyInfo.key, - code: keyInfo.code, - keyCode: keyInfo.keyCode, - bubbles: true + activeElement.dispatchEvent(inputEvent); + + document.execCommand("insertText", false, text); + + const inputEventAfter = new InputEvent("input", { + bubbles: true, + cancelable: false, + inputType: "insertText", + data: text, }); - - document.dispatchEvent(keydownEvent); - await sleep(30); - document.dispatchEvent(keyupEvent); + activeElement.dispatchEvent(inputEventAfter); + } else { + // 普通模式:逐字输入 (用于触发命令菜单等) + for (const char of text) { + const inputEvent = new InputEvent("beforeinput", { + bubbles: true, + cancelable: true, + inputType: "insertText", + data: char, + }); + activeElement.dispatchEvent(inputEvent); + + document.execCommand("insertText", false, char); + + const inputEventAfter = new InputEvent("input", { + bubbles: true, + cancelable: false, + inputType: "insertText", + data: char, + }); + activeElement.dispatchEvent(inputEventAfter); + + await sleep(5); + } + } } + } + + // 单个按键模拟 + async function simulateKey(keyName, target = null) { + const keyInfo = getKeyCode(keyName); + const eventTarget = + target || document.activeElement || document.body || document; + const keydownEvent = new KeyboardEvent("keydown", { + key: keyInfo.key, + code: keyInfo.code, + keyCode: keyInfo.keyCode, + bubbles: true, + }); + const keyupEvent = new KeyboardEvent("keyup", { + key: keyInfo.key, + code: keyInfo.code, + keyCode: keyInfo.keyCode, + bubbles: true, + }); - // 聚焦到目标元素,避免表格单元格或行顺序错位 - async function ensureFocus(element) { - if (!element) return; - element.focus(); - await simulateClick(element); + eventTarget.dispatchEvent(keydownEvent); + await sleep(10); + eventTarget.dispatchEvent(keyupEvent); + } + + // 聚焦到目标元素,避免行顺序错位 + async function ensureFocus(element) { + if (!element) return; + + // 表格单元格需要先激活 td 父元素,才能让内部编辑器真正进入编辑状态 + if (isSimpleTableCellEditor(element)) { + const td = element.closest("td, th"); + if (td && document.activeElement !== element) { + await simulateClick(td); + await sleep(40); + } } - // 检查元素是否在表格内 - function isInTable(element) { - return !!element.closest('.notion-simple-table-block, .notion-table-view, [role="gridcell"], [role="cell"], td, th'); + element.focus(); + await sleep(8); + if (document.activeElement !== element) { + await simulateClick(element); } + } + + // 优化的公式转换 + async function convertFormula(editor, formulaObj) { + try { + let { type, content, start, end, lineStart, lineEnd, standaloneBlock } = + formulaObj; + let renderMode = type; + let useDisplayStyle = false; + + debugLog("convertFormula start", { + editor: describeElement(editor), + type, + syntax: formulaObj.syntax, + start, + end, + content, + }); + + if (type === "block" && !standaloneBlock) { + console.log("检测到非独立行块公式,自动降级为行内公式"); + renderMode = "inline"; + useDisplayStyle = true; + } + + const rangeStart = renderMode === "block" ? lineStart : start; + const rangeEnd = renderMode === "block" ? lineEnd : end; + const range = await selectRange(editor, rangeStart, rangeEnd); + if (!range) { + console.warn("未找到匹配的文本范围"); + return false; + } + + debugLog("convertFormula range selected", { + renderMode, + selectedText: range.toString(), + activeElement: describeElement(document.activeElement), + }); + + const normalizedContent = normalizeFormulaContent(content, { + useDisplayStyle, + }); + + if (renderMode === "block") { + // 块公式:清空整行,再创建 block equation + const originalText = editor.textContent; + document.execCommand("delete"); + await waitForCondition( + () => + editor.textContent !== originalText || + document.activeElement !== editor, + { + timeout: 120, + interval: 10, + }, + ); + + // 重新焦点聚焦,确保光标留在当前块 + await ensureFocus(editor); + await sleep(16); + + // 输入 /block equation 命令 + await simulateTyping("/block equation", true); + await sleep(40); + + // 优先按 Enter 选择命令 + await simulateKey("Enter"); + const blockInput = await waitForInlineInput(editor, 10, 15); + if (!blockInput) { + updateStatus("块公式输入框未打开,已跳过当前公式", 4000); + return false; + } - // 优化的公式转换 - async function convertFormula(editor, formulaObj) { - try { - let { formula, type, content } = formulaObj; - - // 如果在表格内,强制使用行内公式模式(表格内不支持/block equation) - if (type === 'block' && isInTable(editor)) { - console.log('检测到表格内块公式,自动转换为行内模式'); - type = 'inline'; - // 可选:添加 displaystyle 以保持块级显示效果 - // if (!content.trim().startsWith('\\displaystyle')) { - // content = '\\displaystyle ' + content; - // } - } - const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT); - const textNodes = []; - let node; - - while (node = walker.nextNode()) { - if (node.textContent.includes(formula)) { - textNodes.unshift(node); - } - } - - if (!textNodes.length) { - console.warn('未找到匹配的文本'); - return; - } + // 输入公式内容 + if (!selectElementContents(blockInput)) { + updateStatus("无法安全选中块公式输入框,已跳过当前公式", 4000); + return false; + } + await simulateTyping(normalizedContent, true); + await sleep(16); + + // 按 Enter 完成编辑(而非 Escape),避免行序错乱 + await simulateKey("Enter", blockInput); + await waitForCondition(() => document.activeElement !== blockInput, { + timeout: 140, + interval: 10, + }); - 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); - - await ensureFocus(targetNode.parentElement); - await sleep(60); - - if (type === 'block') { - // 块公式:删除选中文本,输入 /block equation 命令 - document.execCommand('delete'); - await sleep(100); - - // 重新焦点聚焦,确保光标在正确位置 - await ensureFocus(targetNode.parentElement); - await sleep(80); - - // 输入 /block equation 命令 - await simulateTyping('/block equation', true); - await sleep(240); - - // 优先按 Enter 选择命令 - await simulateKey('Enter'); - await sleep(100); - - // 清空并输入公式内容(去掉 $$ 符号) - await simulateTyping(content, true); - await sleep(100); - - // 按 Enter 完成编辑(而非 Escape),避免行序错乱 - await simulateKey('Enter'); - await sleep(150); - - // 再次焦点回到原编辑区域,稳定行顺序 - await ensureFocus(targetNode.parentElement); - await sleep(80); - } else { - // 行内公式:使用快捷键 - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; - await ensureFocus(targetNode.parentElement); - await simulateShortcut(isMac ? 'Meta+Shift+E' : 'Ctrl+Shift+E'); - await sleep(100); - - // 清空并输入公式内容(去掉 $ 符号) - document.execCommand('selectAll'); - await sleep(30); - await simulateTyping(content, true); - await sleep(50); - - // 按 Enter 确认 - await simulateKey('Enter'); - await sleep(50); - } + // 再次焦点回到原编辑区域,稳定行顺序 + await ensureFocus(editor); + await sleep(16); + } else { + const opened = await openInlineEquationEditor(editor, range); + if (!opened) { + return false; + } - return true; - } catch (error) { - console.error('转换公式时出错:', error); - updateStatus(`错误: ${error.message}`); - throw error; + // 仅在焦点进入独立公式输入框时继续,避免误伤正文 + const inlineInput = await waitForInlineInput( + editor, + isSimpleTableCellEditor(editor) ? 30 : 8, + isSimpleTableCellEditor(editor) ? 25 : 12, + ); + if (!inlineInput) { + updateStatus( + "行内公式输入框未打开,已跳过当前公式以避免误替换", + 4000, + ); + return false; } - } - // 检测并修复失败的块公式转换 - async function retryFailedBlockEquations() { - try { - updateStatus('扫描未成功转换的公式...'); - - const editors = document.querySelectorAll('[contenteditable="true"]'); - let retryCount = 0; - - for (const editor of editors) { - if (shouldStop) break; - const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT); - const textNodes = []; - let node; - - // 收集所有文本节点 - while (node = walker.nextNode()) { - textNodes.push(node); - } - - // 查找 /block equation - for (let i = 0; i < textNodes.length; i++) { - if (shouldStop) break; - const node = textNodes[i]; - if (node.textContent.includes('/block equation')) { - console.log('找到失败的块公式标记'); - - // 删除 /block equation 文本 - const startOffset = node.textContent.indexOf('/block equation'); - const range = document.createRange(); - range.setStart(node, startOffset); - range.setEnd(node, startOffset + '/block equation'.length); - - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - await ensureFocus(node.parentElement); - await sleep(50); - document.execCommand('delete'); - await sleep(80); - - // 查找该行后面的内容(已无 $$ 格式) - if (i + 1 < textNodes.length) { - const nextNode = textNodes[i + 1]; - const content = nextNode.textContent.trim(); - - if (content && content.length > 0) { - console.log('重新转换失败的块公式,内容:', content); - - // 选中下一行全部内容 - const formulaRange = document.createRange(); - formulaRange.selectNodeContents(nextNode); - - selection.removeAllRanges(); - selection.addRange(formulaRange); - - await ensureFocus(nextNode.parentElement); - await sleep(60); - - // 删除该行内容 - document.execCommand('delete'); - await sleep(80); - - // 重新输入 /block equation 命令 - await simulateTyping('/block equation', true); - await sleep(240); - - // 优先按 Enter 选择命令 - await simulateKey('Enter'); - await sleep(80); - - // 输入公式内容 - await simulateTyping(content, true); - await sleep(80); - - // 按 Escape 完成编辑 - await simulateKey('Escape'); - await sleep(120); - - retryCount++; - updateStatus(`重新转换失败公式... (${retryCount})`); - await sleep(150); - } - } - } - } - } - - if (retryCount > 0) { - updateStatus(`完成修复 ${retryCount} 个失败公式`, 3000); - console.log('修复完成,失败公式数:', retryCount); - } else { - updateStatus('未找到失败的公式', 2000); - } - - return retryCount; - } catch (error) { - console.error('修复失败公式时出错:', error); - updateStatus(`修复出错: ${error.message}`, 3000); - return 0; + if (!selectElementContents(inlineInput)) { + updateStatus("无法安全选中行内公式输入框,已跳过当前公式", 4000); + return false; } - } - // 优化的主转换函数 - async function convertFormulas() { - if (isProcessing) return; - isProcessing = true; - shouldStop = false; - convertBtn.classList.add('processing'); - convertBtn.textContent = '取消'; - - try { - formulaCount = 0; - updateStatus('开始扫描文档... (按ESC取消)'); - - const editors = document.querySelectorAll('[contenteditable="true"]'); - console.log('找到编辑区域数量:', editors.length); - - // 预先收集所有公式 - const allFormulas = []; - let totalFormulas = 0; - for (const editor of editors) { - const text = editor.textContent; - const formulas = findFormulas(text); - totalFormulas += formulas.length; - allFormulas.push({ editor, formulas }); - } + debugLog("inline input ready", { + inlineInput: describeElement(inlineInput), + activeElement: describeElement(document.activeElement), + selectedText: window.getSelection()?.toString() || "", + }); - if (totalFormulas === 0) { - updateStatus('未找到需要转换的公式', 3000); - updateProgress(0, 0); - convertBtn.classList.remove('processing'); - isProcessing = false; - return; - } + await simulateTyping(normalizedContent, true); + await sleep(10); - updateStatus(`找到 ${totalFormulas} 个公式,开始转换...`); - - // 从末尾开始处理公式 - for (const { editor, formulas } of allFormulas.reverse()) { - if (shouldStop) break; - for (const formulaObj of formulas.reverse()) { - if (shouldStop) break; - await convertFormula(editor, formulaObj); - formulaCount++; - updateProgress(formulaCount, totalFormulas); - updateStatus(`正在转换... (${formulaCount}/${totalFormulas}) [${formulaObj.type}]`); - // 给Notion更多时间处理块公式 - if (formulaObj.type === 'block') { - await sleep(150); - } - } - } + debugLog("inline input typed", { + activeElement: describeElement(document.activeElement), + inlineInputText: inlineInput.value || inlineInput.textContent || "", + }); - if (shouldStop) { - updateStatus(`已取消。已完成: ${formulaCount}`, 3000); - } else { - updateStatus(`初始转换完成,开始核对...`); - await sleep(500); - - // 核对并修复失败的块公式转换 - await retryFailedBlockEquations(); - - updateStatus(`Done:${formulaCount}`, 3000); + // 按 Enter 确认 + await simulateKey("Enter", inlineInput); + await waitForCondition(() => document.activeElement !== inlineInput, { + timeout: 140, + interval: 10, + }); + debugLog("inline input confirmed", { + activeElement: describeElement(document.activeElement), + editorText: editor.textContent, + }); + } + + return renderMode; + } catch (error) { + console.error("转换公式时出错:", error); + updateStatus(`错误: ${error.message}`); + throw error; + } + } + + // ---------- 核心转换 ---------- + async function convertFormulas() { + if (isProcessing) return; + isProcessing = true; shouldStop = false; + panel.classList.add("processing"); + try { + // 扫描并获取总数 + const initialTasks = collectFormulaTasks(); + let totalFormulas = initialTasks.reduce((sum, item) => sum + item.formulas.length, 0); + if (totalFormulas === 0) { + progressText.textContent = "0"; + progressBar.style.width = "0%"; + return; + } + + let formulaCount = 0; + updateProgress(0, totalFormulas, "scanning"); + + const phases = [ + { 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(); + 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) { + formulaCount++; + totalConverted++; + GM_setValue("totalConverted", totalConverted); + updateProgress(formulaCount, totalFormulas, `${formulaCount}/${totalFormulas}`); } - - 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); - } finally { - isProcessing = false; - convertBtn.classList.remove('processing'); - - setTimeout(() => { - if (!isProcessing) { - updateProgress(0, 0); - } - }, 1000); + } } + } + + updateProgress(totalFormulas, totalFormulas, shouldStop ? "Stopped" : "Done"); + refreshTotalMenu(); + } finally { + isProcessing = false; + panel.classList.remove("processing"); + if (!shouldStop) { + setTimeout(() => { + panel.classList.remove("hover"); + }, 1200); + } + } + } + + // 点击事件模拟 + 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(8); + } + } + + // 键盘快捷键模拟 + async function simulateShortcut(keyCombination, target = null) { + const keys = keyCombination.split("+"); + const keyEvents = []; + const eventTarget = + target || document.activeElement || document.body || document; + + // 创建键盘事件 + for (const key of keys) { + const keyCode = getKeyCode(key); + + keyEvents.push({ + key: keyCode.key, + code: keyCode.code, + keyCode: keyCode.keyCode, + ctrlKey: keys.includes("Ctrl"), + shiftKey: keys.includes("Shift"), + altKey: keys.includes("Alt"), + metaKey: keys.includes("Meta"), + bubbles: true, + }); } - // 点击事件模拟 - async function simulateClick(element) { - const rect = element.getBoundingClientRect(); - const centerX = rect.left + rect.width / 2; - const centerY = rect.top + rect.height / 2; + // 先按下所有修饰键 + for (let i = 0; i < keyEvents.length - 1; i++) { + const event = keyEvents[i]; + eventTarget.dispatchEvent(new KeyboardEvent("keydown", event)); + } - 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 }) - ]; + // 按下最终按键 + const finalEvent = keyEvents[keyEvents.length - 1]; + eventTarget.dispatchEvent(new KeyboardEvent("keydown", finalEvent)); + eventTarget.dispatchEvent(new KeyboardEvent("keyup", finalEvent)); - for (const event of events) { - element.dispatchEvent(event); - await sleep(20); - } + // 释放所有修饰键 + for (let i = keyEvents.length - 2; i >= 0; i--) { + const event = keyEvents[i]; + eventTarget.dispatchEvent(new KeyboardEvent("keyup", event)); } - // 键盘快捷键模拟 - async function simulateShortcut(keyCombination) { - const keys = keyCombination.split('+'); - const keyEvents = []; - - // 创建键盘事件 - for (const key of keys) { - const isModifier = ['ctrl', 'shift', 'alt', 'meta'].includes(key.toLowerCase()); - const keyCode = getKeyCode(key); - - keyEvents.push({ - key: keyCode.key, - code: keyCode.code, - keyCode: keyCode.keyCode, - ctrlKey: keys.includes('Ctrl'), - shiftKey: keys.includes('Shift'), - altKey: keys.includes('Alt'), - metaKey: keys.includes('Meta'), - bubbles: true - }); - } + await sleep(10); + } + + // 获取键盘按键信息 + function getKeyCode(key) { + const keyMap = { + ctrl: { key: "Control", code: "ControlLeft", keyCode: 17 }, + shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 }, + alt: { key: "Alt", code: "AltLeft", keyCode: 18 }, + meta: { key: "Meta", code: "MetaLeft", keyCode: 91 }, + enter: { key: "Enter", code: "Enter", keyCode: 13 }, + escape: { key: "Escape", code: "Escape", keyCode: 27 }, + e: { key: "e", code: "KeyE", keyCode: 69 }, + }; + + return ( + keyMap[key.toLowerCase()] || { + key: key, + code: `Key${key.toUpperCase()}`, + keyCode: key.toUpperCase().charCodeAt(0), + } + ); + } + + // 初始化 + 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(); + }); + } - // 先按下所有修饰键 - for (let i = 0; i < keyEvents.length - 1; i++) { - const event = keyEvents[i]; - document.dispatchEvent(new KeyboardEvent('keydown', event)); - } + GM_registerMenuCommand("🔄 执行公式转换", () => { if (!isProcessing) convertFormulas(); }); - // 按下最终按键 - const finalEvent = keyEvents[keyEvents.length - 1]; - document.dispatchEvent(new KeyboardEvent('keydown', finalEvent)); - document.dispatchEvent(new KeyboardEvent('keyup', finalEvent)); + refreshToggleMenu(); + refreshSpeedMenu(); - // 释放所有修饰键 - for (let i = keyEvents.length - 2; i >= 0; i--) { - const event = keyEvents[i]; - document.dispatchEvent(new KeyboardEvent('keyup', event)); - } + GM_registerMenuCommand("🔗 反馈问题", () => window.open("https://github.com/skyance/Notion-Formula-Auto-Conversion-Tool/issues")); - await sleep(100); - } + refreshTotalMenu(); - // 获取键盘按键信息 - function getKeyCode(key) { - const keyMap = { - 'ctrl': { key: 'Control', code: 'ControlLeft', keyCode: 17 }, - 'shift': { key: 'Shift', code: 'ShiftLeft', keyCode: 16 }, - 'alt': { key: 'Alt', code: 'AltLeft', keyCode: 18 }, - 'meta': { key: 'Meta', code: 'MetaLeft', keyCode: 91 }, - 'enter': { key: 'Enter', code: 'Enter', keyCode: 13 }, - 'escape': { key: 'Escape', code: 'Escape', keyCode: 27 }, - 'e': { key: 'e', code: 'KeyE', keyCode: 69 } - }; + // ===== 检测 Notion 侧边栏/设置面板/对话框/其他页面,自动隐藏按钮 ===== + function shouldHide() { + return ( + !document.querySelector('.notion-topbar-share-menu') || + document.querySelector('.chat_sidebar') || + document.querySelector('.notion-space-settings') || + document.querySelector('.notion-dialog') + ); + } - return keyMap[key.toLowerCase()] || { key: key, code: `Key${key.toUpperCase()}`, keyCode: key.charCodeAt(0) }; - } + const sidebarObserver = new MutationObserver(() => { + const helper = document.getElementById('formula-helper'); + if (!helper) return; - // 初始化 - createPanel(); - convertBtn.addEventListener('click', () => { - if (isProcessing) { - shouldStop = true; - updateStatus('正在取消...'); - } else { - convertFormulas(); - } - }); - - // 监听ESC键取消 - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && isProcessing) { - shouldStop = true; - updateStatus('正在取消...'); - } - }); + if (!panelVisible) { + if (helper.style.display !== 'none') { + helper.style.display = 'none'; + } + return; + } - // 页面加载完成后检查公式数量 - setTimeout(() => { - const formulas = findFormulas(document.body.textContent); - if (formulas.length > 0) { - convertBtn.textContent = `🔄(${formulas.length})`; - } - }, 1000); + 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) { + shouldStop = true; + progressText.textContent = "Stopping…"; + } + }); - console.log('公式转换工具已加载'); + console.log("Formula hover-to-convert tool loaded"); })(); 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。 ## 🛠️ 一键安装