char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
e21152dc9
char/aliceai-t5-moe
1import { CODE_BLOCK , NEWLINE } from '$lib/constants' ; 2import hljs from 'highlight.js' ; 3 4export interface IncompleteCodeBlock { 5language :string ; 6code :string ; 7openingIndex :number ; 8} 9 10// A fence line: up to 3 leading spaces (CommonMark), 3+ backticks, then 11// whatever trails on the same line. 12const FENCE_LINE_REGEX = / ^ {0,3}(`{3,})(.*)$ / ; 13 14/** 15* Splits text glued to a closing code fence onto its own line: 16* 17* ```ts 18* let foo = 'bar'; 19* ```create this file on ... 20* 21* A closing fence with trailing text is not a fence to the markdown 22* parser, so the block would swallow the text as code. The chat form 23* normally keeps the fence on its own line, but older messages and 24* hand-pasted content can carry the glued form. 25* 26* Only trailing text containing whitespace is split: a single word 27* after the backticks inside a fenced block is more likely nested 28* markdown (a ```python example inside a ```md block) than glued prose. 29*/ 30export function splitGluedClosingCodeFences ( markdown :string ) :string { 31if ( ! markdown . includes ( '```' )) return markdown ; 32 33const lines = markdown . split ( NEWLINE ); 34 35let inside = false ; 36let changed = false ; 37 38for ( let i = 0 ; i < lines . length ; i ++ ) { 39const match = FENCE_LINE_REGEX . exec ( lines [ i ]); 40 41if ( ! match ) continue ; 42 43if ( ! inside ) { 44inside = true ; 45 46continue ; 47} 48 49inside = false ; 50 51const trailing = match [ 2 ]; 52 53if ( trailing . includes ( '`' ) || ! /\s / . test ( trailing )) continue ; 54 55lines [ i ] = lines [ i ]. slice ( 0 , lines [ i ]. length - trailing . length ); 56lines . splice ( i + 1 , 0 , trailing . trim ()); 57i ++ ; 58changed = true ; 59} 60 61return changed ?lines . join ( NEWLINE ) :markdown ; 62} 63 64/** 65* Strips empty lines (whitespace-only) from the start and end of code. 66* 67* Tool call payloads frequently arrive with surrounding whitespace from LLM 68* formatting (`"\nfunction ...\n"`). Preserving those newlines makes hljs emit 69* a leading/trailing empty line that `<pre>` then renders as a phantom row, 70* pushing real content away from the box edge. The trim keeps the body intact 71* so internal blank lines are still rendered as such. 72*/ 73function trimCodePadding ( code :string ) :string { 74return code 75. replace ( CODE_BLOCK . TRIM_LEADING_PADDING_REGEX , '' ) 76. replace ( CODE_BLOCK . TRIM_TRAILING_PADDING_REGEX , '' ); 77} 78 79function escapeCode ( code :string ) :string { 80return code 81. replace ( CODE_BLOCK . AMPERSAND_REGEX , '&' ) 82. replace ( CODE_BLOCK . LT_REGEX , '<' ) 83. replace ( CODE_BLOCK . GT_REGEX , '>' ); 84} 85 86/** Bounded cache for highlightCode results. */ 87const HIGHLIGHT_CACHE_MAX_SIZE = 64 ; 88const highlightCache = new Map < string , string >(); 89 90/** 91* Highlights code using highlight.js 92* @param code - The code to highlight 93* @param language - The programming language 94* @param autoDetect - Fall back to `highlightAuto` when `language` is unknown. 95* Callers rendering a still-streaming block should pass false: auto-detection 96* costs ~38ms per call and re-guesses on every chunk, so the language (and 97* therefore the whole highlight) flickers as the block grows. 98* @returns HTML string with syntax highlighting 99*/ 100export function highlightCode ( code :string , language :string , autoDetect = true ) :string { 101if ( ! code ) return '' ; 102 103// Cache key includes language and autoDetect flag since results differ. 104// During streaming, the same code string may be highlighted repeatedly 105// (e.g., when text after a code block changes but the code itself doesn't). 106const cacheKey = ` ${ language } : ${ autoDetect } : ${ code } ` ; 107const cached = highlightCache . get ( cacheKey ); 108 109if ( cached ) return cached ; 110 111const trimmed = trimCodePadding ( code ); 112 113let result :string ; 114 115try { 116const lang = language . toLowerCase (); 117const isSupported = hljs . getLanguage ( lang ); 118 119if ( isSupported ) { 120result = hljs . highlight ( trimmed , { language :lang }). value ; 121} else if ( autoDetect ) { 122result = hljs . highlightAuto ( trimmed ). value ; 123} else { 124result = escapeCode ( trimmed ); 125} 126} catch { 127result = escapeCode ( trimmed ); 128} 129 130if ( highlightCache . size >= HIGHLIGHT_CACHE_MAX_SIZE ) { 131highlightCache . delete ( highlightCache . keys (). next (). value ! ); 132} 133 134highlightCache . set ( cacheKey , result ); 135 136return result ; 137} 138 139export { trimCodePadding }; 140 141/** 142* Detects if markdown ends with an incomplete code block (opened but not closed). 143* Returns the code block info if found, null otherwise. 144* @param markdown - The raw markdown string to check 145* @returns IncompleteCodeBlock info or null 146*/ 147export function detectIncompleteCodeBlock ( markdown :string ) :IncompleteCodeBlock | null { 148// Count all code fences in the markdown 149// A code block is incomplete if there's an odd number of ``` fences 150const fencePattern = new RegExp ( CODE_BLOCK . FENCE_PATTERN . source , CODE_BLOCK . FENCE_PATTERN . flags ); 151const fences :number [] = []; 152 153let fenceMatch ; 154 155while (( fenceMatch = fencePattern . exec ( markdown )) !== null ) { 156// Store the position after the ``` 157const pos = fenceMatch [ 0 ]. startsWith ( NEWLINE ) ?fenceMatch . index + 1 :fenceMatch . index ; 158 159fences . push ( pos ); 160} 161 162// If even number of fences (including 0), all code blocks are closed 163if ( fences . length % 2 === 0 ) { 164return null ; 165} 166 167// Odd number means last code block is incomplete 168// The last fence is the opening of the incomplete block 169const openingIndex = fences [ fences . length - 1 ]; 170const afterOpening = markdown . slice ( openingIndex + 3 ); 171// Extract language and code content 172const langMatch = afterOpening . match ( CODE_BLOCK . LANG_PATTERN ); 173const language = langMatch ?.[ 1 ] || CODE_BLOCK . DEFAULT_LANGUAGE ; 174const codeStartIndex = openingIndex + 3 + ( langMatch ?.[ 0 ]?. length ?? 0 ); 175const code = markdown . slice ( codeStartIndex ); 176 177return { 178 code, 179 language, 180 openingIndex 181}; 182}