char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
92d1bb0c9
char/aliceai-t5-moe
1import { 2CODE_BLOCK_PLACEHOLDER_REGEXP , 3CODE_BLOCK_REGEXP , 4LATEX_BACKSLASH , 5LATEX_BLOCKQUOTE_PREFIX_REGEXP , 6LATEX_CURRENCY_DOLLAR_REGEXP , 7LATEX_CURRENCY_ESCAPE , 8LATEX_DIGIT_REGEXP , 9LATEX_DISPLAY_BLOCK_REGEXP , 10LATEX_DISPLAY_CLOSE , 11LATEX_DISPLAY_CONVERT_REGEXP , 12LATEX_DISPLAY_DELIMITER , 13LATEX_DISPLAY_OPEN , 14LATEX_INLINE_CLOSE , 15LATEX_INLINE_CONVERT_REGEXP , 16LATEX_INLINE_DELIMITER , 17LATEX_INLINE_OPEN , 18LATEX_LINEBREAK_REGEXP , 19LATEX_MATH_AND_CODE_PATTERN , 20LATEX_MHCHEM_CE , 21LATEX_MHCHEM_PU , 22LATEX_NEIGHBOR_CHAR_REGEXP , 23LATEX_NON_WHITESPACE_REGEXP , 24LATEX_PLACEHOLDER_REGEXP , 25LATEX_PROTECT_REGEXP , 26LATEX_TRIGGER_REGEXP , 27MHCHEM_PATTERN_MAP , 28NEWLINE 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 { 45if ( ! content . includes ( LATEX_INLINE_DELIMITER )) { 46return content ; 47} 48 49return content 50. split ( NEWLINE ) 51. map (( line ) => { 52if ( line . indexOf ( LATEX_INLINE_DELIMITER ) == - 1 ) { 53return line ; 54} 55 56let processedLine = '' ; 57let currentPosition = 0 ; 58 59while ( currentPosition < line . length ) { 60const openDollarIndex = line . indexOf ( LATEX_INLINE_DELIMITER , currentPosition ); 61 62if ( openDollarIndex == - 1 ) { 63processedLine += line . slice ( currentPosition ); 64 65break ; 66} 67 68// Is there a next $-sign? 69const closeDollarIndex = line . indexOf ( LATEX_INLINE_DELIMITER , openDollarIndex + 1 ); 70 71if ( closeDollarIndex == - 1 ) { 72processedLine += line . slice ( currentPosition ); 73 74break ; 75} 76 77const charBeforeOpen = openDollarIndex > 0 ?line [ openDollarIndex - 1 ] :'' ; 78const charAfterOpen = line [ openDollarIndex + 1 ]; 79const charBeforeClose = 80openDollarIndex + 1 < closeDollarIndex ?line [ closeDollarIndex - 1 ] :'' ; 81const charAfterClose = closeDollarIndex + 1 < line . length ?line [ closeDollarIndex + 1 ] :'' ; 82 83let shouldSkipAsNonLatex = false ; 84 85if ( closeDollarIndex == currentPosition + 1 ) { 86// No content 87shouldSkipAsNonLatex = true ; 88} 89 90if ( LATEX_NEIGHBOR_CHAR_REGEXP . test ( charBeforeOpen )) { 91// Character, digit, $, _ or - before first '$', no TeX. 92shouldSkipAsNonLatex = true ; 93} 94 95if ( 96LATEX_DIGIT_REGEXP . test ( charAfterOpen ) && 97( LATEX_NEIGHBOR_CHAR_REGEXP . test ( charAfterClose ) || ' ' == charBeforeClose ) 98) { 99// First $ seems to belong to an amount. 100shouldSkipAsNonLatex = true ; 101} 102 103if ( shouldSkipAsNonLatex ) { 104processedLine += line . slice ( currentPosition , openDollarIndex + 1 ); 105currentPosition = openDollarIndex + 1 ; 106 107continue ; 108} 109 110// Treat as LaTeX 111processedLine += line . slice ( currentPosition , openDollarIndex ); 112const latexContent = line . slice ( openDollarIndex , closeDollarIndex + 1 ); 113 114latexExpressions . push ( latexContent ); 115processedLine += `<<LATEX_ ${ latexExpressions . length - 1 } >>` ; 116currentPosition = closeDollarIndex + 1 ; 117} 118 119return processedLine ; 120}) 121. join ( NEWLINE ); 122} 123 124function escapeBrackets ( text :string ) :string { 125return text . replace ( 126LATEX_MATH_AND_CODE_PATTERN , 127( 128match :string , 129codeBlock :string | undefined , 130squareBracket :string | undefined , 131roundBracket :string | undefined 132) :string => { 133if ( codeBlock != null ) { 134return codeBlock ; 135} else if ( squareBracket != null ) { 136return ` ${ LATEX_DISPLAY_DELIMITER } ${ squareBracket } ${ LATEX_DISPLAY_DELIMITER } ` ; 137} else if ( roundBracket != null ) { 138return ` ${ LATEX_INLINE_DELIMITER } ${ roundBracket } ${ LATEX_INLINE_DELIMITER } ` ; 139} 140 141return match ; 142} 143); 144} 145 146// Escape $\\ce{...} → $\\ce{...} but with proper handling 147function escapeMhchem ( text :string ) :string { 148return MHCHEM_PATTERN_MAP . reduce (( result , [ pattern , replacement ]) => { 149return 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. 184const cached = latexCache . get ( content ); 185 186if ( cached !== undefined ) return cached ; 187 188// Save original before the function mutates `content` through steps 0-8 189const 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. 196if ( ! LATEX_TRIGGER_REGEXP . test ( content )) { 197if ( latexCache . size >= LATEX_CACHE_MAX_SIZE ) { 198latexCache . delete ( latexCache . keys (). next (). value ! ); 199} 200 201latexCache . set ( originalContent , content ); 202 203return content ; 204} 205 206// Step 0: Temporarily remove blockquote markers (>) to process LaTeX correctly 207// Store the structure so we can restore it later 208const blockquoteMarkers :Map < number , string > = new Map (); 209const lines = content . split ( NEWLINE ); 210const processedLines = lines . map (( line , index ) => { 211const match = line . match ( LATEX_BLOCKQUOTE_PREFIX_REGEXP ); 212 213if ( match ) { 214blockquoteMarkers . set ( index , match [ 1 ]); 215 216return line . slice ( match [ 1 ]. length ); 217} 218 219return line ; 220}); 221 222content = processedLines . join ( NEWLINE ); 223 224// Step 1: Protect code blocks 225const codeBlocks :string [] = []; 226 227content = content . replace ( CODE_BLOCK_REGEXP , ( match ) => { 228codeBlocks . push ( match ); 229 230return `<<CODE_BLOCK_ ${ codeBlocks . length - 1 } >>` ; 231}); 232 233// Step 2: Protect existing LaTeX expressions 234const 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. 239if ( content . includes ( LATEX_DISPLAY_OPEN )) { 240content = 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?) 242if ( group1 . endsWith ( LATEX_BACKSLASH )) { 243return match ; // Backslash before \[, do nothing. 244} 245 246const hasSuffix = LATEX_NON_WHITESPACE_REGEXP . test ( group3 ); 247 248let optBreak ; 249 250if ( hasSuffix ) { 251latexExpressions . push ( ` ${ LATEX_INLINE_OPEN } ${ group2 . trim ()} ${ LATEX_INLINE_CLOSE } ` ); // Convert into inline. 252optBreak = '' ; 253} else { 254latexExpressions . push ( ` ${ LATEX_DISPLAY_OPEN } ${ group2 } ${ LATEX_DISPLAY_CLOSE } ` ); 255optBreak = NEWLINE ; 256} 257 258return ` ${ group1 } ${ optBreak } <<LATEX_ ${ latexExpressions . length - 1 } >> ${ optBreak } ${ group3 } ` ; 259}); 260} 261 262// Match \(...\), \[...\], $$...$$ and protect them 263content = content . replace ( LATEX_PROTECT_REGEXP , ( match ) => { 264latexExpressions . push ( match ); 265 266return `<<LATEX_ ${ latexExpressions . length - 1 } >>` ; 267}); 268 269// Protect inline $...$ but NOT if it looks like money (e.g., $10, $3.99) 270content = 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) 274content = content . replace ( LATEX_CURRENCY_DOLLAR_REGEXP , LATEX_CURRENCY_ESCAPE ); 275 276// Step 4: Restore protected LaTeX expressions (they are valid) 277content = content . replace ( LATEX_PLACEHOLDER_REGEXP , ( _ , index ) => { 278let expr = latexExpressions [ parseInt ( index )]; 279 280const match = expr . match ( LATEX_LINEBREAK_REGEXP ); 281 282if ( match ) { 283// Katex: The $$-delimiters should be in their own line 284// if there are \\-line-breaks. 285const formula = match [ 1 ]; 286const prefix = formula . startsWith ( NEWLINE ) ?'' :NEWLINE ; 287const suffix = formula . endsWith ( NEWLINE ) ?'' :NEWLINE ; 288 289expr = LATEX_DISPLAY_DELIMITER + prefix + formula + suffix + LATEX_DISPLAY_DELIMITER ; 290} 291 292return 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 297content = escapeBrackets ( content ); 298 299if ( doEscapeMhchem && ( content . includes ( LATEX_MHCHEM_CE ) || content . includes ( LATEX_MHCHEM_PU ))) { 300content = escapeMhchem ( content ); 301} 302 303// Step 6: Convert remaining \(...\) → $...$, \[...\] → $$...$$ 304// This must happen BEFORE restoring code blocks to avoid affecting code content 305content = 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 ) => { 310return ` ${ 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]`. 315LATEX_DISPLAY_CONVERT_REGEXP , // display, see also PR #16599 316( _ , formula :string ) => { 317return ` ${ 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 323content = content . replace ( CODE_BLOCK_PLACEHOLDER_REGEXP , ( _ , index ) => { 324return codeBlocks [ parseInt ( index )]; 325}); 326 327// Step 8: Restore blockquote markers 328if ( blockquoteMarkers . size > 0 ) { 329const finalLines = content . split ( NEWLINE ); 330const restoredLines = finalLines . map (( line , index ) => { 331const marker = blockquoteMarkers . get ( index ); 332 333return marker ?marker + line :line ; 334}); 335 336content = restoredLines . join ( NEWLINE ); 337} 338 339if ( latexCache . size >= LATEX_CACHE_MAX_SIZE ) { 340latexCache . delete ( latexCache . keys (). next (). value ! ); 341} 342 343latexCache . set ( originalContent , content ); 344 345return content ; 346}