From 3e51ed2cebd6e32c7446a387bbbf82b39053ff68 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sat, 11 Apr 2026 17:07:16 +0300 Subject: [PATCH] Feat: handle latex --- helpfuncs.go | 4 +- latex.go | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 latex.go diff --git a/helpfuncs.go b/helpfuncs.go index 1f0149b..38d5552 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -181,7 +181,9 @@ func colorText() { for i, tb := range thinkBlocks { text = strings.Replace(text, fmt.Sprintf(placeholderThink, i), tb, 1) } - text = strings.ReplaceAll(text, `$\rightarrow$`, "->") + // text = strings.ReplaceAll(text, `$\rightarrow$`, "->") + text = RenderLatex(text) + text = AlignMarkdownTables(text) textView.SetText(text) } diff --git a/latex.go b/latex.go new file mode 100644 index 0000000..53f266f --- /dev/null +++ b/latex.go @@ -0,0 +1,190 @@ +package main + +import ( + "regexp" + "strings" +) + +var ( + mathInline = regexp.MustCompile(`\$([^\$]+)\$`) // $...$ + mathDisplay = regexp.MustCompile(`\$\$([^\$]+)\$\$`) // $$...$$ +) + +// RenderLatex converts all LaTeX math blocks in a string to terminal‑friendly text. +func RenderLatex(text string) string { + // Handle display math ($$...$$) – add newlines for separation + text = mathDisplay.ReplaceAllStringFunc(text, func(match string) string { + inner := mathDisplay.FindStringSubmatch(match)[1] + return "\n" + convertLatex(inner) + "\n" + }) + // Handle inline math ($...$) + text = mathInline.ReplaceAllStringFunc(text, func(match string) string { + inner := mathInline.FindStringSubmatch(match)[1] + return convertLatex(inner) + }) + return text +} + +func convertLatex(s string) string { + // ----- 1. Greek letters ----- + greek := map[string]string{ + `\alpha`: "α", `\beta`: "β", `\gamma`: "γ", `\delta`: "δ", + `\epsilon`: "ε", `\zeta`: "ζ", `\eta`: "η", `\theta`: "θ", + `\iota`: "ι", `\kappa`: "κ", `\lambda`: "λ", `\mu`: "μ", + `\nu`: "ν", `\xi`: "ξ", `\pi`: "π", `\rho`: "ρ", + `\sigma`: "σ", `\tau`: "τ", `\upsilon`: "υ", `\phi`: "φ", + `\chi`: "χ", `\psi`: "ψ", `\omega`: "ω", + `\Gamma`: "Γ", `\Delta`: "Δ", `\Theta`: "Θ", `\Lambda`: "Λ", + `\Xi`: "Ξ", `\Pi`: "Π", `\Sigma`: "Σ", `\Upsilon`: "Υ", + `\Phi`: "Φ", `\Psi`: "Ψ", `\Omega`: "Ω", + } + for cmd, uni := range greek { + s = strings.ReplaceAll(s, cmd, uni) + } + + // ----- 2. Arrows, relations, operators, symbols ----- + symbols := map[string]string{ + // Arrows + `\leftarrow`: "←", `\rightarrow`: "→", `\leftrightarrow`: "↔", + `\Leftarrow`: "⇐", `\Rightarrow`: "⇒", `\Leftrightarrow`: "⇔", + `\uparrow`: "↑", `\downarrow`: "↓", `\updownarrow`: "↕", + `\mapsto`: "↦", `\to`: "→", `\gets`: "←", + // Relations + `\le`: "≤", `\ge`: "≥", `\neq`: "≠", `\approx`: "≈", + `\equiv`: "≡", `\pm`: "±", `\mp`: "∓", `\times`: "×", + `\div`: "÷", `\cdot`: "·", `\circ`: "°", `\bullet`: "•", + // Other symbols + `\infty`: "∞", `\partial`: "∂", `\nabla`: "∇", `\exists`: "∃", + `\forall`: "∀", `\in`: "∈", `\notin`: "∉", `\subset`: "⊂", + `\subseteq`: "⊆", `\supset`: "⊃", `\supseteq`: "⊇", `\cup`: "∪", + `\cap`: "∩", `\emptyset`: "∅", `\ell`: "ℓ", `\Re`: "ℜ", + `\Im`: "ℑ", `\wp`: "℘", `\dag`: "†", `\ddag`: "‡", + `\prime`: "′", `\degree`: "°", // some LLMs output \degree + } + for cmd, uni := range symbols { + s = strings.ReplaceAll(s, cmd, uni) + } + + // ----- 3. Remove \text{...} ----- + textRe := regexp.MustCompile(`\\text\{([^}]*)\}`) + s = textRe.ReplaceAllString(s, "$1") + + // ----- 4. Fractions: \frac{a}{b} → a/b ----- + fracRe := regexp.MustCompile(`\\frac\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}`) + s = fracRe.ReplaceAllString(s, "$1/$2") + + // ----- 5. Remove formatting commands (\mathrm, \mathbf, etc.) ----- + for _, cmd := range []string{"mathrm", "mathbf", "mathit", "mathsf", "mathtt", "mathbb", "mathcal"} { + re := regexp.MustCompile(`\\` + cmd + `\{([^}]*)\}`) + s = re.ReplaceAllString(s, "$1") + } + + // ----- 6. Subscripts and superscripts ----- + s = convertSubscripts(s) + s = convertSuperscripts(s) + + // ----- 7. Clean up leftover braces (but keep backslashes) ----- + s = strings.ReplaceAll(s, "{", "") + s = strings.ReplaceAll(s, "}", "") + + // ----- 8. (Optional) Remove any remaining backslash+word if you really want ----- + // But as discussed, this can break things. I'll leave it commented. + // cmdRe := regexp.MustCompile(`\\([a-zA-Z]+)`) + // s = cmdRe.ReplaceAllString(s, "$1") + + return s +} + +// Subscript converter (handles both _{...} and _x) +func convertSubscripts(s string) string { + subMap := map[rune]string{ + '0': "₀", '1': "₁", '2': "₂", '3': "₃", '4': "₄", + '5': "₅", '6': "₆", '7': "₇", '8': "₈", '9': "₉", + '+': "₊", '-': "₋", '=': "₌", '(': "₍", ')': "₎", + 'a': "ₐ", 'e': "ₑ", 'i': "ᵢ", 'o': "ₒ", 'u': "ᵤ", + 'v': "ᵥ", 'x': "ₓ", + } + // Braced: _{...} + reBraced := regexp.MustCompile(`_\{([^}]*)\}`) + s = reBraced.ReplaceAllStringFunc(s, func(match string) string { + inner := reBraced.FindStringSubmatch(match)[1] + return subscriptify(inner, subMap) + }) + // Unbraced: _x (single character) + reUnbraced := regexp.MustCompile(`_([a-zA-Z0-9])`) + s = reUnbraced.ReplaceAllStringFunc(s, func(match string) string { + ch := rune(match[1]) + if sub, ok := subMap[ch]; ok { + return sub + } + return match // keep original _x + }) + return s +} + +func subscriptify(inner string, subMap map[rune]string) string { + var out strings.Builder + for _, ch := range inner { + if sub, ok := subMap[ch]; ok { + out.WriteString(sub) + } else { + return "_{" + inner + "}" // fallback + } + } + return out.String() +} + +// Superscript converter (handles both ^{...} and ^x) +func convertSuperscripts(s string) string { + supMap := map[rune]string{ + '0': "⁰", '1': "¹", '2': "²", '3': "³", '4': "⁴", + '5': "⁵", '6': "⁶", '7': "⁷", '8': "⁸", '9': "⁹", + '+': "⁺", '-': "⁻", '=': "⁼", '(': "⁽", ')': "⁾", + 'n': "ⁿ", 'i': "ⁱ", + } + // Special single-character superscripts that replace the caret entirely + specialSup := map[string]string{ + "°": "°", // degree + "'": "′", // prime + "\"": "″", // double prime + } + // Braced: ^{...} + reBraced := regexp.MustCompile(`\^\{(.*?)\}`) + s = reBraced.ReplaceAllStringFunc(s, func(match string) string { + inner := reBraced.FindStringSubmatch(match)[1] + return superscriptify(inner, supMap, specialSup) + }) + // Unbraced: ^x (single character) + reUnbraced := regexp.MustCompile(`\^([^\{[:space:]]?)`) + s = reUnbraced.ReplaceAllStringFunc(s, func(match string) string { + if len(match) < 2 { + return match + } + ch := match[1:] + if special, ok := specialSup[ch]; ok { + return special + } + if len(ch) == 1 { + if sup, ok := supMap[rune(ch[0])]; ok { + return sup + } + } + return match // keep ^x + }) + return s +} + +func superscriptify(inner string, supMap map[rune]string, specialSup map[string]string) string { + if special, ok := specialSup[inner]; ok { + return special + } + var out strings.Builder + for _, ch := range inner { + if sup, ok := supMap[ch]; ok { + out.WriteString(sup) + } else { + return "^{" + inner + "}" // fallback + } + } + return out.String() +}