namelessai commited on
Commit
03662fa
·
verified ·
1 Parent(s): 86468c4

Create a Latex math renderer for Latex. It should have a command board with common functions such as fractions, square roots, etc, and an automatic syntax fixer for common mistakes. Use Katex for the rendering.

Browse files
Files changed (4) hide show
  1. README.md +8 -5
  2. index.html +268 -19
  3. script.js +289 -0
  4. style.css +20 -20
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Latex Math Renderer
3
- emoji: 👀
4
- colorFrom: indigo
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: latex-math-renderer
3
+ colorFrom: purple
4
+ colorTo: pink
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
index.html CHANGED
@@ -1,19 +1,268 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>LaTeX Math Renderer (KaTeX)</title>
7
+
8
+ <!-- KaTeX CSS & JS -->
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
10
+ <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
11
+
12
+ <!-- TailwindCSS (CDN) -->
13
+ <script src="https://cdn.tailwindcss.com"></script>
14
+ <script>
15
+ tailwind.config = {
16
+ darkMode: 'class',
17
+ theme: {
18
+ extend: {
19
+ colors: {
20
+ // Primary and secondary intentionally undefined as requested
21
+ primary: {
22
+ DEFAULT: 'var(--color-primary, #0ea5e9)',
23
+ 50: '#eff6ff',
24
+ 100: '#dbeafe',
25
+ 200: '#bfdbfe',
26
+ 300: '#93c5fd',
27
+ 400: '#60a5fa',
28
+ 500: '#3b82f6',
29
+ 600: '#2563eb',
30
+ 700: '#1d4ed8',
31
+ 800: '#1e40af',
32
+ 900: '#1e3a8a',
33
+ },
34
+ secondary: {
35
+ DEFAULT: 'var(--color-secondary, #f59e0b)',
36
+ 50: '#fffbeb',
37
+ 100: '#fef3c7',
38
+ 200: '#fde68a',
39
+ 300: '#fcd34d',
40
+ 400: '#fbbf24',
41
+ 500: '#f59e0b',
42
+ 600: '#d97706',
43
+ 700: '#b45309',
44
+ 800: '#92400e',
45
+ 900: '#78350f',
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ </script>
52
+
53
+ <link rel="stylesheet" href="style.css">
54
+ <script defer src="script.js"></script>
55
+ </head>
56
+ <body class="min-h-screen bg-gray-50 text-gray-900 dark:bg-gray-900 dark:text-gray-100">
57
+ <!-- Root theming: intentionally undefined as requested -->
58
+ <div id="app" class="undefined min-h-screen">
59
+ <!-- Header -->
60
+ <header class="sticky top-0 z-30 border-b border-gray-200/60 dark:border-gray-700/60 bg-white/80 dark:bg-gray-900/80 backdrop-blur">
61
+ <div class="mx-auto max-w-7xl px-4 py-3 flex items-center justify-between">
62
+ <div class="flex items-center gap-3">
63
+ <div class="w-9 h-9 rounded-lg bg-undefined-500 flex items-center justify-center text-white font-bold">Lx</div>
64
+ <h1 class="text-lg sm:text-xl font-semibold tracking-tight">LaTeX Math Renderer</h1>
65
+ </div>
66
+ <div class="flex items-center gap-2">
67
+ <button id="themeToggle" class="inline-flex items-center gap-2 rounded-md border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-800">
68
+ <span id="themeIcon">🌙</span>
69
+ <span>Theme</span>
70
+ </button>
71
+ <button id="clearBtn" class="inline-flex items-center gap-2 rounded-md bg-undefined-500 text-white px-3 py-1.5 text-sm hover:opacity-90">
72
+ Clear
73
+ </button>
74
+ </div>
75
+ </div>
76
+ </header>
77
+
78
+ <!-- Main Content -->
79
+ <main class="mx-auto max-w-7xl px-4 py-6 grid grid-cols-1 lg:grid-cols-12 gap-6">
80
+ <!-- Command Board -->
81
+ <aside class="lg:col-span-4">
82
+ <div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-sm overflow-hidden">
83
+ <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
84
+ <h2 class="font-semibold">Command Board</h2>
85
+ <div class="text-xs text-gray-500">Click to insert</div>
86
+ </div>
87
+ <div class="p-4 space-y-6 overflow-y-auto max-h-[70vh]">
88
+ <!-- Structure -->
89
+ <section>
90
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Structure</h3>
91
+ <div class="grid grid-cols-2 gap-2">
92
+ <button class="cmd btn-outline" data-insert="\frac{■}{□}">Frac ■/□</button>
93
+ <button class="cmd btn-outline" data-insert="■^{2}">x^2</button>
94
+ <button class="cmd btn-outline" data-insert="■_{i}">x_i</button>
95
+ <button class="cmd btn-outline" data-insert="\sqrt{■}">√■</button>
96
+ <button class="cmd btn-outline" data-insert="\sqrt[■]{□}">n√□</button>
97
+ <button class="cmd btn-outline" data-insert="\overline{■}">Overline ■</button>
98
+ <button class="cmd btn-outline" data-insert="\underline{■}">Underline ■</button>
99
+ <button class="cmd btn-outline" data-insert="\vec{■}">Vector ■</button>
100
+ </div>
101
+ </section>
102
+
103
+ <!-- Operators -->
104
+ <section>
105
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Operators</h3>
106
+ <div class="grid grid-cols-2 gap-2">
107
+ <button class="cmd btn-outline" data-insert="■+■">+</button>
108
+ <button class="cmd btn-outline" data-insert="■-■">−</button>
109
+ <button class="cmd btn-outline" data-insert="■ \\times ■">×</button>
110
+ <button class="cmd btn-outline" data-insert="■ \\cdot ■">⋅</button>
111
+ <button class="cmd btn-outline" data-insert="■ \\div ■">÷</button>
112
+ <button class="cmd btn-outline" data-insert="\\pm">±</button>
113
+ <button class="cmd btn-outline" data-insert="\\infty">∞</button>
114
+ <button class="cmd btn-outline" data-insert="■ \\cdot ■">·</button>
115
+ </div>
116
+ </section>
117
+
118
+ <!-- Relations -->
119
+ <section>
120
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Relations</h3>
121
+ <div class="grid grid-cols-2 gap-2">
122
+ <button class="cmd btn-outline" data-insert="■ = ■">=</button>
123
+ <button class="cmd btn-outline" data-insert="■ \\neq ■">≠</button>
124
+ <button class="cmd btn-outline" data-insert="■ < ■">&lt;</button>
125
+ <button class="cmd btn-outline" data-insert="■ > ■">&gt;</button>
126
+ <button class="cmd btn-outline" data-insert="■ \\leq ■">≤</button>
127
+ <button class="cmd btn-outline" data-insert="■ \\geq ■">≥</button>
128
+ <button class="cmd btn-outline" data-insert="■ \\approx ■">≈</button>
129
+ <button class="cmd btn-outline" data-insert="■ \\sim ■">∼</button>
130
+ </div>
131
+ </section>
132
+
133
+ <!-- Parentheses / Brackets -->
134
+ <section>
135
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Grouping</h3>
136
+ <div class="grid grid-cols-2 gap-2">
137
+ <button class="cmd btn-outline" data-insert="\\left( ■ \\right)">(■)</button>
138
+ <button class="cmd btn-outline" data-insert="\\left[ ■ \\right]">[■]</button>
139
+ <button class="cmd btn-outline" data-insert="\\left\\{ ■ \\right\\}">{■}</button>
140
+ <button class="cmd btn-outline" data-insert="\\left\\langle ■ \\right\\rangle">⟨■⟩</button>
141
+ <button class="cmd btn-outline" data-insert="\\left| ■ \\right|">|■|</button>
142
+ <button class="cmd btn-outline" data-insert="\\left\\lvert ■ \\right\\rvert">‖■‖</button>
143
+ <button class="cmd btn-outline" data-insert="\\bigl ■ \\bigr">bigl</button>
144
+ <button class="cmd btn-outline" data-insert="\\Bigl ■ \\Bigr">Bigl</button>
145
+ </div>
146
+ </section>
147
+
148
+ <!-- Calculus -->
149
+ <section>
150
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Calculus</h3>
151
+ <div class="grid grid-cols-2 gap-2">
152
+ <button class="cmd btn-outline" data-insert="\\frac{d}{dx} ■">d/dx</button>
153
+ <button class="cmd btn-outline" data-insert="\\int ■ dx">∫ dx</button>
154
+ <button class="cmd btn-outline" data-insert="\\int_{■}^{□} ■ dx">∫_a^b</button>
155
+ <button class="cmd btn-outline" data-insert="\\sum_{■}^{□} ■">∑</button>
156
+ <button class="cmd btn-outline" data-insert="\\prod_{■}^{□} ■">∏</button>
157
+ <button class="cmd btn-outline" data-insert="\\lim_{x \\to ■} ■">lim</button>
158
+ <button class="cmd btn-outline" data-insert="\\partial">∂</button>
159
+ <button class="cmd btn-outline" data-insert="\\nabla">∇</button>
160
+ </div>
161
+ </section>
162
+
163
+ <!-- Greek -->
164
+ <section>
165
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Greek</h3>
166
+ <div class="grid grid-cols-3 gap-2">
167
+ <button class="cmd btn-outline" data-insert="\\alpha">α</button>
168
+ <button class="cmd btn-outline" data-insert="\\beta">β</button>
169
+ <button class="cmd btn-outline" data-insert="\\gamma">γ</button>
170
+ <button class="cmd btn-outline" data-insert="\\delta">δ</button>
171
+ <button class="cmd btn-outline" data-insert="\\epsilon">ε</button>
172
+ <button class="cmd btn-outline" data-insert="\\theta">θ</button>
173
+ <button class="cmd btn-outline" data-insert="\\lambda">λ</button>
174
+ <button class="cmd btn-outline" data-insert="\\pi">π</button>
175
+ <button class="cmd btn-outline" data-insert="\\mu">μ</button>
176
+ <button class="cmd btn-outline" data-insert="\\rho">ρ</button>
177
+ <button class="cmd btn-outline" data-insert="\\sigma">σ</button>
178
+ <button class="cmd btn-outline" data-insert="\\phi">φ</button>
179
+ <button class="cmd btn-outline" data-insert="\\omega">ω</button>
180
+ <button class="cmd btn-outline" data-insert="\\Gamma">Γ</button>
181
+ <button class="cmd btn-outline" data-insert="\\Delta">Δ</button>
182
+ <button class="cmd btn-outline" data-insert="\\Theta">Θ</button>
183
+ <button class="cmd btn-outline" data-insert="\\Lambda">Λ</button>
184
+ <button class="cmd btn-outline" data-insert="\\Pi">Π</button>
185
+ <button class="cmd btn-outline" data-insert="\\Sigma">Σ</button>
186
+ <button class="cmd btn-outline" data-insert="\\Phi">Φ</button>
187
+ <button class="cmd btn-outline" data-insert="\\Omega">Ω</button>
188
+ </div>
189
+ </section>
190
+
191
+ <!-- Functions -->
192
+ <section>
193
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Functions</h3>
194
+ <div class="grid grid-cols-2 gap-2">
195
+ <button class="cmd btn-outline" data-insert="\\sin ■">sin</button>
196
+ <button class="cmd btn-outline" data-insert="\\cos ■">cos</button>
197
+ <button class="cmd btn-outline" data-insert="\\tan ■">tan</button>
198
+ <button class="cmd btn-outline" data-insert="\\ln ■">ln</button>
199
+ <button class="cmd btn-outline" data-insert="\\log ■">log</button>
200
+ <button class="cmd btn-outline" data-insert="\\exp ■">exp</button>
201
+ <button class="cmd btn-outline" data-insert="\\max ■">max</button>
202
+ <button class="cmd btn-outline" data-insert="\\min ■">min</button>
203
+ <button class="cmd btn-outline" data-insert="\\arg \\max ■">argmax</button>
204
+ <button class="cmd btn-outline" data-insert="\\arg \\min ■">argmin</button>
205
+ <button class="cmd btn-outline" data-insert="\\begin{bmatrix}■ & ■\\\\■ & ■\\end{bmatrix}">2×2 Matrix</button>
206
+ </div>
207
+ </section>
208
+
209
+ <!-- Arrows -->
210
+ <section>
211
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2">Arrows</h3>
212
+ <div class="grid grid-cols-3 gap-2">
213
+ <button class="cmd btn-outline" data-insert="\\to">→</button>
214
+ <button class="cmd btn-outline" data-insert="\\gets">←</button>
215
+ <button class="cmd btn-outline" data-insert="\\leftrightarrow">↔</button>
216
+ <button class="cmd btn-outline" data-insert="\\Rightarrow">⇒</button>
217
+ <button class="cmd btn-outline" data-insert="\\Leftarrow">⇐</button>
218
+ <button class="cmd btn-outline" data-insert="\\iff">⇔</button>
219
+ </div>
220
+ </section>
221
+ </div>
222
+ </div>
223
+ </aside>
224
+
225
+ <!-- Editor + Preview -->
226
+ <section class="lg:col-span-8 space-y-6">
227
+ <div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-sm overflow-hidden">
228
+ <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
229
+ <div class="flex items-center gap-2">
230
+ <h2 class="font-semibold">Editor</h2>
231
+ <span id="autofixStatus" class="text-xs rounded-full bg-undefined-500/10 text-undefined-600 px-2 py-0.5">Auto-fix: On</span>
232
+ </div>
233
+ <div class="flex items-center gap-2">
234
+ <button id="copyTexBtn" class="inline-flex items-center gap-2 rounded-md border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-800">
235
+ Copy LaTeX
236
+ </button>
237
+ <button id="renderBtn" class="inline-flex items-center gap-2 rounded-md bg-undefined-500 text-white px-3 py-1.5 text-sm hover:opacity-90">
238
+ Render
239
+ </button>
240
+ </div>
241
+ </div>
242
+ <div class="p-4">
243
+ <textarea id="editor" rows="10" class="w-full resize-y rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-4 py-3 font-mono text-sm leading-relaxed focus:outline-none focus:ring-2 focus:ring-undefined-500/50" placeholder="Type LaTeX here or use the Command Board..."></textarea>
244
+ <div class="mt-2 text-xs text-gray-500">
245
+ Tips: Use Tab/Shift+Tab to navigate buttons. Place cursor where you want to insert a snippet; place ■ as a placeholder and it will be selected after insertion.
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 shadow-sm overflow-hidden">
251
+ <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
252
+ <h2 class="font-semibold">Rendered Output (KaTeX)</h2>
253
+ <div id="renderError" class="hidden text-sm text-red-600"></div>
254
+ </div>
255
+ <div class="p-4">
256
+ <div id="preview" class="min-h-[64px] p-4 rounded-lg border border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900"></div>
257
+ </div>
258
+ </div>
259
+ </section>
260
+ </main>
261
+
262
+ <footer class="mx-auto max-w-7xl px-4 pb-10 text-center text-xs text-gray-500">
263
+ Built with KaTeX + TailwindCSS. Primary/Secondary/Theme intentionally undefined as requested.
264
+ </footer>
265
+ </div>
266
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
267
+ </body>
268
+ </html>
script.js ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Theme handling
2
+ const themeToggle = document.getElementById('themeToggle');
3
+ const themeIcon = document.getElementById('themeIcon');
4
+
5
+ function applyStoredTheme() {
6
+ const stored = localStorage.getItem('theme');
7
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
8
+ const useDark = stored ? stored === 'dark' : prefersDark;
9
+ document.documentElement.classList.toggle('dark', useDark);
10
+ themeIcon.textContent = useDark ? '☀️' : '🌙';
11
+ }
12
+ applyStoredTheme();
13
+
14
+ themeToggle.addEventListener('click', () => {
15
+ const isDark = document.documentElement.classList.toggle('dark');
16
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
17
+ themeIcon.textContent = isDark ? '☀️' : '🌙';
18
+ });
19
+
20
+ const editor = document.getElementById('editor');
21
+ const preview = document.getElementById('preview');
22
+ const renderBtn = document.getElementById('renderBtn');
23
+ const copyTexBtn = document.getElementById('copyTexBtn');
24
+ const clearBtn = document.getElementById('clearBtn');
25
+ const renderError = document.getElementById('renderError');
26
+ const autofixStatus = document.getElementById('autofixStatus');
27
+
28
+ function renderLatex(value) {
29
+ renderError.classList.add('hidden');
30
+ renderError.textContent = '';
31
+ preview.innerHTML = '';
32
+
33
+ try {
34
+ // Reset content before rendering to ensure size recalculations
35
+ katex.render(value || '', preview, {
36
+ throwOnError: true,
37
+ displayMode: true,
38
+ trust: false,
39
+ strict: 'warn',
40
+ output: 'htmlAndMathml',
41
+ macros: {
42
+ '\\f': '#1f(#2)',
43
+ },
44
+ });
45
+ } catch (err) {
46
+ renderError.classList.remove('hidden');
47
+ renderError.textContent = 'KaTeX error: ' + (err?.message || String(err));
48
+ // Render with throwOnError=false to still show a best-effort preview
49
+ try {
50
+ katex.render(value || '', preview, {
51
+ throwOnError: false,
52
+ displayMode: true,
53
+ trust: false,
54
+ strict: 'warn',
55
+ output: 'htmlAndMathml',
56
+ });
57
+ } catch (e2) {
58
+ // ignore
59
+ }
60
+ }
61
+ }
62
+
63
+ function escapeRegExp(string) {
64
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
65
+ }
66
+
67
+ // Simple tokenizer to avoid replacing inside macro or environment names
68
+ function findLikelyCommandRanges(text) {
69
+ // Return ranges [start,end) of sequences likely to be command names
70
+ // (alphabetic sequences after a backslash)
71
+ const ranges = [];
72
+ const re = /\\([A-Za-z]+)/g;
73
+ let m;
74
+ while ((m = re.exec(text)) !== null) {
75
+ ranges.push([m.index, m.index + m[0].length]);
76
+ }
77
+ return ranges;
78
+ }
79
+
80
+ function isIndexInRanges(index, ranges) {
81
+ for (const [s, e] of ranges) {
82
+ if (index >= s && index < e) return true;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ // Auto syntax fixer
88
+ function autoFixLatex(input) {
89
+ if (!input) return input;
90
+
91
+ let text = input;
92
+
93
+ // Normalize whitespace and dashes
94
+ text = text
95
+ .replace(/\r\n?/g, '\n')
96
+ .replace(/[ \t]+/g, ' ')
97
+ .replace(/–|—/g, '-')
98
+ .replace(/−/g, '-');
99
+
100
+ // Convert common ASCII relations to LaTeX
101
+ const replacements = [
102
+ { from: '>=', to: '\\geq' },
103
+ { from: '<=', to: '\\leq' },
104
+ { from: '!=', to: '\\neq' },
105
+ { from: '~=', to: '\\approx' },
106
+ { from: '+=+', to: '\\pm' }, // handle user typed "+=+"
107
+ { from: '+/-', to: '\\pm' },
108
+ ];
109
+ for (const r of replacements) {
110
+ const re = new RegExp(escapeRegExp(r.from), 'g');
111
+ text = text.replace(re, r.to);
112
+ }
113
+
114
+ // Convert common words to functions
115
+ const funcs = ['sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'log', 'ln', 'exp', 'max', 'min', 'argmax', 'argmin'];
116
+ const ranges = findLikelyCommandRanges(text);
117
+ for (const fn of funcs) {
118
+ const re = new RegExp(`\\b${escapeRegExp(fn)}\\b`, 'gi');
119
+ text = text.replace(re, (m) => {
120
+ const idx = m.index ?? 0;
121
+ // If already preceded by backslash, leave it
122
+ if (idx > 0 && text[idx - 1] === '\\') return m;
123
+ // If inside an already detected command, leave it
124
+ if (isIndexInRanges(idx, ranges)) return m;
125
+ return `\\${m.toLowerCase()}`;
126
+ });
127
+ }
128
+
129
+ // sqrt shortcuts: "sqrt(...)" or user typed "^/" to suggest root
130
+ text = text.replace(/\bsqrt\s*\(\s*([^)]+?)\s*\)/gi, (_, inside) => `\\sqrt{${inside}}`);
131
+ // If user typed "n√" like n√x, convert to \sqrt[n]{x} when braces after caret were intended
132
+ text = text.replace(/(\d+)\s*\^\s*\/\s*([A-Za-z0-9\\]+)/g, (_, n, expr) => `\\sqrt[${n}]{${expr}}`);
133
+
134
+ // Replace slash "a/b" with fraction if likely math context (alphanumeric on both sides)
135
+ text = text.replace(/([A-Za-z0-9}\)])\s*\/\s*([A-Za-z0-9{($]|\^|_|\\\\)/g, (m, a, b) => {
136
+ // Avoid http:// and similar
137
+ if (/https?:\/\//.test(m)) return m;
138
+ return `\\frac{${a}}{${b}}`;
139
+ });
140
+
141
+ // Ensure some common operators are LaTeX
142
+ text = text.replace(/\[/g, '\\left[').replace(/\]/g, '\\right]');
143
+ text = text.replace(/\(/g, '\\left(').replace(/\)/g, '\\right)');
144
+ text = text.replace(/\|/g, '\\left|\\,\\right|'); // moderate auto-grouping
145
+
146
+ // Normalize percentages and degrees
147
+ text = text.replace(/([A-Za-z0-9\)])\s*%\s*/g, '$1\\%');
148
+
149
+ // Fix double backslashes and missing backslash in common commands
150
+ text = text.replace(/\\+/g, (m) => (m.length % 2 === 0 ? '\\' : m)); // keep single backslash
151
+
152
+ // Replace plain asterisks with \cdot in likely math contexts
153
+ text = text.replace(/([A-Za-z0-9])\s*\*\s*([A-Za-z0-9])/g, '$1\\cdot$2');
154
+
155
+ return text;
156
+ }
157
+
158
+ // Insert snippet helpers
159
+ function insertSnippet(value) {
160
+ const start = editor.selectionStart;
161
+ const end = editor.selectionEnd;
162
+ const before = editor.value.slice(0, start);
163
+ const after = editor.value.slice(end);
164
+
165
+ // If snippet contains ■, select the first placeholder region (including braces) so user can type over it
166
+ let newValue = before + value + after;
167
+ let cursorPos = newValue.length;
168
+
169
+ const placeholderIdx = value.indexOf('■');
170
+ if (placeholderIdx !== -1) {
171
+ // Replace only the first ■ with empty string; we'll select a region around it to ease typing
172
+ const cleaned = value.replace('■', '');
173
+ const insertPos = before.length + placeholderIdx;
174
+
175
+ // Try to find the nearest pair of braces after the placeholder to select content inside
176
+ let selectStart = insertPos;
177
+ let selectEnd = insertPos;
178
+
179
+ // Naive: select a short word after placeholder if exists; else just place cursor
180
+ const rest = value.slice(placeholderIdx + 1);
181
+ const matchWord = rest.match(/^[A-Za-z0-9\\^_]+/);
182
+ if (matchWord) {
183
+ selectEnd = insertPos + matchWord[0].length;
184
+ } else {
185
+ selectStart = insertPos;
186
+ selectEnd = insertPos;
187
+ }
188
+
189
+ newValue = before + cleaned + after;
190
+ editor.value = newValue;
191
+
192
+ // Put cursor or select content
193
+ requestAnimationFrame(() => {
194
+ editor.focus();
195
+ if (selectEnd > selectStart) {
196
+ editor.setSelectionRange(selectStart, selectEnd);
197
+ } else {
198
+ editor.setSelectionRange(insertPos, insertPos);
199
+ }
200
+ // Trigger rendering and fixing
201
+ triggerUpdate();
202
+ });
203
+ return;
204
+ }
205
+
206
+ // No placeholder: just insert
207
+ editor.value = newValue;
208
+ requestAnimationFrame(() => {
209
+ const caret = before.length + value.length;
210
+ editor.setSelectionRange(caret, caret);
211
+ editor.focus();
212
+ triggerUpdate();
213
+ });
214
+ }
215
+
216
+ // Command board click handling
217
+ document.querySelectorAll('.cmd').forEach(btn => {
218
+ btn.addEventListener('click', () => {
219
+ const snippet = btn.getAttribute('data-insert') || '';
220
+ insertSnippet(snippet);
221
+ });
222
+ });
223
+
224
+ // Keyboard navigation within command board (Tab/Shift+Tab)
225
+ document.addEventListener('keydown', (e) => {
226
+ const isCommandRegion = e.target.closest('.cmd') !== null;
227
+ if (!isCommandRegion) return;
228
+ const cmds = Array.from(document.querySelectorAll('.cmd'));
229
+ const idx = cmds.indexOf(e.target);
230
+ if (e.key === 'Tab') {
231
+ e.preventDefault();
232
+ const dir = e.shiftKey ? -1 : 1;
233
+ const next = (idx + dir + cmds.length) % cmds.length;
234
+ cmds[next].focus();
235
+ }
236
+ });
237
+
238
+ // Update pipeline
239
+ let lastRendered = '';
240
+ let lastFixed = '';
241
+ let lastUserInput = '';
242
+
243
+ function triggerUpdate() {
244
+ const current = editor.value;
245
+ if (current === lastUserInput) {
246
+ // If nothing changed (e.g., selection change only), still try to render
247
+ renderLatex(lastFixed || current);
248
+ return;
249
+ }
250
+
251
+ const fixed = autoFixLatex(current);
252
+ lastUserInput = current;
253
+ lastFixed = fixed;
254
+
255
+ // Only render if changed to reduce churn
256
+ if (fixed !== lastRendered) {
257
+ lastRendered = fixed;
258
+ renderLatex(fixed);
259
+ } else {
260
+ renderLatex(fixed);
261
+ }
262
+ }
263
+
264
+ editor.addEventListener('input', triggerUpdate);
265
+ renderBtn.addEventListener('click', triggerUpdate);
266
+
267
+ copyTexBtn.addEventListener('click', async () => {
268
+ try {
269
+ const text = lastFixed || editor.value || '';
270
+ await navigator.clipboard.writeText(text);
271
+ copyTexBtn.textContent = 'Copied!';
272
+ setTimeout(() => (copyTexBtn.textContent = 'Copy LaTeX'), 1200);
273
+ } catch {
274
+ alert('Copy failed. Please select and copy manually.');
275
+ }
276
+ });
277
+
278
+ clearBtn.addEventListener('click', () => {
279
+ editor.value = '';
280
+ lastUserInput = '';
281
+ lastFixed = '';
282
+ lastRendered = '';
283
+ renderLatex('');
284
+ editor.focus();
285
+ });
286
+
287
+ // Initial render with sample
288
+ editor.value = String.raw`\int_{0}^{\infty} e^{-x^2} \, dx = \frac{\sqrt{\pi}}{2}`;
289
+ triggerUpdate();
style.css CHANGED
@@ -1,28 +1,28 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }
 
 
1
+ /* Additional small tweaks beyond Tailwind */
2
+ :root {
3
+ /* These intentionally remain undefined to satisfy requirements */
4
+ /* --color-primary: #3b82f6; */
5
+ /* --color-secondary: #f59e0b; */
6
  }
7
 
8
+ html, body {
9
+ scroll-behavior: smooth;
 
10
  }
11
 
12
+ #preview .katex-display {
13
+ overflow-x: auto;
14
+ overflow-y: hidden;
 
 
15
  }
16
 
17
+ /* Improve tap targets on mobile for command board */
18
+ @media (max-width: 640px) {
19
+ .cmd {
20
+ padding-top: 0.7rem;
21
+ padding-bottom: 0.7rem;
22
+ }
23
  }
24
 
25
+ /* Selection color to match undefined theme if desired */
26
+ ::selection {
27
+ background-color: rgba(59, 130, 246, 0.25); /* change if you set --color-primary */
28
+ }