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