Leon4gr45/builder
0
1/**2 * Content normalizer for LLM output before markdown rendering3 * Fixes formatting issues from models that output excessive whitespace4 */5 6/**7 * Normalizes content to fix common LLM output formatting issues8 * - Removes excessive leading whitespace that causes false code blocks9 * - Preserves intentional markdown formatting (fenced code blocks, lists, etc.)10 * - Normalizes excessive blank lines11 */12export function normalizeContent(content: string): string {13 if (!content || typeof content !== 'string') return '';14 15 const lines = content.split('\n');16 const normalized: string[] = [];17 let inFencedCodeBlock = false;18 let consecutiveBlankLines = 0;19 20 // Detect if a line is a fenced code block delimiter21 const isFencedCodeDelimiter = (line: string): boolean => {22 const trimmed = line.trim();23 return /^```/.test(trimmed);24 };25 26 // Detect if a line is an intentional list item27 const isListItem = (line: string): boolean => {28 const trimmed = line.trim();29 return /^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed);30 };31 32 // Detect if a line is an intentional blockquote33 const isBlockquote = (line: string): boolean => {34 return /^\s*>/.test(line);35 };36 37 // Detect if line is likely code (heuristic)38 const looksLikeCode = (line: string): boolean => {39 const trimmed = line.trim();40 // Empty lines or very short lines are not code41 if (!trimmed || trimmed.length < 3) return false;42 43 // Check for code-like patterns44 const codePatterns = [45 /^(const|let|var|function|class|import|export|return|if|for|while)\s/,46 /^[a-zA-Z_$][a-zA-Z0-9_$]*\s*[=:({]/,47 /[{};()[\]]/,48 /^\/\//,49 /^#/,50 ];51 52 return codePatterns.some(pattern => pattern.test(trimmed));53 };54 55 for (let i = 0; i < lines.length; i++) {56 const line = lines[i];57 58 // Track fenced code block state59 if (isFencedCodeDelimiter(line)) {60 inFencedCodeBlock = !inFencedCodeBlock;61 normalized.push(line);62 consecutiveBlankLines = 0;63 continue;64 }65 66 // Inside fenced code blocks, preserve everything as-is67 if (inFencedCodeBlock) {68 normalized.push(line);69 consecutiveBlankLines = 0;70 continue;71 }72 73 // Handle blank lines74 if (!line.trim()) {75 consecutiveBlankLines++;76 // Allow max 2 consecutive blank lines77 if (consecutiveBlankLines <= 2) {78 normalized.push('');79 }80 continue;81 }82 83 consecutiveBlankLines = 0;84 85 // Preserve list items with their indentation86 if (isListItem(line)) {87 normalized.push(line);88 continue;89 }90 91 // Preserve blockquotes92 if (isBlockquote(line)) {93 normalized.push(line);94 continue;95 }96 97 // Check for excessive leading whitespace98 const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0;99 100 // If line has 4+ leading spaces and doesn't look like code, trim it101 if (leadingSpaces >= 4 && !looksLikeCode(line)) {102 // This is likely accidental indentation from the model103 normalized.push(line.trim());104 continue;105 }106 107 // If line has 2-3 leading spaces, reduce to 0 (likely unintentional)108 if (leadingSpaces >= 2 && leadingSpaces < 4) {109 normalized.push(line.trim());110 continue;111 }112 113 // Otherwise preserve the line as-is114 normalized.push(line);115 }116 117 return normalized.join('\n').trim();118}119 