191 lines
6.2 KiB
Go
191 lines
6.2 KiB
Go
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()
|
||
}
|