Files
gf-lt/latex.go
2026-04-12 07:45:03 +03:00

191 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"regexp"
"strings"
)
var (
mathInline = regexp.MustCompile(`\$([^\$]+)\$`) // $...$
mathDisplay = regexp.MustCompile(`\$\$([^\$]+)\$\$`) // $$...$$
)
// RenderLatex converts all LaTeX math blocks in a string to terminalfriendly 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()
}