fix: latex math support (#6)

* fix: latex math support

* move latex junk to another file
This commit is contained in:
Rasmus Wejlgaard 2025-11-08 23:22:37 +00:00 committed by GitHub
parent 015feb3637
commit 23c7095477
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 418 additions and 11 deletions

380
internal/ui/latex.go Normal file
View file

@ -0,0 +1,380 @@
package ui
import "strings"
// renderLatexMath converts LaTeX math expressions to Unicode for terminal display
func renderLatexMath(latex string) string {
result := latex
// Remove LaTeX math delimiters
result = strings.ReplaceAll(result, `\(`, "")
result = strings.ReplaceAll(result, `\)`, "")
result = strings.ReplaceAll(result, `\[`, "")
result = strings.ReplaceAll(result, `\]`, "")
result = strings.ReplaceAll(result, `$$`, "")
// Remove single $ delimiters (being careful not to remove actual dollar signs in text)
// Simple approach: if line starts/ends with $, remove it
result = strings.TrimSpace(result)
if strings.HasPrefix(result, "$") && strings.HasSuffix(result, "$") && len(result) > 2 {
result = result[1 : len(result)-1]
result = strings.TrimSpace(result)
}
// Map of common LaTeX commands to Unicode equivalents
replacements := map[string]string{
// Greek letters (lowercase)
`\alpha`: "α",
`\beta`: "β",
`\gamma`: "γ",
`\delta`: "δ",
`\epsilon`: "ε",
`\zeta`: "ζ",
`\eta`: "η",
`\theta`: "θ",
`\iota`: "ι",
`\kappa`: "κ",
`\lambda`: "λ",
`\mu`: "μ",
`\nu`: "ν",
`\xi`: "ξ",
`\pi`: "π",
`\rho`: "ρ",
`\sigma`: "σ",
`\tau`: "τ",
`\upsilon`: "υ",
`\phi`: "φ",
`\chi`: "χ",
`\psi`: "ψ",
`\omega`: "ω",
// Greek letters (uppercase)
`\Gamma`: "Γ",
`\Delta`: "Δ",
`\Theta`: "Θ",
`\Lambda`: "Λ",
`\Xi`: "Ξ",
`\Pi`: "Π",
`\Sigma`: "Σ",
`\Upsilon`: "Υ",
`\Phi`: "Φ",
`\Psi`: "Ψ",
`\Omega`: "Ω",
// Math operators
`\times`: "×",
`\div`: "÷",
`\pm`: "±",
`\mp`: "∓",
`\cdot`: "·",
`\star`: "⋆",
`\ast`: "",
`\circ`: "∘",
`\bullet`: "•",
// Relations
`\le`: "≤",
`\ge`: "≥",
`\leq`: "≤",
`\geq`: "≥",
`\ne`: "≠",
`\neq`: "≠",
`\approx`: "≈",
`\equiv`: "≡",
`\sim`: "",
`\simeq`: "≃",
`\propto`: "∝",
// Arrows
`\to`: "→",
`\rightarrow`: "→",
`\leftarrow`: "←",
`\Rightarrow`: "⇒",
`\Leftarrow`: "⇐",
`\mapsto`: "↦",
// Set theory
`\in`: "∈",
`\notin`: "∉",
`\subset`: "⊂",
`\supset`: "⊃",
`\subseteq`: "⊆",
`\supseteq`: "⊇",
`\cup`: "",
`\cap`: "∩",
`\emptyset`: "∅",
`\forall`: "∀",
`\exists`: "∃",
// Calculus
`\partial`: "∂",
`\nabla`: "∇",
`\int`: "∫",
`\sum`: "∑",
`\prod`: "∏",
`\infty`: "∞",
// Logic
`\land`: "∧",
`\lor`: "",
`\lnot`: "¬",
`\neg`: "¬",
`\wedge`: "∧",
`\vee`: "",
// Special symbols
`\hbar`: "ℏ",
`\ell`: "",
`\Re`: "",
`\Im`: "",
`\angle`: "∠",
`\triangle`: "△",
`\square`: "□",
`\degree`: "°",
// Superscripts (common ones)
`^0`: "⁰",
`^1`: "¹",
`^2`: "²",
`^3`: "³",
`^4`: "⁴",
`^5`: "⁵",
`^6`: "⁶",
`^7`: "⁷",
`^8`: "⁸",
`^9`: "⁹",
`^+`: "⁺",
`^-`: "⁻",
`^=`: "⁼",
`^(`: "⁽",
`^)`: "⁾",
// Subscripts (common ones)
`_0`: "₀",
`_1`: "₁",
`_2`: "₂",
`_3`: "₃",
`_4`: "₄",
`_5`: "₅",
`_6`: "₆",
`_7`: "₇",
`_8`: "₈",
`_9`: "₉",
`_+`: "₊",
`_-`: "₋",
`_=`: "₌",
`_(`: "₍",
`_)`: "₎",
}
// Apply all replacements
for latexCmd, unicode := range replacements {
result = strings.ReplaceAll(result, latexCmd, unicode)
}
// Handle simple fractions \frac{a}{b} -> a/b
result = handleFractions(result)
// Handle square roots \sqrt{x} -> √(x)
result = handleSquareRoots(result)
// Handle superscripts ^{...} and subscripts _{...}
result = handleSuperscripts(result)
result = handleSubscripts(result)
return result
}
// handleFractions converts \frac{numerator}{denominator} to numerator/denominator
func handleFractions(text string) string {
// Simple regex-free approach for basic fractions
result := text
for {
start := strings.Index(result, `\frac{`)
if start == -1 {
break
}
// Find the numerator
numStart := start + 6
numEnd, numerator := findBracedContent(result, numStart)
if numEnd == -1 {
break
}
// Find the denominator
if numEnd >= len(result) || result[numEnd] != '{' {
break
}
denomEnd, denominator := findBracedContent(result, numEnd+1)
if denomEnd == -1 {
break
}
// Replace \frac{num}{denom} with (num)/(denom)
replacement := "(" + numerator + ")/(" + denominator + ")"
result = result[:start] + replacement + result[denomEnd:]
}
return result
}
// handleSquareRoots converts \sqrt{x} to √(x)
func handleSquareRoots(text string) string {
result := text
for {
start := strings.Index(result, `\sqrt{`)
if start == -1 {
break
}
// Find the content
contentStart := start + 6
contentEnd, content := findBracedContent(result, contentStart)
if contentEnd == -1 {
break
}
// Replace \sqrt{content} with √(content)
replacement := "√(" + content + ")"
result = result[:start] + replacement + result[contentEnd:]
}
return result
}
// findBracedContent finds content within braces starting at position i
// Returns the position after the closing brace and the content
func findBracedContent(text string, start int) (int, string) {
if start >= len(text) {
return -1, ""
}
depth := 1
i := start
for i < len(text) && depth > 0 {
if text[i] == '{' {
depth++
} else if text[i] == '}' {
depth--
if depth == 0 {
return i + 1, text[start:i]
}
}
i++
}
return -1, ""
}
// handleSuperscripts converts ^{...} to Unicode superscripts where possible
func handleSuperscripts(text string) string {
superscriptMap := map[rune]string{
'0': "⁰", '1': "¹", '2': "²", '3': "³", '4': "⁴",
'5': "⁵", '6': "⁶", '7': "⁷", '8': "⁸", '9': "⁹",
'a': "ᵃ", 'b': "ᵇ", 'c': "ᶜ", 'd': "ᵈ", 'e': "ᵉ",
'f': "ᶠ", 'g': "ᵍ", 'h': "ʰ", 'i': "ⁱ", 'j': "ʲ",
'k': "ᵏ", 'l': "ˡ", 'm': "ᵐ", 'n': "ⁿ", 'o': "ᵒ",
'p': "ᵖ", 'r': "ʳ", 's': "ˢ", 't': "ᵗ", 'u': "ᵘ",
'v': "ᵛ", 'w': "ʷ", 'x': "ˣ", 'y': "ʸ", 'z': "ᶻ",
'A': "ᴬ", 'B': "ᴮ", 'D': "ᴰ", 'E': "ᴱ", 'G': "ᴳ",
'H': "ᴴ", 'I': "ᴵ", 'J': "ᴶ", 'K': "ᴷ", 'L': "ᴸ",
'M': "ᴹ", 'N': "ᴺ", 'O': "ᴼ", 'P': "ᴾ", 'R': "ᴿ",
'T': "ᵀ", 'U': "ᵁ", 'V': "ⱽ", 'W': "ᵂ",
'+': "⁺", '-': "⁻", '=': "⁼", '(': "⁽", ')': "⁾",
}
result := text
// Handle ^{...} format
for {
start := strings.Index(result, "^{")
if start == -1 {
break
}
contentStart := start + 2
contentEnd, content := findBracedContent(result, contentStart)
if contentEnd == -1 {
break
}
// Convert content to superscript
var superscript strings.Builder
for _, ch := range content {
if sup, ok := superscriptMap[ch]; ok {
superscript.WriteString(sup)
} else {
// If no superscript version exists, wrap in parentheses
superscript.WriteRune(ch)
}
}
result = result[:start] + superscript.String() + result[contentEnd:]
}
// Handle simple ^x format (single character without braces)
for i := 0; i < len(result)-1; i++ {
if result[i] == '^' && result[i+1] != '{' {
ch := rune(result[i+1])
if sup, ok := superscriptMap[ch]; ok {
result = result[:i] + sup + result[i+2:]
}
}
}
return result
}
// handleSubscripts converts _{...} to Unicode subscripts where possible
func handleSubscripts(text string) string {
subscriptMap := map[rune]string{
'0': "₀", '1': "₁", '2': "₂", '3': "₃", '4': "₄",
'5': "₅", '6': "₆", '7': "₇", '8': "₈", '9': "₉",
'a': "ₐ", 'e': "ₑ", 'h': "ₕ", 'i': "ᵢ", 'j': "ⱼ",
'k': "ₖ", 'l': "ₗ", 'm': "ₘ", 'n': "ₙ", 'o': "ₒ",
'p': "ₚ", 'r': "ᵣ", 's': "ₛ", 't': "ₜ", 'u': "ᵤ",
'v': "ᵥ", 'x': "ₓ",
'+': "₊", '-': "₋", '=': "₌", '(': "₍", ')': "₎",
}
result := text
// Handle _{...} format
for {
start := strings.Index(result, "_{")
if start == -1 {
break
}
contentStart := start + 2
contentEnd, content := findBracedContent(result, contentStart)
if contentEnd == -1 {
break
}
// Convert content to subscript
var subscript strings.Builder
for _, ch := range content {
if sub, ok := subscriptMap[ch]; ok {
subscript.WriteString(sub)
} else {
// If no subscript version exists, wrap in parentheses
subscript.WriteRune(ch)
}
}
result = result[:start] + subscript.String() + result[contentEnd:]
}
// Handle simple _x format (single character without braces)
for i := 0; i < len(result)-1; i++ {
if result[i] == '_' && result[i+1] != '{' {
ch := rune(result[i+1])
if sub, ok := subscriptMap[ch]; ok {
result = result[:i] + sub + result[i+2:]
}
}
}
return result
}

View file

@ -660,11 +660,20 @@ func renderNotesWithHighlighting(notes []string) []string {
} else if codeBlockDelimiter == "markdown" {
// Ending a markdown code block
inCodeBlock = false
// Highlight and add the code
// Highlight and add the code (or render LaTeX)
if len(codeLines) > 0 {
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
highlightedLines := strings.Split(highlighted, "\n")
result = append(result, highlightedLines...)
var processedLines []string
if codeLanguage == "latex" {
// Apply LaTeX-to-Unicode conversion
for _, line := range codeLines {
processedLines = append(processedLines, renderLatexMath(line))
}
} else {
// Apply syntax highlighting
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
processedLines = strings.Split(highlighted, "\n")
}
result = append(result, processedLines...)
}
result = append(result, note) // Keep the delimiter visible
codeLines = []string{}
@ -677,11 +686,20 @@ func renderNotesWithHighlighting(notes []string) []string {
// Check for org-mode style code block end
if strings.HasPrefix(trimmed, "#+END_SRC") {
inCodeBlock = false
// Highlight and add the code
// Highlight and add the code (or render LaTeX)
if len(codeLines) > 0 {
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
highlightedLines := strings.Split(highlighted, "\n")
result = append(result, highlightedLines...)
var processedLines []string
if codeLanguage == "latex" {
// Apply LaTeX-to-Unicode conversion
for _, line := range codeLines {
processedLines = append(processedLines, renderLatexMath(line))
}
} else {
// Apply syntax highlighting
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
processedLines = strings.Split(highlighted, "\n")
}
result = append(result, processedLines...)
}
result = append(result, note) // Keep the delimiter visible
codeLines = []string{}
@ -700,9 +718,18 @@ func renderNotesWithHighlighting(notes []string) []string {
// Handle case where code block wasn't closed
if inCodeBlock && len(codeLines) > 0 {
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
highlightedLines := strings.Split(highlighted, "\n")
result = append(result, highlightedLines...)
var processedLines []string
if codeLanguage == "latex" {
// Apply LaTeX-to-Unicode conversion
for _, line := range codeLines {
processedLines = append(processedLines, renderLatexMath(line))
}
} else {
// Apply syntax highlighting
highlighted := highlightCode(strings.Join(codeLines, "\n"), codeLanguage)
processedLines = strings.Split(highlighted, "\n")
}
result = append(result, processedLines...)
}
return result