Gemini Enterprise Inline Math Fix

Render inline and block math that appears as raw delimiters in Gemini Enterprise.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Gemini Enterprise Inline Math Fix
// @namespace    https://github.com/lueluelue2006
// @author       schweigen
// @version      1.2.1
// @license      MIT
// @description  Render inline and block math that appears as raw delimiters in Gemini Enterprise.
// @match        https://business.gemini.google/*
// @run-at       document-start
// @grant        unsafeWindow
// ==/UserScript==

(() => {
  'use strict';

  try {
    if (typeof unsafeWindow !== 'undefined') {
      unsafeWindow.__geminiInlineMathFix = { version: '1.2.1' };
    }
  } catch (e) {
    // Ignore if unsafeWindow is blocked.
  }

  const mathRegex = /\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$\$([\s\S]+?)\$\$|\$([^$\n]+?)\$/g;
  const bareMathRegex =
    /\\implies\s*(?:\\\{[^}]{1,200}\\\}|\{[^}]{1,200}\})(?:_(?:\{[^}]{1,40}\}|[a-zA-Z0-9]+))?|\\square\b|\{(?=[^}\n]*[\\_^0-9,+=|\\-])[^\n}]{1,200}\}(?:_(?:\{[^}]{1,40}\}|[a-zA-Z0-9]+))?/g;
  const PATCH_SKIP_WINDOW_MS = 800;

  const getKatex = () => {
    if (window.katex) return window.katex;
    if (typeof unsafeWindow !== 'undefined' && unsafeWindow.katex) return unsafeWindow.katex;
    return null;
  };

  const isSkippable = (node) => {
    const el = node.parentElement;
    if (!el) return true;
    return !!el.closest('code, pre, textarea, script, style, .katex, .katex-display, .math-block');
  };

  const isKatexError = (el) => {
    if (!el) return true;
    if (el.classList && el.classList.contains('katex-error')) return true;
    return !!el.querySelector?.('.katex-error');
  };

  const repairMarkdownBold = (root) => {
    const boldRegex = /\*\*([^\n*]{1,200}?)\*\*/g;

    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    const nodes = [];
    let node;

    while ((node = walker.nextNode())) {
      if (!node.nodeValue || !node.nodeValue.includes('**')) continue;
      if (isSkippable(node)) continue;
      const parent = node.parentElement;
      if (!parent) continue;
      if (parent.closest('strong, b')) continue;
      nodes.push(node);
    }

    for (const n of nodes) {
      const text = n.nodeValue;
      if (!text) continue;

      boldRegex.lastIndex = 0;
      let match;
      let last = 0;
      let changed = false;
      const frag = document.createDocumentFragment();

      while ((match = boldRegex.exec(text)) !== null) {
        const start = match.index;
        const end = start + match[0].length;
        const before = text.slice(last, start);
        if (before) frag.appendChild(document.createTextNode(before));

        const content = match[1] || '';
        if (content.trim()) {
          const strong = document.createElement('strong');
          strong.textContent = content;
          frag.appendChild(strong);
        } else {
          frag.appendChild(document.createTextNode(match[0]));
        }

        last = end;
        changed = true;
      }

      if (!changed) continue;
      const after = text.slice(last);
      if (after) frag.appendChild(document.createTextNode(after));

      const parent = n.parentNode;
      if (!parent) continue;
      parent.replaceChild(frag, n);
    }
  };

  const isProbablyPlainTextMath = (latex) => {
    if (!latex) return false;
    const t = latex.trim();
    if (t.length < 12) return false;
    if (!/\s/.test(t)) return false;
    if (/\\[a-zA-Z]+|[0-9]|[_^{}]|[=<>+*/-]/.test(t)) return false;

    try {
      return /^[\p{L}\s.,;:!?'"()–—-]+$/u.test(t);
    } catch (e) {
      // Fallback for environments without Unicode property escapes.
      return /^[A-Za-z\u00C0-\u024F\u1E00-\u1EFF\s.,;:!?'"()–—-]+$/.test(t);
    }
  };

  const restoreOuterSetBraces = (latex) => {
    if (!latex || latex.includes('\\{') || latex.includes('\\}')) return latex;

    const start = latex.search(/\S/);
    if (start < 0) return latex;
    let end = latex.length - 1;
    while (end >= 0 && /\s/.test(latex[end])) end -= 1;

    if (latex[start] !== '{' || latex[end] !== '}') return latex;

    let depth = 0;
    for (let i = start; i <= end; i += 1) {
      const ch = latex[i];
      if (ch === '{' && latex[i - 1] !== '\\') depth += 1;
      if (ch === '}' && latex[i - 1] !== '\\') depth -= 1;
      if (depth === 0) {
        if (i === end) {
          return `${latex.slice(0, start)}\\{${latex.slice(start + 1, end)}\\}${latex.slice(end + 1)}`;
        }
        return latex;
      }
    }

    return latex;
  };

  const restoreIndexedSetBraces = (latex) => {
    if (!latex || !latex.includes('{') || !latex.includes('}')) return latex;

    const isEscaped = (s, idx) => idx > 0 && s[idx - 1] === '\\';

    const findMatchingBrace = (s, start) => {
      let depth = 0;
      for (let i = start; i < s.length; i += 1) {
        const ch = s[i];
        if (ch === '{' && !isEscaped(s, i)) depth += 1;
        if (ch === '}' && !isEscaped(s, i)) depth -= 1;
        if (depth === 0) return i;
      }
      return -1;
    };

    const isInfinity = (s, caretIndex) => {
      let i = caretIndex + 1;
      while (i < s.length && /\s/.test(s[i])) i += 1;

      if (i >= s.length) return null;

      if (s[i] === '{' && !isEscaped(s, i)) {
        const end = findMatchingBrace(s, i);
        if (end > i) {
          const inner = s.slice(i + 1, end).trim();
          if (inner === '\\infty' || inner === '∞') return end + 1;
        }
        return null;
      }

      if (s.slice(i).startsWith('\\infty')) return i + '\\infty'.length;
      if (s[i] === '∞') return i + 1;
      return null;
    };

    let out = '';
    let cursor = 0;

    while (cursor < latex.length) {
      const open = latex.indexOf('{', cursor);
      if (open < 0) {
        out += latex.slice(cursor);
        break;
      }

      out += latex.slice(cursor, open);

      if (isEscaped(latex, open)) {
        out += '{';
        cursor = open + 1;
        continue;
      }

      const groupAEnd = findMatchingBrace(latex, open);
      if (groupAEnd < 0) {
        out += latex.slice(open);
        break;
      }

      let idx = groupAEnd + 1;
      while (idx < latex.length && /\s/.test(latex[idx])) idx += 1;

      let groupBStart = -1;
      if (idx < latex.length && latex[idx] === '_' && !isEscaped(latex, idx)) {
        idx += 1;
        while (idx < latex.length && /\s/.test(latex[idx])) idx += 1;
        if (idx < latex.length && latex[idx] === '{' && !isEscaped(latex, idx)) {
          groupBStart = idx;
        }
      } else if (idx < latex.length && latex[idx] === '{' && !isEscaped(latex, idx)) {
        // Common Gemini markdown escape bug: "\\}_{k=1}" becomes "}{k=1}" (underscore eaten by markdown).
        groupBStart = idx;
      }

      if (groupBStart < 0) {
        out += latex.slice(open, groupAEnd + 1);
        cursor = groupAEnd + 1;
        continue;
      }

      const groupBEnd = findMatchingBrace(latex, groupBStart);
      if (groupBEnd < 0) {
        out += latex.slice(open, groupAEnd + 1);
        cursor = groupAEnd + 1;
        continue;
      }

      const groupBInner = latex.slice(groupBStart + 1, groupBEnd).trim();
      if (!/^[a-zA-Z]\s*=\s*\d+$/.test(groupBInner)) {
        out += latex.slice(open, groupAEnd + 1);
        cursor = groupAEnd + 1;
        continue;
      }

      idx = groupBEnd + 1;
      while (idx < latex.length && /\s/.test(latex[idx])) idx += 1;
      if (idx >= latex.length || latex[idx] !== '^' || isEscaped(latex, idx)) {
        out += latex.slice(open, groupAEnd + 1);
        cursor = groupAEnd + 1;
        continue;
      }

      const afterInfinity = isInfinity(latex, idx);
      if (!afterInfinity) {
        out += latex.slice(open, groupAEnd + 1);
        cursor = groupAEnd + 1;
        continue;
      }

      const groupAInner = latex.slice(open + 1, groupAEnd);
      const groupB = latex.slice(groupBStart, groupBEnd + 1);

      out += `\\{${groupAInner}\\}_${groupB}${latex.slice(idx, afterInfinity)}`;
      cursor = afterInfinity;
    }

    return out;
  };

  const restoreSetBracesAfterEquals = (latex) => {
    if (!latex || !latex.includes('{') || !latex.includes('}')) return latex;

    const findPrevNonSpace = (s, idx) => {
      for (let i = idx - 1; i >= 0; i -= 1) {
        if (!/\s/.test(s[i])) return i;
      }
      return -1;
    };

    const findMatchingBrace = (s, start) => {
      let depth = 0;
      for (let i = start; i < s.length; i += 1) {
        const ch = s[i];
        if (ch === '{' && s[i - 1] !== '\\') depth += 1;
        if (ch === '}' && s[i - 1] !== '\\') depth -= 1;
        if (depth === 0) return i;
      }
      return -1;
    };

    let out = '';
    let depth = 0;

    for (let i = 0; i < latex.length; i += 1) {
      const ch = latex[i];
      const escaped = i > 0 && latex[i - 1] === '\\';

      if (ch === '{' && !escaped) {
        if (depth === 0) {
          const prevIdx = findPrevNonSpace(latex, i);
          const prevCh = prevIdx >= 0 ? latex[prevIdx] : '';
          if (prevCh === '=') {
            const end = findMatchingBrace(latex, i);
            if (end > i) {
              out += `\\{${latex.slice(i + 1, end)}\\}`;
              i = end;
              continue;
            }
          }
        }
        depth += 1;
      } else if (ch === '}' && !escaped) {
        depth = Math.max(0, depth - 1);
      }

      out += ch;
    }

    return out;
  };

  const restoreOperatorSetBraces = (latex) => {
    if (!latex) return latex;
    if (!latex.includes('\\max') && !latex.includes('\\min')) return latex;

    const isEscaped = (s, idx) => idx > 0 && s[idx - 1] === '\\';

    const findMatchingBrace = (s, start) => {
      let depth = 0;
      for (let i = start; i < s.length; i += 1) {
        const ch = s[i];
        if (ch === '{' && !isEscaped(s, i)) depth += 1;
        if (ch === '}' && !isEscaped(s, i)) depth -= 1;
        if (depth === 0) return i;
      }
      return -1;
    };

    const hasTopLevelComma = (inner) => {
      if (!inner || !inner.includes(',')) return false;
      let depth = 0;
      for (let i = 0; i < inner.length; i += 1) {
        const ch = inner[i];
        if (ch === '{' && !isEscaped(inner, i)) depth += 1;
        if (ch === '}' && !isEscaped(inner, i)) depth = Math.max(0, depth - 1);
        if (depth === 0 && ch === ',' && !isEscaped(inner, i)) return true;
      }
      return false;
    };

    const ops = ['max', 'min'];
    let out = '';
    let cursor = 0;

    while (cursor < latex.length) {
      let bestIdx = -1;
      let bestOp = null;
      for (const op of ops) {
        const idx = latex.indexOf(`\\${op}`, cursor);
        if (idx < 0) continue;
        if (bestIdx < 0 || idx < bestIdx) {
          bestIdx = idx;
          bestOp = op;
        }
      }

      if (bestIdx < 0 || !bestOp) {
        out += latex.slice(cursor);
        break;
      }

      out += latex.slice(cursor, bestIdx);
      out += `\\${bestOp}`;

      let i = bestIdx + bestOp.length + 1;
      const wsStart = i;
      while (i < latex.length && /\s/.test(latex[i])) i += 1;
      out += latex.slice(wsStart, i);

      if (i >= latex.length || latex[i] !== '{' || isEscaped(latex, i)) {
        cursor = i;
        continue;
      }

      const end = findMatchingBrace(latex, i);
      if (end <= i) {
        out += latex.slice(i);
        break;
      }

      const inner = latex.slice(i + 1, end);
      if (!hasTopLevelComma(inner)) {
        out += latex.slice(i, end + 1);
        cursor = end + 1;
        continue;
      }

      out += `\\{${inner}\\}`;
      cursor = end + 1;
    }

    return out;
  };

  const repairLatex = (latex) => {
    let out = latex;

    // Gemini sometimes leaves a dangling underscore that KaTeX treats as a parse error.
    // Fix the common pattern seen in linear combinations: "... \\lambda_i v_".
    if (/\\lambda_i\s*v_\s*$/.test(out)) {
      out = out.replace(/v_\s*$/, 'v_i');
    }

    // Fix the common truncated fragment: "\\implies (-u_".
    if (/\\implies\s*\(-u_\s*$/.test(out)) {
      out = out.replace(/\(-u_\s*$/, '(-u_1');
    }

    // Generic: make trailing sub/sup syntactically valid.
    out = out.replace(/_(\s*)$/, '_{}$1');
    out = out.replace(/\^(\s*)$/, '^{}$1');

    return out;
  };

  const normalizeBareLatex = (latex) => {
    let out = latex;

    // If Gemini stripped the surrounding $...$ but kept the TeX command, Markdown may have
    // consumed \\{ and \\} into literal braces. Restore set braces for KaTeX.
    if (/\\implies/.test(out) && !out.includes('\\{') && out.includes('{') && out.includes('}')) {
      out = out.replace(/\{([^}]*)\}/g, '\\{$1\\}');
    }

    return out;
  };

  const sanitizeLatexForKatex = (latex) => {
    let out = typeof latex === 'string' ? latex : String(latex ?? '');
    out = restoreSetBracesAfterEquals(out);
    out = restoreOperatorSetBraces(out);
    out = restoreOuterSetBraces(out);
    out = restoreIndexedSetBraces(out);
    out = normalizeBareLatex(out);
    out = repairLatex(out);
    return out;
  };

  const renderLatex = (latex, displayMode, katex) => {
    const el = document.createElement(displayMode ? 'div' : 'span');
    const opts = {
      displayMode,
      throwOnError: false,
      strict: 'ignore'
    };

    const doRender = (tex) => {
      while (el.firstChild) el.removeChild(el.firstChild);
      el.className = '';
      katex.render(tex, el, opts);
    };

    try {
      doRender(latex);
      if (isKatexError(el)) {
        const repaired = repairLatex(latex);
        if (repaired !== latex) doRender(repaired);
      }
      if (isKatexError(el)) return null;
      el.setAttribute('data-gemini-inline-math-fix', '1');
      return el;
    } catch (e) {
      return null;
    }
  };

  const replacePipesInLatex = (latex) => {
    let out = '';
    for (let i = 0; i < latex.length; i += 1) {
      const ch = latex[i];
      if (ch === '|' && latex[i - 1] !== '\\') {
        out += '\\vert{}';
      } else {
        out += ch;
      }
    }
    return out;
  };

  const patchTableLine = (line) => {
    if (!line.includes('|')) return line;
    if (!line.includes('$') && !line.includes('\\(') && !line.includes('\\[')) return line;
    let out = line;
    const wrapInline = (latex) => {
      const inner = replacePipesInLatex(latex);
      const spaced = inner && inner.startsWith(' ') ? inner : ` ${inner}`;
      // Gemini's table renderer sometimes fails when \\( is immediately followed by [ or \\\\.
      return `\\(${spaced}\\)`;
    };
    const wrapDisplay = (latex) => {
      const inner = replacePipesInLatex(latex);
      const spaced = inner && inner.startsWith(' ') ? inner : ` ${inner}`;
      return `\\[${spaced}\\]`;
    };

    out = out.replace(/\$\$([\s\S]+?)\$\$/g, (m, latex) => wrapDisplay(latex));
    out = out.replace(/\\\(([\\s\S]+?)\\\)/g, (m, latex) => wrapInline(latex));
    out = out.replace(/\\\[([\\s\S]+?)\\\]/g, (m, latex) => wrapDisplay(latex));
    out = out.replace(/\$([^$\n]+?)\$/g, (m, latex) => wrapInline(latex));
    return out;
  };

  const patchMarkdownTables = (markdown) => {
    if (!markdown || !markdown.includes('|')) return markdown;
    if (!markdown.includes('$') && !markdown.includes('\\(') && !markdown.includes('\\[')) return markdown;

    const countPipesOutsideMathAndCode = (line) => {
      if (!line || !line.includes('|')) return 0;
      let inBackticks = false;
      let inMath = false;
      let mathMode = null;
      let count = 0;

      for (let i = 0; i < line.length; i += 1) {
        const ch = line[i];

        if (ch === '`' && line[i - 1] !== '\\') {
          inBackticks = !inBackticks;
          continue;
        }
        if (inBackticks) continue;

        if (ch === '\\') {
          const next = line[i + 1];
          if (!inMath && next === '(') {
            inMath = true;
            mathMode = '\\(';
            i += 1;
            continue;
          }
          if (inMath && mathMode === '\\(' && next === ')') {
            inMath = false;
            mathMode = null;
            i += 1;
            continue;
          }
          if (!inMath && next === '[') {
            inMath = true;
            mathMode = '\\[';
            i += 1;
            continue;
          }
          if (inMath && mathMode === '\\[' && next === ']') {
            inMath = false;
            mathMode = null;
            i += 1;
            continue;
          }
        }

        if (ch === '$' && line[i - 1] !== '\\') {
          if (line[i + 1] === '$') {
            if (!inMath) {
              inMath = true;
              mathMode = '$$';
            } else if (mathMode === '$$') {
              inMath = false;
              mathMode = null;
            }
            i += 1;
            continue;
          }
          if (!inMath) {
            inMath = true;
            mathMode = '$';
            continue;
          }
          if (mathMode === '$') {
            inMath = false;
            mathMode = null;
          }
          continue;
        }

        if (ch === '|' && !inMath) count += 1;
      }
      return count;
    };

    const lines = markdown.split('\n');
    let inFence = false;
    for (let i = 0; i < lines.length; i += 1) {
      const line = lines[i];
      if (/^```/.test(line) || /^~~~/.test(line)) {
        inFence = !inFence;
        continue;
      }
      if (inFence) continue;
      const pipeCountOutside = countPipesOutsideMathAndCode(line);
      if (pipeCountOutside >= 2 && (line.includes('$') || line.includes('\\(') || line.includes('\\['))) {
        lines[i] = patchTableLine(line);
      }
    }
    return lines.join('\n');
  };

  const patchMarkdownBold = (markdown) => {
    if (!markdown || !markdown.includes('**')) return markdown;

    const patchBoldText = (text) => {
      if (!text || !text.includes('**')) return text;
      let out = text;

      out = out.replace(/\*\*“([^”\n*]{1,200}?)”\*\*/g, '“**$1**”');
      out = out.replace(/\*\*‘([^’\n*]{1,200}?)’\*\*/g, '‘**$1**’');
      out = out.replace(/\*\*《([^》\n*]{1,200}?)》\*\*/g, '《**$1**》');
      out = out.replace(/\*\*「([^」\n*]{1,200}?)」\*\*/g, '「**$1**」');
      out = out.replace(/\*\*(([^)\n*]{1,200}?))\*\*/g, '(**$1**)');
      out = out.replace(/\*\*\(([^)\n*]{1,200}?)\)\*\*/g, '(**$1**)');

      const moveTrailingPunctuationOut = (regex) => {
        out = out.replace(regex, (m, body, punct) => {
          const first = (body || '').trimStart()[0];
          if (!first) return m;
          if (/^[“‘"'((《「]/.test(first)) return m;
          return `**${body}**${punct}`;
        });
      };

      try {
        moveTrailingPunctuationOut(/\*\*([^\n*]{1,200}?)([”’"'))\]}】》」])\*\*(?=[\p{L}\p{N}])/gu);
      } catch (e) {
        moveTrailingPunctuationOut(
          /\*\*([^\n*]{1,200}?)([”’"'))\]}】》」])\*\*(?=[A-Za-z0-9\u00C0-\u024F\u1E00-\u1EFF\u4E00-\u9FFF])/g
        );
      }

      return out;
    };

    const lines = markdown.split('\n');
    let inFence = false;
    for (let i = 0; i < lines.length; i += 1) {
      const line = lines[i];
      if (/^```/.test(line) || /^~~~/.test(line)) {
        inFence = !inFence;
        continue;
      }
      if (inFence) continue;
      if (!line.includes('**')) continue;

      const parts = line.split('`');
      for (let pi = 0; pi < parts.length; pi += 2) {
        parts[pi] = patchBoldText(parts[pi]);
      }
      lines[i] = parts.join('`');
    }

    return lines.join('\n');
  };

  const findRealSegment = (segments, startIndex, direction) => {
    for (let i = startIndex; i >= 0 && i < segments.length; i += direction) {
      if (segments[i].node) return segments[i];
    }
    return null;
  };

  const locate = (segments, index, preferNext) => {
    for (let i = 0; i < segments.length; i += 1) {
      const seg = segments[i];
      const start = seg.start;
      const end = seg.start + seg.length;
      if (index < start) return null;
      if (index === end) {
        if (preferNext && i + 1 < segments.length) {
          const next = findRealSegment(segments, i + 1, 1);
          if (!next) return null;
          return { node: next.node, offset: 0 };
        }
        if (!seg.node) {
          const prev = findRealSegment(segments, i - 1, -1);
          if (!prev) return null;
          return { node: prev.node, offset: prev.length };
        }
        return { node: seg.node, offset: seg.length };
      }
      if (index >= start && index < end) {
        if (seg.node) {
          return { node: seg.node, offset: index - start };
        }
        const target = preferNext ? findRealSegment(segments, i + 1, 1) : findRealSegment(segments, i - 1, -1);
        if (!target) return null;
        return { node: target.node, offset: preferNext ? 0 : target.length };
      }
    }
    return null;
  };

  const collectMatches = (text) => {
    const matches = [];

    mathRegex.lastIndex = 0;
    let match;
    while ((match = mathRegex.exec(text)) !== null) {
      const latex = match[1] || match[2] || match[3] || match[4];
      if (!latex) continue;
      matches.push({
        kind: 'delimited',
        start: match.index,
        end: match.index + match[0].length,
        latex,
        displayMode: !!(match[1] || match[3])
      });
    }

    bareMathRegex.lastIndex = 0;
    while ((match = bareMathRegex.exec(text)) !== null) {
      matches.push({
        kind: 'bare',
        start: match.index,
        end: match.index + match[0].length,
        latex: match[0],
        displayMode: false
      });
    }

    if (!matches.length) return matches;

    matches.sort((a, b) => a.start - b.start || b.end - a.end);
    const filtered = [];
    let cursor = -1;
    for (const m of matches) {
      if (m.start < cursor) continue;
      filtered.push(m);
      cursor = m.end;
    }
    return filtered;
  };

  const safeReplaceRange = (range, node) => {
    let extracted;
    try {
      extracted = range.extractContents();
    } catch (e) {
      return false;
    }

    try {
      range.insertNode(node);
      return true;
    } catch (e) {
      try {
        range.insertNode(extracted);
      } catch (restoreErr) {
        // Ignore restore failures.
      }
      return false;
    }
  };

  const processSequence = (text, segments, katex) => {
    if (!text || !segments.length) return;
    const matches = collectMatches(text);
    if (!matches.length) return;

    for (let i = matches.length - 1; i >= 0; i -= 1) {
      const m = matches[i];
      let start = m.start;
      let end = m.end;
      if (m.kind === 'bare') {
        if (start > 0 && text[start - 1] === '$') start -= 1;
        if (end < text.length && text[end] === '$') end += 1;
      }

      const startLoc = locate(segments, start, true);
      const endLoc = locate(segments, end, false);
      if (!startLoc || !endLoc) continue;

      let latex = m.latex;
      if (m.kind === 'delimited' && !m.displayMode && isProbablyPlainTextMath(latex)) {
        const range = document.createRange();
        range.setStart(startLoc.node, startLoc.offset);
        range.setEnd(endLoc.node, endLoc.offset);
        safeReplaceRange(range, document.createTextNode(latex));
        continue;
      }

      latex = sanitizeLatexForKatex(latex);

      const rendered = renderLatex(latex, m.displayMode, katex);
      if (!rendered) continue;

      const range = document.createRange();
      range.setStart(startLoc.node, startLoc.offset);
      range.setEnd(endLoc.node, endLoc.offset);
      safeReplaceRange(range, rendered);
    }
  };

  const getLeafBlocks = (root) => {
    const blockSelector = 'p, li, h1, h2, h3, h4, h5, h6, blockquote, td, th';
    const divSelector = 'div:not(.math-block)';
    const selector = `${blockSelector}, ${divSelector}`;

    const blocks = Array.from(root.querySelectorAll(selector)).filter((el) => {
      if (el.closest('code, pre, textarea, script, style, .katex, .katex-display, .math-block')) return false;
      const nestedSelector = el.tagName === 'DIV' ? selector : blockSelector;
      return !el.querySelector(nestedSelector);
    });
    if (!blocks.length) return [root];
    return blocks;
  };

  const processBlock = (block, katex) => {
    repairMarkdownBold(block);

    const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
    let node;
    let text = '';
    let segments = [];

    const flush = () => {
      processSequence(text, segments, katex);
      text = '';
      segments = [];
    };

    while ((node = walker.nextNode())) {
      if (!node.nodeValue) continue;
      if (isSkippable(node)) {
        flush();
        continue;
      }
      segments.push({ node, start: text.length, length: node.nodeValue.length });
      text += node.nodeValue;
    }
    flush();
  };

  const processRoot = (root, katex) => {
    const blocks = getLeafBlocks(root);
    for (const block of blocks) {
      processBlock(block, katex);
    }
  };

  const collectRowSegments = (row) => {
    const cells = Array.from(row.querySelectorAll('td, th'));
    const segments = [];
    let text = '';
    const addNode = (node) => {
      segments.push({ node, start: text.length, length: node.nodeValue.length });
      text += node.nodeValue;
    };
    for (let ci = 0; ci < cells.length; ci += 1) {
      const walker = document.createTreeWalker(cells[ci], NodeFilter.SHOW_TEXT);
      let node;
      while ((node = walker.nextNode())) {
        if (!node.nodeValue) continue;
        if (isSkippable(node)) continue;
        addNode(node);
      }
      if (ci < cells.length - 1) {
        segments.push({ node: null, start: text.length, length: 1 });
        text += '|';
      }
    }
    return { text, segments };
  };

  const countUnescaped = (text, char) => {
    let count = 0;
    for (let i = 0; i < text.length; i += 1) {
      if (text[i] === char && text[i - 1] !== '\\') count += 1;
    }
    return count;
  };

  const hasUnbalancedMath = (text) => {
    if (!text) return false;
    const dollarCount = countUnescaped(text, '$');
    if (dollarCount % 2 === 1) return true;
    const openParen = (text.match(/\\\(/g) || []).length;
    const closeParen = (text.match(/\\\)/g) || []).length;
    if (openParen !== closeParen) return true;
    const openBracket = (text.match(/\\\[/g) || []).length;
    const closeBracket = (text.match(/\\\]/g) || []).length;
    if (openBracket !== closeBracket) return true;
    return false;
  };

  const processTableRows = (root, katex) => {
    const rows = Array.from(root.querySelectorAll('tr'));
    for (const row of rows) {
      const getCells = () => Array.from(row.querySelectorAll('td, th'));
      const initialCells = getCells();
      if (!initialCells.length) continue;
      const table = row.closest('table');
      const headerRow = table ? table.querySelector('tr') : null;
      const headerCells = headerRow ? headerRow.querySelectorAll('td, th') : null;
      const desiredCols = headerCells && headerCells.length ? headerCells.length : initialCells.length;

      const moveLooseKatexIntoCells = () => {
        const cells = getCells();
        if (!cells.length) return;
        const children = Array.from(row.childNodes);
        for (const child of children) {
          if (child.nodeType !== Node.ELEMENT_NODE) continue;
          const tag = child.tagName;
          if (tag === 'TD' || tag === 'TH') continue;
          if (!child.classList?.contains('katex') && !child.querySelector?.('.katex, .katex-display')) continue;

          let target = child.previousElementSibling;
          while (target && target.tagName !== 'TD' && target.tagName !== 'TH') {
            target = target.previousElementSibling;
          }
          if (!target) {
            target = child.nextElementSibling;
            while (target && target.tagName !== 'TD' && target.tagName !== 'TH') {
              target = target.nextElementSibling;
            }
          }
          if (!target) target = cells[0];

          target.appendChild(child);
        }
      };

      const cleanupRowMarkers = () => {
        const cells = getCells();
        for (const cell of cells) {
          const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
          const toRemove = [];
          let node;
          while ((node = walker.nextNode())) {
            const t = node.nodeValue ? node.nodeValue.trim() : '';
            if (!t) continue;
            if (/^(\*{1,3}|_{1,3})$/.test(t)) toRemove.push(node);
          }
          for (const n of toRemove) {
            n.nodeValue = '';
          }
        }
      };

      const splitSpanningCell = () => {
        if (desiredCols <= 1) return;
        const cells = getCells();
        if (cells.length !== 1) return;
        const cell = cells[0];
        if (cell.colSpan <= 1) return;
        const annotation = cell.querySelector('.katex-mathml annotation');
        if (!annotation || !annotation.textContent) return;
        const latex = annotation.textContent;
        const pipeIdx = latex.indexOf('|');
        if (pipeIdx < 0) return;

        const leftLatex = `${latex.slice(0, pipeIdx)}|`;
        const rightLatex = latex.slice(pipeIdx + 1);
        if (!leftLatex || !rightLatex) return;

        const makeCell = () => {
          const c = cell.cloneNode(false);
          c.removeAttribute('colspan');
          c.textContent = '';
          return c;
        };

        const leftCell = makeCell();
        const rightCell = makeCell();
        const leftRendered = renderLatex(leftLatex, false, katex);
        const rightRendered = renderLatex(rightLatex, false, katex);
        if (leftRendered) leftCell.appendChild(leftRendered);
        if (rightRendered) rightCell.appendChild(rightRendered);

        cell.replaceWith(leftCell, rightCell);
        for (let i = 2; i < desiredCols; i += 1) {
          row.appendChild(makeCell());
        }
      };

      const mergeIfSingleCell = () => {
        if (desiredCols > 1) return;
        const cells = getCells();
        const meaningful = cells.filter((cell) => {
          const text = cell.innerText.replace(/[\s*\u200b_]/g, '').trim();
          if (text) return true;
          return !!cell.querySelector('.katex, .katex-display');
        });
        if (meaningful.length !== 1 || cells.length <= 1) return;
        const keep = meaningful[0];
        keep.colSpan = cells.length;
        for (const cell of cells) {
          if (cell !== keep) cell.remove();
        }
      };

      const cellsForBalance = getCells();
      const needsCrossCell = cellsForBalance.some((cell) => hasUnbalancedMath(cell.textContent || ''));
      if (!needsCrossCell) {
        cleanupRowMarkers();
        moveLooseKatexIntoCells();
        splitSpanningCell();
        mergeIfSingleCell();
        continue;
      }

      moveLooseKatexIntoCells();

      if (!row.textContent || (!row.textContent.includes('$') && !row.textContent.includes('\\(') && !row.textContent.includes('\\['))) {
        cleanupRowMarkers();
        moveLooseKatexIntoCells();
        splitSpanningCell();
        mergeIfSingleCell();
        continue;
      }
      const { text, segments } = collectRowSegments(row);
      if (!text.includes('$') && !text.includes('\\(') && !text.includes('\\[')) {
        cleanupRowMarkers();
        moveLooseKatexIntoCells();
        splitSpanningCell();
        mergeIfSingleCell();
        continue;
      }

      let rowText = text;
      const rowSegments = segments.slice();
      const dollarCount = (rowText.match(/\$/g) || []).length;
      if (dollarCount % 2 === 1) {
        rowSegments.push({ node: null, start: rowText.length, length: 1 });
        rowText += '$';
      }

      processSequence(rowText, rowSegments, katex);
      cleanupRowMarkers();
      moveLooseKatexIntoCells();
      splitSpanningCell();
      mergeIfSingleCell();
    }
  };

  const patchedMarkdownCache = new WeakMap();
  const patchedMarkdownAt = new WeakMap();

  const getMarkdownHost = (node) => {
    if (!node) return null;
    if (node.closest) {
      const direct = node.closest('ucs-fast-markdown, ucs-markdown, ucs-response-markdown');
      if (direct) return direct;
    }
    const root = node.getRootNode ? node.getRootNode() : null;
    if (root && root.host && root.host.matches && root.host.matches('ucs-fast-markdown, ucs-markdown, ucs-response-markdown')) {
      return root.host;
    }
    return null;
  };

  const patchMarkdownHosts = (root) => {
    const hosts = root.querySelectorAll('ucs-fast-markdown, ucs-markdown, ucs-response-markdown');
    for (const host of hosts) {
      if (!host || typeof host.markdown !== 'string') continue;
      const current = host.markdown;
      if (patchedMarkdownCache.get(host) === current) continue;
      const patched = patchMarkdownBold(patchMarkdownTables(current));
      patchedMarkdownCache.set(host, patched);
      if (patched !== current) {
        try {
          const hostRoot = host.shadowRoot || host;
          hostRoot.querySelectorAll('[data-gemini-inline-math-fix]').forEach((el) => el.remove());
          host.markdown = patched;
          if (typeof host.requestUpdate === 'function') host.requestUpdate();
          if (typeof host.scheduleRender === 'function') host.scheduleRender();
          patchedMarkdownAt.set(host, Date.now());
          setTimeout(schedule, PATCH_SKIP_WINDOW_MS + 50);
        } catch (e) {
          // Ignore readonly markdown or render errors.
        }
      }
    }
  };

  const observedRoots = new WeakSet();
  const observeRoot = (root) => {
    if (!root || observedRoots.has(root)) return;
    observedRoots.add(root);
    const observer = new MutationObserver(schedule);
    observer.observe(root, { subtree: true, childList: true, characterData: true });
  };

  const ROOT_STYLE_ID = 'gemini-inline-math-fix-style';
  const ensureRootStyles = (root) => {
    if (!root || typeof root.querySelector !== 'function') return;
    if (root.querySelector(`#${ROOT_STYLE_ID}`)) return;
    const style = document.createElement('style');
    style.id = ROOT_STYLE_ID;
    style.textContent = `
      .disclaimer { display: none !important; }
      .main.chat-mode.omnibar { padding-bottom: 0 !important; }
      .main.chat-mode.omnibar form.omnibar { margin-bottom: 0 !important; }

      /* KaTeX inside shadow roots may miss base CSS, causing MathML+HTML duplication. */
      .katex .katex-mathml {
        position: absolute !important;
        overflow: hidden !important;
        clip: rect(1px, 1px, 1px, 1px) !important;
        width: 1px !important;
        height: 1px !important;
        padding: 0 !important;
        border: 0 !important;
      }
      .katex .katex-html { position: relative; }
    `;
    root.appendChild(style);
  };

  const collectShadowRoots = (root, out) => {
    if (!root) return;
    out.push(root);
    observeRoot(root);
    ensureRootStyles(root);
    const els = root.querySelectorAll('*');
    for (const el of els) {
      if (el.shadowRoot) collectShadowRoots(el.shadowRoot, out);
    }
  };

  const REFRESH_BUTTON_ID = 'gemini-inline-math-fix-refresh';
  const ensureRefreshButton = () => {
    if (document.getElementById(REFRESH_BUTTON_ID)) return;
    const btn = document.createElement('button');
    btn.id = REFRESH_BUTTON_ID;
    btn.type = 'button';
    btn.textContent = '↻';
    btn.title = '刷新公式渲染';
    btn.setAttribute('aria-label', '刷新公式渲染');
    btn.style.cssText = [
      'position:fixed',
      'right:16px',
      'bottom:110px',
      'z-index:2147483647',
      'width:40px',
      'height:40px',
      'border-radius:20px',
      'border:1px solid rgba(255,255,255,0.14)',
      'background:rgba(32,33,36,0.86)',
      'color:#fff',
      'font-size:18px',
      'line-height:40px',
      'text-align:center',
      'cursor:pointer',
      'box-shadow:0 6px 18px rgba(0,0,0,0.35)',
      'backdrop-filter:blur(6px)'
    ].join(';');
    btn.addEventListener('click', (e) => {
      e.preventDefault();
      schedule();
    });
    (document.body || document.documentElement).appendChild(btn);
  };

  let katexHooked = false;
  const hookKatex = (katex) => {
    if (!katex || typeof katex.render !== 'function') return false;
    if (katex.__geminiInlineMathFixWrapped) return true;

    try {
      const originalRender = katex.render.bind(katex);
      const wrappedRender = (latex, element, options) => {
        try {
          return originalRender(sanitizeLatexForKatex(latex), element, options);
        } catch (e) {
          return originalRender(latex, element, options);
        }
      };
      wrappedRender.__geminiInlineMathFixWrapped = true;
      wrappedRender.__geminiInlineMathFixOriginal = originalRender;
      katex.render = wrappedRender;

      if (typeof katex.renderToString === 'function') {
        const originalRenderToString = katex.renderToString.bind(katex);
        const wrappedRenderToString = (latex, options) => {
          try {
            return originalRenderToString(sanitizeLatexForKatex(latex), options);
          } catch (e) {
            return originalRenderToString(latex, options);
          }
        };
        wrappedRenderToString.__geminiInlineMathFixWrapped = true;
        wrappedRenderToString.__geminiInlineMathFixOriginal = originalRenderToString;
        katex.renderToString = wrappedRenderToString;
      }

      katex.__geminiInlineMathFixWrapped = true;
      return true;
    } catch (e) {
      return false;
    }
  };

  let katexHookAttempts = 0;
  const KATEX_HOOK_WAIT_MAX_ATTEMPTS = 200;
  const KATEX_HOOK_WAIT_MS = 50;
  const scheduleKatexHook = () => {
    if (katexHooked) return;
    const katex = getKatex();
    if (katex) {
      katexHooked = hookKatex(katex);
      return;
    }
    if (katexHookAttempts >= KATEX_HOOK_WAIT_MAX_ATTEMPTS) return;
    katexHookAttempts += 1;
    setTimeout(scheduleKatexHook, KATEX_HOOK_WAIT_MS);
  };

  let katexWaitAttempts = 0;
  const KATEX_WAIT_MAX_ATTEMPTS = 30;
  const KATEX_WAIT_MS = 600;
  let followUpAttempts = 0;
  const FOLLOW_UP_MAX_ATTEMPTS = 4;
  const FOLLOW_UP_DELAY_MS = 650;

  const hasUnrenderedMathDelimiters = (text) => {
    if (!text) return false;
    if (!text.includes('$') && !text.includes('\\(') && !text.includes('\\[')) return false;
    const re = /\\\[([\s\S]+?)\\\]|\\\(([\s\S]+?)\\\)|\$\$([\s\S]+?)\$\$|\$([^$\n]+?)\$/g;
    return re.test(text);
  };

  const EXISTING_KATEX_REPAIRED_ATTR = 'data-gemini-inline-math-fix-existing';
  const repairExistingKatexOperators = (root, katex) => {
    if (!root || typeof root.querySelectorAll !== 'function') return;

    const katexEls = Array.from(root.querySelectorAll('.katex'));
    for (const el of katexEls) {
      if (!el || typeof el.querySelector !== 'function') continue;
      if (el.closest(`[${EXISTING_KATEX_REPAIRED_ATTR}]`)) continue;
      if (el.closest('[data-gemini-inline-math-fix]')) continue;

      const ann = el.querySelector('.katex-mathml annotation');
      const tex = ann && ann.textContent ? ann.textContent : '';
      if (!tex) continue;

      const repaired = restoreOperatorSetBraces(tex);
      if (repaired === tex) continue;

      const displayContainer = el.closest('.katex-display');
      const container = displayContainer || el;
      if (!container || container.closest('[data-gemini-inline-math-fix]')) continue;
      if (container.hasAttribute(EXISTING_KATEX_REPAIRED_ATTR)) continue;

      const displayMode = !!displayContainer;
      try {
        katex.render(repaired, container, {
          displayMode,
          throwOnError: false,
          strict: 'ignore'
        });
        if (isKatexError(container)) {
          katex.render(tex, container, {
            displayMode,
            throwOnError: false,
            strict: 'ignore'
          });
          continue;
        }
        container.setAttribute(EXISTING_KATEX_REPAIRED_ATTR, '1');
      } catch (e) {
        // Ignore render failures.
      }
    }
  };

  const processAll = () => {
    ensureRefreshButton();

    const katex = getKatex();
    if (!katex) {
      if (katexWaitAttempts < KATEX_WAIT_MAX_ATTEMPTS) {
        katexWaitAttempts += 1;
        setTimeout(schedule, KATEX_WAIT_MS);
      }
      return;
    }
    hookKatex(katex);
    katexWaitAttempts = 0;

    const app = document.querySelector('ucs-standalone-app');
    if (!app || !app.shadowRoot) return;

    const roots = [];
    collectShadowRoots(app.shadowRoot, roots);
    const processedDocs = new Set();

    for (const r of roots) {
      patchMarkdownHosts(r);
      const docs = r.querySelectorAll('.markdown-document');
      for (const doc of docs) {
        const host = getMarkdownHost(doc);
        if (host) {
          const patchedAt = patchedMarkdownAt.get(host);
          if (patchedAt && Date.now() - patchedAt < PATCH_SKIP_WINDOW_MS) continue;
        }
        processRoot(doc, katex);
        processTableRows(doc, katex);
        processedDocs.add(doc);
      }

      const markdowns = r.querySelectorAll('ucs-fast-markdown, ucs-markdown, ucs-response-markdown');
      for (const fm of markdowns) {
        if (fm.shadowRoot) {
          const patchedAt = patchedMarkdownAt.get(fm);
          if (patchedAt && Date.now() - patchedAt < PATCH_SKIP_WINDOW_MS) continue;
          processRoot(fm.shadowRoot, katex);
          processTableRows(fm.shadowRoot, katex);
          fm.shadowRoot.querySelectorAll('.markdown-document').forEach((d) => processedDocs.add(d));
        }
      }

      repairExistingKatexOperators(r, katex);
    }

    if (followUpAttempts < FOLLOW_UP_MAX_ATTEMPTS) {
      let needsFollowUp = false;
      for (const doc of processedDocs) {
        if (hasUnrenderedMathDelimiters(doc.textContent || '')) {
          needsFollowUp = true;
          break;
        }
      }
      if (needsFollowUp) {
        followUpAttempts += 1;
        setTimeout(schedule, FOLLOW_UP_DELAY_MS);
      } else {
        followUpAttempts = 0;
      }
    }
  };

  let scheduled = false;
  const schedule = () => {
    if (scheduled) return;
    scheduled = true;
    setTimeout(() => {
      scheduled = false;
      processAll();
    }, 200);
  };

  const observer = new MutationObserver(schedule);
  observer.observe(document.documentElement, { subtree: true, childList: true, characterData: true });

  try {
    if (typeof unsafeWindow !== 'undefined' && unsafeWindow.__geminiInlineMathFix) {
      unsafeWindow.__geminiInlineMathFix.refresh = () => schedule();
    }
  } catch (e) {
    // Ignore if unsafeWindow is blocked.
  }

  scheduleKatexHook();
  schedule();
})();