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
11.4 KiB346 linesraw
1import {
2	CODE_BLOCK_PLACEHOLDER_REGEXP,
3	CODE_BLOCK_REGEXP,
4	LATEX_BACKSLASH,
5	LATEX_BLOCKQUOTE_PREFIX_REGEXP,
6	LATEX_CURRENCY_DOLLAR_REGEXP,
7	LATEX_CURRENCY_ESCAPE,
8	LATEX_DIGIT_REGEXP,
9	LATEX_DISPLAY_BLOCK_REGEXP,
10	LATEX_DISPLAY_CLOSE,
11	LATEX_DISPLAY_CONVERT_REGEXP,
12	LATEX_DISPLAY_DELIMITER,
13	LATEX_DISPLAY_OPEN,
14	LATEX_INLINE_CLOSE,
15	LATEX_INLINE_CONVERT_REGEXP,
16	LATEX_INLINE_DELIMITER,
17	LATEX_INLINE_OPEN,
18	LATEX_LINEBREAK_REGEXP,
19	LATEX_MATH_AND_CODE_PATTERN,
20	LATEX_MHCHEM_CE,
21	LATEX_MHCHEM_PU,
22	LATEX_NEIGHBOR_CHAR_REGEXP,
23	LATEX_NON_WHITESPACE_REGEXP,
24	LATEX_PLACEHOLDER_REGEXP,
25	LATEX_PROTECT_REGEXP,
26	LATEX_TRIGGER_REGEXP,
27	MHCHEM_PATTERN_MAP,
28	NEWLINE
29} from '$lib/constants';
30
31/**
32 * Replaces inline LaTeX expressions enclosed in `$...$` with placeholders, avoiding dollar signs
33 * that appear to be part of monetary values or identifiers.
34 *
35 * This function processes the input line by line and skips `$` sequences that are likely
36 * part of money amounts (e.g., `$5`, `$100.99`) or code-like tokens (e.g., `var$`, `$var`).
37 * Valid LaTeX inline math is replaced with a placeholder like `<<LATEX_0>>`, and the
38 * actual LaTeX content is stored in the provided `latexExpressions` array.
39 *
40 * @param content - The input text potentially containing LaTeX expressions.
41 * @param latexExpressions - An array used to collect extracted LaTeX expressions.
42 * @returns The processed string with LaTeX replaced by placeholders.
43 */
44export function maskInlineLaTeX(content: string, latexExpressions: string[]): string {
45	if (!content.includes(LATEX_INLINE_DELIMITER)) {
46		return content;
47	}
48
49	return content
50		.split(NEWLINE)
51		.map((line) => {
52			if (line.indexOf(LATEX_INLINE_DELIMITER) == -1) {
53				return line;
54			}
55
56			let processedLine = '';
57			let currentPosition = 0;
58
59			while (currentPosition < line.length) {
60				const openDollarIndex = line.indexOf(LATEX_INLINE_DELIMITER, currentPosition);
61
62				if (openDollarIndex == -1) {
63					processedLine += line.slice(currentPosition);
64
65					break;
66				}
67
68				// Is there a next $-sign?
69				const closeDollarIndex = line.indexOf(LATEX_INLINE_DELIMITER, openDollarIndex + 1);
70
71				if (closeDollarIndex == -1) {
72					processedLine += line.slice(currentPosition);
73
74					break;
75				}
76
77				const charBeforeOpen = openDollarIndex > 0 ? line[openDollarIndex - 1] : '';
78				const charAfterOpen = line[openDollarIndex + 1];
79				const charBeforeClose =
80					openDollarIndex + 1 < closeDollarIndex ? line[closeDollarIndex - 1] : '';
81				const charAfterClose = closeDollarIndex + 1 < line.length ? line[closeDollarIndex + 1] : '';
82
83				let shouldSkipAsNonLatex = false;
84
85				if (closeDollarIndex == currentPosition + 1) {
86					// No content
87					shouldSkipAsNonLatex = true;
88				}
89
90				if (LATEX_NEIGHBOR_CHAR_REGEXP.test(charBeforeOpen)) {
91					// Character, digit, $, _ or - before first '$', no TeX.
92					shouldSkipAsNonLatex = true;
93				}
94
95				if (
96					LATEX_DIGIT_REGEXP.test(charAfterOpen) &&
97					(LATEX_NEIGHBOR_CHAR_REGEXP.test(charAfterClose) || ' ' == charBeforeClose)
98				) {
99					// First $ seems to belong to an amount.
100					shouldSkipAsNonLatex = true;
101				}
102
103				if (shouldSkipAsNonLatex) {
104					processedLine += line.slice(currentPosition, openDollarIndex + 1);
105					currentPosition = openDollarIndex + 1;
106
107					continue;
108				}
109
110				// Treat as LaTeX
111				processedLine += line.slice(currentPosition, openDollarIndex);
112				const latexContent = line.slice(openDollarIndex, closeDollarIndex + 1);
113
114				latexExpressions.push(latexContent);
115				processedLine += `<<LATEX_${latexExpressions.length - 1}>>`;
116				currentPosition = closeDollarIndex + 1;
117			}
118
119			return processedLine;
120		})
121		.join(NEWLINE);
122}
123
124function escapeBrackets(text: string): string {
125	return text.replace(
126		LATEX_MATH_AND_CODE_PATTERN,
127		(
128			match: string,
129			codeBlock: string | undefined,
130			squareBracket: string | undefined,
131			roundBracket: string | undefined
132		): string => {
133			if (codeBlock != null) {
134				return codeBlock;
135			} else if (squareBracket != null) {
136				return `${LATEX_DISPLAY_DELIMITER}${squareBracket}${LATEX_DISPLAY_DELIMITER}`;
137			} else if (roundBracket != null) {
138				return `${LATEX_INLINE_DELIMITER}${roundBracket}${LATEX_INLINE_DELIMITER}`;
139			}
140
141			return match;
142		}
143	);
144}
145
146// Escape $\\ce{...} → $\\ce{...} but with proper handling
147function escapeMhchem(text: string): string {
148	return MHCHEM_PATTERN_MAP.reduce((result, [pattern, replacement]) => {
149		return result.replace(pattern, replacement);
150	}, text);
151}
152
153const doEscapeMhchem = false;
154/**
155 * Preprocesses markdown content to safely handle LaTeX math expressions while protecting
156 * against false positives (e.g., dollar amounts like $5.99) and ensuring proper rendering.
157 *
158 * This function:
159 * - Protects code blocks (```) and inline code (`...`)
160 * - Safeguards block and inline LaTeX: \(...\), \[...\], $$...$$, and selective $...$
161 * - Escapes standalone dollar signs before numbers (e.g., $5 → \$5) to prevent misinterpretation
162 * - Restores protected LaTeX and code blocks after processing
163 * - Converts \(...\) → $...$ and \[...\] → $$...$$ for compatibility with math renderers
164 * - Applies additional escaping for brackets and mhchem syntax if needed
165 *
166 * @param content - The raw text (e.g., markdown) that may contain LaTeX or code blocks.
167 * @returns The preprocessed string with properly escaped and normalized LaTeX.
168 *
169 * @example
170 * preprocessLaTeX("Price: $10. The equation is \\(x^2\\).")
171 * // → "Price: $10. The equation is $x^2$."
172 */
173/** Bounded cache for preprocessLaTeX results. */
174const LATEX_CACHE_MAX_SIZE = 64;
175const latexCache = new Map<string, string>();
176
177export function preprocessLaTeX(content: string): string {
178	// See also:
179	// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
180
181	// Memoize on the input string. During streaming the prefix before an
182	// incomplete code block stays the same across multiple tokens, so the
183	// full protect/restore pipeline would re-run unnecessarily.
184	const cached = latexCache.get(content);
185
186	if (cached !== undefined) return cached;
187
188	// Save original before the function mutates `content` through steps 0-8
189	const originalContent = content;
190
191	// Every step below keys off a `$` or a backslash escape (\[ \] \( \) \ce{ \pu{).
192	// With neither present the protect/restore passes round-trip the input
193	// unchanged, so skip them: the step 2 scan is O(n^2) in line length and costs
194	// ~90ms on a 26KB single-line message that contains no math at all. This
195	// matters during streaming, where the whole message is reprocessed per frame.
196	if (!LATEX_TRIGGER_REGEXP.test(content)) {
197		if (latexCache.size >= LATEX_CACHE_MAX_SIZE) {
198			latexCache.delete(latexCache.keys().next().value!);
199		}
200
201		latexCache.set(originalContent, content);
202
203		return content;
204	}
205
206	// Step 0: Temporarily remove blockquote markers (>) to process LaTeX correctly
207	// Store the structure so we can restore it later
208	const blockquoteMarkers: Map<number, string> = new Map();
209	const lines = content.split(NEWLINE);
210	const processedLines = lines.map((line, index) => {
211		const match = line.match(LATEX_BLOCKQUOTE_PREFIX_REGEXP);
212
213		if (match) {
214			blockquoteMarkers.set(index, match[1]);
215
216			return line.slice(match[1].length);
217		}
218
219		return line;
220	});
221
222	content = processedLines.join(NEWLINE);
223
224	// Step 1: Protect code blocks
225	const codeBlocks: string[] = [];
226
227	content = content.replace(CODE_BLOCK_REGEXP, (match) => {
228		codeBlocks.push(match);
229
230		return `<<CODE_BLOCK_${codeBlocks.length - 1}>>`;
231	});
232
233	// Step 2: Protect existing LaTeX expressions
234	const latexExpressions: string[] = [];
235
236	// Match \S...\[...\] and protect them and insert a line-break.
237	// Guarded: with no `\[` present this pattern still probes every start offset,
238	// expanding `.*?` to the end of each line before failing - O(n^2) for nothing.
239	if (content.includes(LATEX_DISPLAY_OPEN)) {
240		content = content.replace(LATEX_DISPLAY_BLOCK_REGEXP, (match, group1, group2, group3) => {
241			// Check if there are characters following the formula (display-formula in a table-cell?)
242			if (group1.endsWith(LATEX_BACKSLASH)) {
243				return match; // Backslash before \[, do nothing.
244			}
245
246			const hasSuffix = LATEX_NON_WHITESPACE_REGEXP.test(group3);
247
248			let optBreak;
249
250			if (hasSuffix) {
251				latexExpressions.push(`${LATEX_INLINE_OPEN}${group2.trim()}${LATEX_INLINE_CLOSE}`); // Convert into inline.
252				optBreak = '';
253			} else {
254				latexExpressions.push(`${LATEX_DISPLAY_OPEN}${group2}${LATEX_DISPLAY_CLOSE}`);
255				optBreak = NEWLINE;
256			}
257
258			return `${group1}${optBreak}<<LATEX_${latexExpressions.length - 1}>>${optBreak}${group3}`;
259		});
260	}
261
262	// Match \(...\), \[...\], $$...$$ and protect them
263	content = content.replace(LATEX_PROTECT_REGEXP, (match) => {
264		latexExpressions.push(match);
265
266		return `<<LATEX_${latexExpressions.length - 1}>>`;
267	});
268
269	// Protect inline $...$ but NOT if it looks like money (e.g., $10, $3.99)
270	content = maskInlineLaTeX(content, latexExpressions);
271
272	// Step 3: Escape standalone $ before digits (currency like $5 → \$5)
273	// (Now that inline math is protected, this will only escape dollars not already protected)
274	content = content.replace(LATEX_CURRENCY_DOLLAR_REGEXP, LATEX_CURRENCY_ESCAPE);
275
276	// Step 4: Restore protected LaTeX expressions (they are valid)
277	content = content.replace(LATEX_PLACEHOLDER_REGEXP, (_, index) => {
278		let expr = latexExpressions[parseInt(index)];
279
280		const match = expr.match(LATEX_LINEBREAK_REGEXP);
281
282		if (match) {
283			// Katex: The $$-delimiters should be in their own line
284			// if there are \\-line-breaks.
285			const formula = match[1];
286			const prefix = formula.startsWith(NEWLINE) ? '' : NEWLINE;
287			const suffix = formula.endsWith(NEWLINE) ? '' : NEWLINE;
288
289			expr = LATEX_DISPLAY_DELIMITER + prefix + formula + suffix + LATEX_DISPLAY_DELIMITER;
290		}
291
292		return expr;
293	});
294
295	// Step 5: Apply additional escaping functions (brackets and mhchem)
296	// This must happen BEFORE restoring code blocks to avoid affecting code content
297	content = escapeBrackets(content);
298
299	if (doEscapeMhchem && (content.includes(LATEX_MHCHEM_CE) || content.includes(LATEX_MHCHEM_PU))) {
300		content = escapeMhchem(content);
301	}
302
303	// Step 6: Convert remaining \(...\) → $...$, \[...\] → $$...$$
304	// This must happen BEFORE restoring code blocks to avoid affecting code content
305	content = content
306		// Using the look‑behind pattern `(?<!\\)` we skip matches
307		// that are preceded by a backslash, e.g.
308		// `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook).
309		.replace(LATEX_INLINE_CONVERT_REGEXP, (_, formula: string) => {
310			return `${LATEX_INLINE_DELIMITER}${formula}${LATEX_INLINE_DELIMITER}`;
311		}) // inline
312		.replace(
313			// Using the look‑behind pattern `(?<!\\)` we skip matches
314			// that are preceded by a backslash, e.g. `\\[4pt]`.
315			LATEX_DISPLAY_CONVERT_REGEXP, // display, see also PR #16599
316			(_, formula: string) => {
317				return `${LATEX_DISPLAY_DELIMITER}${formula}${LATEX_DISPLAY_DELIMITER}`;
318			}
319		);
320
321	// Step 7: Restore code blocks
322	// This happens AFTER all LaTeX conversions to preserve code content
323	content = content.replace(CODE_BLOCK_PLACEHOLDER_REGEXP, (_, index) => {
324		return codeBlocks[parseInt(index)];
325	});
326
327	// Step 8: Restore blockquote markers
328	if (blockquoteMarkers.size > 0) {
329		const finalLines = content.split(NEWLINE);
330		const restoredLines = finalLines.map((line, index) => {
331			const marker = blockquoteMarkers.get(index);
332
333			return marker ? marker + line : line;
334		});
335
336		content = restoredLines.join(NEWLINE);
337	}
338
339	if (latexCache.size >= LATEX_CACHE_MAX_SIZE) {
340		latexCache.delete(latexCache.keys().next().value!);
341	}
342
343	latexCache.set(originalContent, content);
344
345	return content;
346}