返回 DeepSeek-Reasonix
latex.go
根目录 / internal / cli / latex.go
1 package cli
2
3 import (
4 "strings"
5 "unicode/utf8"
6 )
7
8 // latexToUnicode renders a LaTeX math expression as a best-effort Unicode
9 // approximation suitable for a terminal: Greek letters, operators/relations,
10 // super/subscripts, \frac, \sqrt and accents map to real glyphs; anything it
11 // can't represent degrades to a readable plain-text form. There is no Go
12 // library for this, so the symbol table below is maintained by hand.
13 func latexToUnicode(expr string) string {
14 return convertMath([]rune(expr))
15 }
16
17 func convertMath(rs []rune) string {
18 var b strings.Builder
19 b.Grow(len(rs))
20 for i := 0; i < len(rs); {
21 switch r := rs[i]; r {
22 case '\\':
23 i = convertCommand(&b, rs, i)
24 case '^':
25 arg, ni := readAtom(rs, i+1, false)
26 b.WriteString(superscript(convertMath([]rune(arg))))
27 i = ni
28 case '_':
29 arg, ni := readAtom(rs, i+1, false)
30 b.WriteString(subscript(convertMath([]rune(arg))))
31 i = ni
32 case '{', '}':
33 i++
34 case '&', '~':
35 b.WriteByte(' ')
36 i++
37 case '$':
38 i++
39 default:
40 b.WriteRune(r)
41 i++
42 }
43 }
44 return b.String()
45 }
46
47 // convertCommand consumes the backslash command starting at rs[i] and writes
48 // its rendering; it returns the index just past everything it consumed.
49 func convertCommand(b *strings.Builder, rs []rune, i int) int {
50 j := i + 1
51 if j >= len(rs) {
52 return j
53 }
54 if !isASCIILetter(rs[j]) {
55 switch ch := rs[j]; ch {
56 case '\\':
57 b.WriteString(" ")
58 case ',', ';', ':', '!', ' ':
59 b.WriteByte(' ')
60 default:
61 b.WriteRune(ch)
62 }
63 return j + 1
64 }
65
66 k := j
67 for k < len(rs) && isASCIILetter(rs[k]) {
68 k++
69 }
70 cmd := string(rs[j:k])
71
72 switch cmd {
73 case "frac", "tfrac", "dfrac":
74 num, k2 := readAtom(rs, k, true)
75 den, k3 := readAtom(rs, k2, true)
76 b.WriteString(renderFrac(convertMath([]rune(num)), convertMath([]rune(den))))
77 return k3
78 case "sqrt":
79 idx := ""
80 if k < len(rs) && rs[k] == '[' {
81 idx, k = readBracket(rs, k)
82 }
83 arg, k2 := readAtom(rs, k, true)
84 b.WriteString(renderSqrt(idx, convertMath([]rune(arg))))
85 return k2
86 case "text", "textrm", "textbf", "textit", "mathrm", "mathsf", "mathtt", "mathit", "mathbf", "mathcal", "operatorname":
87 arg, k2 := readAtom(rs, k, true)
88 b.WriteString(arg)
89 return k2
90 case "mathbb":
91 arg, k2 := readAtom(rs, k, true)
92 b.WriteString(blackboard(arg))
93 return k2
94 case "boxed":
95 // A terminal can't draw a box, so render only the inner content.
96 // Without this, \boxed{x} printed the literal word "boxedx".
97 arg, k2 := readAtom(rs, k, true)
98 b.WriteString(convertMath([]rune(arg)))
99 return k2
100 case "left", "right":
101 // \left. / \right. are invisible delimiters — drop the dot; any other
102 // delimiter follows as ordinary text and renders its glyph.
103 if k < len(rs) && rs[k] == '.' {
104 return k + 1
105 }
106 return k
107 case "big", "Big", "bigg", "Bigg", "bigl", "bigr", "Bigl", "Bigr", "displaystyle", "textstyle", "limits", "nolimits":
108 return k
109 case "begin", "end":
110 _, k2 := readAtom(rs, k, true)
111 return k2
112 }
113
114 if combining, ok := accents[cmd]; ok {
115 arg, k2 := readAtom(rs, k, true)
116 b.WriteString(applyCombining(convertMath([]rune(arg)), combining))
117 return k2
118 }
119 if sym, ok := symbols[cmd]; ok {
120 b.WriteString(sym)
121 return k
122 }
123 b.WriteString(cmd)
124 return k
125 }
126
127 // readAtom reads the argument of a command or script: a {balanced group}, a
128 // \command, or a single rune. Returns the inner text (no surrounding braces)
129 // and the index just past it.
130 func readAtom(rs []rune, i int, skipSpaces bool) (string, int) {
131 if skipSpaces {
132 for i < len(rs) && rs[i] == ' ' {
133 i++
134 }
135 }
136 if i >= len(rs) {
137 return "", i
138 }
139 switch rs[i] {
140 case '{':
141 depth := 0
142 start := i + 1
143 for j := i; j < len(rs); j++ {
144 switch rs[j] {
145 case '{':
146 depth++
147 case '}':
148 depth--
149 if depth == 0 {
150 return string(rs[start:j]), j + 1
151 }
152 }
153 }
154 return string(rs[start:]), len(rs)
155 case '\\':
156 k := i + 1
157 if k < len(rs) && !isASCIILetter(rs[k]) {
158 return string(rs[i : k+1]), k + 1
159 }
160 for k < len(rs) && isASCIILetter(rs[k]) {
161 k++
162 }
163 return string(rs[i:k]), k
164 default:
165 return string(rs[i]), i + 1
166 }
167 }
168
169 func readBracket(rs []rune, i int) (string, int) {
170 start := i + 1
171 for j := start; j < len(rs); j++ {
172 if rs[j] == ']' {
173 return string(rs[start:j]), j + 1
174 }
175 }
176 return "", len(rs)
177 }
178
179 func renderFrac(num, den string) string {
180 return wrapIfCompound(num) + "/" + wrapIfCompound(den)
181 }
182
183 func renderSqrt(idx, arg string) string {
184 if utf8.RuneCountInString(arg) > 1 {
185 arg = "(" + arg + ")"
186 }
187 switch idx {
188 case "", "2":
189 return "√" + arg
190 case "3":
191 return "∛" + arg
192 case "4":
193 return "∜" + arg
194 }
195 return superscript(idx) + "√" + arg
196 }
197
198 func wrapIfCompound(s string) string {
199 if utf8.RuneCountInString(s) > 1 {
200 return "(" + s + ")"
201 }
202 return s
203 }
204
205 func applyCombining(s string, mark rune) string {
206 rs := []rune(s)
207 if len(rs) == 0 {
208 return string(mark)
209 }
210 return string(rs[0]) + string(mark) + string(rs[1:])
211 }
212
213 func blackboard(s string) string {
214 var b strings.Builder
215 for _, r := range s {
216 if bb, ok := blackboardCaps[r]; ok {
217 b.WriteRune(bb)
218 } else {
219 b.WriteRune(r)
220 }
221 }
222 return b.String()
223 }
224
225 func superscript(s string) string {
226 if t, ok := mapAll(s, superMap); ok {
227 return t
228 }
229 if utf8.RuneCountInString(s) == 1 {
230 return "^" + s
231 }
232 return "^(" + s + ")"
233 }
234
235 func subscript(s string) string {
236 if t, ok := mapAll(s, subMap); ok {
237 return t
238 }
239 if utf8.RuneCountInString(s) == 1 {
240 return "_" + s
241 }
242 return "_(" + s + ")"
243 }
244
245 func mapAll(s string, m map[rune]rune) (string, bool) {
246 if s == "" {
247 return "", true
248 }
249 var b strings.Builder
250 for _, r := range s {
251 c, ok := m[r]
252 if !ok {
253 return "", false
254 }
255 b.WriteRune(c)
256 }
257 return b.String(), true
258 }
259
260 func isASCIILetter(r rune) bool {
261 return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
262 }
263
264 // normalizeMath rewrites the alternate math delimiters \(..\) and \[..\] to
265 // $..$ / $$..$$ and collapses newlines inside a $$ display block onto one line
266 // so the inline math parser sees a single contiguous run. It tracks fenced and
267 // inline code so literal delimiters inside code are never rewritten.
268 func normalizeMath(s string) string {
269 rs := []rune(s)
270 n := len(rs)
271 var b strings.Builder
272 b.Grow(len(s))
273
274 inFenced, inCode, inDisplay := false, false, false
275
276 for i := 0; i < n; {
277 r := rs[i]
278
279 if r == '`' && i+2 < n && rs[i+1] == '`' && rs[i+2] == '`' {
280 inFenced = !inFenced
281 b.WriteString("```")
282 i += 3
283 continue
284 }
285 if r == '`' && !inFenced {
286 inCode = !inCode
287 b.WriteRune(r)
288 i++
289 continue
290 }
291 if inFenced || inCode {
292 b.WriteRune(r)
293 i++
294 continue
295 }
296
297 if r == '\\' && i+1 < n {
298 switch rs[i+1] {
299 case '\\':
300 b.WriteString("\\\\")
301 i += 2
302 continue
303 case '[':
304 b.WriteString("$$")
305 inDisplay = true
306 i += 2
307 continue
308 case ']':
309 b.WriteString("$$")
310 inDisplay = false
311 i += 2
312 continue
313 case '(':
314 b.WriteString("$")
315 i += 2
316 continue
317 case ')':
318 b.WriteString("$")
319 i += 2
320 continue
321 }
322 }
323 if r == '$' && i+1 < n && rs[i+1] == '$' {
324 b.WriteString("$$")
325 inDisplay = !inDisplay
326 i += 2
327 continue
328 }
329 if r == '\n' && inDisplay {
330 b.WriteByte(' ')
331 i++
332 continue
333 }
334
335 b.WriteRune(r)
336 i++
337 }
338 return b.String()
339 }
340
341 var symbols = map[string]string{
342 "alpha": "α", "beta": "β", "gamma": "γ", "delta": "δ", "epsilon": "ε",
343 "varepsilon": "ε", "zeta": "ζ", "eta": "η", "theta": "θ", "vartheta": "ϑ",
344 "iota": "ι", "kappa": "κ", "lambda": "λ", "mu": "μ", "nu": "ν", "xi": "ξ",
345 "omicron": "ο", "pi": "π", "varpi": "ϖ", "rho": "ρ", "varrho": "ϱ",
346 "sigma": "σ", "varsigma": "ς", "tau": "τ", "upsilon": "υ", "phi": "φ",
347 "varphi": "ϕ", "chi": "χ", "psi": "ψ", "omega": "ω",
348 "Gamma": "Γ", "Delta": "Δ", "Theta": "Θ", "Lambda": "Λ", "Xi": "Ξ",
349 "Pi": "Π", "Sigma": "Σ", "Upsilon": "Υ", "Phi": "Φ", "Psi": "Ψ", "Omega": "Ω",
350
351 "times": "×", "div": "÷", "cdot": "·", "ast": "∗", "star": "⋆",
352 "pm": "±", "mp": "∓", "oplus": "⊕", "ominus": "⊖", "otimes": "⊗",
353 "oslash": "⊘", "odot": "⊙", "circ": "∘", "bullet": "•", "setminus": "∖",
354
355 "leq": "≤", "le": "≤", "geq": "≥", "ge": "≥", "neq": "≠", "ne": "≠",
356 "equiv": "≡", "approx": "≈", "cong": "≅", "sim": "∼", "simeq": "≃",
357 "propto": "∝", "ll": "≪", "gg": "≫", "doteq": "≐", "asymp": "≍",
358
359 "leftarrow": "←", "rightarrow": "→", "to": "→", "gets": "←",
360 "leftrightarrow": "↔", "Leftarrow": "⇐", "Rightarrow": "⇒",
361 "Leftrightarrow": "⇔", "implies": "⇒", "iff": "⇔", "mapsto": "↦",
362 "uparrow": "↑", "downarrow": "↓", "longrightarrow": "⟶", "longleftarrow": "⟵",
363
364 "sum": "∑", "prod": "∏", "coprod": "∐", "int": "∫", "iint": "∬",
365 "iiint": "∭", "oint": "∮", "nabla": "∇", "partial": "∂",
366 "infty": "∞", "sqrt": "√", "surd": "√",
367
368 "in": "∈", "notin": "∉", "ni": "∋", "subset": "⊂", "supset": "⊃",
369 "subseteq": "⊆", "supseteq": "⊇", "cup": "∪", "cap": "∩",
370 "emptyset": "∅", "varnothing": "∅", "forall": "∀", "exists": "∃",
371 "nexists": "∄", "neg": "¬", "lnot": "¬", "land": "∧", "wedge": "∧",
372 "lor": "∨", "vee": "∨",
373
374 "angle": "∠", "perp": "⊥", "parallel": "∥", "mid": "∣", "nmid": "∤",
375 "vert": "∣", "lvert": "∣", "rvert": "∣",
376 "Vert": "‖", "lVert": "‖", "rVert": "‖",
377 "triangle": "△", "square": "□", "diamond": "◇", "top": "⊤", "bot": "⊥",
378 "vdash": "⊢", "models": "⊨", "therefore": "∴", "because": "∵",
379
380 "ldots": "…", "dots": "…", "cdots": "⋯", "vdots": "⋮", "ddots": "⋱",
381 "prime": "′", "degree": "°", "deg": "°", "hbar": "ℏ", "ell": "ℓ",
382 "Re": "ℜ", "Im": "ℑ", "aleph": "ℵ", "wp": "℘",
383 "langle": "⟨", "rangle": "⟩", "lceil": "⌈", "rceil": "⌉",
384 "lfloor": "⌊", "rfloor": "⌋", "backslash": "\\",
385
386 "quad": " ", "qquad": " ", "space": " ", "thinspace": " ",
387 "lim": "lim", "sin": "sin", "cos": "cos", "tan": "tan", "log": "log",
388 "ln": "ln", "exp": "exp", "min": "min", "max": "max", "det": "det",
389 "gcd": "gcd", "dim": "dim", "ker": "ker",
390 }
391
392 var accents = map[string]rune{
393 "hat": '̂', "widehat": '̂', "bar": '̄', "overline": '̄',
394 "vec": '⃗', "dot": '̇', "ddot": '̈', "tilde": '̃',
395 "widetilde": '̃', "acute": '́', "grave": '̀', "check": '̌',
396 }
397
398 var superMap = map[rune]rune{
399 '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶',
400 '7': '⁷', '8': '⁸', '9': '⁹', '+': '⁺', '-': '⁻', '=': '⁼', '(': '⁽',
401 ')': '⁾', 'a': 'ᵃ', 'b': 'ᵇ', 'c': 'ᶜ', 'd': 'ᵈ', 'e': 'ᵉ', 'f': 'ᶠ',
402 'g': 'ᵍ', 'h': 'ʰ', 'i': 'ⁱ', 'j': 'ʲ', 'k': 'ᵏ', 'l': 'ˡ', 'm': 'ᵐ',
403 'n': 'ⁿ', 'o': 'ᵒ', 'p': 'ᵖ', 'r': 'ʳ', 's': 'ˢ', 't': 'ᵗ', 'u': 'ᵘ',
404 'v': 'ᵛ', 'w': 'ʷ', 'x': 'ˣ', 'y': 'ʸ', 'z': 'ᶻ',
405 }
406
407 var subMap = map[rune]rune{
408 '0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄', '5': '₅', '6': '₆',
409 '7': '₇', '8': '₈', '9': '₉', '+': '₊', '-': '₋', '=': '₌', '(': '₍',
410 ')': '₎', 'a': 'ₐ', 'e': 'ₑ', 'h': 'ₕ', 'i': 'ᵢ', 'j': 'ⱼ', 'k': 'ₖ',
411 'l': 'ₗ', 'm': 'ₘ', 'n': 'ₙ', 'o': 'ₒ', 'p': 'ₚ', 'r': 'ᵣ', 's': 'ₛ',
412 't': 'ₜ', 'u': 'ᵤ', 'v': 'ᵥ', 'x': 'ₓ',
413 }
414
415 var blackboardCaps = map[rune]rune{
416 'A': '𝔸', 'B': '𝔹', 'C': 'ℂ', 'D': '𝔻', 'E': '𝔼', 'F': '𝔽', 'G': '𝔾',
417 'H': 'ℍ', 'I': '𝕀', 'J': '𝕁', 'K': '𝕂', 'L': '𝕃', 'M': '𝕄', 'N': 'ℕ',
418 'O': '𝕆', 'P': 'ℙ', 'Q': 'ℚ', 'R': 'ℝ', 'S': '𝕊', 'T': '𝕋', 'U': '𝕌',
419 'V': '𝕍', 'W': '𝕎', 'X': '𝕏', 'Y': '𝕐', 'Z': 'ℤ',
420 }
421
421 lines GO