如果Gemini的Deep Research產出研究報告裡面有數學公式或上下標,研究報告匯出成Google文件時,就會有很多$….$的符號,無法轉換成正確格式。
找Gemini解決這個問題非常不成功,就換成ChatGPT,大致上解決了。
解決的方法是土法煉鋼,將文章中LaTex的語法一一對照取代,用窮舉式的寫法,一個代碼一個代碼設定在程式碼裡面。未來若遇到程式不認得的代碼,就再加進去。
當初有個瓶頸是替換LaTex代碼時,文章格式會跑掉,但AI最後還是想辦法解決了。
程式碼寫在Google文件的Apps Script裡面,跟著一份Google文件走。所以複製這份Google文件就可以有這個功能,不用輸入程式碼。
這是可以複製去用的文件範本,點選連結後,網頁畫面就會直接要求你建立一個副本,文件中有使用說明。
結尾有我和AI的對話,其中有如何將APP Script加入Google文件的流程。未來如果有LaTex代碼沒解析到的,可以把程式碼貼給AI叫他加,或是自己手動加在程式碼裡面。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | function onOpen() { DocumentApp.getUi() .createMenu('LaTeX 轉換') .addItem('建立轉換後的新文件(保留格式)', 'createLatexConvertedCopy') .addToUi(); } // 1. 複製整份文件 → 2. 在副本中逐段處理 $...$ 與 frac{...}{...} function createLatexConvertedCopy() { var srcDoc = DocumentApp.getActiveDocument(); var srcId = srcDoc.getId(); // 1. 用 Drive 複製整份文件(格式、圖片、表格都保留) var file = DriveApp.getFileById(srcId); var copy = file.makeCopy(srcDoc.getName() + '(LaTeX 轉換版)'); var newDoc = DocumentApp.openById(copy.getId()); var body = newDoc.getBody(); // 2. 在副本裡處理所有段落 / 清單 / 表格中的文字 processElement_(body); newDoc.saveAndClose(); DocumentApp.getUi().alert( '已建立 LaTeX 轉換版文件(格式及圖片保留):\n' + newDoc.getUrl() ); } /** * 遞迴走訪整個文件樹: * - Paragraph / ListItem → 以「整行文字」為單位處理 * - Table / Cell → 繼續往下走 */ function processElement_(elem) { var ElementType = DocumentApp.ElementType; var type = elem.getType && elem.getType(); if (type === ElementType.PARAGRAPH || type === ElementType.LIST_ITEM) { var text = elem.editAsText(); if (text) { convertParagraphText_(text); } } if (elem.getNumChildren) { var n = elem.getNumChildren(); for (var i = 0; i < n; i++) { processElement_(elem.getChild(i)); } } } /** * 對單一段落 (editAsText): * 1. 先處理整段裡所有 $...$ * 2. 再處理「裸的」 frac{a}{b} */ function convertParagraphText_(text) { var full = text.getText(); if (full.indexOf('$') === -1 && full.indexOf('frac{') === -1) return; // --- 找出所有 $...$ 的位置(用原始 full 索引)--- var dollarMatches = []; var re = /\$([^$]+)\$/g; var m; while ((m = re.exec(full)) !== null) { dollarMatches.push({ start: m.index, end: re.lastIndex - 1, inner: m[1] }); } // 從後往前改,避免索引被前面的修改影響 for (var i = dollarMatches.length - 1; i >= 0; i--) { var dm = dollarMatches[i]; var converted = latexToUnicode_(dm.inner); text.deleteText(dm.start, dm.end); if (converted.length > 0) { text.insertText(dm.start, converted); } } // --- 再處理沒有 $ 包起來的 frac{a}{b} --- full = text.getText(); var fracRe = /frac\{([^}]+)\}\{([^}]+)\}/g; var fracMatches = []; while ((m = fracRe.exec(full)) !== null) { fracMatches.push({ start: m.index, end: fracRe.lastIndex - 1, num: m[1], den: m[2] }); } for (var j = fracMatches.length - 1; j >= 0; j--) { var fm = fracMatches[j]; var rep = '(' + fm.num + '/' + fm.den + ')'; text.deleteText(fm.start, fm.end); text.insertText(fm.start, rep); } } /** * 處理一小段 LaTeX(不含外層 $) * 例如:"V_E", "R_{\\text{ref}}", "\\mu = 3.986 \\times 10^{14}" */ function latexToUnicode_(latex) { var t = latex; // 1. \text{ km } → km t = t.replace(/\\text\{([^}]*)\}/g, '$1'); // 2. \frac{a}{b} → (a/b) t = t.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, function(match, num, den) { return '(' + num + '/' + den + ')'; }); // 3. LaTeX 指令 → 對應符號(μ, ρ, ×, ≈, ∞...) var map = getSymbolMap_(); t = t.replace(/\\[a-zA-Z]+/g, function(cmd) { return map[cmd] || cmd.slice(1); // 未定義 → 去掉反斜線 }); // 4. _{...} / _x / ^{...} / ^x → Unicode 上下標 return applySubSupUnicode_(t); } /** * 把 _、^ 語法變成 Unicode 上/下標字元 */ function applySubSupUnicode_(str) { var result = ''; var i = 0; while (i < str.length) { var ch = str.charAt(i); if ((ch === '_' || ch === '^') && i + 1 < str.length) { var isSub = (ch === '_'); var next = str.charAt(i + 1); var content = ''; var j; if (next === '{') { j = i + 2; while (j < str.length && str.charAt(j) !== '}') { content += str.charAt(j); j++; } if (j < str.length && str.charAt(j) === '}') j++; } else { content = next; j = i + 2; } result += mapToSubSup_(content, isSub); i = j; } else { result += ch; i++; } } return result; } /** * 把 content 裡每個字元映射成上標/下標 * 支援 a–z / A–Z / 0–9 */ function mapToSubSup_(content, isSub) { // 下標對照表 var subMap = { '0':'₀','1':'₁','2':'₂','3':'₃','4':'₄','5':'₅','6':'₆','7':'₇','8':'₈','9':'₉', 'a':'ₐ','b':'ᵦ','c':'꜀','d':'꜁','e':'ₑ','f':'ᵮ','g':'ᵧ','h':'ₕ', 'i':'ᵢ','j':'ⱼ','k':'ₖ','l':'ₗ','m':'ₘ','n':'ₙ','o':'ₒ','p':'ₚ', 'q':'ᑫ','r':'ᵣ','s':'ₛ','t':'ₜ','u':'ᵤ','v':'ᵥ','w':'𝓌','x':'ₓ', 'y':'ᵧ','z':'𝓏', // 大寫下標(無正式 Unicode,下列用近似字元) 'A':'ₐ','B':'ᵦ','C':'꜀','D':'ᵭ','E':'ₑ','F':'𝆑','G':'ᵍ', 'H':'ₕ','I':'ᵢ','J':'ⱼ','K':'ₖ','L':'ₗ','M':'ₘ','N':'ₙ','O':'ₒ', 'P':'ₚ','Q':'ᑫ','R':'ᵣ','S':'ₛ','T':'ₜ','U':'ᵤ','V':'ᵥ', 'W':'𝓌','X':'ₓ','Y':'ᵧ','Z':'𝓏' }; // 上標對照表 var supMap = { '0':'⁰','1':'¹','2':'²','3':'³','4':'⁴','5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹', 'a':'ᵃ','b':'ᵇ','c':'ᶜ','d':'ᵈ','e':'ᵉ','f':'ᶠ','g':'ᵍ','h':'ʰ', 'i':'ⁱ','j':'ʲ','k':'ᵏ','l':'ˡ','m':'ᵐ','n':'ⁿ','o':'ᵒ','p':'ᵖ', 'q':'ᑫ','r':'ʳ','s':'ˢ','t':'ᵗ','u':'ᵘ','v':'ᵛ','w':'ʷ','x':'ˣ', 'y':'ʸ','z':'ᶻ', // 大寫上標 'A':'ᴬ','B':'ᴮ','C':'ᶜ','D':'ᴰ','E':'ᴱ','F':'ᶠ','G':'ᴳ', 'H':'ᴴ','I':'ᴵ','J':'ᴶ','K':'ᴷ','L':'ᴸ','M':'ᴹ','N':'ᴺ', 'O':'ᴼ','P':'ᴾ','Q':'ᑫ','R':'ᴿ','S':'ˢ','T':'ᵀ','U':'ᵁ', 'V':'ⱽ','W':'ᵂ','X':'ˣ','Y':'ʸ','Z':'ᶻ' }; var table = isSub ? subMap : supMap; var out = ''; for (var i = 0; i < content.length; i++) { var c = content.charAt(i); out += (table[c] || c); } return out; } /** * LaTeX → Unicode 符號表(可再擴充) */ function getSymbolMap_() { return { // 希臘字母 '\\mu': 'μ', '\\rho': 'ρ', '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', '\\epsilon': 'ε', '\\varepsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ', '\\sigma': 'σ', '\\omega': 'ω', '\\Omega': 'Ω', // 常用數學符號 '\\infty': '∞', '\\times': '×', '\\cdot': '·', '\\approx': '≈', '\\propto': '∝', '\\leq': '≤', '\\geq': '≥', '\\neq': '≠', '\\gg': '≫', '\\ll': '≪', '\\pm': '±', '\\sqrt': '√' }; } |
程式碼寫在Google文件的Apps Script裡面,跟著一份Google文件走。所以複製這份Google文件就可以有這個功能,不用輸入程式碼。
這是可以複製去用的文件範本,點選連結後,網頁畫面就會直接要求你建立一個副本,文件中有使用說明。
這是和AI的對話過程。
非常謝謝強哥老師提供的 LaTex 語法轉換程式 真的太好用了 非常感謝~~