char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Linting & Formatting scripts (#26819)92d1bb0c9

char/aliceai-t5-moe
16.0 KiB418 linesraw
1/* eslint-disable no-irregular-whitespace */
2import { maskInlineLaTeX, preprocessLaTeX } from '$lib/utils/latex-protection';
3import { describe, expect, it, test } from 'vitest';
4
5describe('maskInlineLaTeX', () => {
6	it('should protect LaTeX $x + y$ but not money $3.99', () => {
7		const latexExpressions: string[] = [];
8		const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
9		const output = maskInlineLaTeX(input, latexExpressions);
10
11		expect(output).toBe('I have $10, $3.99 and <<LATEX_0>> and <<LATEX_1>>. The amount is $2,000.');
12		expect(latexExpressions).toEqual(['$x + y$', '$100x$']);
13	});
14
15	it('should ignore money like $5 and $12.99', () => {
16		const latexExpressions: string[] = [];
17		const input = 'Prices are $12.99 and $5. Tax?';
18		const output = maskInlineLaTeX(input, latexExpressions);
19
20		expect(output).toBe('Prices are $12.99 and $5. Tax?');
21		expect(latexExpressions).toEqual([]);
22	});
23
24	it('should protect inline math $a^2 + b^2$ even after text', () => {
25		const latexExpressions: string[] = [];
26		const input = 'Pythagorean: $a^2 + b^2 = c^2$.';
27		const output = maskInlineLaTeX(input, latexExpressions);
28
29		expect(output).toBe('Pythagorean: <<LATEX_0>>.');
30		expect(latexExpressions).toEqual(['$a^2 + b^2 = c^2$']);
31	});
32
33	it('should not protect math that has letter after closing $ (e.g. units)', () => {
34		const latexExpressions: string[] = [];
35		const input = 'The cost is $99 and change.';
36		const output = maskInlineLaTeX(input, latexExpressions);
37
38		expect(output).toBe('The cost is $99 and change.');
39		expect(latexExpressions).toEqual([]);
40	});
41
42	it('should allow $x$ followed by punctuation', () => {
43		const latexExpressions: string[] = [];
44		const input = 'We know $x$, right?';
45		const output = maskInlineLaTeX(input, latexExpressions);
46
47		expect(output).toBe('We know <<LATEX_0>>, right?');
48		expect(latexExpressions).toEqual(['$x$']);
49	});
50
51	it('should work across multiple lines', () => {
52		const latexExpressions: string[] = [];
53		const input = `Emma buys cupcakes for $3 each.\nHow much is $x + y$?`;
54		const output = maskInlineLaTeX(input, latexExpressions);
55
56		expect(output).toBe(`Emma buys cupcakes for $3 each.\nHow much is <<LATEX_0>>?`);
57		expect(latexExpressions).toEqual(['$x + y$']);
58	});
59
60	it('should not protect $100 but protect $matrix$', () => {
61		const latexExpressions: string[] = [];
62		const input = '$100 and $\\mathrm{GL}_2(\\mathbb{F}_7)$ are different.';
63		const output = maskInlineLaTeX(input, latexExpressions);
64
65		expect(output).toBe('$100 and <<LATEX_0>> are different.');
66		expect(latexExpressions).toEqual(['$\\mathrm{GL}_2(\\mathbb{F}_7)$']);
67	});
68
69	it('should skip if $ is followed by digit and alphanumeric after close (money)', () => {
70		const latexExpressions: string[] = [];
71		const input = 'I paid $5 quickly.';
72		const output = maskInlineLaTeX(input, latexExpressions);
73
74		expect(output).toBe('I paid $5 quickly.');
75		expect(latexExpressions).toEqual([]);
76	});
77
78	it('should protect LaTeX even with special chars inside', () => {
79		const latexExpressions: string[] = [];
80		const input = 'Consider $\\alpha_1 + \\beta_2$ now.';
81		const output = maskInlineLaTeX(input, latexExpressions);
82
83		expect(output).toBe('Consider <<LATEX_0>> now.');
84		expect(latexExpressions).toEqual(['$\\alpha_1 + \\beta_2$']);
85	});
86
87	it('short text', () => {
88		const latexExpressions: string[] = ['$0$'];
89		const input = '$a$\n$a$ and $b$';
90		const output = maskInlineLaTeX(input, latexExpressions);
91
92		expect(output).toBe('<<LATEX_1>>\n<<LATEX_2>> and <<LATEX_3>>');
93		expect(latexExpressions).toEqual(['$0$', '$a$', '$a$', '$b$']);
94	});
95
96	it('empty text', () => {
97		const latexExpressions: string[] = [];
98		const input = '$\n$$\n';
99		const output = maskInlineLaTeX(input, latexExpressions);
100
101		expect(output).toBe('$\n$$\n');
102		expect(latexExpressions).toEqual([]);
103	});
104
105	it('LaTeX-spacer preceded by backslash', () => {
106		const latexExpressions: string[] = [];
107		const input = `\\[
108\\boxed{
109\\begin{aligned}
110N_{\\text{att}}^{\\text{(MHA)}} &=
111h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr]   && (\\text{Q,K,V の重み})\\\\
112&\\quad+ h(d_{k}+d_{k}+d_{v})                                          && (\\text{バイアス Q,K,V)}\\\\[4pt]
113&\\quad+ (h d_{v})\\, d_{\\text{model}}                                 && (\\text{出力射影 }W^{O})\\\\
114&\\quad+ d_{\\text{model}}                                            && (\\text{バイアス }b^{O})
115\\end{aligned}}
116\\]`;
117		const output = maskInlineLaTeX(input, latexExpressions);
118
119		expect(output).toBe(input);
120		expect(latexExpressions).toEqual([]);
121	});
122});
123
124describe('preprocessLaTeX', () => {
125	test('converts inline \\( ... \\) to $...$', () => {
126		const input =
127			'\\( \\mathrm{GL}_2(\\mathbb{F}_7) \\): Group of invertible matrices with entries in \\(\\mathbb{F}_7\\).';
128		const output = preprocessLaTeX(input);
129
130		expect(output).toBe(
131			'$ \\mathrm{GL}_2(\\mathbb{F}_7) $: Group of invertible matrices with entries in $\\mathbb{F}_7$.'
132		);
133	});
134
135	test("don't inline \\\\( ... \\) to $...$", () => {
136		const input =
137			'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula \\((x_1,\\ldots,x_n)\\).';
138		const output = preprocessLaTeX(input);
139
140		expect(output).toBe(
141			'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula $(x_1,\\ldots,x_n)$.'
142		);
143	});
144
145	test('preserves display math \\[ ... \\] and protects adjacent text', () => {
146		const input = `Some kernel of \\(\\mathrm{SL}_2(\\mathbb{F}_7)\\):
147  \\[
148  \\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
149  \\]`;
150		const output = preprocessLaTeX(input);
151
152		expect(output).toBe(`Some kernel of $\\mathrm{SL}_2(\\mathbb{F}_7)$:
153  $$
154  \\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
155  $$`);
156	});
157
158	test('handles standalone display math equation', () => {
159		const input = `Algebra:
160\\[
161x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
162\\]`;
163		const output = preprocessLaTeX(input);
164
165		expect(output).toBe(`Algebra:
166$$
167x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
168$$`);
169	});
170
171	test('does not interpret currency values as LaTeX', () => {
172		const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
173		const output = preprocessLaTeX(input);
174
175		expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
176	});
177
178	test('ignores dollar signs followed by digits (money), but keeps valid math $x + y$', () => {
179		const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
180		const output = preprocessLaTeX(input);
181
182		expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
183	});
184
185	test('handles real-world word problems with amounts and no math delimiters', () => {
186		const input =
187			'Emma buys 2 cupcakes for $3 each and 1 cookie for $1.50. How much money does she spend in total?';
188		const output = preprocessLaTeX(input);
189
190		expect(output).toBe(
191			'Emma buys 2 cupcakes for \\$3 each and 1 cookie for \\$1.50. How much money does she spend in total?'
192		);
193	});
194
195	test('handles decimal amounts in word problem correctly', () => {
196		const input =
197			'Maria has $20. She buys a notebook for $4.75 and a pack of pencils for $3.25. How much change does she receive?';
198		const output = preprocessLaTeX(input);
199
200		expect(output).toBe(
201			'Maria has \\$20. She buys a notebook for \\$4.75 and a pack of pencils for \\$3.25. How much change does she receive?'
202		);
203	});
204
205	test('preserves display math with surrounding non-ASCII text', () => {
206		const input = `1 kg の質量は
207  \\[
208  E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
209  \\]
210  というエネルギーに相当します。これは約 21 百万トンの TNT が爆発したときのエネルギーに匹敵します。`;
211		const output = preprocessLaTeX(input);
212
213		expect(output).toBe(
214			`1 kg の質量は
215  $$
216  E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
217  $$
218  というエネルギーに相当します。これは約 21 百万トンの TNT が爆発したときのエネルギーに匹敵します。`
219		);
220	});
221
222	test('LaTeX-spacer preceded by backslash', () => {
223		const input = `\\[
224\\boxed{
225\\begin{aligned}
226N_{\\text{att}}^{\\text{(MHA)}} &=
227h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr]   && (\\text{Q,K,V の重み})\\\\
228&\\quad+ h(d_{k}+d_{k}+d_{v})                                          && (\\text{バイアス Q,K,V)}\\\\[4pt]
229&\\quad+ (h d_{v})\\, d_{\\text{model}}                                 && (\\text{出力射影 }W^{O})\\\\
230&\\quad+ d_{\\text{model}}                                            && (\\text{バイアス }b^{O})
231\\end{aligned}}
232\\]`;
233		const output = preprocessLaTeX(input);
234
235		expect(output).toBe(
236			`$$
237\\boxed{
238\\begin{aligned}
239N_{\\text{att}}^{\\text{(MHA)}} &=
240h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr]   && (\\text{Q,K,V の重み})\\\\
241&\\quad+ h(d_{k}+d_{k}+d_{v})                                          && (\\text{バイアス Q,K,V)}\\\\[4pt]
242&\\quad+ (h d_{v})\\, d_{\\text{model}}                                 && (\\text{出力射影 }W^{O})\\\\
243&\\quad+ d_{\\text{model}}                                            && (\\text{バイアス }b^{O})
244\\end{aligned}}
245$$`
246		);
247	});
248
249	test('converts \\[ ... \\] even when preceded by text without space', () => {
250		const input = 'Some line ...\nAlgebra: \\[x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}\\]';
251		const output = preprocessLaTeX(input);
252
253		expect(output).toBe(
254			'Some line ...\nAlgebra: \n$$x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$$\n'
255		);
256	});
257
258	test('converts \\[ ... \\] in table-cells', () => {
259		const input = `| ID | Expression |\n| #1 | \\[
260			x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
261\\] |`;
262		const output = preprocessLaTeX(input);
263
264		expect(output).toBe(
265			'| ID | Expression |\n| #1 | $x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$ |'
266		);
267	});
268
269	test('escapes isolated $ before digits ($5 → \\$5), but not valid math', () => {
270		const input = 'This costs $5 and this is math $x^2$. $100 is money.';
271		const output = preprocessLaTeX(input);
272
273		expect(output).toBe('This costs \\$5 and this is math $x^2$. \\$100 is money.');
274		// Note: Since $x^2$ is detected as valid LaTeX, it's preserved.
275		// $5 becomes \$5 only *after* real math is masked — but here it's correct because the masking logic avoids treating $5 as math.
276	});
277
278	test('display with LaTeX-line-breaks', () => {
279		const input = String.raw`- Algebraic topology, Homotopy Groups of $\mathbb{S}^3$:
280$$\pi_n(\mathbb{S}^3) = \begin{cases}
281\mathbb{Z} & n = 3 \\
2820 & n > 3, n \neq 4 \\
283\mathbb{Z}_2 & n = 4 \\
284\end{cases}$$`;
285		const output = preprocessLaTeX(input);
286
287		// If the formula contains '\\' the $$-delimiters should be in their own line.
288		expect(output).toBe(`- Algebraic topology, Homotopy Groups of $\\mathbb{S}^3$:
289$$\n\\pi_n(\\mathbb{S}^3) = \\begin{cases}
290\\mathbb{Z} & n = 3 \\\\
2910 & n > 3, n \\neq 4 \\\\
292\\mathbb{Z}_2 & n = 4 \\\\
293\\end{cases}\n$$`);
294	});
295
296	test('handles mhchem notation safely if present', () => {
297		const input = 'Chemical reaction: \\( \\ce{H2O} \\) and $\\ce{CO2}$';
298		const output = preprocessLaTeX(input);
299
300		expect(output).toBe('Chemical reaction: $ \\ce{H2O} $ and $\\ce{CO2}$');
301	});
302
303	test('preserves code blocks', () => {
304		const input = 'Inline code: `sum $total` and block:\n```\ndollar $amount\n```\nEnd.';
305		const output = preprocessLaTeX(input);
306
307		expect(output).toBe(input); // Code blocks prevent misinterpretation
308	});
309
310	test('preserves backslash parentheses in code blocks (GitHub issue)', () => {
311		const input = '```python\nfoo = "\\(bar\\)"\n```';
312		const output = preprocessLaTeX(input);
313
314		expect(output).toBe(input); // Code blocks should not have LaTeX conversion applied
315	});
316
317	test('preserves backslash brackets in code blocks', () => {
318		const input = '```python\nfoo = "\\[bar\\]"\n```';
319		const output = preprocessLaTeX(input);
320
321		expect(output).toBe(input); // Code blocks should not have LaTeX conversion applied
322	});
323
324	test('preserves backslash parentheses in inline code', () => {
325		const input = 'Use `foo = "\\(bar\\)"` in your code.';
326		const output = preprocessLaTeX(input);
327
328		expect(output).toBe(input);
329	});
330
331	test('escape backslash in mchem ce', () => {
332		const input = 'mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$';
333		const output = preprocessLaTeX(input);
334
335		// mhchem-escape would insert a backslash here.
336		expect(output).toBe('mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$');
337	});
338
339	test('escape backslash in mchem pu', () => {
340		const input = 'mchem pu:\n$\\pu{-572 kJ mol^{-1}}$';
341		const output = preprocessLaTeX(input);
342
343		// mhchem-escape would insert a backslash here.
344		expect(output).toBe('mchem pu:\n$\\pu{-572 kJ mol^{-1}}$');
345	});
346
347	test('LaTeX in blockquotes with display math', () => {
348		const input =
349			'> **Definition (limit):**  \n>  \\[\n>  \\lim_{x\\to a} f(x) = L\n>  \\]\n>  means that as \\(x\\) gets close to \\(a\\).';
350		const output = preprocessLaTeX(input);
351
352		// Blockquote markers should be preserved, LaTeX should be converted
353		expect(output).toContain('> **Definition (limit):**');
354		expect(output).toContain('$$');
355		expect(output).toContain('$x$');
356		expect(output).not.toContain('\\[');
357		expect(output).not.toContain('\\]');
358		expect(output).not.toContain('\\(');
359		expect(output).not.toContain('\\)');
360	});
361
362	test('LaTeX in blockquotes with inline math', () => {
363		const input =
364			"> The derivative \\(f'(x)\\) at point \\(x=a\\) measures slope.\n> Formula: \\(f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}\\)";
365		const output = preprocessLaTeX(input);
366
367		// Blockquote markers should be preserved, inline LaTeX converted to $...$
368		expect(output).toContain("> The derivative $f'(x)$ at point $x=a$ measures slope.");
369		expect(output).toContain("> Formula: $f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}$");
370	});
371
372	test('Mixed content with blockquotes and regular text', () => {
373		const input =
374			'Regular text with \\(x^2\\).\n\n> Quote with \\(y^2\\).\n\nMore text with \\(z^2\\).';
375		const output = preprocessLaTeX(input);
376
377		// All LaTeX should be converted, blockquote markers preserved
378		expect(output).toBe('Regular text with $x^2$.\n\n> Quote with $y^2$.\n\nMore text with $z^2$.');
379	});
380});
381
382// The fast path skips the whole protect/restore pipeline when the content has
383// neither `$` nor a backslash. These pin the assumption it relies on: such
384// content is returned byte-for-byte unchanged, and anything carrying a trigger
385// character still goes through the full pipeline.
386describe('preprocessLaTeX fast path', () => {
387	const untouched = [
388		['plain prose', 'The quick brown fox jumps over the lazy dog.'],
389		['headings and lists', '# Title\n\n- one\n- two\n\n## Sub\n\n1. first\n2. second'],
390		['fenced code without escapes', '```ts\nconst x = 1;\n```'],
391		['inline code and emphasis', 'Use `npm run build` with **bold** and _italic_.'],
392		['blockquotes', '> quoted line\n> another line'],
393		['tables', '| a | b |\n| --- | --- |\n| 1 | 2 |'],
394		['brackets and parens without escapes', 'array[0] and call(arg) and [link](/url)'],
395		['multi-paragraph prose', 'First para.\n\nSecond para.\n\nThird para.']
396	];
397
398	for (const [label, input] of untouched) {
399		test(`returns ${label} unchanged`, () => {
400			expect(preprocessLaTeX(input)).toBe(input);
401		});
402	}
403
404	test('still processes content once a trigger character is present', () => {
405		expect(preprocessLaTeX('inline \\(x^2\\) here')).toBe('inline $x^2$ here');
406		expect(preprocessLaTeX('Price: $5')).toBe('Price: \\$5');
407	});
408
409	test('fast path result matches the full pipeline for trigger-free content', () => {
410		// Appending a trigger char forces the slow path; stripping it back off
411		// must agree with what the fast path returned for the same prefix.
412		const body = 'Plain paragraph with no math.\n\nAnother paragraph here.';
413		const viaFastPath = preprocessLaTeX(body);
414		const viaFullPipeline = preprocessLaTeX(`${body}\n\n\\(z\\)`);
415
416		expect(viaFullPipeline.startsWith(viaFastPath)).toBe(true);
417	});
418});