rovecode 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +737 -0
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/bin/rovecode.ts +21 -0
  5. package/package.json +56 -0
  6. package/src/acp/server.ts +374 -0
  7. package/src/cli/auth-login.ts +122 -0
  8. package/src/cli/connect.ts +244 -0
  9. package/src/cli/context-cmd.ts +199 -0
  10. package/src/cli/dispatch.ts +82 -0
  11. package/src/cli/doctor.ts +362 -0
  12. package/src/cli/export.ts +276 -0
  13. package/src/cli/help.ts +293 -0
  14. package/src/cli/is-tui-invocation.ts +8 -0
  15. package/src/cli/main.ts +583 -0
  16. package/src/cli/market-cmd.ts +658 -0
  17. package/src/cli/mcp-login.ts +141 -0
  18. package/src/cli/mcp-market-cmd.ts +302 -0
  19. package/src/cli/output.ts +382 -0
  20. package/src/cli/repl.ts +250 -0
  21. package/src/cli/repomap-root.ts +14 -0
  22. package/src/cli/resume.ts +57 -0
  23. package/src/cli/run-flags.ts +43 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +931 -0
  26. package/src/cli/session-arg.ts +30 -0
  27. package/src/cli/sessions-cmd.ts +145 -0
  28. package/src/cli/setup.ts +153 -0
  29. package/src/cli/skills-cmd.ts +194 -0
  30. package/src/cli/start-chat.ts +65 -0
  31. package/src/cli/trust-cmd.ts +52 -0
  32. package/src/coding/bash.ts +148 -0
  33. package/src/coding/checkpoints.ts +327 -0
  34. package/src/coding/diff.ts +138 -0
  35. package/src/coding/files.ts +341 -0
  36. package/src/coding/hashline.ts +274 -0
  37. package/src/coding/lsp-gate.ts +254 -0
  38. package/src/coding/lsp-servers.ts +147 -0
  39. package/src/coding/lsp.ts +283 -0
  40. package/src/coding/repomap-cache.ts +99 -0
  41. package/src/coding/repomap-files.ts +192 -0
  42. package/src/coding/repomap.ts +481 -0
  43. package/src/core/agents.ts +255 -0
  44. package/src/core/compaction.ts +259 -0
  45. package/src/core/config.ts +289 -0
  46. package/src/core/context-report.ts +228 -0
  47. package/src/core/context.ts +60 -0
  48. package/src/core/count-remote.ts +107 -0
  49. package/src/core/execpolicy-rules.ts +196 -0
  50. package/src/core/execpolicy.ts +385 -0
  51. package/src/core/executor.ts +454 -0
  52. package/src/core/guardrails.ts +400 -0
  53. package/src/core/hooks.ts +411 -0
  54. package/src/core/images.ts +230 -0
  55. package/src/core/intro.ts +266 -0
  56. package/src/core/loop.ts +567 -0
  57. package/src/core/modes.ts +372 -0
  58. package/src/core/orchestrator.ts +245 -0
  59. package/src/core/proc-group.ts +48 -0
  60. package/src/core/project-trust.ts +98 -0
  61. package/src/core/reflection.ts +165 -0
  62. package/src/core/sandbox-config.ts +186 -0
  63. package/src/core/session-id.ts +24 -0
  64. package/src/core/session-images.ts +73 -0
  65. package/src/core/session-ops.ts +183 -0
  66. package/src/core/session-text.ts +29 -0
  67. package/src/core/session.ts +469 -0
  68. package/src/core/settings.ts +170 -0
  69. package/src/core/tasks.ts +646 -0
  70. package/src/core/token-scale.ts +108 -0
  71. package/src/core/tools.ts +309 -0
  72. package/src/core/trust.ts +104 -0
  73. package/src/core/types.ts +330 -0
  74. package/src/core/update-check.ts +171 -0
  75. package/src/core/usage.ts +204 -0
  76. package/src/core/validate.ts +121 -0
  77. package/src/core/verify-gate.ts +159 -0
  78. package/src/core/verify.ts +236 -0
  79. package/src/core/voice.ts +158 -0
  80. package/src/core/win-job.ts +183 -0
  81. package/src/core/workspace.ts +184 -0
  82. package/src/design/audit.ts +797 -0
  83. package/src/design/direction.ts +190 -0
  84. package/src/design/rules.ts +157 -0
  85. package/src/eval/bench.ts +150 -0
  86. package/src/eval/gauntlet-runner.ts +215 -0
  87. package/src/eval/gauntlet-support.ts +84 -0
  88. package/src/eval/gauntlet-wave3.ts +269 -0
  89. package/src/eval/gauntlet-wave4.ts +217 -0
  90. package/src/eval/gauntlet.ts +253 -0
  91. package/src/index.ts +17 -0
  92. package/src/lanes/agy.ts +95 -0
  93. package/src/lanes/approval.ts +24 -0
  94. package/src/lanes/claude.ts +129 -0
  95. package/src/lanes/codex.ts +127 -0
  96. package/src/lanes/events.ts +130 -0
  97. package/src/lanes/job.ts +142 -0
  98. package/src/lanes/opencode.ts +122 -0
  99. package/src/lanes/process.ts +184 -0
  100. package/src/lanes/progress.ts +183 -0
  101. package/src/lanes/registry.ts +178 -0
  102. package/src/lanes/runner.ts +124 -0
  103. package/src/lanes/types.ts +112 -0
  104. package/src/market/catalogs/mcp-docs.json +111 -0
  105. package/src/market/catalogs/plugins.json +111 -0
  106. package/src/market/catalogs/skills.json +478 -0
  107. package/src/market/clone.ts +72 -0
  108. package/src/market/context-cost.ts +121 -0
  109. package/src/market/digest.ts +106 -0
  110. package/src/market/index.ts +22 -0
  111. package/src/market/install.ts +578 -0
  112. package/src/market/manifest.ts +187 -0
  113. package/src/market/prereq.ts +145 -0
  114. package/src/market/registry.ts +363 -0
  115. package/src/market/resolve.ts +111 -0
  116. package/src/market/types.ts +236 -0
  117. package/src/market/validate.ts +227 -0
  118. package/src/mcp/client.ts +449 -0
  119. package/src/mcp/config.ts +252 -0
  120. package/src/mcp/local-package.ts +211 -0
  121. package/src/mcp/market-catalog.ts +84 -0
  122. package/src/mcp/market-install.ts +289 -0
  123. package/src/mcp/market.ts +362 -0
  124. package/src/mcp/oauth.ts +251 -0
  125. package/src/mcp/prompts-resources.ts +249 -0
  126. package/src/mcp/shared.ts +149 -0
  127. package/src/mcp/status.ts +67 -0
  128. package/src/mcp/tools.ts +275 -0
  129. package/src/mcp/transport.ts +122 -0
  130. package/src/mcp/trust.ts +25 -0
  131. package/src/memory/blocks.ts +278 -0
  132. package/src/memory/recall.ts +355 -0
  133. package/src/memory/scope.ts +182 -0
  134. package/src/memory/store.ts +105 -0
  135. package/src/memory/tools.ts +99 -0
  136. package/src/plugins/cli.ts +119 -0
  137. package/src/plugins/discover.ts +108 -0
  138. package/src/plugins/index.ts +50 -0
  139. package/src/plugins/install.ts +184 -0
  140. package/src/plugins/load.ts +124 -0
  141. package/src/plugins/manifest.ts +92 -0
  142. package/src/plugins/state.ts +83 -0
  143. package/src/providers/auth.ts +408 -0
  144. package/src/providers/cache.ts +223 -0
  145. package/src/providers/catalog-local.ts +160 -0
  146. package/src/providers/catalog.ts +421 -0
  147. package/src/providers/middleware-context.ts +86 -0
  148. package/src/providers/middleware.ts +373 -0
  149. package/src/providers/model-list.ts +23 -0
  150. package/src/providers/models-index.json +1 -0
  151. package/src/providers/oauth/common.ts +105 -0
  152. package/src/providers/oauth/device-code.ts +107 -0
  153. package/src/providers/oauth/github-copilot.ts +146 -0
  154. package/src/providers/oauth/loopback.ts +158 -0
  155. package/src/providers/oauth/openai.ts +163 -0
  156. package/src/providers/oauth/openrouter.ts +89 -0
  157. package/src/providers/oauth/pkce.ts +45 -0
  158. package/src/providers/oauth/registry.ts +39 -0
  159. package/src/providers/oauth/seam.ts +89 -0
  160. package/src/providers/profile-glm53.ts +111 -0
  161. package/src/providers/profile-sonnet5-persona.ts +65 -0
  162. package/src/providers/profile-sonnet5-voice.ts +23 -0
  163. package/src/providers/profiles.ts +156 -0
  164. package/src/providers/provider-config.ts +311 -0
  165. package/src/providers/registry.ts +333 -0
  166. package/src/providers/responses.ts +209 -0
  167. package/src/providers/retry.ts +234 -0
  168. package/src/providers/router.ts +294 -0
  169. package/src/providers/sse.ts +26 -0
  170. package/src/providers/stream-errors.ts +117 -0
  171. package/src/providers/stream.ts +566 -0
  172. package/src/providers/thinking.ts +189 -0
  173. package/src/providers/wire-messages.ts +129 -0
  174. package/src/providers/wire-responses.ts +79 -0
  175. package/src/providers/wire-select.ts +53 -0
  176. package/src/server/http.ts +291 -0
  177. package/src/server/openapi.ts +246 -0
  178. package/src/sextant/card-hits.ts +102 -0
  179. package/src/sextant/card-keys.ts +55 -0
  180. package/src/sextant/context-source.ts +157 -0
  181. package/src/sextant/crew-cards.ts +350 -0
  182. package/src/sextant/draw-agents.ts +273 -0
  183. package/src/sextant/draw-code.ts +388 -0
  184. package/src/sextant/draw-context.ts +222 -0
  185. package/src/sextant/draw-frame.ts +164 -0
  186. package/src/sextant/draw-market.ts +573 -0
  187. package/src/sextant/draw-messages.ts +386 -0
  188. package/src/sextant/draw-pet.ts +230 -0
  189. package/src/sextant/draw-plan.ts +187 -0
  190. package/src/sextant/draw-tabs.ts +85 -0
  191. package/src/sextant/draw-util.ts +65 -0
  192. package/src/sextant/draw-wizard.ts +378 -0
  193. package/src/sextant/engine.ts +230 -0
  194. package/src/sextant/frame-hits.ts +25 -0
  195. package/src/sextant/frame.ts +101 -0
  196. package/src/sextant/git-status.ts +197 -0
  197. package/src/sextant/grid.ts +59 -0
  198. package/src/sextant/input.ts +119 -0
  199. package/src/sextant/keys.ts +521 -0
  200. package/src/sextant/layout.ts +86 -0
  201. package/src/sextant/local-commands.ts +169 -0
  202. package/src/sextant/market-source.ts +287 -0
  203. package/src/sextant/mentions.ts +200 -0
  204. package/src/sextant/message-hits.ts +26 -0
  205. package/src/sextant/model.ts +387 -0
  206. package/src/sextant/overlays.ts +456 -0
  207. package/src/sextant/panel-hits.ts +38 -0
  208. package/src/sextant/pet.ts +399 -0
  209. package/src/sextant/screen.ts +324 -0
  210. package/src/sextant/scroll-hits.ts +66 -0
  211. package/src/sextant/scrollbar.ts +82 -0
  212. package/src/sextant/sextant-bridge.ts +174 -0
  213. package/src/sextant/sextant-cards.ts +142 -0
  214. package/src/sextant/sextant-diff-base.ts +63 -0
  215. package/src/sextant/sextant-files.ts +154 -0
  216. package/src/sextant/sextant-frame-loop.ts +335 -0
  217. package/src/sextant/sextant-renderer.ts +574 -0
  218. package/src/sextant/sextant-repo.ts +140 -0
  219. package/src/sextant/theme.ts +66 -0
  220. package/src/sextant/tool-rows.ts +189 -0
  221. package/src/sextant/types.ts +493 -0
  222. package/src/skills/index.ts +387 -0
  223. package/src/skills/pack.ts +220 -0
  224. package/src/skills/spec.ts +162 -0
  225. package/src/skills/tools.ts +69 -0
  226. package/src/skills/versioned.ts +227 -0
  227. package/src/telemetry/otel-export.ts +122 -0
  228. package/src/telemetry/otel-lanes.ts +89 -0
  229. package/src/telemetry/otel-logs.ts +131 -0
  230. package/src/telemetry/otel-metrics.ts +136 -0
  231. package/src/telemetry/otel.ts +397 -0
  232. package/src/telemetry/otlp.ts +76 -0
  233. package/src/tools/ask-user.ts +156 -0
  234. package/src/tools/bash-bg.ts +94 -0
  235. package/src/tools/bash-jobs.ts +237 -0
  236. package/src/tools/design.ts +151 -0
  237. package/src/tools/evalcell.ts +338 -0
  238. package/src/tools/html-text.ts +139 -0
  239. package/src/tools/provider.ts +149 -0
  240. package/src/tools/task.ts +250 -0
  241. package/src/tools/todo.ts +320 -0
  242. package/src/tools/webfetch.ts +332 -0
  243. package/src/tools/websearch.ts +359 -0
  244. package/src/tui/agents-cmd.ts +41 -0
  245. package/src/tui/app.ts +749 -0
  246. package/src/tui/attach.ts +127 -0
  247. package/src/tui/boot-notes.ts +41 -0
  248. package/src/tui/builtin-prompts.ts +59 -0
  249. package/src/tui/checkpoints-cmd.ts +70 -0
  250. package/src/tui/clipboard-image.ts +81 -0
  251. package/src/tui/clipboard.ts +78 -0
  252. package/src/tui/commands.ts +283 -0
  253. package/src/tui/config-view.ts +53 -0
  254. package/src/tui/context-cmds.ts +282 -0
  255. package/src/tui/cost.ts +108 -0
  256. package/src/tui/crash-guard.ts +173 -0
  257. package/src/tui/focus-terminal.ts +34 -0
  258. package/src/tui/git-cmds.ts +273 -0
  259. package/src/tui/git-plain.ts +58 -0
  260. package/src/tui/info-cmd.ts +150 -0
  261. package/src/tui/input-plain.ts +76 -0
  262. package/src/tui/mcp-cmd.ts +128 -0
  263. package/src/tui/memory-note.ts +77 -0
  264. package/src/tui/modes-cmd.ts +45 -0
  265. package/src/tui/notify-seq.ts +100 -0
  266. package/src/tui/notify.ts +318 -0
  267. package/src/tui/overlays.ts +97 -0
  268. package/src/tui/pi-renderer.ts +428 -0
  269. package/src/tui/providers-cmd.ts +377 -0
  270. package/src/tui/reasoning-view.ts +56 -0
  271. package/src/tui/renderer.ts +128 -0
  272. package/src/tui/replay-marker.ts +29 -0
  273. package/src/tui/session-cmd.ts +148 -0
  274. package/src/tui/session-manage.ts +95 -0
  275. package/src/tui/sextant-attach.ts +102 -0
  276. package/src/tui/sextant-io.ts +202 -0
  277. package/src/tui/sextant-smoke.ts +110 -0
  278. package/src/tui/shell-cmd.ts +158 -0
  279. package/src/tui/smoke.ts +72 -0
  280. package/src/tui/staged-terminal.ts +50 -0
  281. package/src/tui/startup.ts +12 -0
  282. package/src/tui/theme.ts +59 -0
  283. package/src/tui/todo-label.ts +7 -0
  284. package/src/tui/trust-card.ts +107 -0
  285. package/src/tui/tui-commands.ts +87 -0
  286. package/tsconfig.json +30 -0
  287. package/vendor/pi-tui/LICENSE +21 -0
  288. package/vendor/pi-tui/PATCHES.md +12 -0
  289. package/vendor/pi-tui/PROVENANCE.md +12 -0
  290. package/vendor/pi-tui/README.upstream.md +854 -0
  291. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  292. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  293. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  294. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  295. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  296. package/vendor/pi-tui/src/components/box.ts +138 -0
  297. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  298. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  299. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  300. package/vendor/pi-tui/src/components/image.ts +128 -0
  301. package/vendor/pi-tui/src/components/input.ts +448 -0
  302. package/vendor/pi-tui/src/components/loader.ts +93 -0
  303. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  304. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  305. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  306. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  307. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  308. package/vendor/pi-tui/src/components/stack.ts +155 -0
  309. package/vendor/pi-tui/src/components/text.ts +108 -0
  310. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  311. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  312. package/vendor/pi-tui/src/editor-component.ts +75 -0
  313. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  314. package/vendor/pi-tui/src/index.ts +149 -0
  315. package/vendor/pi-tui/src/keybindings.ts +321 -0
  316. package/vendor/pi-tui/src/keys.ts +1402 -0
  317. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  318. package/vendor/pi-tui/src/latex.ts +1381 -0
  319. package/vendor/pi-tui/src/layout-node.ts +52 -0
  320. package/vendor/pi-tui/src/layout.ts +411 -0
  321. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  322. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  323. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  324. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  325. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  326. package/vendor/pi-tui/src/terminal.ts +554 -0
  327. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  328. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  329. package/vendor/pi-tui/src/tui.ts +1264 -0
  330. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  331. package/vendor/pi-tui/src/utils.ts +1327 -0
  332. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  333. package/vendor/pi-tui/test/test-themes.ts +39 -0
  334. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
@@ -0,0 +1,1381 @@
1
+ // @ts-nocheck -- vendored by rovecode: upstream-checked at pinned SHA 853a80d2; see ../PATCHES.md
2
+ import { visibleWidth } from "./utils.ts";
3
+
4
+ const SYMBOLS: Readonly<Record<string, string>> = {
5
+ alpha: "α",
6
+ beta: "β",
7
+ gamma: "γ",
8
+ delta: "δ",
9
+ epsilon: "ϵ",
10
+ varepsilon: "ε",
11
+ zeta: "ζ",
12
+ eta: "η",
13
+ theta: "θ",
14
+ vartheta: "ϑ",
15
+ iota: "ι",
16
+ kappa: "κ",
17
+ varkappa: "ϰ",
18
+ lambda: "λ",
19
+ mu: "μ",
20
+ nu: "ν",
21
+ xi: "ξ",
22
+ pi: "π",
23
+ varpi: "ϖ",
24
+ rho: "ρ",
25
+ varrho: "ϱ",
26
+ sigma: "σ",
27
+ varsigma: "ς",
28
+ tau: "τ",
29
+ upsilon: "υ",
30
+ phi: "ϕ",
31
+ varphi: "φ",
32
+ chi: "χ",
33
+ psi: "ψ",
34
+ omega: "ω",
35
+ Gamma: "Γ",
36
+ Delta: "Δ",
37
+ Theta: "Θ",
38
+ Lambda: "Λ",
39
+ Xi: "Ξ",
40
+ Pi: "Π",
41
+ Sigma: "Σ",
42
+ Upsilon: "Υ",
43
+ Phi: "Φ",
44
+ Psi: "Ψ",
45
+ Omega: "Ω",
46
+ pm: "±",
47
+ mp: "∓",
48
+ times: "×",
49
+ div: "÷",
50
+ cdot: "·",
51
+ ast: "∗",
52
+ star: "⋆",
53
+ circ: "∘",
54
+ bullet: "•",
55
+ oplus: "⊕",
56
+ ominus: "⊖",
57
+ otimes: "⊗",
58
+ oslash: "⊘",
59
+ odot: "⊙",
60
+ bigcirc: "○",
61
+ dagger: "†",
62
+ ddagger: "‡",
63
+ amalg: "⨿",
64
+ uplus: "⊎",
65
+ sqcap: "⊓",
66
+ sqcup: "⊔",
67
+ triangleleft: "◁",
68
+ triangleright: "▷",
69
+ wr: "≀",
70
+ cap: "∩",
71
+ cup: "∪",
72
+ bigcap: "⋂",
73
+ bigcup: "⋃",
74
+ bigwedge: "⋀",
75
+ bigvee: "⋁",
76
+ bigsqcup: "⨆",
77
+ biguplus: "⨄",
78
+ bigoplus: "⨁",
79
+ bigotimes: "⨂",
80
+ bigodot: "⨀",
81
+ setminus: "∖",
82
+ in: "∈",
83
+ notin: "∉",
84
+ ni: "∋",
85
+ subset: "⊂",
86
+ supset: "⊃",
87
+ subseteq: "⊆",
88
+ supseteq: "⊇",
89
+ sqsubset: "⊏",
90
+ sqsupset: "⊐",
91
+ sqsubseteq: "⊑",
92
+ sqsupseteq: "⊒",
93
+ prec: "≺",
94
+ preceq: "≼",
95
+ succ: "≻",
96
+ succeq: "≽",
97
+ ll: "≪",
98
+ gg: "≫",
99
+ le: "≤",
100
+ leq: "≤",
101
+ leqslant: "≤",
102
+ ge: "≥",
103
+ geq: "≥",
104
+ geqslant: "≥",
105
+ ne: "≠",
106
+ neq: "≠",
107
+ equiv: "≡",
108
+ approx: "≈",
109
+ sim: "∼",
110
+ simeq: "≃",
111
+ cong: "≅",
112
+ asymp: "≍",
113
+ doteq: "≐",
114
+ propto: "∝",
115
+ parallel: "∥",
116
+ perp: "⊥",
117
+ mid: "∣",
118
+ vdash: "⊢",
119
+ dashv: "⊣",
120
+ models: "⊨",
121
+ Vdash: "⊩",
122
+ Vvdash: "⊪",
123
+ nvdash: "⊬",
124
+ nvDash: "⊭",
125
+ forall: "∀",
126
+ exists: "∃",
127
+ nexists: "∄",
128
+ neg: "¬",
129
+ land: "∧",
130
+ wedge: "∧",
131
+ lor: "∨",
132
+ vee: "∨",
133
+ to: "→",
134
+ rightarrow: "→",
135
+ longrightarrow: "→",
136
+ leftarrow: "←",
137
+ longleftarrow: "←",
138
+ gets: "←",
139
+ leftrightarrow: "↔",
140
+ longleftrightarrow: "↔",
141
+ hookleftarrow: "↩",
142
+ hookrightarrow: "↪",
143
+ twoheadleftarrow: "↞",
144
+ twoheadrightarrow: "↠",
145
+ leftharpoonup: "↼",
146
+ leftharpoondown: "↽",
147
+ rightharpoonup: "⇀",
148
+ rightharpoondown: "⇁",
149
+ rightleftharpoons: "⇌",
150
+ leftrightharpoons: "⇋",
151
+ nearrow: "↗",
152
+ searrow: "↘",
153
+ swarrow: "↙",
154
+ nwarrow: "↖",
155
+ rightsquigarrow: "⇝",
156
+ leadsto: "⇝",
157
+ Rightarrow: "⇒",
158
+ Longrightarrow: "⇒",
159
+ Leftarrow: "⇐",
160
+ Longleftarrow: "⇐",
161
+ Leftrightarrow: "⇔",
162
+ Longleftrightarrow: "⇔",
163
+ implies: "⇒",
164
+ iff: "⇔",
165
+ mapsto: "↦",
166
+ longmapsto: "↦",
167
+ uparrow: "↑",
168
+ downarrow: "↓",
169
+ partial: "∂",
170
+ nabla: "∇",
171
+ int: "∫",
172
+ iint: "∬",
173
+ iiint: "∭",
174
+ oint: "∮",
175
+ sum: "∑",
176
+ prod: "∏",
177
+ coprod: "∐",
178
+ infty: "∞",
179
+ emptyset: "∅",
180
+ varnothing: "∅",
181
+ angle: "∠",
182
+ therefore: "∴",
183
+ because: "∵",
184
+ aleph: "ℵ",
185
+ beth: "ℶ",
186
+ gimel: "ℷ",
187
+ daleth: "ℸ",
188
+ top: "⊤",
189
+ bot: "⊥",
190
+ triangle: "△",
191
+ square: "□",
192
+ lozenge: "◊",
193
+ checkmark: "✓",
194
+ complement: "∁",
195
+ wp: "℘",
196
+ prime: "′",
197
+ ldots: "…",
198
+ dots: "…",
199
+ cdots: "⋯",
200
+ vdots: "⋮",
201
+ ddots: "⋱",
202
+ ell: "ℓ",
203
+ hbar: "ℏ",
204
+ Im: "ℑ",
205
+ Re: "ℜ",
206
+ langle: "⟨",
207
+ rangle: "⟩",
208
+ vert: "|",
209
+ lvert: "|",
210
+ rvert: "|",
211
+ Vert: "‖",
212
+ lVert: "‖",
213
+ rVert: "‖",
214
+ lbrace: "{",
215
+ rbrace: "}",
216
+ backslash: "\\",
217
+ lfloor: "⌊",
218
+ rfloor: "⌋",
219
+ lceil: "⌈",
220
+ rceil: "⌉",
221
+ colon: ":",
222
+ };
223
+
224
+ const NAMED_OPERATORS = new Set([
225
+ "arccos",
226
+ "arcsin",
227
+ "arctan",
228
+ "arg",
229
+ "cos",
230
+ "cosh",
231
+ "cot",
232
+ "coth",
233
+ "csc",
234
+ "deg",
235
+ "det",
236
+ "dim",
237
+ "exp",
238
+ "gcd",
239
+ "hom",
240
+ "inf",
241
+ "ker",
242
+ "lg",
243
+ "lim",
244
+ "liminf",
245
+ "limsup",
246
+ "ln",
247
+ "log",
248
+ "max",
249
+ "min",
250
+ "Pr",
251
+ "sec",
252
+ "sin",
253
+ "sinh",
254
+ "sup",
255
+ "tan",
256
+ "tanh",
257
+ ]);
258
+
259
+ const LIMIT_OPERATORS = new Set([
260
+ "argmax",
261
+ "argmin",
262
+ "inf",
263
+ "injlim",
264
+ "lim",
265
+ "liminf",
266
+ "limsup",
267
+ "max",
268
+ "min",
269
+ "projlim",
270
+ "sup",
271
+ ]);
272
+
273
+ const DISPLAY_LIMIT_SYMBOLS = new Set([
274
+ "bigcap",
275
+ "bigcup",
276
+ "bigodot",
277
+ "bigoplus",
278
+ "bigotimes",
279
+ "bigsqcup",
280
+ "biguplus",
281
+ "bigvee",
282
+ "bigwedge",
283
+ "coprod",
284
+ "int",
285
+ "iint",
286
+ "iiint",
287
+ "oint",
288
+ "prod",
289
+ "sum",
290
+ ]);
291
+
292
+ const RELATION_COMMANDS = new Set([
293
+ "Leftarrow",
294
+ "Leftrightarrow",
295
+ "Longleftarrow",
296
+ "Longleftrightarrow",
297
+ "Longrightarrow",
298
+ "Rightarrow",
299
+ "Vdash",
300
+ "Vvdash",
301
+ "approx",
302
+ "asymp",
303
+ "cong",
304
+ "dashv",
305
+ "doteq",
306
+ "downarrow",
307
+ "equiv",
308
+ "ge",
309
+ "geq",
310
+ "geqslant",
311
+ "gets",
312
+ "gg",
313
+ "hookleftarrow",
314
+ "hookrightarrow",
315
+ "iff",
316
+ "implies",
317
+ "in",
318
+ "leadsto",
319
+ "le",
320
+ "leftarrow",
321
+ "leftharpoondown",
322
+ "leftharpoonup",
323
+ "leftrightarrow",
324
+ "leftrightharpoons",
325
+ "leq",
326
+ "leqslant",
327
+ "ll",
328
+ "longleftarrow",
329
+ "longleftrightarrow",
330
+ "longmapsto",
331
+ "longrightarrow",
332
+ "mapsto",
333
+ "mid",
334
+ "models",
335
+ "ne",
336
+ "nearrow",
337
+ "neq",
338
+ "ni",
339
+ "notin",
340
+ "nvdash",
341
+ "nvDash",
342
+ "nwarrow",
343
+ "parallel",
344
+ "perp",
345
+ "prec",
346
+ "preceq",
347
+ "propto",
348
+ "rightharpoondown",
349
+ "rightharpoonup",
350
+ "rightleftharpoons",
351
+ "rightarrow",
352
+ "rightsquigarrow",
353
+ "searrow",
354
+ "sim",
355
+ "simeq",
356
+ "sqsubset",
357
+ "sqsubseteq",
358
+ "sqsupset",
359
+ "sqsupseteq",
360
+ "subset",
361
+ "subseteq",
362
+ "succ",
363
+ "succeq",
364
+ "supset",
365
+ "supseteq",
366
+ "swarrow",
367
+ "to",
368
+ "triangleleft",
369
+ "triangleright",
370
+ "twoheadleftarrow",
371
+ "twoheadrightarrow",
372
+ "uparrow",
373
+ "vdash",
374
+ ]);
375
+
376
+ const NEGATED_SYMBOLS: Readonly<Record<string, string>> = {
377
+ "<": "≮",
378
+ ">": "≯",
379
+ "=": "≠",
380
+ "∈": "∉",
381
+ "∋": "∌",
382
+ "∣": "∤",
383
+ "∥": "∦",
384
+ "∼": "≁",
385
+ "≃": "≄",
386
+ "≅": "≇",
387
+ "≈": "≉",
388
+ "≡": "≢",
389
+ "≤": "≰",
390
+ "≥": "≱",
391
+ "≺": "⊀",
392
+ "≻": "⊁",
393
+ "⊂": "⊄",
394
+ "⊃": "⊅",
395
+ "⊆": "⊈",
396
+ "⊇": "⊉",
397
+ "⊢": "⊬",
398
+ "⊨": "⊭",
399
+ "↔": "↮",
400
+ "←": "↚",
401
+ "→": "↛",
402
+ "⇒": "⇏",
403
+ "⇐": "⇍",
404
+ "⇔": "⇎",
405
+ "≼": "⋠",
406
+ "≽": "⋡",
407
+ };
408
+
409
+ const BLACKBOARD: Readonly<Record<string, string>> = {
410
+ C: "ℂ",
411
+ H: "ℍ",
412
+ N: "ℕ",
413
+ P: "ℙ",
414
+ Q: "ℚ",
415
+ R: "ℝ",
416
+ Z: "ℤ",
417
+ };
418
+
419
+ const SUPERSCRIPTS: Readonly<Record<string, string>> = {
420
+ "0": "⁰",
421
+ "1": "¹",
422
+ "2": "²",
423
+ "3": "³",
424
+ "4": "⁴",
425
+ "5": "⁵",
426
+ "6": "⁶",
427
+ "7": "⁷",
428
+ "8": "⁸",
429
+ "9": "⁹",
430
+ "+": "⁺",
431
+ "-": "⁻",
432
+ "=": "⁼",
433
+ "(": "⁽",
434
+ ")": "⁾",
435
+ a: "ᵃ",
436
+ b: "ᵇ",
437
+ c: "ᶜ",
438
+ d: "ᵈ",
439
+ e: "ᵉ",
440
+ f: "ᶠ",
441
+ g: "ᵍ",
442
+ h: "ʰ",
443
+ i: "ⁱ",
444
+ j: "ʲ",
445
+ k: "ᵏ",
446
+ l: "ˡ",
447
+ m: "ᵐ",
448
+ n: "ⁿ",
449
+ o: "ᵒ",
450
+ p: "ᵖ",
451
+ r: "ʳ",
452
+ s: "ˢ",
453
+ t: "ᵗ",
454
+ u: "ᵘ",
455
+ v: "ᵛ",
456
+ w: "ʷ",
457
+ x: "ˣ",
458
+ y: "ʸ",
459
+ z: "ᶻ",
460
+ };
461
+
462
+ const SUBSCRIPTS: Readonly<Record<string, string>> = {
463
+ "0": "₀",
464
+ "1": "₁",
465
+ "2": "₂",
466
+ "3": "₃",
467
+ "4": "₄",
468
+ "5": "₅",
469
+ "6": "₆",
470
+ "7": "₇",
471
+ "8": "₈",
472
+ "9": "₉",
473
+ "+": "₊",
474
+ "-": "₋",
475
+ "=": "₌",
476
+ "(": "₍",
477
+ ")": "₎",
478
+ a: "ₐ",
479
+ e: "ₑ",
480
+ h: "ₕ",
481
+ i: "ᵢ",
482
+ j: "ⱼ",
483
+ k: "ₖ",
484
+ l: "ₗ",
485
+ m: "ₘ",
486
+ n: "ₙ",
487
+ o: "ₒ",
488
+ p: "ₚ",
489
+ r: "ᵣ",
490
+ s: "ₛ",
491
+ t: "ₜ",
492
+ u: "ᵤ",
493
+ v: "ᵥ",
494
+ x: "ₓ",
495
+ };
496
+
497
+ const SPACING_COMMANDS = new Set([
498
+ ",",
499
+ ":",
500
+ ";",
501
+ " ",
502
+ ">",
503
+ "enspace",
504
+ "enskip",
505
+ "medspace",
506
+ "quad",
507
+ "qquad",
508
+ "thickspace",
509
+ "thinspace",
510
+ ]);
511
+ const NEGATIVE_SPACING_COMMANDS = new Set(["!", "negmedspace", "negthickspace", "negthinspace"]);
512
+ const NEGATIVE_SPACE = "\u0000";
513
+ const IGNORED_COMMANDS = new Set([
514
+ "displaystyle",
515
+ "limits",
516
+ "nolimits",
517
+ "scriptstyle",
518
+ "scriptscriptstyle",
519
+ "textstyle",
520
+ ]);
521
+ const SIZE_COMMANDS = new Set([
522
+ "big",
523
+ "Big",
524
+ "bigg",
525
+ "Bigg",
526
+ "bigl",
527
+ "Bigl",
528
+ "biggl",
529
+ "Biggl",
530
+ "bigr",
531
+ "Bigr",
532
+ "biggr",
533
+ "Biggr",
534
+ ]);
535
+ const PLAIN_WRAPPERS = new Set([
536
+ "emph",
537
+ "mathcal",
538
+ "mathbf",
539
+ "mathfrak",
540
+ "mathit",
541
+ "mathrm",
542
+ "mathnormal",
543
+ "mathscr",
544
+ "mathsf",
545
+ "mathtt",
546
+ "mathup",
547
+ "mbox",
548
+ "overbrace",
549
+ "pmb",
550
+ "smash",
551
+ "substack",
552
+ "text",
553
+ "textbf",
554
+ "textit",
555
+ "textmd",
556
+ "textnormal",
557
+ "textrm",
558
+ "textsc",
559
+ "textsf",
560
+ "textsl",
561
+ "texttt",
562
+ "textup",
563
+ "underbrace",
564
+ "bm",
565
+ "boldsymbol",
566
+ ]);
567
+ const ACCENTS: Readonly<Record<string, string>> = {
568
+ acute: "\u0301",
569
+ bar: "\u0305",
570
+ breve: "\u0306",
571
+ check: "\u030c",
572
+ ddot: "\u0308",
573
+ dot: "\u0307",
574
+ grave: "\u0300",
575
+ hat: "\u0302",
576
+ mathring: "\u030a",
577
+ overleftarrow: "\u20d6",
578
+ overleftrightarrow: "\u20e1",
579
+ overline: "\u0305",
580
+ overrightarrow: "\u20d7",
581
+ tilde: "\u0303",
582
+ underline: "\u0332",
583
+ vec: "\u20d7",
584
+ widehat: "\u0302",
585
+ widetilde: "\u0303",
586
+ };
587
+
588
+ function replaceCharacters(value: string, replacements: Readonly<Record<string, string>>): string | undefined {
589
+ let result = "";
590
+ for (const character of value) {
591
+ const replacement = replacements[character];
592
+ if (replacement === undefined) {
593
+ return undefined;
594
+ }
595
+ result += replacement;
596
+ }
597
+ return result;
598
+ }
599
+
600
+ function formatScript(value: string, kind: "sub" | "sup"): string {
601
+ value = value.trim();
602
+ const replacements = kind === "sub" ? SUBSCRIPTS : SUPERSCRIPTS;
603
+ const unicode = replaceCharacters(value.replace(/\s*([=+-])\s*/g, "$1"), replacements);
604
+ if (unicode !== undefined) {
605
+ return unicode;
606
+ }
607
+
608
+ const prefix = kind === "sub" ? "_" : "^";
609
+ if (Array.from(value).length === 1 || (kind === "sub" && /^[A-Za-z]+$/.test(value))) {
610
+ return `${prefix}${value}`;
611
+ }
612
+ return `${prefix}(${value})`;
613
+ }
614
+
615
+ function formatFraction(numerator: string, denominator: string): string {
616
+ numerator = numerator.trim();
617
+ denominator = denominator.trim();
618
+ const simpleNumerator = /^[\p{L}\p{N}.]+$/u.test(numerator);
619
+ const simpleDenominator = /^[\p{N}.]+$/u.test(denominator) || Array.from(denominator).length === 1;
620
+ return `${simpleNumerator ? numerator : `(${numerator})`}/${simpleDenominator ? denominator : `(${denominator})`}`;
621
+ }
622
+
623
+ function formatRoot(value: string, symbol = "√"): string {
624
+ value = value.trim();
625
+ return /^[\p{L}\p{N}.]+$/u.test(value) ? `${symbol}${value}` : `${symbol}(${value})`;
626
+ }
627
+
628
+ const NAMED_OPERATOR_START = "\u{f0004}";
629
+ const NAMED_OPERATOR_END = "\u{f0005}";
630
+ const NAMED_OPERATOR_LEFT_SPACING_PATTERN = /(?<=[\p{L}\p{N})\]}\u{f0001}])\u{f0004}/gu;
631
+ const NAMED_OPERATOR_RIGHT_SPACING_PATTERN = /\u{f0005}(?=[\p{L}\p{N}√\u{f0000}])/gu;
632
+
633
+ function normalizeOutput(value: string): string {
634
+ return value
635
+ .replace(NAMED_OPERATOR_LEFT_SPACING_PATTERN, " ")
636
+ .replaceAll(NAMED_OPERATOR_START, "")
637
+ .replace(NAMED_OPERATOR_RIGHT_SPACING_PATTERN, " ")
638
+ .replaceAll(NAMED_OPERATOR_END, "")
639
+ .split("\n")
640
+ .map((line) => line.replace(/[ \t]+/g, " ").trim())
641
+ .filter((line, index, lines) => line.length > 0 || (index > 0 && index < lines.length - 1))
642
+ .join("\n")
643
+ .trim();
644
+ }
645
+
646
+ interface FractionNode {
647
+ type: "fraction";
648
+ numerator: string;
649
+ denominator: string;
650
+ }
651
+
652
+ interface OperatorNode {
653
+ type: "operator";
654
+ operator: string;
655
+ lower?: string;
656
+ upper?: string;
657
+ }
658
+
659
+ interface MatrixNode {
660
+ type: "matrix";
661
+ lines: string[];
662
+ baseline: number;
663
+ }
664
+
665
+ type LayoutNode = FractionNode | OperatorNode | MatrixNode;
666
+
667
+ interface Layout {
668
+ lines: string[];
669
+ width: number;
670
+ baseline: number;
671
+ }
672
+
673
+ const LAYOUT_MARKER_START = "\u{f0000}";
674
+ const LAYOUT_MARKER_END = "\u{f0001}";
675
+ const LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}/gu;
676
+ const TRAILING_LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}$/u;
677
+ const PROTECTED_SPACE = "\u{f0002}";
678
+
679
+ function padLayoutLine(line: string, width: number, centered = false): string {
680
+ const padding = Math.max(0, width - visibleWidth(line));
681
+ const left = centered ? Math.floor(padding / 2) : 0;
682
+ return `${" ".repeat(left)}${line}${" ".repeat(padding - left)}`;
683
+ }
684
+
685
+ function joinLayouts(layouts: readonly Layout[]): Layout {
686
+ if (layouts.length === 0) {
687
+ return { lines: [""], width: 0, baseline: 0 };
688
+ }
689
+ const baseline = Math.max(...layouts.map((layout) => layout.baseline));
690
+ const below = Math.max(...layouts.map((layout) => layout.lines.length - layout.baseline - 1));
691
+ const lines: string[] = [];
692
+ for (let row = 0; row <= baseline + below; row++) {
693
+ let line = "";
694
+ for (const layout of layouts) {
695
+ const sourceRow = row - baseline + layout.baseline;
696
+ line +=
697
+ sourceRow >= 0 && sourceRow < layout.lines.length
698
+ ? padLayoutLine(layout.lines[sourceRow] ?? "", layout.width)
699
+ : " ".repeat(layout.width);
700
+ }
701
+ lines.push(line.trimEnd());
702
+ }
703
+ return {
704
+ lines,
705
+ width: layouts.reduce((width, layout) => width + layout.width, 0),
706
+ baseline,
707
+ };
708
+ }
709
+
710
+ function renderLayout(source: string, nodes: readonly LayoutNode[]): Layout {
711
+ const renderedLines: string[] = [];
712
+ let firstBaseline = 0;
713
+ for (const sourceLine of source.split("\n")) {
714
+ const layouts: Layout[] = [];
715
+ let position = 0;
716
+ let previousNode: LayoutNode | undefined;
717
+ for (const match of sourceLine.matchAll(LAYOUT_MARKER_PATTERN)) {
718
+ const index = match.index;
719
+ const node = nodes[Number(match[1])];
720
+ if (!node) {
721
+ continue;
722
+ }
723
+ if (index > position) {
724
+ const sliced = sourceLine.slice(position, index);
725
+ const trimmed = (previousNode ? sliced.trimStart() : sliced).trimEnd();
726
+ const preserveLeadingSpace = previousNode?.type === "matrix" && /^\s/.test(sliced);
727
+ const preserveTrailingSpace = node.type === "matrix" && /\s$/.test(sliced);
728
+ const text = trimmed
729
+ ? `${preserveLeadingSpace ? " " : ""}${trimmed}${preserveTrailingSpace ? " " : ""}`
730
+ : preserveLeadingSpace || preserveTrailingSpace
731
+ ? " "
732
+ : "";
733
+ layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
734
+ }
735
+ if (node.type === "fraction") {
736
+ const numerator = renderLayout(node.numerator, nodes);
737
+ const denominator = renderLayout(node.denominator, nodes);
738
+ const contentWidth = Math.max(numerator.width, denominator.width, 1);
739
+ const width = contentWidth + 2;
740
+ layouts.push({
741
+ lines: [
742
+ ...numerator.lines.map((line) => padLayoutLine(line, width, true)),
743
+ ` ${"─".repeat(contentWidth)} `,
744
+ ...denominator.lines.map((line) => padLayoutLine(line, width, true)),
745
+ ],
746
+ width,
747
+ baseline: numerator.lines.length,
748
+ });
749
+ } else if (node.type === "operator") {
750
+ const contentWidth = Math.max(
751
+ visibleWidth(node.operator),
752
+ node.lower === undefined ? 0 : visibleWidth(node.lower),
753
+ node.upper === undefined ? 0 : visibleWidth(node.upper),
754
+ );
755
+ const lines: string[] = [];
756
+ if (node.upper !== undefined) {
757
+ lines.push(`${padLayoutLine(node.upper, contentWidth, true)} `);
758
+ }
759
+ lines.push(`${padLayoutLine(node.operator, contentWidth, true)} `);
760
+ if (node.lower !== undefined) {
761
+ lines.push(`${padLayoutLine(node.lower, contentWidth, true)} `);
762
+ }
763
+ layouts.push({
764
+ lines,
765
+ width: contentWidth + 1,
766
+ baseline: node.upper === undefined ? 0 : 1,
767
+ });
768
+ } else {
769
+ const width = Math.max(0, ...node.lines.map((line) => visibleWidth(line)));
770
+ layouts.push({
771
+ lines: node.lines.map((line) => padLayoutLine(line, width)),
772
+ width,
773
+ baseline: node.baseline,
774
+ });
775
+ }
776
+ position = index + match[0].length;
777
+ previousNode = node;
778
+ }
779
+ if (position < sourceLine.length) {
780
+ const sliced = sourceLine.slice(position);
781
+ const trimmed = previousNode ? sliced.trimStart() : sliced;
782
+ const text = previousNode?.type === "matrix" && /^\s/.test(sliced) ? ` ${trimmed}` : trimmed;
783
+ layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
784
+ }
785
+ const lineLayout = joinLayouts(layouts);
786
+ if (renderedLines.length === 0) {
787
+ firstBaseline = lineLayout.baseline;
788
+ }
789
+ renderedLines.push(...lineLayout.lines);
790
+ }
791
+ return {
792
+ lines: renderedLines,
793
+ width: Math.max(0, ...renderedLines.map((line) => visibleWidth(line))),
794
+ baseline: firstBaseline,
795
+ };
796
+ }
797
+
798
+ class LatexParser {
799
+ private readonly source: string;
800
+ private readonly layoutNodes: LayoutNode[];
801
+ private readonly display: boolean;
802
+ private position = 0;
803
+ private supported = true;
804
+ private stackFractions = true;
805
+
806
+ constructor(source: string, layoutNodes: LayoutNode[], display: boolean) {
807
+ this.source = source;
808
+ this.layoutNodes = layoutNodes;
809
+ this.display = display;
810
+ }
811
+
812
+ render(): string | undefined {
813
+ const rendered = this.parseSequence();
814
+ if (!this.supported || this.position !== this.source.length) {
815
+ return undefined;
816
+ }
817
+ return normalizeOutput(rendered);
818
+ }
819
+
820
+ private parseSequence(endCharacter?: string): string {
821
+ let result = "";
822
+ while (this.position < this.source.length) {
823
+ const character = this.source[this.position];
824
+ if (endCharacter && character === endCharacter) {
825
+ this.position++;
826
+ return result;
827
+ }
828
+
829
+ if (character === "}") {
830
+ this.supported = false;
831
+ return result;
832
+ }
833
+
834
+ if (character === "{") {
835
+ this.position++;
836
+ result += this.parseSequence("}");
837
+ continue;
838
+ }
839
+
840
+ if (character === "\\") {
841
+ const command = this.parseCommand();
842
+ if (command === NEGATIVE_SPACE) {
843
+ result = result.trimEnd();
844
+ if (result.endsWith(NAMED_OPERATOR_END)) {
845
+ result = result.slice(0, -NAMED_OPERATOR_END.length);
846
+ }
847
+ } else {
848
+ result += command;
849
+ }
850
+ continue;
851
+ }
852
+
853
+ if (character === "^" || character === "_") {
854
+ this.position++;
855
+ result = result.trimEnd();
856
+ const script = formatScript(this.parseRequiredArgument(false), character === "_" ? "sub" : "sup");
857
+ if (result.endsWith(NAMED_OPERATOR_END)) {
858
+ result = `${result.slice(0, -NAMED_OPERATOR_END.length)}${script}${NAMED_OPERATOR_END}`;
859
+ } else {
860
+ result += script;
861
+ }
862
+ continue;
863
+ }
864
+
865
+ if (/\s/.test(character)) {
866
+ result += this.parseWhitespace();
867
+ continue;
868
+ }
869
+
870
+ if (character === "=" || character === "<" || character === ">") {
871
+ result = `${result.trimEnd()} ${character} `;
872
+ this.position++;
873
+ continue;
874
+ }
875
+
876
+ if (character === "&") {
877
+ this.position++;
878
+ continue;
879
+ }
880
+
881
+ if (character === "~") {
882
+ this.position++;
883
+ result += " ";
884
+ continue;
885
+ }
886
+
887
+ if (character === ".") {
888
+ const marker = TRAILING_LAYOUT_MARKER_PATTERN.exec(result);
889
+ const node = marker ? this.layoutNodes[Number(marker[1])] : undefined;
890
+ if (node?.type === "matrix") {
891
+ const lastLine = node.lines.length - 1;
892
+ node.lines[lastLine] = `${node.lines[lastLine] ?? ""}${character}`;
893
+ this.position++;
894
+ continue;
895
+ }
896
+ }
897
+
898
+ result += character;
899
+ this.position++;
900
+ }
901
+
902
+ if (endCharacter) {
903
+ this.supported = false;
904
+ }
905
+ return result;
906
+ }
907
+
908
+ private parseWhitespace(): string {
909
+ while (this.position < this.source.length && /\s/.test(this.source[this.position] ?? "")) {
910
+ this.position++;
911
+ }
912
+ return " ";
913
+ }
914
+
915
+ private parseCommand(): string {
916
+ this.position++;
917
+ if (this.position >= this.source.length) {
918
+ this.supported = false;
919
+ return "";
920
+ }
921
+
922
+ let command = "";
923
+ const first = this.source[this.position] ?? "";
924
+ if (first === "\n" || first === "\r") {
925
+ this.position++;
926
+ if (first === "\r" && this.source[this.position] === "\n") {
927
+ this.position++;
928
+ }
929
+ return " ";
930
+ }
931
+ if (/[A-Za-z]/.test(first)) {
932
+ const start = this.position;
933
+ while (this.position < this.source.length && /[A-Za-z]/.test(this.source[this.position] ?? "")) {
934
+ this.position++;
935
+ }
936
+ command = this.source.slice(start, this.position);
937
+ } else {
938
+ command = first;
939
+ this.position++;
940
+ }
941
+
942
+ if (command === "\\") {
943
+ return "\n";
944
+ }
945
+ if (SPACING_COMMANDS.has(command)) {
946
+ return " ";
947
+ }
948
+ if (NEGATIVE_SPACING_COMMANDS.has(command)) {
949
+ return NEGATIVE_SPACE;
950
+ }
951
+ if (IGNORED_COMMANDS.has(command)) {
952
+ return "";
953
+ }
954
+ if (
955
+ command === "{" ||
956
+ command === "}" ||
957
+ command === "$" ||
958
+ command === "%" ||
959
+ command === "#" ||
960
+ command === "_" ||
961
+ command === "&"
962
+ ) {
963
+ return command;
964
+ }
965
+ if (command === "|") {
966
+ return "‖";
967
+ }
968
+ if (command === "not") {
969
+ const value = this.parseRequiredArgument(false).trim();
970
+ const negated = NEGATED_SYMBOLS[value];
971
+ if (negated !== undefined) {
972
+ return ` ${negated} `;
973
+ }
974
+ const characters = Array.from(value);
975
+ if (characters.length === 0) {
976
+ this.supported = false;
977
+ return "";
978
+ }
979
+ return ` ${characters[0]}\u0338${characters.slice(1).join("")} `;
980
+ }
981
+ if (LIMIT_OPERATORS.has(command)) {
982
+ return this.parseOperator(command, "bracket", true, true);
983
+ }
984
+
985
+ const symbol = SYMBOLS[command];
986
+ if (symbol !== undefined) {
987
+ if (DISPLAY_LIMIT_SYMBOLS.has(command)) {
988
+ return this.parseOperator(symbol, "script", true);
989
+ }
990
+ return command === "cdot" || command === "times" || RELATION_COMMANDS.has(command) ? ` ${symbol} ` : symbol;
991
+ }
992
+ if (NAMED_OPERATORS.has(command)) {
993
+ return `${NAMED_OPERATOR_START}${command}${NAMED_OPERATOR_END}`;
994
+ }
995
+ if (SIZE_COMMANDS.has(command)) {
996
+ return "";
997
+ }
998
+ if (command === "left" || command === "middle" || command === "right") {
999
+ if (this.source[this.position] === ".") {
1000
+ this.position++;
1001
+ }
1002
+ return "";
1003
+ }
1004
+ if (command === "frac" || command === "dfrac" || command === "tfrac") {
1005
+ const shouldStack = this.display && this.stackFractions && command !== "tfrac";
1006
+ const numerator = this.parseRequiredArgument(!shouldStack);
1007
+ const denominator = this.parseRequiredArgument(!shouldStack);
1008
+ if (shouldStack) {
1009
+ const index =
1010
+ this.layoutNodes.push({
1011
+ type: "fraction",
1012
+ numerator: normalizeOutput(numerator),
1013
+ denominator: normalizeOutput(denominator),
1014
+ }) - 1;
1015
+ return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
1016
+ }
1017
+ return formatFraction(numerator, denominator);
1018
+ }
1019
+ if (command === "sqrt") {
1020
+ const degree = this.parseOptionalArgument()?.trim();
1021
+ const value = this.parseRequiredArgument();
1022
+ if (degree === undefined || degree === "2") {
1023
+ return formatRoot(value);
1024
+ }
1025
+ if (degree === "3") {
1026
+ return formatRoot(value, "∛");
1027
+ }
1028
+ if (degree === "4") {
1029
+ return formatRoot(value, "∜");
1030
+ }
1031
+ return `${formatScript(degree, "sup")}${formatRoot(value)}`;
1032
+ }
1033
+ if (command === "boxed" || command === "fbox") {
1034
+ return `[${this.parseRequiredArgument().trim()}]`;
1035
+ }
1036
+ if (command === "binom" || command === "dbinom" || command === "tbinom") {
1037
+ return `(${this.parseRequiredArgument()} choose ${this.parseRequiredArgument()})`;
1038
+ }
1039
+ const accent = ACCENTS[command];
1040
+ if (accent !== undefined) {
1041
+ const value = this.parseRequiredArgument();
1042
+ return Array.from(value).length === 1 ? `${value}${accent}` : `${command}(${value})`;
1043
+ }
1044
+ if (command === "mathbb") {
1045
+ const value = this.parseRequiredArgument();
1046
+ return Array.from(value, (character) => BLACKBOARD[character] ?? character).join("");
1047
+ }
1048
+ if (command === "operatorname") {
1049
+ const starred = this.source[this.position] === "*";
1050
+ if (starred) {
1051
+ this.position++;
1052
+ }
1053
+ const operator = normalizeOutput(this.parseRequiredArgument()).trim();
1054
+ return this.parseOperator(operator, "bracket", starred, true);
1055
+ }
1056
+ if (command === "mod" || command === "bmod") {
1057
+ return " mod ";
1058
+ }
1059
+ if (command === "pmod" || command === "pod") {
1060
+ const value = this.parseRequiredArgument().trim();
1061
+ return command === "pmod" ? ` (mod ${value})` : ` (${value})`;
1062
+ }
1063
+ if (command === "overset" || command === "stackrel") {
1064
+ const upper = this.parseRequiredArgument();
1065
+ const value = this.parseRequiredArgument().trim();
1066
+ return `${value}${formatScript(upper, "sup")}`;
1067
+ }
1068
+ if (command === "underset") {
1069
+ const lower = this.parseRequiredArgument();
1070
+ const value = this.parseRequiredArgument().trim();
1071
+ return `${value}${formatScript(lower, "sub")}`;
1072
+ }
1073
+ if (PLAIN_WRAPPERS.has(command)) {
1074
+ const value = this.parseRequiredArgument();
1075
+ return command.startsWith("text") || command === "mbox" ? value : value.trim();
1076
+ }
1077
+ if (command === "begin") {
1078
+ return this.parseEnvironment();
1079
+ }
1080
+ if (command === "end") {
1081
+ this.supported = false;
1082
+ return "";
1083
+ }
1084
+
1085
+ this.supported = false;
1086
+ return `\\${command}`;
1087
+ }
1088
+
1089
+ private parseOperator(
1090
+ operator: string,
1091
+ inlineLowerStyle: "bracket" | "script",
1092
+ displayLimits: boolean,
1093
+ spaced = false,
1094
+ ): string {
1095
+ let useDisplayLimits = displayLimits;
1096
+ let modifierPosition = this.position;
1097
+ while (modifierPosition < this.source.length && /[ \t]/.test(this.source[modifierPosition] ?? "")) {
1098
+ modifierPosition++;
1099
+ }
1100
+ const modifier = /^\\(limits|nolimits)(?![A-Za-z])/.exec(this.source.slice(modifierPosition));
1101
+ if (modifier) {
1102
+ useDisplayLimits = modifier[1] === "limits";
1103
+ this.position = modifierPosition + modifier[0].length;
1104
+ }
1105
+
1106
+ let lower: string | undefined;
1107
+ let upper: string | undefined;
1108
+ while (true) {
1109
+ let scriptPosition = this.position;
1110
+ while (scriptPosition < this.source.length && /[ \t]/.test(this.source[scriptPosition] ?? "")) {
1111
+ scriptPosition++;
1112
+ }
1113
+ const kind = this.source[scriptPosition];
1114
+ if (kind !== "_" && kind !== "^") {
1115
+ break;
1116
+ }
1117
+ this.position = scriptPosition + 1;
1118
+ const value = normalizeOutput(this.parseRequiredArgument(false)).replaceAll(" ", "");
1119
+ if (kind === "_") {
1120
+ if (lower !== undefined) {
1121
+ this.supported = false;
1122
+ }
1123
+ lower = value;
1124
+ } else {
1125
+ if (upper !== undefined) {
1126
+ this.supported = false;
1127
+ }
1128
+ upper = value;
1129
+ }
1130
+ }
1131
+
1132
+ if (this.display && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
1133
+ const index = this.layoutNodes.push({ type: "operator", operator, lower, upper }) - 1;
1134
+ return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
1135
+ }
1136
+
1137
+ let rendered = operator;
1138
+ if (lower !== undefined) {
1139
+ rendered += inlineLowerStyle === "bracket" ? `[${lower}]` : formatScript(lower, "sub");
1140
+ }
1141
+ if (upper !== undefined) {
1142
+ rendered += formatScript(upper, "sup");
1143
+ }
1144
+ return spaced ? ` ${rendered} ` : rendered;
1145
+ }
1146
+
1147
+ private parseRequiredArgument(stackFractions = true): string {
1148
+ const previousStackFractions = this.stackFractions;
1149
+ this.stackFractions = previousStackFractions && stackFractions;
1150
+ const value = this.parseRequiredArgumentValue();
1151
+ this.stackFractions = previousStackFractions;
1152
+ return value;
1153
+ }
1154
+
1155
+ private parseRequiredArgumentValue(): string {
1156
+ while (this.position < this.source.length && /\s/.test(this.source[this.position] ?? "")) {
1157
+ this.position++;
1158
+ }
1159
+ if (this.position >= this.source.length) {
1160
+ this.supported = false;
1161
+ return "";
1162
+ }
1163
+ if (this.source[this.position] === "{") {
1164
+ this.position++;
1165
+ return this.parseSequence("}");
1166
+ }
1167
+ if (this.source[this.position] === "\\") {
1168
+ return this.parseCommand();
1169
+ }
1170
+ const value = this.source[this.position] ?? "";
1171
+ this.position++;
1172
+ return value;
1173
+ }
1174
+
1175
+ private parseOptionalArgument(): string | undefined {
1176
+ while (this.position < this.source.length && /[ \t]/.test(this.source[this.position] ?? "")) {
1177
+ this.position++;
1178
+ }
1179
+ if (this.source[this.position] !== "[") {
1180
+ return undefined;
1181
+ }
1182
+ const end = this.source.indexOf("]", this.position + 1);
1183
+ if (end < 0) {
1184
+ this.supported = false;
1185
+ return undefined;
1186
+ }
1187
+ const value = this.source.slice(this.position + 1, end);
1188
+ this.position = end + 1;
1189
+ return this.renderNested(value);
1190
+ }
1191
+
1192
+ private readRawGroup(): string | undefined {
1193
+ while (this.position < this.source.length && /[ \t]/.test(this.source[this.position] ?? "")) {
1194
+ this.position++;
1195
+ }
1196
+ if (this.source[this.position] !== "{") {
1197
+ this.supported = false;
1198
+ return undefined;
1199
+ }
1200
+
1201
+ const start = ++this.position;
1202
+ let depth = 1;
1203
+ while (this.position < this.source.length) {
1204
+ const character = this.source[this.position];
1205
+ if (character === "\\") {
1206
+ this.position += 2;
1207
+ continue;
1208
+ }
1209
+ if (character === "{") depth++;
1210
+ if (character === "}") depth--;
1211
+ if (depth === 0) {
1212
+ const value = this.source.slice(start, this.position);
1213
+ this.position++;
1214
+ return value;
1215
+ }
1216
+ this.position++;
1217
+ }
1218
+ this.supported = false;
1219
+ return undefined;
1220
+ }
1221
+
1222
+ private splitEnvironmentRows(body: string): string[] {
1223
+ return body.split(/\\\\(?:\[[^\]\n]*\])?/);
1224
+ }
1225
+
1226
+ private parseEnvironment(): string {
1227
+ const environment = this.readRawGroup();
1228
+ if (!environment) {
1229
+ return "";
1230
+ }
1231
+ const endMarker = `\\end{${environment}}`;
1232
+ const end = this.source.indexOf(endMarker, this.position);
1233
+ if (end < 0) {
1234
+ this.supported = false;
1235
+ return "";
1236
+ }
1237
+ const body = this.source.slice(this.position, end);
1238
+ this.position = end + endMarker.length;
1239
+
1240
+ if (environment === "equation" || environment === "equation*" || environment === "displaymath") {
1241
+ return this.renderNested(body).trim();
1242
+ }
1243
+
1244
+ if (
1245
+ environment === "aligned" ||
1246
+ environment === "align" ||
1247
+ environment === "align*" ||
1248
+ environment === "alignedat" ||
1249
+ environment === "alignat" ||
1250
+ environment === "alignat*" ||
1251
+ environment === "gather" ||
1252
+ environment === "gathered" ||
1253
+ environment === "multline" ||
1254
+ environment === "multline*" ||
1255
+ environment === "split"
1256
+ ) {
1257
+ const alignedAt = ["alignedat", "alignat", "alignat*"].includes(environment);
1258
+ const alignedBody = alignedAt ? body.replace(/^\s*\{[^}]*\}/, "") : body;
1259
+ return this.splitEnvironmentRows(alignedBody)
1260
+ .map((row) => {
1261
+ const cells = row.split("&");
1262
+ const source = alignedAt
1263
+ ? Array.from({ length: Math.ceil(cells.length / 2) }, (_, index) =>
1264
+ cells.slice(index * 2, index * 2 + 2).join(""),
1265
+ ).join(" ")
1266
+ : cells.join("");
1267
+ return this.renderNested(source).trim();
1268
+ })
1269
+ .filter(Boolean)
1270
+ .join("\n");
1271
+ }
1272
+
1273
+ if (environment === "cases" || environment === "cases*") {
1274
+ const rows = this.splitEnvironmentRows(body)
1275
+ .map((row) => row.split("&").map((cell) => this.renderNested(cell, false).trim()))
1276
+ .filter((row) => row.some(Boolean));
1277
+ return rows
1278
+ .map((row, index) => {
1279
+ const value = (row[0] ?? "").replace(/,\s*$/, "");
1280
+ const condition = row[1] ?? "";
1281
+ const delimiter = index === 0 ? "⎧" : index === rows.length - 1 ? "⎩" : "⎨";
1282
+ const conditionPrefix = /^(?:if|when|for|otherwise)\b/i.test(condition) ? " " : " if ";
1283
+ return `${delimiter} ${value}${condition ? `${conditionPrefix}${condition}` : ""}`;
1284
+ })
1285
+ .join("\n");
1286
+ }
1287
+
1288
+ if (
1289
+ ["array", "matrix", "smallmatrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix"].includes(environment)
1290
+ ) {
1291
+ const matrixBody = environment === "array" ? body.replace(/^\s*\{[^}]*\}/, "") : body;
1292
+ return this.renderMatrix(environment, matrixBody);
1293
+ }
1294
+
1295
+ this.supported = false;
1296
+ return body;
1297
+ }
1298
+
1299
+ private renderMatrix(environment: string, body: string): string {
1300
+ const matrix = this.splitEnvironmentRows(body)
1301
+ .map((row) => row.split("&").map((cell) => this.renderNested(cell, false).trim()))
1302
+ .filter((row) => row.some(Boolean));
1303
+ const columnCount = Math.max(0, ...matrix.map((row) => row.length));
1304
+ const columnWidths = Array.from({ length: columnCount }, (_, column) =>
1305
+ Math.max(0, ...matrix.map((row) => visibleWidth(row[column] ?? ""))),
1306
+ );
1307
+ const rows = matrix.map((row) =>
1308
+ Array.from({ length: columnCount }, (_, column) => {
1309
+ const cell = row[column] ?? "";
1310
+ return `${cell}${PROTECTED_SPACE.repeat(Math.max(0, (columnWidths[column] ?? 0) - visibleWidth(cell)))}`;
1311
+ }).join(" │ "),
1312
+ );
1313
+
1314
+ let lines: string[];
1315
+ if (environment === "array" || environment === "matrix" || environment === "smallmatrix") {
1316
+ lines = rows;
1317
+ } else {
1318
+ const delimiters: Readonly<Record<string, readonly [string, string, string, string, string, string]>> = {
1319
+ pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
1320
+ bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
1321
+ Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
1322
+ vmatrix: ["│", "│", "│", "│", "│", "│"],
1323
+ Vmatrix: ["║", "║", "║", "║", "║", "║"],
1324
+ };
1325
+ const delimiter = delimiters[environment];
1326
+ if (!delimiter) {
1327
+ this.supported = false;
1328
+ return rows.join("\n");
1329
+ }
1330
+ lines = rows.map((row, index) => {
1331
+ const left = index === 0 ? delimiter[0] : index === rows.length - 1 ? delimiter[4] : delimiter[2];
1332
+ const right = index === 0 ? delimiter[1] : index === rows.length - 1 ? delimiter[5] : delimiter[3];
1333
+ return `${left} ${row} ${right}`;
1334
+ });
1335
+ }
1336
+
1337
+ if (lines.length <= 1) {
1338
+ return lines[0] ?? "";
1339
+ }
1340
+ const index = this.layoutNodes.push({ type: "matrix", lines, baseline: 0 }) - 1;
1341
+ return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
1342
+ }
1343
+
1344
+ private renderNested(source: string, stackFractions = true): string {
1345
+ const rendered = new LatexParser(source, this.layoutNodes, this.display && stackFractions).render();
1346
+ if (rendered === undefined) {
1347
+ this.supported = false;
1348
+ return source;
1349
+ }
1350
+ return rendered;
1351
+ }
1352
+ }
1353
+
1354
+ export interface RenderLatexOptions {
1355
+ /** Stack fractions and operator limits vertically for display math (default: false). */
1356
+ display?: boolean;
1357
+ }
1358
+
1359
+ /**
1360
+ * Render a basic LaTeX math expression as terminal-friendly Unicode text.
1361
+ * Returns undefined when the expression contains unsupported or malformed syntax.
1362
+ */
1363
+ export function renderLatex(source: string, options: RenderLatexOptions = {}): string | undefined {
1364
+ const layoutNodes: LayoutNode[] = [];
1365
+ const rendered = new LatexParser(source, layoutNodes, options.display === true).render();
1366
+ if (rendered === undefined) {
1367
+ return undefined;
1368
+ }
1369
+ if (layoutNodes.length === 0) {
1370
+ return rendered.replaceAll(PROTECTED_SPACE, " ");
1371
+ }
1372
+ const lines = renderLayout(rendered, layoutNodes).lines;
1373
+ const indentation = Math.min(
1374
+ ...lines.filter((line) => line.trim()).map((line) => line.length - line.trimStart().length),
1375
+ );
1376
+ return lines
1377
+ .map((line) => line.slice(indentation).trimEnd())
1378
+ .join("\n")
1379
+ .trimEnd()
1380
+ .replaceAll(PROTECTED_SPACE, " ");
1381
+ }