Feat: handle latex
This commit is contained in:
190
latex.go
Normal file
190
latex.go
Normal file
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user