您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
LaTeX snippets with full options, visual snippets, matrix shortcuts, brace-args jump (\frac{x}{y} -> Tab selects x/y), env switch, smart fraction, tabout, auto-enlarge, robust CE range replacement. Circle toggle.
// ==UserScript== // @name LaTeX Suite Snippets // @namespace https://nekko-obsidian-latex-suite // @version 0.7.1 // @description LaTeX snippets with full options, visual snippets, matrix shortcuts, brace-args jump (\frac{x}{y} -> Tab selects x/y), env switch, smart fraction, tabout, auto-enlarge, robust CE range replacement. Circle toggle. // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (() => { 'use strict'; /************************************************************************* * 配置 *************************************************************************/ const NON_AUTO_TRIGGER_KEY = 'Tab'; const FRACTION_CMD = "\\frac"; const EXCLUDED_ENVIRONMENTS = [ ["^{", "}"], ["\\pu{", "}"] ]; const BREAKING_CHARS = "+-="; const TABOUT_ENABLED = true; const AUTO_ENLARGE_ENABLED = true; const AUTO_ENLARGE_TRIGGERS = ["\\sum", "\\int", "\\frac", "\\prod"]; const MATRIX_ENV_NAMES = [ "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix", "array", "aligned", "align", "align*", "cases" ]; const ENV_CYCLES = [ ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix"], ["align", "align*", "aligned"], ["array"], ["cases"] ]; /************************************************************************* * 你的 snippets(把你的数组粘贴到这里) *************************************************************************/ const SNIPPETS = [ // Math mode {trigger: "mk", replacement: "$$0$", options: "tA"}, {trigger: "dm", replacement: "$$\n$0\n$$", options: "tAw"}, {trigger: "beg", replacement: "\\begin{$0}\n$1\n\\end{$0}", options: "mA"}, // Dashes // {trigger: "--", replacement: "–", options: "tA"}, // {trigger: "–-", replacement: "—", options: "tA"}, // {trigger: "—-", replacement: "---", options: "tA"}, // Greek letters {trigger: "@a", replacement: "\\alpha", options: "mA"}, {trigger: "@A", replacement: "\\alpha", options: "mA"}, {trigger: "@b", replacement: "\\beta", options: "mA"}, {trigger: "@B", replacement: "\\beta", options: "mA"}, {trigger: "@c", replacement: "\\chi", options: "mA"}, {trigger: "@C", replacement: "\\chi", options: "mA"}, {trigger: "@g", replacement: "\\gamma", options: "mA"}, {trigger: "@G", replacement: "\\Gamma", options: "mA"}, {trigger: "@d", replacement: "\\delta", options: "mA"}, {trigger: "@D", replacement: "\\Delta", options: "mA"}, {trigger: "@e", replacement: "\\epsilon", options: "mA"}, {trigger: "@E", replacement: "\\epsilon", options: "mA"}, {trigger: ":e", replacement: "\\varepsilon", options: "mA"}, {trigger: ":E", replacement: "\\varepsilon", options: "mA"}, {trigger: "@z", replacement: "\\zeta", options: "mA"}, {trigger: "@Z", replacement: "\\zeta", options: "mA"}, {trigger: "@t", replacement: "\\theta", options: "mA"}, {trigger: "@T", replacement: "\\Theta", options: "mA"}, {trigger: "@k", replacement: "\\kappa", options: "mA"}, {trigger: "@K", replacement: "\\kappa", options: "mA"}, {trigger: "@l", replacement: "\\lambda", options: "mA"}, {trigger: "@L", replacement: "\\Lambda", options: "mA"}, {trigger: "@m", replacement: "\\mu", options: "mA"}, {trigger: "@M", replacement: "\\mu", options: "mA"}, {trigger: "@r", replacement: "\\rho", options: "mA"}, {trigger: "@R", replacement: "\\rho", options: "mA"}, {trigger: "@s", replacement: "\\sigma", options: "mA"}, {trigger: "@S", replacement: "\\Sigma", options: "mA"}, {trigger: "ome", replacement: "\\omega", options: "mA"}, {trigger: "@o", replacement: "\\omega", options: "mA"}, {trigger: "@O", replacement: "\\Omega", options: "mA"}, {trigger: "([^\\\\])(${GREEK}|${SYMBOL})", replacement: "[[0]]\\[[1]]", options: "rmA", description: "Add backslash before greek letters and symbols"}, // Insert space after greek letters and symbols, etc {trigger: "\\\\(${GREEK}|${SYMBOL}|${SHORT_SYMBOL})([A-Za-z])", replacement: "\\[[0]] [[1]]", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) sr", replacement: "\\[[0]]^{2}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) cb", replacement: "\\[[0]]^{3}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) rd", replacement: "\\[[0]]^{$0}$1", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) hat", replacement: "\\hat{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) dot", replacement: "\\dot{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) bar", replacement: "\\bar{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) vec", replacement: "\\vec{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) tilde", replacement: "\\tilde{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}|${SYMBOL}) und", replacement: "\\underline{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK}),\\.", replacement: "\\boldsymbol{\\[[0]]}", options: "rmA"}, {trigger: "\\\\(${GREEK})\\.,", replacement: "\\boldsymbol{\\[[0]]}", options: "rmA"}, // Operations {trigger: "te", replacement: "\\text{$0}", options: "m"}, {trigger: "text", replacement: "\\text{$0}", options: "mA"}, {trigger: "bf", replacement: "\\mathbf{$0}", options: "mA"}, {trigger: "sr", replacement: "^{2}", options: "mA"}, {trigger: "cb", replacement: "^{3}", options: "mA"}, {trigger: "rd", replacement: "^{$0}$1", options: "mA"}, {trigger: "_", replacement: "_{$0}$1", options: "mA"}, {trigger: "sts", replacement: "_\\text{$0}", options: "rmA"}, {trigger: "sq", replacement: "\\sqrt{ $0 }$1", options: "mA"}, {trigger: "//", replacement: "\\frac{$0}{$1}$2", options: "mA"}, {trigger: "ee", replacement: "e^{ $0 }$1", options: "mA"}, {trigger: "rm", replacement: "\\mathrm{$0}$1", options: "mA"}, {trigger: "conj", replacement: "^{*}", options: "mA"}, {trigger: "trace", replacement: "\\mathrm{Tr}", options: "mA"}, {trigger: "det", replacement: "\\det", options: "mA"}, {trigger: "re", replacement: "\\mathrm{Re}", options: "mA"}, {trigger: "im", replacement: "\\mathrm{Im}", options: "mA"}, {trigger: "([a-zA-Z]),\\.", replacement: "\\mathbf{[[0]]}", options: "rmA"}, {trigger: "([a-zA-Z])\\.,", replacement: "\\mathbf{[[0]]}", options: "rmA"}, {trigger: "([A-Za-z])(\\d)", replacement: "[[0]]_{[[1]]}", options: "rmA", description: "Auto letter subscript", priority: -1}, {trigger: "([A-Za-z])_(\\d\\d)", replacement: "[[0]]_{[[1]]}", options: "rmA"}, {trigger: "\\hat{([A-Za-z])}(\\d)", replacement: "hat{[[0]]}_{[[1]]}", options: "rmA"}, {trigger: "\\\\mathbf{([A-Za-z])}(\\d)", replacement: "\\mathbf{[[0]]}_{[[1]]}", options: "rmA"}, {trigger: "\\\\vec{([A-Za-z])}(\\d)", replacement: "\\vec{[[0]]}_{[[1]]}", options: "rmA"}, {trigger: "([a-zA-Z])bar", replacement: "\\bar{[[0]]}", options: "rmA"}, {trigger: "([a-zA-Z])hat", replacement: "\\hat{[[0]]}", options: "rmA"}, {trigger: "([a-zA-Z])ddot", replacement: "\\ddot{[[0]]}", options: "rmA", priority: 3}, {trigger: "([a-zA-Z])dot", replacement: "\\dot{[[0]]}", options: "rmA", priority: 1}, {trigger: "([a-zA-Z])vec", replacement: "\\vec{[[0]]}", options: "rmA"}, {trigger: "([a-zA-Z])tilde", replacement: "\\tilde{[[0]]}", options: "rmA"}, {trigger: "([a-zA-Z])und", replacement: "\\underline{[[0]]}", options: "rmA"}, {trigger: "bar", replacement: "\\bar{$0}$1", options: "mA"}, {trigger: "hat", replacement: "\\hat{$0}$1", options: "mA"}, {trigger: "dot", replacement: "\\dot{$0}$1", options: "mA"}, {trigger: "ddot", replacement: "\\ddot{$0}$1", options: "mA", priority: 2}, {trigger: "cdot", replacement: "\\cdot", options: "mA", priority: 2}, {trigger: "vec", replacement: "\\vec{$0}$1", options: "mA"}, {trigger: "tilde", replacement: "\\tilde{$0}$1", options: "mA"}, {trigger: "und", replacement: "\\underline{$0}$1", options: "mA"}, {trigger: "([^\\\\])(arcsin|arccos|arctan|arccot|arccsc|arcsec|sin|cos|tan|cot|csc|sec)", replacement: "[[0]]\\[[1]]", options: "rmA"}, {trigger: "\\\\(arcsin|arccos|arctan|arccot|arccsc|arcsec|sin|cos|tan|cot|csc|sec)([A-Za-gi-z])", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after trig funcs. Skips letter "h" to allow sinh, cosh, etc. {trigger: "\\\\(arcsinh|arccosh|arctanh|arccoth|arcsch|arcsech|sinh|cosh|tanh|coth|csch|sech)([A-Za-z])", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after trig funcs {trigger: "\\\\(neq|geq|leq|gg|ll|sim)([0-9]+)", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after inequality symbols // Visual operations {trigger: "U", replacement: "\\underbrace{ ${VISUAL} }_{ $0 }", options: "mA"}, {trigger: "B", replacement: "\\underset{ $0 }{ ${VISUAL} }", options: "mA"}, {trigger: "C", replacement: "\\cancel{ ${VISUAL} }", options: "mA"}, {trigger: "K", replacement: "\\cancelto{ $0 }{ ${VISUAL} }", options: "mA"}, {trigger: "S", replacement: "\\sqrt{ ${VISUAL} }", options: "mA"}, // Symbols {trigger: "ooo", replacement: "\\infty", options: "mA"}, {trigger: "sum", replacement: "\\sum", options: "mA"}, {trigger: "prod", replacement: "\\prod", options: "mA"}, {trigger: "lim", replacement: "\\lim_{ ${0:n} \\to ${1:\\infty} } $2", options: "mA"}, {trigger: "([^\\\\])pm", replacement: "[[0]]\\pm", options: "rm"}, {trigger: "([^\\\\])mp", replacement: "[[0]]\\mp", options: "rm"}, {trigger: "+-", replacement: "\\pm", options: "mA"}, {trigger: "-+", replacement: "\\mp", options: "mA"}, {trigger: "...", replacement: "\\dots", options: "mA"}, {trigger: "<->", replacement: "\\leftrightarrow ", options: "mA"}, {trigger: "->", replacement: "\\to", options: "mA"}, {trigger: "!>", replacement: "\\mapsto", options: "mA"}, {trigger: "invs", replacement: "^{-1}", options: "mA"}, {trigger: "\\\\\\", replacement: "\\setminus", options: "mA"}, {trigger: "||", replacement: "\\mid", options: "mA"}, {trigger: "and", replacement: "\\cap", options: "mA"}, {trigger: "orr", replacement: "\\cup", options: "mA"}, {trigger: "inn", replacement: "\\in", options: "mA"}, {trigger: "\\subset eq", replacement: "\\subseteq", options: "mA"}, {trigger: "set", replacement: "\\{ $0 \\}$1", options: "mA"}, {trigger: "=>", replacement: "\\implies", options: "mA"}, {trigger: "=<", replacement: "\\impliedby", options: "mA"}, {trigger: "iff", replacement: "\\iff", options: "mA"}, {trigger: "e\\xi sts", replacement: "\\exists", options: "mA", priority: 1}, {trigger: "===", replacement: "\\equiv", options: "mA"}, {trigger: "Sq", replacement: "\\square", options: "mA"}, {trigger: "!=", replacement: "\\neq", options: "mA"}, {trigger: ">=", replacement: "\\geq", options: "mA"}, {trigger: "<=", replacement: "\\leq", options: "mA"}, {trigger: ">>", replacement: "\\gg", options: "mA"}, {trigger: "<<", replacement: "\\ll", options: "mA"}, {trigger: "~~", replacement: "\\sim", options: "mA"}, {trigger: "\\sim ~", replacement: "\\approx", options: "mA"}, {trigger: "prop", replacement: "\\propto", options: "mA"}, {trigger: "nabl", replacement: "\\nabla", options: "mA"}, {trigger: "del", replacement: "\\nabla", options: "mA"}, {trigger: "xx", replacement: "\\times", options: "mA"}, {trigger: "**", replacement: "\\cdot", options: "mA"}, {trigger: "para", replacement: "\\parallel", options: "mA"}, {trigger: "xnn", replacement: "x_{n}", options: "mA"}, {trigger: "xii", replacement: "x_{i}", options: "mA"}, {trigger: "xjj", replacement: "x_{j}", options: "mA"}, {trigger: "xp1", replacement: "x_{n+1}", options: "mA"}, {trigger: "ynn", replacement: "y_{n}", options: "mA"}, {trigger: "yii", replacement: "y_{i}", options: "mA"}, {trigger: "yjj", replacement: "y_{j}", options: "mA"}, {trigger: "mcal", replacement: "\\mathcal{$0}$1", options: "mA"}, {trigger: "mbb", replacement: "\\mathbb{$0}$1", options: "mA"}, {trigger: "ell", replacement: "\\ell", options: "mA"}, {trigger: "lll", replacement: "\\ell", options: "mA"}, {trigger: "LL", replacement: "\\mathcal{L}", options: "mA"}, {trigger: "HH", replacement: "\\mathcal{H}", options: "mA"}, {trigger: "CC", replacement: "\\mathbb{C}", options: "mA"}, {trigger: "RR", replacement: "\\mathbb{R}", options: "mA"}, {trigger: "ZZ", replacement: "\\mathbb{Z}", options: "mA"}, {trigger: "NN", replacement: "\\mathbb{N}", options: "mA"}, {trigger: "II", replacement: "\\mathbb{1}", options: "mA"}, {trigger: "\\mathbb{1}I", replacement: "\\hat{\\mathbb{1}}", options: "mA"}, {trigger: "AA", replacement: "\\mathcal{A}", options: "mA"}, {trigger: "BB", replacement: "\\mathbf{B}", options: "mA"}, {trigger: "EE", replacement: "\\mathbf{E}", options: "mA"}, // Unit vectors {trigger: ":i", replacement: "\\mathbf{i}", options: "mA"}, {trigger: ":j", replacement: "\\mathbf{j}", options: "mA"}, {trigger: ":k", replacement: "\\mathbf{k}", options: "mA"}, {trigger: ":x", replacement: "\\hat{\\mathbf{x}}", options: "mA"}, {trigger: ":y", replacement: "\\hat{\\mathbf{y}}", options: "mA"}, {trigger: ":z", replacement: "\\hat{\\mathbf{z}}", options: "mA"}, // Derivatives {trigger: "par", replacement: "\\frac{ \\partial ${0:y} }{ \\partial ${1:x} } $2", options: "m"}, {trigger: "pa2", replacement: "\\frac{ \\partial^{2} ${0:y} }{ \\partial ${1:x}^{2} } $2", options: "mA"}, {trigger: "pa3", replacement: "\\frac{ \\partial^{3} ${0:y} }{ \\partial ${1:x}^{3} } $2", options: "mA"}, {trigger: "pa([A-Za-z])([A-Za-z])", replacement: "\\frac{ \\partial [[0]] }{ \\partial [[1]] } ", options: "rm"}, {trigger: "pa([A-Za-z])([A-Za-z])([A-Za-z])", replacement: "\\frac{ \\partial^{2} [[0]] }{ \\partial [[1]] \\partial [[2]] } ", options: "rm"}, {trigger: "pa([A-Za-z])([A-Za-z])2", replacement: "\\frac{ \\partial^{2} [[0]] }{ \\partial [[1]]^{2} } ", options: "rmA"}, {trigger: "de([A-Za-z])([A-Za-z])", replacement: "\\frac{ d[[0]] }{ d[[1]] } ", options: "rm"}, {trigger: "de([A-Za-z])([A-Za-z])2", replacement: "\\frac{ d^{2}[[0]] }{ d[[1]]^{2} } ", options: "rmA"}, {trigger: "ddt", replacement: "\\frac{d}{dt} ", options: "mA"}, // Integrals {trigger: "oinf", replacement: "\\int_{0}^{\\infty} $0 \\, d${1:x} $2", options: "mA"}, {trigger: "infi", replacement: "\\int_{-\\infty}^{\\infty} $0 \\, d${1:x} $2", options: "mA"}, {trigger: "dint", replacement: "\\int_{${0:0}}^{${1:\\infty}} $2 \\, d${3:x} $4", options: "mA"}, {trigger: "oint", replacement: "\\oint", options: "mA"}, {trigger: "iiint", replacement: "\\iiint", options: "mA"}, {trigger: "iint", replacement: "\\iint", options: "mA"}, {trigger: "int", replacement: "\\int $0 \\, d${1:x} $2", options: "mA"}, // Physics {trigger: "kbt", replacement: "k_{B}T", options: "mA"}, // Quantum mechanics {trigger: "hba", replacement: "\\hbar", options: "mA"}, {trigger: "dag", replacement: "^{\\dagger}", options: "mA"}, {trigger: "o+", replacement: "\\oplus ", options: "mA"}, {trigger: "ox", replacement: "\\otimes ", options: "mA"}, {trigger: "ot\\mathrm{Im}es", replacement: "\\otimes ", options: "mA"}, // Handle conflict with "im" snippet {trigger: "bra", replacement: "\\bra{$0} $1", options: "mA"}, {trigger: "ket", replacement: "\\ket{$0} $1", options: "mA"}, {trigger: "brk", replacement: "\\braket{ $0 | $1 } $2", options: "mA"}, {trigger: "\\\\bra{([^|]+)\\|", replacement: "\\braket{ [[0]] | $0 ", options: "rmA", description: "Convert bra into braket"}, {trigger: "\\\\bra{(.+)}([^ ]+)>", replacement: "\\braket{ [[0]] | $0 ", options: "rmA", description: "Convert bra into braket (alternate)"}, {trigger: "outp", replacement: "\\ket{${0:\\psi}} \\bra{${0:\\psi}} $1", options: "mA"}, // Chemistry {trigger: "pu", replacement: "\\pu{ $0 }", options: "mA"}, {trigger: "msun", replacement: "M_{\\odot}", options: "mA"}, {trigger: "solm", replacement: "M_{\\odot}", options: "mA"}, {trigger: "ce", replacement: "\\ce{ $0 }", options: "mA"}, {trigger: "iso", replacement: "{}^{${0:4}}_{${1:2}}${2:He}", options: "mA"}, {trigger: "hel4", replacement: "{}^{4}_{2}He ", options: "mA"}, {trigger: "hel3", replacement: "{}^{3}_{2}He ", options: "mA"}, // Environments {trigger: "pmat", replacement: "\\begin{pmatrix}\n$0\n\\end{pmatrix}", options: "mA"}, {trigger: "bmat", replacement: "\\begin{bmatrix}\n$0\n\\end{bmatrix}", options: "mA"}, {trigger: "Bmat", replacement: "\\begin{Bmatrix}\n$0\n\\end{Bmatrix}", options: "mA"}, {trigger: "vmat", replacement: "\\begin{vmatrix}\n$0\n\\end{vmatrix}", options: "mA"}, {trigger: "Vmat", replacement: "\\begin{Vmatrix}\n$0\n\\end{Vmatrix}", options: "mA"}, {trigger: "case", replacement: "\\begin{cases}\n$0\n\\end{cases}", options: "mA"}, {trigger: "align", replacement: "\\begin{align}\n$0\n\\end{align}", options: "mA"}, {trigger: "array", replacement: "\\begin{array}\n$0\n\\end{array}", options: "mA"}, {trigger: "matrix", replacement: "\\begin{matrix}\n$0\n\\end{matrix}", options: "mA"}, // Brackets {trigger: "avg", replacement: "\\langle $0 \\rangle $1", options: "mA"}, {trigger: "norm", replacement: "\\lvert $0 \\rvert $1", options: "mA", priority: 1}, {trigger: "mod", replacement: "|$0|$1", options: "mA"}, {trigger: "(", replacement: "(${VISUAL})", options: "mA"}, {trigger: "[", replacement: "[${VISUAL}]", options: "mA"}, {trigger: "{", replacement: "{${VISUAL}}", options: "mA"}, {trigger: "(", replacement: "($0)$1", options: "mA"}, {trigger: "{", replacement: "{$0}$1", options: "mA"}, {trigger: "[", replacement: "[$0]$1", options: "mA"}, {trigger: "lr(", replacement: "\\left( $0 \\right) $1", options: "mA"}, {trigger: "lr|", replacement: "\\left| $0 \\right| $1", options: "mA"}, {trigger: "lr{", replacement: "\\left\\{ $0 \\right\\} $1", options: "mA"}, {trigger: "lr[", replacement: "\\left[ $0 \\right] $1", options: "mA"}, {trigger: "lra", replacement: "\\left< $0 \\right> $1", options: "mA"}, // Misc {trigger: "tayl", replacement: "${0:f}(${1:x} + ${2:h}) = ${0:f}(${1:x}) + ${0:f}'(${1:x})${2:h} + ${0:f}''(${1:x}) \\frac{${2:h}^{2}}{2!} + \\dots$3", options: "mA"}, ]; /************************************************************************* * 状态 *************************************************************************/ const MAX_LOOKBEHIND = 256; const CARET_TOKEN = '\uFFF9'; const FIELD_START = '\uFFF0'; const FIELD_END = '\uFFF1'; const STORE_KEY_ENABLED = 'latex_suite_enabled'; const STORE_KEY_BTN_POS = 'latex_suite_button_pos'; // 默认 OFF let enabled = !!GM_getValue(STORE_KEY_ENABLED, false); let _latexSuiteBusy = false; let _isComposing = false; let _allowOnceAfterComposition = false; /************************************************************************* * 圆形小球按钮(右上角,拖动,ON=不透明,OFF=0.5) *************************************************************************/ GM_addStyle(` .latex-suite-toggle { position: fixed; top: 12px; right: 12px; z-index: 2147483647; width: 44px; height: 44px; border-radius: 50%; box-shadow: 0 2px 10px rgba(0,0,0,0.2); cursor: move; user-select: none; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; font-family: system-ui,-apple-system,"Segoe UI",Roboto,Arial,"Noto Sans","Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif; background: #22c55e; opacity: ${enabled ? '1' : '0.5'}; } .latex-suite-off { background: #ef4444; } `); const btn = document.createElement('div'); btn.className = `latex-suite-toggle ${enabled ? '' : 'latex-suite-off'}`; btn.textContent = 'La'; document.documentElement.appendChild(btn); (function restoreBtnPos() { const pos = GM_getValue(STORE_KEY_BTN_POS); if (!pos) return; const { top, left } = pos; btn.style.top = top + 'px'; btn.style.left = left + 'px'; btn.style.right = 'unset'; })(); (function initDragAndToggle() { let dragging = false, moved = false, startX = 0, startY = 0, startTop = 0, startLeft = 0; btn.addEventListener('mousedown', (e) => { dragging = true; moved = false; btn.style.cursor = 'grabbing'; startX = e.clientX; startY = e.clientY; const r = btn.getBoundingClientRect(); startTop = r.top; startLeft = r.left; e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; if (Math.abs(dx) > 2 || Math.abs(dy) > 2) moved = true; btn.style.top = Math.max(0, startTop + dy) + 'px'; btn.style.left = Math.max(0, startLeft + dx) + 'px'; btn.style.right = 'unset'; }, true); window.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; btn.style.cursor = 'move'; if (!moved) { enabled = !enabled; GM_setValue(STORE_KEY_ENABLED, enabled); btn.classList.toggle('latex-suite-off', !enabled); btn.style.opacity = enabled ? '1' : '0.5'; } else { const r = btn.getBoundingClientRect(); GM_setValue(STORE_KEY_BTN_POS, { top: r.top, left: r.left }); } }, true); })(); /************************************************************************* * 编辑区/CE 工具 *************************************************************************/ function getActiveEditable() { const el = document.activeElement; if (!el) return null; const isTextarea = el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && el.type === 'text'); const isCE = el.isContentEditable; if (isTextarea || isCE) return el; return null; } function getSelectionInCE(root) { const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return null; const range = sel.getRangeAt(0); if (!root.contains(range.startContainer)) return null; return range; } function getTextAndCaret(el) { if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { return { type: 'textarea', text: el.value, start: el.selectionStart, end: el.selectionEnd }; } else { const range = getSelectionInCE(el); if (!range) return null; const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null); let text = '', startOff = -1, endOff = -1, cur = 0; while (walker.nextNode()) { const node = walker.currentNode; if (node === range.startContainer) startOff = cur + range.startOffset; if (node === range.endContainer) endOff = cur + range.endOffset; text += node.nodeValue; cur += node.nodeValue.length; } return { type: 'contenteditable', text, start: Math.min(startOff, endOff), end: Math.max(startOff, endOff), root: el }; } } function locateNodeByOffset(root, targetOffset) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); let offset = 0; while (walker.nextNode()) { const node = walker.currentNode; const next = offset + node.nodeValue.length; if (targetOffset <= next) return { node, local: targetOffset - offset }; offset = next; } return { node: root, local: root.childNodes.length }; } function setCaretOnly(ed, sel, newPos) { if (sel.type === 'textarea') { ed.setSelectionRange(newPos, newPos); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } else { const range = document.createRange(); const { node, local } = locateNodeByOffset(ed, newPos); if (node.nodeType === Node.TEXT_NODE) range.setStart(node, local); else range.setStart(ed, ed.childNodes.length); range.collapse(true); const s = window.getSelection(); s.removeAllRanges(); s.addRange(range); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } } function setSelectionInEditor(ed, sel, start, end) { if (sel.type === 'textarea') { ed.setSelectionRange(start, end); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } else { const a = locateNodeByOffset(ed, start); const b = locateNodeByOffset(ed, end); const range = document.createRange(); if (a.node.nodeType === Node.TEXT_NODE) range.setStart(a.node, a.local); else range.setStart(ed, ed.childNodes.length); if (b.node.nodeType === Node.TEXT_NODE) range.setEnd(b.node, b.local); else range.setEnd(ed, ed.childNodes.length); const s = window.getSelection(); s.removeAllRanges(); s.addRange(range); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } } function replaceRangeInEditor(ed, sel, start, end, replacement, newCaret) { if (sel.type === 'textarea') { ed.setSelectionRange(start, end); ed.setRangeText(replacement, start, end, 'end'); ed.setSelectionRange(newCaret, newCaret); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } else { const range = document.createRange(); const a = locateNodeByOffset(ed, start); const b = locateNodeByOffset(ed, end); if (a.node.nodeType === Node.TEXT_NODE) range.setStart(a.node, a.local); else range.setStart(ed, ed.childNodes.length); if (b.node.nodeType === Node.TEXT_NODE) range.setEnd(b.node, b.local); else range.setEnd(ed, ed.childNodes.length); range.deleteContents(); const textNode = document.createTextNode(replacement); range.insertNode(textNode); const caretRange = document.createRange(); const caretLocal = Math.max(0, Math.min(replacement.length, newCaret - start)); caretRange.setStart(textNode, caretLocal); caretRange.collapse(true); const s = window.getSelection(); s.removeAllRanges(); s.addRange(caretRange); ed.dispatchEvent(new InputEvent('input', { bubbles: true })); } } /************************************************************************* * 组合输入保护 *************************************************************************/ window.addEventListener('compositionstart', () => { _isComposing = true; }, true); window.addEventListener('compositionend', () => { _isComposing = false; _allowOnceAfterComposition = true; setTimeout(() => { _allowOnceAfterComposition = false; }, 0); }, true); function isCharacterInsertion(e) { if (!(e instanceof InputEvent)) return false; if (_isComposing) return false; const t = e.inputType || ''; if (t === 'insertText' || t === 'insertCompositionText') return true; if (_allowOnceAfterComposition) return true; return false; } /************************************************************************* * 模式与数学判断 *************************************************************************/ function isInsideCodeFence(text, pos) { const fence = /```/g; let count = 0, m; while ((m = fence.exec(text)) && m.index < pos) count++; return (count % 2) === 1; } function isEscaped(text, idx) { let backslashes = 0; for (let i = idx - 1; i >= 0 && text[i] === '\\'; i--) backslashes++; return (backslashes % 2) === 1; } function nearestUnescapedPairPositions(text, pos, token) { const re = token === '$$' ? /\$\$/g : /\$/g; const idxs = []; let m; while ((m = re.exec(text))) { if (!isEscaped(text, m.index)) idxs.push(m.index); } let left = -1, right = -1; for (let i = 0; i < idxs.length; i++) { const a = idxs[i], b = idxs[i + 1]; if (a < pos && b !== undefined && b > pos) { left = a; right = b; break; } } return { left, right }; } function inBlockMath(text, pos) { const { left, right } = nearestUnescapedPairPositions(text, pos, '$$'); return left !== -1 && right !== -1; } function inInlineMath(text, pos) { if (inBlockMath(text, pos)) return false; const { left, right } = nearestUnescapedPairPositions(text, pos, '$'); return left !== -1 && right !== -1; } function inMath(text, pos) { return inInlineMath(text, pos) || inBlockMath(text, pos); } /************************************************************************* * 词边界 / 模式过滤 *************************************************************************/ function passesModeOptions(options, fullText, caretPos) { if (!options) return true; const inCode = isInsideCodeFence(fullText, caretPos); const inBlk = inBlockMath(fullText, caretPos); const inInl = inInlineMath(fullText, caretPos); const inM = inBlk || inInl; if (options.includes('c') && !inCode) return false; if (options.includes('t') && inM) return false; if (options.includes('m') && !inM) return false; if (options.includes('M') && !inBlk) return false; if (options.includes('n') && !inInl) return false; return true; } function passesWordBoundary(options, fullText, from, to) { if (!options || !options.includes('w')) return true; const isDelim = (ch) => { if (!ch) return true; return /\s|[.,;:!?()\[\]{}<>\-+*/=|\\]/.test(ch); }; const prev = fullText[from - 1]; const next = fullText[to]; return isDelim(prev) && isDelim(next); } /************************************************************************* * 触发匹配 *************************************************************************/ function matchSnippet(beforeText, fullText, caretPos, snippet) { const { trigger, options = '' } = snippet; const hay = beforeText.slice(-MAX_LOOKBEHIND); if (!passesModeOptions(options, fullText, caretPos)) return null; function prevCharIsBackslash(globalBefore, fromIndex) { const prevIdx = fromIndex - 1; if (prevIdx < 0) return false; return globalBefore[prevIdx] === '\\'; } if (options.includes('r')) { let re; try { re = new RegExp(trigger + '$', options.includes('m') ? 'm' : ''); } catch { return null; } const m = hay.match(re); if (!m) return null; const from = beforeText.length - m[0].length; const globalFrom = caretPos - m[0].length, globalTo = caretPos; if (!passesWordBoundary(options, fullText, globalFrom, globalTo)) return null; if (prevCharIsBackslash(fullText, globalFrom)) return null; return { from: globalFrom, to: globalTo, match: m }; } else { if (!hay.endsWith(trigger)) return null; const globalFrom = caretPos - trigger.length, globalTo = caretPos; if (!passesWordBoundary(options, fullText, globalFrom, globalTo)) return null; if (prevCharIsBackslash(fullText, globalFrom)) return null; return { from: globalFrom, to: globalTo, match: null }; } } /************************************************************************* * expand(就地) *************************************************************************/ function parseSnippetTemplate(tpl, visualText = '') { tpl = tpl.replace(/\$\{?VISUAL\}?/g, visualText); const fields = []; tpl = tpl.replace(/\$\{(\d+):([^}]*)\}/g, (m, nStr, def) => FIELD_START + parseInt(nStr, 10) + ':' + def + FIELD_END); tpl = tpl.replace(/\$(\d+)/g, (m, nStr) => FIELD_START + parseInt(nStr, 10) + ':' + '' + FIELD_END); tpl = tpl.replace(/\$0/g, CARET_TOKEN); let out = '', i = 0; while (i < tpl.length) { if (tpl[i] === FIELD_START) { let j = i + 1, buf = ''; while (j < tpl.length && tpl[j] !== FIELD_END) buf += tpl[j++]; const m = buf.match(/^(\d+):(.*)$/s); const n = parseInt(m[1], 10), def = m[2]; const start = out.length; out += def; const end = out.length; fields.push({ n, start, end }); i = j + 1; } else { out += tpl[i++]; } } const caretIndex = out.indexOf(CARET_TOKEN); const text = out.replace(CARET_TOKEN, ''); fields.sort((a, b) => a.n - b.n); return { text, fields, caret: (caretIndex === -1 ? null : caretIndex) }; } function expandWithSnippet(ed, sel, snip, m, visualText = '') { let replTpl = snip.replacement; if (m && m.match && m.match.length && /\[\[\d+\]\]/.test(replTpl)) { replTpl = replTpl.replace(/\[\[(\d+)\]\]/g, (mm, k) => m.match[parseInt(k, 10)] ?? ''); } const { text: replaced, fields, caret } = parseSnippetTemplate(replTpl, visualText); const start = m ? m.from : sel.start; const end = m ? m.to : sel.end; let newCaret = start + replaced.length; if (caret !== null) newCaret = start + caret; else if (fields.length > 0) newCaret = start + fields[0].start; _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, start, end, replaced, newCaret); setTimeout(() => { _latexSuiteBusy = false; }, 0); return true; } /************************************************************************* * 自动片段 *************************************************************************/ const autoSnipsByLastChar = new Map(), autoRegexSnips = []; (function prepareAutoIndex() { for (const s of SNIPPETS) { const hasA = (s.options || '').includes('A'); const auto = (s.auto !== undefined) ? !!s.auto : !!hasA; if (!auto) continue; if ((s.options || '').includes('r')) autoRegexSnips.push(s); else if (typeof s.trigger === 'string' && s.trigger.length > 0) { const last = s.trigger.slice(-1); if (!autoSnipsByLastChar.has(last)) autoSnipsByLastChar.set(last, []); autoSnipsByLastChar.get(last).push(s); } } })(); function tryAutoExpand(e) { if (!enabled) return; if (_latexSuiteBusy) return; if (!isCharacterInsertion(e)) return; const ed = getActiveEditable(); if (!ed) return; const sel = getTextAndCaret(ed); if (!sel) return; if (sel.start !== sel.end) return; const full = sel.text; const before = full.slice(0, sel.start); const lastChar = (e && typeof e.data === 'string' && e.data.length === 1) ? e.data : null; if (lastChar && autoSnipsByLastChar.has(lastChar)) { for (const snip of autoSnipsByLastChar.get(lastChar)) { const m = matchSnippet(before, full, sel.start, snip); if (!m) continue; expandWithSnippet(ed, sel, snip, m); return; } } for (const snip of autoRegexSnips) { const m = matchSnippet(before, full, sel.start, snip); if (!m) continue; expandWithSnippet(ed, sel, snip, m); return; } } window.addEventListener('input', tryAutoExpand, true); /************************************************************************* * Visual snippets:选区 + 单字符触发 *************************************************************************/ window.addEventListener('keydown', (e) => { if (!enabled) return; const ed = getActiveEditable(); if (!ed) return; const sel = getTextAndCaret(ed); if (!sel) return; if (sel.start === sel.end) return; if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { const visualText = sel.text.slice(sel.start, sel.end); for (const snip of SNIPPETS) { const opts = snip.options || ''; if (!opts.includes('v')) continue; if (opts.includes('r')) continue; if (snip.trigger !== e.key) continue; if (!passesModeOptions(opts, sel.text, sel.start)) continue; e.preventDefault(); expandWithSnippet(ed, sel, snip, null, visualText); return; } } }, true); /************************************************************************* * 环境定位/切换 与 花括号参数跳转 *************************************************************************/ function findEnvAtPos(fullText, pos) { const beginRe = /\\begin\{([a-zA-Z*]+)\}/g; let beginMatch, begins = []; while ((beginMatch = beginRe.exec(fullText))) { const name = beginMatch[1]; begins.push({ name, beginNameStart: beginMatch.index + "\\begin{".length, beginNameEnd: beginMatch.index + "\\begin{".length + name.length, beginTokenStart: beginMatch.index, beginTokenEnd: beginMatch.index + beginMatch[0].length }); } let cand = null; for (const b of begins) if (b.beginTokenStart <= pos && (!cand || b.beginTokenStart > cand.beginTokenStart)) cand = b; if (!cand) return null; const name = cand.name; const endRe = new RegExp(String.raw`\\(begin|end)\{(${name.replace('*','\\*')})\}`, 'g'); endRe.lastIndex = cand.beginTokenEnd; let depth = 1, m, endTokenStart=-1, endNameStart=-1, endNameEnd=-1, endTokenEnd=-1; while ((m = endRe.exec(fullText))) { const kind = m[1]; if (kind === 'begin') depth++; else depth--; if (depth === 0) { endTokenStart = m.index; endNameStart = m.index + "\\end{".length; endNameEnd = endNameStart + name.length; endTokenEnd = m.index + m[0].length; break; } } if (depth !== 0) return null; if (!(cand.beginTokenStart <= pos && pos <= endTokenEnd)) return null; return { name, beginNameStart: cand.beginNameStart, beginNameEnd: cand.beginNameEnd, endNameStart, endNameEnd, beginTokenStart: cand.beginTokenStart, beginTokenEnd: cand.beginTokenEnd, endTokenStart, endTokenEnd }; } function nextEnvName(current, dir) { for (const cycle of ENV_CYCLES) { const idx = cycle.indexOf(current); if (idx !== -1) { if (cycle.length <= 1) return current; const n = (idx + (dir > 0 ? 1 : -1) + cycle.length) % cycle.length; return cycle[n]; } } return current; } // 找到以命令为中心的连续 {…} 参数,并返回各参数内部区间 function findBraceArgsNear(fullText, pos) { let i = pos - 1; while (i >= 0 && /\s/.test(fullText[i])) i--; while (i >= 0 && /[a-zA-Z*]/.test(fullText[i])) i--; if (i < 0 || fullText[i] !== '\\') return null; const cmdStart = i; let j = i + 1; while (j < fullText.length && /[a-zA-Z*]/.test(fullText[j])) j++; const cmdEnd = j; const args = []; let k = j; while (k < fullText.length) { while (k < fullText.length && /\s/.test(fullText[k])) k++; if (fullText[k] !== '{') break; let depth = 0, a = k, b = k; while (b < fullText.length) { if (fullText[b] === '{') depth++; else if (fullText[b] === '}') { depth--; if (depth === 0) break; } b++; } if (depth !== 0) break; args.push({ innerStart: a + 1, innerEnd: b }); k = b + 1; } if (args.length === 0) return null; return { cmdStart, cmdEnd, args }; } // 关键:在参数内部按 Tab,直接跳到“下一个”;已选中某参数时继续跳到下一/上一 function pickArgIndex(args, selStart, selEnd, dir) { const step = (dir > 0 ? 1 : -1); const L = args.length; for (let idx = 0; idx < L; idx++) { const a = args[idx]; if (selStart === a.innerStart && selEnd === a.innerEnd) { return (idx + step + L) % L; } } for (let idx = 0; idx < L; idx++) { const a = args[idx]; if (selStart >= a.innerStart && selStart <= a.innerEnd) { return (idx + step + L) % L; } } return dir > 0 ? 0 : L - 1; } /************************************************************************* * 键盘:环境切换 / 花括号参数跳转 / matrix 快捷 / Tabout / 非自动片段 *************************************************************************/ function onKeyDown(e) { const ed = getActiveEditable(); if (!ed) return; const sel = getTextAndCaret(ed); if (!sel) return; if (!enabled) return; // 1) 花括号参数跳转:Tab / Shift+Tab(设置“选区”,让后续输入覆盖选中文本) if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { const full = sel.text; const brace = findBraceArgsNear(full, sel.start); if (brace && inMath(full, sel.start)) { e.preventDefault(); const dir = e.shiftKey ? -1 : +1; const idx = pickArgIndex(brace.args, sel.start, sel.end, dir); const target = brace.args[idx]; setSelectionInEditor(ed, sel, target.innerStart, target.innerEnd); return; } } // 2) 环境切换:Tab / Shift+Tab(光标在 begin..end 范围内) if ((e.key === 'Tab') && !e.ctrlKey && !e.metaKey && !e.altKey) { const full = sel.text; if (sel.start === sel.end) { const env = findEnvAtPos(full, sel.start); if (env) { const dir = e.shiftKey ? -1 : +1; const newName = nextEnvName(env.name, dir); if (newName !== env.name) { e.preventDefault(); _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, env.endNameStart, env.endNameEnd, newName, sel.start); replaceRangeInEditor(ed, getTextAndCaret(ed), env.beginNameStart, env.beginNameEnd, newName, sel.start); setTimeout(() => { _latexSuiteBusy = false; }, 0); return; } } } } // 3) Matrix 快捷 if (e.key === 'Tab' || e.key === 'Enter') { const full = sel.text; if (inMatrixLikeEnv(full, sel.start)) { if (e.key === 'Tab' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, sel.start, sel.end, '&', sel.start + 1); setTimeout(() => { _latexSuiteBusy = false; }, 0); return; } if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); if (!e.shiftKey) { const rep = '\\\\\n'; _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, sel.start, sel.end, rep, sel.start + rep.length); setTimeout(() => { _latexSuiteBusy = false; }, 0); return; } else { const after = full.slice(sel.end); const nIdx = after.indexOf('\n'); const jumpPos = (nIdx === -1) ? full.length : (sel.end + nIdx); setCaretOnly(ed, sel, jumpPos); e.preventDefault(); return; } } } } // 4) Tabout if (TABOUT_ENABLED && e.key === NON_AUTO_TRIGGER_KEY && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { const after = sel.text.slice(sel.end); if (after.startsWith('$$')) { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 2); return; } if (after.startsWith('$')) { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 1); return; } const first = after[0]; if (')]}>|'.includes(first)) { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 1); return; } } // 5) 非自动片段(Tab 触发) if (e.key === NON_AUTO_TRIGGER_KEY && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { if (_latexSuiteBusy) return; if (sel.start !== sel.end) return; const before = sel.text.slice(0, sel.start); for (const snip of SNIPPETS) { const opts = snip.options || ''; const hasA = opts.includes('A'); const auto = (snip.auto !== undefined) ? !!snip.auto : !!hasA; if (auto) continue; const m = matchSnippet(before, sel.text, sel.start, snip); if (!m) continue; e.preventDefault(); expandWithSnippet(ed, sel, snip, m); return; } } } window.addEventListener('keydown', onKeyDown, true); function inMatrixLikeEnv(text, pos) { const left = text.lastIndexOf('\\begin{', pos); if (left === -1) return false; const right = text.indexOf('}', left + 7); if (right === -1 || right > pos) return false; const env = text.slice(left + 7, right); return MATRIX_ENV_NAMES.includes(env); } /************************************************************************* * Auto-fraction(仅字符输入、数学环境,“/”) *************************************************************************/ window.addEventListener('input', function onSlashAutoFrac(e) { if (!enabled) return; if (_latexSuiteBusy) return; if (!isCharacterInsertion(e)) return; const ed = getActiveEditable(); if (!ed) return; const sel = getTextAndCaret(ed); if (!sel) return; if (sel.start !== sel.end) return; const full = sel.text; const before = full.slice(0, sel.start); const after = full.slice(sel.end); if (!before.endsWith('/')) return; if (!inMath(full, sel.start)) return; if (inExcludedEnvironment(before, after, EXCLUDED_ENVIRONMENTS)) return; const leftBound = findNumeratorStart(before.slice(0, -1), BREAKING_CHARS); const rightBound = findDenominatorEnd(after, BREAKING_CHARS); const numerator = before.slice(leftBound, -1); const denominator = after.slice(0, rightBound); if (!numerator.trim() || !denominator.trim()) return; const globalStart = leftBound; const globalEnd = sel.start + rightBound; const replacement = `${FRACTION_CMD}{${numerator}}{${denominator}}`; const newCaret = globalStart + FRACTION_CMD.length + 2; _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, globalStart, globalEnd, replacement, newCaret); setTimeout(() => { _latexSuiteBusy = false; }, 0); }, true); function isBreakingChar(ch, breakingStr) { return breakingStr.includes(ch); } function findNumeratorStart(leftText, breakingStr) { let i = leftText.length - 1, depth = 0; while (i >= 0) { const ch = leftText[i]; if (ch === ')' || ch === ']' || ch === '}') depth++; else if (ch === '(' || ch === '[' || ch === '{') { if (depth === 0) break; depth--; } if (depth === 0 && (isBreakingChar(ch, breakingStr) || /\s/.test(ch))) break; i--; } return i + 1; } function findDenominatorEnd(rightText, breakingStr) { let i = 0, depth = 0; while (i < rightText.length) { const ch = rightText[i]; if (ch === '(' || ch === '[' || ch === '{') depth++; else if (ch === ')' || ch === ']' || ch === '}') { if (depth === 0) break; depth--; } if (depth === 0 && (isBreakingChar(ch, breakingStr) || /\s/.test(ch))) break; i++; } return i; } function inExcludedEnvironment(before, _after, envs) { for (const [startTok, _endTok] of envs) { const idx = before.lastIndexOf(startTok); if (idx === -1) continue; const segment = before.slice(idx + startTok.length); let balance = 0; for (const ch of segment) { if (ch === '{') balance++; else if (ch === '}') balance--; } if (balance > 0) return true; } return false; } /************************************************************************* * Auto-enlarge:右括号时放大(数学环境) *************************************************************************/ window.addEventListener('input', function onAutoEnlarge(e) { if (!enabled) return; if (!AUTO_ENLARGE_ENABLED) return; if (_latexSuiteBusy) return; if (!isCharacterInsertion(e)) return; const ed = getActiveEditable(); if (!ed) return; const sel = getTextAndCaret(ed); if (!sel) return; if (sel.start !== sel.end) return; const full = sel.text; const before = full.slice(0, sel.start); const after = full.slice(sel.end); const lastCh = before.slice(-1); if (!')]}'.includes(lastCh)) return; if (!inMath(full, sel.start)) return; const matchOpen = (ch) => ch === ')' ? '(' : (ch === ']' ? '[' : '{'); const openCh = matchOpen(lastCh); const openPos = findMatchingOpen(before.slice(0, -1), openCh, lastCh); if (openPos === -1) return; const inner = before.slice(openPos + 1, -1); if (!AUTO_ENLARGE_TRIGGERS.some(t => inner.includes(t))) return; const alreadyLeft = before.slice(Math.max(0, openPos - 6), openPos + 1).includes('\\left'); const alreadyRight = after.startsWith('\\right' + lastCh); if (alreadyLeft || alreadyRight) return; const openChar = before[openPos]; const closeChar = lastCh; const enlarged = `\\left${openChar}${inner}\\right${closeChar}`; const globalStart = openPos; const globalEnd = sel.start; const newCaret = globalStart + enlarged.length; _latexSuiteBusy = true; replaceRangeInEditor(ed, sel, globalStart, globalEnd, enlarged, newCaret); setTimeout(() => { _latexSuiteBusy = false; }, 0); }, true); function findMatchingOpen(text, openCh, closeCh) { let depth = 1; for (let i = text.length - 1; i >= 0; i--) { const ch = text[i]; if (ch === closeCh) depth++; else if (ch === openCh) { depth--; if (depth === 0) return i; } } return -1; } })();