Feat: handle latex
This commit is contained in:
@@ -181,7 +181,9 @@ func colorText() {
|
|||||||
for i, tb := range thinkBlocks {
|
for i, tb := range thinkBlocks {
|
||||||
text = strings.Replace(text, fmt.Sprintf(placeholderThink, i), tb, 1)
|
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)
|
textView.SetText(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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