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
+
+ `;
+ 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/217f2e86-a4d6-4a98-8940-49f24ca90e32
-## 新版本UI:
+## ✨ 特点
-
-
-
转换前:
-

-
-
-
转换中:
-

-
-
-
转换后:
-

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