/** * Парсер для обработки ссылок на файлы (@/@@) и промпты (#/##) в тексте сообщений. * Поддерживает поиск активных ссылок при вводе и валидацию промптов. */ // ----------------------------------------------- Interfaces --------------------------------------------------------------- export interface ReferenceMatch { fullMatch: string; // "@filename" или "@@filename" prefix: string; // "@" или "@@" name: string; // "filename" startIndex: number; endIndex: number; type: 'file' | 'prompt'; } export interface FileReference { type: 'file' | 'prompt'; name: string; path: string; permanent: boolean; // true для @@ и ## // TODO: добавить snapshotId при реализации снапшотов } // ----------------------------------------------- Reference Parser --------------------------------------------------------------- export class ReferenceParser { /** * Парсит текст и находит все @/@@ и #/## ссылки * @param text - текст для парсинга * @returns массив найденных ссылок */ static parseReferences(text: string): ReferenceMatch[] { const matches: ReferenceMatch[] = []; // Регулярное выражение для поиска @/@@ и #/## ссылок // Поддерживает имена файлов с пробелами, заключенные в кавычки или без пробелов const regex = /(@{1,2}|#{1,2})(?:"([^"]+)"|([^\s@#"]+))/g; let match; while ((match = regex.exec(text)) !== null) { const prefix = match[1]; const quotedName = match[2]; const unquotedName = match[3]; const name = quotedName || unquotedName; matches.push({ fullMatch: match[0], prefix: prefix, name: name, startIndex: match.index, endIndex: match.index + match[0].length, type: prefix.startsWith('@') ? 'file' : 'prompt' }); } return matches; } /** * Проверяет, является ли позиция курсора внутри незавершённой ссылки * @param text - текст сообщения * @param cursorPosition - позиция курсора * @returns информация о текущей ссылке или null */ static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null { // Ищем незавершенные ссылки перед курсором const beforeCursor = text.substring(0, cursorPosition); // Регулярное выражение для поиска начала ссылки без завершения const incompleteRegex = /(@{1,2}|#{1,2})([^\s@#"]*?)$/; const match = beforeCursor.match(incompleteRegex); if (match) { const prefix = match[1]; const partialName = match[2]; const startIndex = beforeCursor.length - match[0].length; return { fullMatch: match[0], prefix: prefix, name: partialName, startIndex: startIndex, endIndex: cursorPosition, type: prefix.startsWith('@') ? 'file' : 'prompt' }; } return null; } /** * Валидирует промпт на корректность @{...} ссылок * @param promptContent - содержимое промпта * @returns результат валидации */ static validatePromptReferences(promptContent: string): { valid: boolean; errors: string[] } { const errors: string[] = []; // Ищем шаблонные ссылки @{...} const templateRegex = /@\{([^}]+)\}/g; const matches = promptContent.matchAll(templateRegex); for (const match of matches) { const referenceName = match[1].trim(); // Проверяем, что имя ссылки не пустое if (!referenceName) { errors.push(`Пустая ссылка в позиции ${match.index}`); continue; } // Проверяем, что имя содержит только допустимые символы if (!/^[a-zA-Z0-9_-]+$/.test(referenceName)) { errors.push(`Недопустимые символы в ссылке "${referenceName}"`); } } return { valid: errors.length === 0, errors: errors }; } /** * Извлекает все шаблонные ссылки из промпта, основываясь на заданных префиксах. * По умолчанию ищет ссылки с префиксом @{...}. * @param promptContent - содержимое промпта * @param prefixes - массив префиксов, по которым нужно искать ссылки (например, ['@'], ['#'], ['@', '#']). По умолчанию ['@']. * @returns массив имен ссылок */ static extractTemplateReferences(promptContent: string, prefixes: Array<'@' | '#'> = ['@']): string[] { const references: string[] = []; let prefixRegexPart: string; if (prefixes.includes('@') && prefixes.includes('#')) { prefixRegexPart = '[#@]'; // Ищет как @, так и # } else if (prefixes.includes('@')) { prefixRegexPart = '@'; // Ищет только @ } else if (prefixes.includes('#')) { prefixRegexPart = '#'; // Ищет только # } else { // Если пустой массив или неподдерживаемые префиксы, не ищем ничего return []; } // Регулярное выражение для поиска ссылок с динамическим префиксом const templateRegex = new RegExp(`${prefixRegexPart}\\{([^}]+)\\}`, 'g'); const matches = promptContent.matchAll(templateRegex); for (const match of matches) { const referenceName = match[1].trim(); if (referenceName && !references.includes(referenceName)) { references.push(referenceName); } } return references; } }