cvc-tui 0.4.0 → 0.4.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 (133) hide show
  1. package/dist/entry.js +71148 -61
  2. package/package.json +2 -2
  3. package/dist/app/completion.js +0 -102
  4. package/dist/app/createGatewayEventHandler.js +0 -508
  5. package/dist/app/createSlashHandler.js +0 -101
  6. package/dist/app/delegationStore.js +0 -51
  7. package/dist/app/gatewayContext.js +0 -17
  8. package/dist/app/historyStore.js +0 -123
  9. package/dist/app/inputBuffer.js +0 -120
  10. package/dist/app/inputSelectionStore.js +0 -8
  11. package/dist/app/inputStore.js +0 -28
  12. package/dist/app/interfaces.js +0 -6
  13. package/dist/app/overlayStore.js +0 -40
  14. package/dist/app/promptStore.js +0 -44
  15. package/dist/app/queueStore.js +0 -25
  16. package/dist/app/scroll.js +0 -44
  17. package/dist/app/setupHandoff.js +0 -28
  18. package/dist/app/slash/commands/core.js +0 -479
  19. package/dist/app/slash/commands/debug.js +0 -44
  20. package/dist/app/slash/commands/ops.js +0 -498
  21. package/dist/app/slash/commands/session.js +0 -431
  22. package/dist/app/slash/commands/setup.js +0 -20
  23. package/dist/app/slash/commands/toggles.js +0 -40
  24. package/dist/app/slash/registry.js +0 -18
  25. package/dist/app/slash/types.js +0 -1
  26. package/dist/app/spawnHistoryStore.js +0 -105
  27. package/dist/app/turnController.js +0 -650
  28. package/dist/app/turnStore.js +0 -48
  29. package/dist/app/uiStore.js +0 -36
  30. package/dist/app/useComposerState.js +0 -265
  31. package/dist/app/useConfigSync.js +0 -144
  32. package/dist/app/useInputHandlers.js +0 -403
  33. package/dist/app/useLongRunToolCharms.js +0 -50
  34. package/dist/app/useMainApp.js +0 -629
  35. package/dist/app/useSessionLifecycle.js +0 -175
  36. package/dist/app/useSubmission.js +0 -287
  37. package/dist/app.js +0 -15
  38. package/dist/banner.js +0 -57
  39. package/dist/components/agentsOverlay.js +0 -474
  40. package/dist/components/appChrome.js +0 -252
  41. package/dist/components/appLayout.js +0 -121
  42. package/dist/components/appOverlays.js +0 -65
  43. package/dist/components/branding.js +0 -97
  44. package/dist/components/fpsOverlay.js +0 -22
  45. package/dist/components/helpHint.js +0 -21
  46. package/dist/components/markdown.js +0 -501
  47. package/dist/components/maskedPrompt.js +0 -12
  48. package/dist/components/messageLine.js +0 -82
  49. package/dist/components/modelPicker.js +0 -254
  50. package/dist/components/overlayControls.js +0 -30
  51. package/dist/components/overlays/confirmPrompt.js +0 -25
  52. package/dist/components/overlays/helpOverlay.js +0 -76
  53. package/dist/components/overlays/historySearch.js +0 -49
  54. package/dist/components/overlays/modelPicker.js +0 -60
  55. package/dist/components/overlays/overlayUtils.js +0 -19
  56. package/dist/components/overlays/secretPrompt.js +0 -36
  57. package/dist/components/overlays/sessionPicker.js +0 -93
  58. package/dist/components/overlays/skillsHub.js +0 -71
  59. package/dist/components/prompts.js +0 -95
  60. package/dist/components/queuedMessages.js +0 -24
  61. package/dist/components/sessionPicker.js +0 -130
  62. package/dist/components/skillsHub.js +0 -165
  63. package/dist/components/streamingAssistant.js +0 -35
  64. package/dist/components/streamingMarkdown.js +0 -144
  65. package/dist/components/textInput.js +0 -794
  66. package/dist/components/themed.js +0 -12
  67. package/dist/components/thinking.js +0 -496
  68. package/dist/components/todoPanel.js +0 -40
  69. package/dist/components/transcript.js +0 -22
  70. package/dist/config/env.js +0 -18
  71. package/dist/config/limits.js +0 -22
  72. package/dist/config/timing.js +0 -18
  73. package/dist/content/charms.js +0 -5
  74. package/dist/content/faces.js +0 -21
  75. package/dist/content/fortunes.js +0 -29
  76. package/dist/content/hotkeys.js +0 -38
  77. package/dist/content/placeholders.js +0 -15
  78. package/dist/content/setup.js +0 -14
  79. package/dist/content/verbs.js +0 -41
  80. package/dist/domain/details.js +0 -53
  81. package/dist/domain/messages.js +0 -63
  82. package/dist/domain/paths.js +0 -16
  83. package/dist/domain/providers.js +0 -11
  84. package/dist/domain/roles.js +0 -6
  85. package/dist/domain/slash.js +0 -11
  86. package/dist/domain/usage.js +0 -1
  87. package/dist/domain/viewport.js +0 -33
  88. package/dist/gateway/client.js +0 -312
  89. package/dist/gatewayClient.js +0 -574
  90. package/dist/gatewayTypes.js +0 -1
  91. package/dist/hooks/useCompletion.js +0 -86
  92. package/dist/hooks/useGitBranch.js +0 -58
  93. package/dist/hooks/useInputHistory.js +0 -12
  94. package/dist/hooks/useQueue.js +0 -57
  95. package/dist/hooks/useVirtualHistory.js +0 -401
  96. package/dist/lib/circularBuffer.js +0 -43
  97. package/dist/lib/clipboard.js +0 -126
  98. package/dist/lib/editor.js +0 -41
  99. package/dist/lib/editor.test.js +0 -58
  100. package/dist/lib/emoji.js +0 -49
  101. package/dist/lib/externalCli.js +0 -11
  102. package/dist/lib/forceTruecolor.js +0 -26
  103. package/dist/lib/fpsStore.js +0 -36
  104. package/dist/lib/gracefulExit.js +0 -29
  105. package/dist/lib/history.js +0 -69
  106. package/dist/lib/inputMetrics.js +0 -143
  107. package/dist/lib/liveProgress.js +0 -51
  108. package/dist/lib/liveProgress.test.js +0 -89
  109. package/dist/lib/mathUnicode.js +0 -685
  110. package/dist/lib/memory.js +0 -123
  111. package/dist/lib/memoryMonitor.js +0 -76
  112. package/dist/lib/messages.js +0 -3
  113. package/dist/lib/messages.test.js +0 -25
  114. package/dist/lib/osc52.js +0 -53
  115. package/dist/lib/perfPane.js +0 -94
  116. package/dist/lib/platform.js +0 -312
  117. package/dist/lib/precisionWheel.js +0 -25
  118. package/dist/lib/reasoning.js +0 -39
  119. package/dist/lib/rpc.js +0 -26
  120. package/dist/lib/subagentTree.js +0 -287
  121. package/dist/lib/syntax.js +0 -89
  122. package/dist/lib/terminalModes.js +0 -46
  123. package/dist/lib/terminalParity.js +0 -48
  124. package/dist/lib/terminalSetup.js +0 -321
  125. package/dist/lib/text.js +0 -203
  126. package/dist/lib/text.test.js +0 -18
  127. package/dist/lib/todo.js +0 -2
  128. package/dist/lib/todo.test.js +0 -22
  129. package/dist/lib/viewportStore.js +0 -82
  130. package/dist/lib/virtualHeights.js +0 -61
  131. package/dist/lib/wheelAccel.js +0 -143
  132. package/dist/theme.js +0 -398
  133. package/dist/types.js +0 -1
@@ -1,685 +0,0 @@
1
- // @ts-nocheck
2
- // SPDX-License-Identifier: MIT
3
- // Ported from CVC Agent (https://github.com/NousResearch/cvc)
4
- // Original Copyright (c) 2025 Nous Research. CVC adaptations (c) 2026 Jai Kumar Meena.
5
- // Best-effort LaTeX → Unicode for inline / display math captured by the
6
- // markdown renderer. The terminal can't typeset LaTeX, but Unicode covers
7
- // most of what models actually emit: Greek letters, blackboard / fraktur /
8
- // calligraphic capitals, set theory + logic operators, common arrows,
9
- // sub/superscripts, and `\frac{a}{b}` collapsed to `a/b`.
10
- //
11
- // Design rules:
12
- // • Pure regex pipeline. Anything we don't recognise is preserved
13
- // verbatim (so a `\foo{bar}` we've never heard of still survives).
14
- // A real LaTeX parser would be more correct but throws on partial
15
- // input — terminal users would rather see the raw command than a
16
- // parse-error placeholder.
17
- // • Longest-match-first ordering on commands so `\le` doesn't shadow
18
- // `\leq`, `\sub` doesn't shadow `\subseteq`, etc.
19
- // • Word-boundary lookahead `(?![A-Za-z])` after each command so
20
- // `\pix` (made-up command) doesn't get partially substituted as `π`.
21
- // • `\mathbb{X}`, `\mathcal{X}`, `\mathfrak{X}` only handle a single
22
- // letter argument — multi-letter `\mathbb{NN}` is rare and would
23
- // need a real parser to do correctly.
24
- // • Sub/super scripts only convert if EVERY character has a Unicode
25
- // equivalent. Mixed content like `^{n+1}` falls back to the raw
26
- // LaTeX so we don't emit `ⁿ+¹` (which has no `+` superscript glyph
27
- // in some fonts and reads worse than the source).
28
- const SYMBOLS = {
29
- // Greek lowercase
30
- '\\alpha': 'α',
31
- '\\beta': 'β',
32
- '\\gamma': 'γ',
33
- '\\delta': 'δ',
34
- '\\epsilon': 'ε',
35
- '\\varepsilon': 'ε',
36
- '\\zeta': 'ζ',
37
- '\\eta': 'η',
38
- '\\theta': 'θ',
39
- '\\vartheta': 'ϑ',
40
- '\\iota': 'ι',
41
- '\\kappa': 'κ',
42
- '\\lambda': 'λ',
43
- '\\mu': 'μ',
44
- '\\nu': 'ν',
45
- '\\xi': 'ξ',
46
- '\\pi': 'π',
47
- '\\varpi': 'ϖ',
48
- '\\rho': 'ρ',
49
- '\\varrho': 'ϱ',
50
- '\\sigma': 'σ',
51
- '\\varsigma': 'ς',
52
- '\\tau': 'τ',
53
- '\\upsilon': 'υ',
54
- '\\phi': 'φ',
55
- '\\varphi': 'φ',
56
- '\\chi': 'χ',
57
- '\\psi': 'ψ',
58
- '\\omega': 'ω',
59
- // Greek uppercase
60
- '\\Gamma': 'Γ',
61
- '\\Delta': 'Δ',
62
- '\\Theta': 'Θ',
63
- '\\Lambda': 'Λ',
64
- '\\Xi': 'Ξ',
65
- '\\Pi': 'Π',
66
- '\\Sigma': 'Σ',
67
- '\\Upsilon': 'Υ',
68
- '\\Phi': 'Φ',
69
- '\\Psi': 'Ψ',
70
- '\\Omega': 'Ω',
71
- // Big operators
72
- '\\sum': '∑',
73
- '\\prod': '∏',
74
- '\\coprod': '∐',
75
- '\\int': '∫',
76
- '\\iint': '∬',
77
- '\\iiint': '∭',
78
- '\\oint': '∮',
79
- '\\bigcup': '⋃',
80
- '\\bigcap': '⋂',
81
- '\\bigvee': '⋁',
82
- '\\bigwedge': '⋀',
83
- '\\bigoplus': '⨁',
84
- '\\bigotimes': '⨂',
85
- // Calculus
86
- '\\partial': '∂',
87
- '\\nabla': '∇',
88
- '\\sqrt': '√',
89
- // Sets
90
- '\\emptyset': '∅',
91
- '\\varnothing': '∅',
92
- '\\infty': '∞',
93
- '\\in': '∈',
94
- '\\notin': '∉',
95
- '\\ni': '∋',
96
- '\\subset': '⊂',
97
- '\\supset': '⊃',
98
- '\\subseteq': '⊆',
99
- '\\supseteq': '⊇',
100
- '\\subsetneq': '⊊',
101
- '\\supsetneq': '⊋',
102
- '\\cup': '∪',
103
- '\\cap': '∩',
104
- '\\setminus': '∖',
105
- '\\complement': '∁',
106
- // Logic
107
- '\\forall': '∀',
108
- '\\exists': '∃',
109
- '\\nexists': '∄',
110
- '\\land': '∧',
111
- '\\lor': '∨',
112
- '\\lnot': '¬',
113
- '\\neg': '¬',
114
- '\\therefore': '∴',
115
- '\\because': '∵',
116
- // Relations
117
- '\\le': '≤',
118
- '\\leq': '≤',
119
- '\\ge': '≥',
120
- '\\geq': '≥',
121
- '\\ne': '≠',
122
- '\\neq': '≠',
123
- '\\ll': '≪',
124
- '\\gg': '≫',
125
- '\\approx': '≈',
126
- '\\equiv': '≡',
127
- '\\cong': '≅',
128
- '\\sim': '∼',
129
- '\\simeq': '≃',
130
- '\\propto': '∝',
131
- '\\perp': '⊥',
132
- '\\parallel': '∥',
133
- '\\models': '⊨',
134
- '\\vdash': '⊢',
135
- '\\mid': '∣',
136
- '\\nmid': '∤',
137
- '\\divides': '∣',
138
- // Common standalone glyphs
139
- '\\blacksquare': '■',
140
- '\\square': '□',
141
- '\\Box': '□',
142
- '\\qed': '∎',
143
- '\\bigstar': '★',
144
- // Modular arithmetic — the `\pmod{p}` form (with arg) is handled below;
145
- // the bare `\bmod` / `\mod` commands are simple text substitutions.
146
- '\\bmod': 'mod',
147
- '\\mod': 'mod',
148
- // Brackets / fences (named delimiter commands; the `\left\X` / `\right\X`
149
- // unwrapping below leaves these behind for the symbol pass to resolve).
150
- '\\langle': '⟨',
151
- '\\rangle': '⟩',
152
- '\\lceil': '⌈',
153
- '\\rceil': '⌉',
154
- '\\lfloor': '⌊',
155
- '\\rfloor': '⌋',
156
- '\\|': '‖',
157
- // Arrows
158
- '\\to': '→',
159
- '\\rightarrow': '→',
160
- '\\leftarrow': '←',
161
- '\\leftrightarrow': '↔',
162
- '\\Rightarrow': '⇒',
163
- '\\Leftarrow': '⇐',
164
- '\\Leftrightarrow': '⇔',
165
- '\\implies': '⟹',
166
- '\\impliedby': '⟸',
167
- '\\iff': '⟺',
168
- '\\mapsto': '↦',
169
- '\\hookrightarrow': '↪',
170
- '\\hookleftarrow': '↩',
171
- '\\uparrow': '↑',
172
- '\\downarrow': '↓',
173
- '\\updownarrow': '↕',
174
- // Binary operators
175
- '\\cdot': '⋅',
176
- '\\cdots': '⋯',
177
- '\\ldots': '…',
178
- '\\dots': '…',
179
- '\\dotsb': '…',
180
- '\\dotsc': '…',
181
- '\\vdots': '⋮',
182
- '\\ddots': '⋱',
183
- '\\times': '×',
184
- '\\div': '÷',
185
- '\\pm': '±',
186
- '\\mp': '∓',
187
- '\\circ': '∘',
188
- '\\bullet': '•',
189
- '\\star': '⋆',
190
- '\\ast': '∗',
191
- '\\oplus': '⊕',
192
- '\\ominus': '⊖',
193
- '\\otimes': '⊗',
194
- '\\odot': '⊙',
195
- '\\diamond': '⋄',
196
- '\\angle': '∠',
197
- '\\triangle': '△',
198
- // Spacing — collapse to varying widths of regular space
199
- '\\,': ' ',
200
- '\\;': ' ',
201
- '\\:': ' ',
202
- '\\!': '',
203
- '\\ ': ' ',
204
- '\\quad': ' ',
205
- '\\qquad': ' ',
206
- // Functions (LaTeX renders these in roman; we just keep the name)
207
- '\\sin': 'sin',
208
- '\\cos': 'cos',
209
- '\\tan': 'tan',
210
- '\\cot': 'cot',
211
- '\\sec': 'sec',
212
- '\\csc': 'csc',
213
- '\\arcsin': 'arcsin',
214
- '\\arccos': 'arccos',
215
- '\\arctan': 'arctan',
216
- '\\sinh': 'sinh',
217
- '\\cosh': 'cosh',
218
- '\\tanh': 'tanh',
219
- '\\log': 'log',
220
- '\\ln': 'ln',
221
- '\\exp': 'exp',
222
- '\\det': 'det',
223
- '\\dim': 'dim',
224
- '\\ker': 'ker',
225
- '\\lim': 'lim',
226
- '\\liminf': 'liminf',
227
- '\\limsup': 'limsup',
228
- '\\sup': 'sup',
229
- '\\inf': 'inf',
230
- '\\max': 'max',
231
- '\\min': 'min',
232
- '\\arg': 'arg',
233
- '\\gcd': 'gcd',
234
- // Escaped literals — model occasionally emits these for display
235
- '\\&': '&',
236
- '\\%': '%',
237
- '\\$': '$',
238
- '\\#': '#',
239
- '\\_': '_',
240
- '\\{': '{',
241
- '\\}': '}'
242
- };
243
- const BB = {
244
- A: '𝔸',
245
- B: '𝔹',
246
- C: 'ℂ',
247
- D: '𝔻',
248
- E: '𝔼',
249
- F: '𝔽',
250
- G: '𝔾',
251
- H: 'ℍ',
252
- I: '𝕀',
253
- J: '𝕁',
254
- K: '𝕂',
255
- L: '𝕃',
256
- M: '𝕄',
257
- N: 'ℕ',
258
- O: '𝕆',
259
- P: 'ℙ',
260
- Q: 'ℚ',
261
- R: 'ℝ',
262
- S: '𝕊',
263
- T: '𝕋',
264
- U: '𝕌',
265
- V: '𝕍',
266
- W: '𝕎',
267
- X: '𝕏',
268
- Y: '𝕐',
269
- Z: 'ℤ'
270
- };
271
- const CAL = {
272
- A: '𝒜',
273
- B: 'ℬ',
274
- C: '𝒞',
275
- D: '𝒟',
276
- E: 'ℰ',
277
- F: 'ℱ',
278
- G: '𝒢',
279
- H: 'ℋ',
280
- I: 'ℐ',
281
- J: '𝒥',
282
- K: '𝒦',
283
- L: 'ℒ',
284
- M: 'ℳ',
285
- N: '𝒩',
286
- O: '𝒪',
287
- P: '𝒫',
288
- Q: '𝒬',
289
- R: 'ℛ',
290
- S: '𝒮',
291
- T: '𝒯',
292
- U: '𝒰',
293
- V: '𝒱',
294
- W: '𝒲',
295
- X: '𝒳',
296
- Y: '𝒴',
297
- Z: '𝒵'
298
- };
299
- const FRAK = {
300
- A: '𝔄',
301
- B: '𝔅',
302
- C: 'ℭ',
303
- D: '𝔇',
304
- E: '𝔈',
305
- F: '𝔉',
306
- G: '𝔊',
307
- H: 'ℌ',
308
- I: 'ℑ',
309
- J: '𝔍',
310
- K: '𝔎',
311
- L: '𝔏',
312
- M: '𝔐',
313
- N: '𝔑',
314
- O: '𝔒',
315
- P: '𝔓',
316
- Q: '𝔔',
317
- R: 'ℜ',
318
- S: '𝔖',
319
- T: '𝔗',
320
- U: '𝔘',
321
- V: '𝔙',
322
- W: '𝔚',
323
- X: '𝔛',
324
- Y: '𝔜',
325
- Z: 'ℨ'
326
- };
327
- const SUPERSCRIPT = {
328
- '0': '⁰',
329
- '1': '¹',
330
- '2': '²',
331
- '3': '³',
332
- '4': '⁴',
333
- '5': '⁵',
334
- '6': '⁶',
335
- '7': '⁷',
336
- '8': '⁸',
337
- '9': '⁹',
338
- '+': '⁺',
339
- '-': '⁻',
340
- '=': '⁼',
341
- '(': '⁽',
342
- ')': '⁾',
343
- a: 'ᵃ',
344
- b: 'ᵇ',
345
- c: 'ᶜ',
346
- d: 'ᵈ',
347
- e: 'ᵉ',
348
- f: 'ᶠ',
349
- g: 'ᵍ',
350
- h: 'ʰ',
351
- i: 'ⁱ',
352
- j: 'ʲ',
353
- k: 'ᵏ',
354
- l: 'ˡ',
355
- m: 'ᵐ',
356
- n: 'ⁿ',
357
- o: 'ᵒ',
358
- p: 'ᵖ',
359
- r: 'ʳ',
360
- s: 'ˢ',
361
- t: 'ᵗ',
362
- u: 'ᵘ',
363
- v: 'ᵛ',
364
- w: 'ʷ',
365
- x: 'ˣ',
366
- y: 'ʸ',
367
- z: 'ᶻ'
368
- };
369
- const SUBSCRIPT = {
370
- '0': '₀',
371
- '1': '₁',
372
- '2': '₂',
373
- '3': '₃',
374
- '4': '₄',
375
- '5': '₅',
376
- '6': '₆',
377
- '7': '₇',
378
- '8': '₈',
379
- '9': '₉',
380
- '+': '₊',
381
- '-': '₋',
382
- '=': '₌',
383
- '(': '₍',
384
- ')': '₎',
385
- a: 'ₐ',
386
- e: 'ₑ',
387
- h: 'ₕ',
388
- i: 'ᵢ',
389
- j: 'ⱼ',
390
- k: 'ₖ',
391
- l: 'ₗ',
392
- m: 'ₘ',
393
- n: 'ₙ',
394
- o: 'ₒ',
395
- p: 'ₚ',
396
- r: 'ᵣ',
397
- s: 'ₛ',
398
- t: 'ₜ',
399
- u: 'ᵤ',
400
- v: 'ᵥ',
401
- x: 'ₓ'
402
- };
403
- // Sentinel control characters used to mark `\boxed` / `\fbox` regions in
404
- // the converted output. The renderer splits on these to apply a highlight
405
- // style; consumers that don't want highlighting can strip them with the
406
- // exported `BOX_RE` below.
407
- export const BOX_OPEN = '\u0001';
408
- export const BOX_CLOSE = '\u0002';
409
- export const BOX_RE = /\u0001([^\u0001\u0002]*)\u0002/g;
410
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
411
- // Pre-compile two symbol regexes: one for letter-ending commands (`\pi`,
412
- // `\sum`) which need a `(?![A-Za-z])` lookahead so they don't partially
413
- // match `\pix` or `\summa`, and one for punctuation-ending commands
414
- // (`\{`, `\,`, `\|`) which must NOT have the lookahead — otherwise
415
- // `\{p` would refuse to substitute because `p` is a letter.
416
- //
417
- // Longest commands first inside each group so `\leq` beats `\le`.
418
- const splitByEnding = (keys) => {
419
- const letter = [];
420
- const punct = [];
421
- for (const k of keys) {
422
- if (/[A-Za-z]$/.test(k)) {
423
- letter.push(k);
424
- }
425
- else {
426
- punct.push(k);
427
- }
428
- }
429
- return { letter, punct };
430
- };
431
- const buildAlt = (cmds) => cmds
432
- .sort((a, b) => b.length - a.length)
433
- .map(escapeRe)
434
- .join('|');
435
- const { letter: LETTER_CMDS, punct: PUNCT_CMDS } = splitByEnding(Object.keys(SYMBOLS));
436
- const SYMBOL_LETTER_RE = new RegExp('(?:' + buildAlt(LETTER_CMDS) + ')(?![A-Za-z])', 'g');
437
- const SYMBOL_PUNCT_RE = new RegExp('(?:' + buildAlt(PUNCT_CMDS) + ')', 'g');
438
- const convertScript = (input, table, sigil) => {
439
- let out = '';
440
- let allMapped = true;
441
- for (const ch of input) {
442
- const mapped = table[ch];
443
- if (!mapped) {
444
- allMapped = false;
445
- break;
446
- }
447
- out += mapped;
448
- }
449
- if (allMapped) {
450
- return out;
451
- }
452
- // Fallback: if the body is a single visible character (e.g. `∞` after
453
- // earlier symbol substitution), render it without braces — `^∞` reads
454
- // far better than `^{∞}` in a terminal. Multi-char bodies that don't
455
- // fully convert use parens (`e^(iπ)`) instead of braces (`e^{iπ}`)
456
- // because parens are normal punctuation while braces look like
457
- // unrendered LaTeX.
458
- const trimmed = input.trim();
459
- if ([...trimmed].length === 1) {
460
- return `${sigil}${trimmed}`;
461
- }
462
- return `${sigil}(${trimmed})`;
463
- };
464
- // Walk the string and parse `{...}` honouring nested braces. Unlike a
465
- // `\{[^{}]*\}` regex this survives `\frac{|t|^{p-1}|P(t)|^p}{...}` where
466
- // the numerator contains its own braces from a superscript. Returns the
467
- // inner content (without the outer braces) and the offset just past the
468
- // closing `}`. Returns null if there is no balanced brace at `start`.
469
- const readBraced = (s, start) => {
470
- if (s[start] !== '{') {
471
- return null;
472
- }
473
- let depth = 1;
474
- let i = start + 1;
475
- while (i < s.length && depth > 0) {
476
- const c = s[i];
477
- // Skip escapes — `\{` and `\}` inside a body are literal braces and
478
- // should not change the brace counter.
479
- if (c === '\\' && i + 1 < s.length) {
480
- i += 2;
481
- continue;
482
- }
483
- if (c === '{') {
484
- depth++;
485
- }
486
- else if (c === '}') {
487
- depth--;
488
- }
489
- if (depth > 0) {
490
- i++;
491
- }
492
- }
493
- if (depth !== 0) {
494
- return null;
495
- }
496
- return { content: s.slice(start + 1, i), end: i + 1 };
497
- };
498
- // Replace every occurrence of `\command{arg}` using balanced-brace parsing
499
- // (so `\boxed{x^{n+1}}` works where a `[^{}]*` regex would fail). The
500
- // `render` callback receives the inner content already recursed-into, so
501
- // `\boxed{\boxed{x}}` resolves outside-in cleanly. Unmatched `\command`
502
- // (no following `{...}`) is preserved verbatim.
503
- const replaceBracedCommand = (input, command, render) => {
504
- const cmdLen = command.length;
505
- let out = '';
506
- let i = 0;
507
- while (i < input.length) {
508
- const idx = input.indexOf(command, i);
509
- if (idx < 0) {
510
- out += input.slice(i);
511
- return out;
512
- }
513
- const after = input[idx + cmdLen];
514
- if (after && /[A-Za-z]/.test(after)) {
515
- out += input.slice(i, idx + cmdLen);
516
- i = idx + cmdLen;
517
- continue;
518
- }
519
- out += input.slice(i, idx);
520
- let p = idx + cmdLen;
521
- while (input[p] === ' ' || input[p] === '\t')
522
- p++;
523
- const arg = readBraced(input, p);
524
- if (!arg) {
525
- out += input.slice(idx, p + 1);
526
- i = p + 1;
527
- continue;
528
- }
529
- out += render(replaceBracedCommand(arg.content, command, render));
530
- i = arg.end;
531
- }
532
- return out;
533
- };
534
- // Replace every `\frac{num}{den}` with `num/den` (parens around either
535
- // side when its precedence demands it). The recursion handles nested
536
- // fractions naturally: `\frac{1}{\frac{1}{x}}` collapses to `1/(1/x)`
537
- // because we recurse into `den` before deciding whether to parenthesise.
538
- const replaceFracs = (input) => {
539
- let out = '';
540
- let i = 0;
541
- while (i < input.length) {
542
- const idx = input.indexOf('\\frac', i);
543
- if (idx < 0) {
544
- out += input.slice(i);
545
- return out;
546
- }
547
- const after = input[idx + 5];
548
- // `(?![A-Za-z])` — protect hypothetical commands like `\fraction`.
549
- if (after && /[A-Za-z]/.test(after)) {
550
- out += input.slice(i, idx + 5);
551
- i = idx + 5;
552
- continue;
553
- }
554
- out += input.slice(i, idx);
555
- let p = idx + 5;
556
- while (input[p] === ' ' || input[p] === '\t')
557
- p++;
558
- const num = readBraced(input, p);
559
- if (!num) {
560
- out += input.slice(idx, p + 1);
561
- i = p + 1;
562
- continue;
563
- }
564
- p = num.end;
565
- while (input[p] === ' ' || input[p] === '\t')
566
- p++;
567
- const den = readBraced(input, p);
568
- if (!den) {
569
- out += input.slice(idx, p + 1);
570
- i = p + 1;
571
- continue;
572
- }
573
- out += `${wrapForFrac(replaceFracs(num.content))}/${wrapForFrac(replaceFracs(den.content))}`;
574
- i = den.end;
575
- }
576
- return out;
577
- };
578
- // Wrap multi-token expressions in parens so `\frac{a+b}{c}` becomes
579
- // `(a+b)/c` rather than `a+b/c`. We wrap whenever inline `/` would
580
- // change the meaning — that's any binary operator (`+`, `-`, `*`, `/`)
581
- // or whitespace separating tokens. `*` and `/` matter because nested
582
- // fractions and products like `\frac{a*b}{c}` and `\frac{1/x}{y}` would
583
- // otherwise read as `a*b/c` (right-associative ambiguity) and `1/x/y`.
584
- // Atomic factors like `n!`, `x^2`, `\sin x` don't trigger any of these
585
- // and stay un-parenthesised — wrapping them just clutters the output.
586
- const wrapForFrac = (expr) => {
587
- const trimmed = expr.trim();
588
- if (!trimmed) {
589
- return trimmed;
590
- }
591
- if (/^\(.*\)$/.test(trimmed)) {
592
- return trimmed;
593
- }
594
- if (/[+\-/*]|\s/.test(trimmed)) {
595
- return `(${trimmed})`;
596
- }
597
- return trimmed;
598
- };
599
- export function texToUnicode(input) {
600
- let s = input;
601
- s = s.replace(/\\mathbb\s*\{([A-Za-z])\}/g, (raw, c) => BB[c] ?? raw);
602
- s = s.replace(/\\mathcal\s*\{([A-Za-z])\}/g, (raw, c) => CAL[c] ?? raw);
603
- s = s.replace(/\\mathfrak\s*\{([A-Za-z])\}/g, (raw, c) => FRAK[c] ?? raw);
604
- s = s.replace(/\\mathbf\s*\{([^{}]+)\}/g, (_, c) => c);
605
- s = s.replace(/\\mathit\s*\{([^{}]+)\}/g, (_, c) => c);
606
- s = s.replace(/\\mathrm\s*\{([^{}]+)\}/g, (_, c) => c);
607
- s = s.replace(/\\text\s*\{([^{}]+)\}/g, (_, c) => c);
608
- s = s.replace(/\\operatorname\s*\{([^{}]+)\}/g, (_, c) => c);
609
- s = s.replace(/\\overline\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0305`);
610
- s = s.replace(/\\hat\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0302`);
611
- s = s.replace(/\\bar\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0304`);
612
- s = s.replace(/\\tilde\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0303`);
613
- s = s.replace(/\\vec\s*\{([^{}]+)\}/g, (_, c) => `${c}\u20D7`);
614
- s = s.replace(/\\dot\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0307`);
615
- s = s.replace(/\\ddot\s*\{([^{}]+)\}/g, (_, c) => `${c}\u0308`);
616
- s = replaceFracs(s);
617
- // `\boxed{X}` / `\fbox{X}` highlight a final answer. Terminals can't
618
- // draw a real box, so we wrap the content in U+0001 / U+0002 control
619
- // characters — non-printable, never present in real text — and let the
620
- // markdown renderer split on them and apply a highlight style (inverse
621
- // video) to the bracketed region. This keeps `texToUnicode` pure-string
622
- // while letting the React layer do the actual visual emphasis.
623
- // Argument is parsed with balanced braces so nested `{...}` from
624
- // superscripts / fractions inside the box survive.
625
- s = replaceBracedCommand(s, '\\boxed', body => `${BOX_OPEN}${body.trim()}${BOX_CLOSE}`);
626
- s = replaceBracedCommand(s, '\\fbox', body => `${BOX_OPEN}${body.trim()}${BOX_CLOSE}`);
627
- // `\xrightarrow{label}` / `\xleftarrow{label}` collapse to an arrow with
628
- // the label inline. LaTeX renders the label above the arrow; in monospace
629
- // we put it adjacent — `─label→` is the closest readable approximation.
630
- // Run before the symbol pass so the label can still pick up Greek and
631
- // operator substitutions afterwards.
632
- s = s.replace(/\\xrightarrow\s*\{([^{}]*)\}/g, (_, label) => `─${label.trim()}→`);
633
- s = s.replace(/\\xleftarrow\s*\{([^{}]*)\}/g, (_, label) => `←${label.trim()}─`);
634
- s = s.replace(/\\Longrightarrow/g, '⟹');
635
- s = s.replace(/\\Longleftarrow/g, '⟸');
636
- s = s.replace(/\\Longleftrightarrow/g, '⟺');
637
- // `\pmod{p}` → ` (mod p)` (LaTeX adds parens automatically); `\pod{p}`
638
- // is a paren-less variant; `\tag{n}` is the equation-number annotation
639
- // shown to the right of an equation. Collapse to a single-space-prefixed
640
- // bracketed form. The leading `\s*` in the pattern absorbs any whitespace
641
- // already in the source so we don't end up with `b (mod p)` (double
642
- // space) when the user wrote `b \pmod{p}`.
643
- s = s.replace(/\s*\\pmod\s*\{([^{}]*)\}/g, (_, p) => ` (mod ${p.trim()})`);
644
- s = s.replace(/\s*\\pod\s*\{([^{}]*)\}/g, (_, p) => ` (${p.trim()})`);
645
- s = s.replace(/\s*\\tag\s*\{([^{}]*)\}/g, (_, n) => ` (${n.trim()})`);
646
- // `\big`, `\Big`, `\bigg`, `\Bigg` (with optional `l`/`r`/`m` suffix)
647
- // are sizing wrappers analogous to `\left`/`\right` but without the
648
- // automatic-pairing semantics. Strip them and leave whatever delimiter
649
- // follows. The trailing `(?![A-Za-z])` protects `\bigtriangleup` and
650
- // any other letter-continuation command from being shaved.
651
- s = s.replace(/\\(?:Bigg|bigg|Big|big)[lrm]?(?![A-Za-z])/g, '');
652
- // Style / size hints that don't typeset any glyph and only affect how
653
- // things would be sized in a real LaTeX engine. In a terminal every
654
- // glyph is one monospace cell, so there's nothing to do — drop them
655
- // (with any trailing whitespace) so they don't leak through as raw
656
- // `\displaystyle` in the output.
657
- s = s.replace(/\\(?:scriptscriptstyle|displaystyle|scriptstyle|textstyle|nolimits|limits)(?![A-Za-z])\s*/g, '');
658
- // `\left` and `\right` are sizing wrappers around any delimiter — bare
659
- // (`\left(`), escaped (`\left\{`), or named (`\left\langle`). Strip the
660
- // wrapper unconditionally and let the rest of the pipeline (or the
661
- // upcoming symbol pass) handle whatever delimiter follows. The optional
662
- // `.?` consumes `\left.` / `\right.` which mean "no delimiter".
663
- // Lookahead `(?![A-Za-z])` keeps `\leftarrow` / `\leftrightarrow` safe.
664
- s = s.replace(/\\left(?![A-Za-z])\.?/g, '');
665
- s = s.replace(/\\right(?![A-Za-z])\.?/g, '');
666
- // Run symbol substitution BEFORE scripts so a body like `^{\infty}`
667
- // becomes `^{∞}` first; convertScript can then either map ∞ to a
668
- // superscript (it can't — Unicode lacks one) or fall back to `^∞`
669
- // by stripping braces around the now-single-character body.
670
- //
671
- // Punctuation pass first — these can be followed by letters (`\{p`
672
- // is "open-brace then p"), so the letter pass's `(?![A-Za-z])` rule
673
- // would wrongly block them.
674
- s = s.replace(SYMBOL_PUNCT_RE, m => SYMBOLS[m] ?? m);
675
- s = s.replace(SYMBOL_LETTER_RE, m => SYMBOLS[m] ?? m);
676
- // Bare `^c` / `_c` handles ONLY alphanumerics and `+`/`-`/`=`. Parens
677
- // are intentionally excluded because the braced-fallback above can
678
- // emit `(...)` and we don't want a second pass to greedily convert
679
- // its opening paren into `⁽` and orphan the closing one.
680
- s = s.replace(/\^\s*\{([^{}]+)\}/g, (_, body) => convertScript(body, SUPERSCRIPT, '^'));
681
- s = s.replace(/\^([A-Za-z0-9+\-=])/g, (raw, ch) => SUPERSCRIPT[ch] ?? raw);
682
- s = s.replace(/_\s*\{([^{}]+)\}/g, (_, body) => convertScript(body, SUBSCRIPT, '_'));
683
- s = s.replace(/_([A-Za-z0-9+\-=])/g, (raw, ch) => SUBSCRIPT[ch] ?? raw);
684
- return s;
685
- }