182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
/**
|
||
* Парсер для обработки ссылок на файлы (@/@@) и промпты (#/##) в тексте сообщений.
|
||
* Поддерживает поиск активных ссылок при вводе и валидацию промптов.
|
||
*/
|
||
|
||
// ----------------------------------------------- Interfaces ---------------------------------------------------------------
|
||
|
||
export interface ReferenceMatch {
|
||
fullMatch: string;
|
||
prefix: string; // Теперь может быть @, !@, #, !#, $, !$
|
||
name: string;
|
||
startIndex: number;
|
||
endIndex: number;
|
||
type: 'file' | 'prompt' | 'external' | 'url';
|
||
permanent: boolean;
|
||
}
|
||
|
||
export interface FileReference {
|
||
type: 'file' | 'prompt' | 'external' | 'url';
|
||
name: string;
|
||
path: string;
|
||
permanent: boolean;
|
||
uuid?: string;
|
||
}
|
||
|
||
// ----------------------------------------------- Reference Parser ---------------------------------------------------------------
|
||
|
||
export class ReferenceParser {
|
||
/**
|
||
* Парсит текст и находит все @/@@ и #/## ссылки
|
||
* @param text - текст для парсинга
|
||
* @returns массив найденных ссылок
|
||
*/
|
||
static parseReferences(text: string): ReferenceMatch[] {
|
||
const matches: ReferenceMatch[] = [];
|
||
|
||
const regex = /(![@#$%]|[@#$%])(?:"([^"]+)"|([^\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;
|
||
|
||
// Определяем тип по первому символу префикса (после !)
|
||
const typeChar = prefix.replace('!', '');
|
||
let type: 'file' | 'prompt' | 'external'| 'url';
|
||
|
||
if (typeChar === '@') type = 'file';
|
||
else if (typeChar === '#') type = 'prompt';
|
||
else if (typeChar === '$') type = 'external';
|
||
else type = 'url';
|
||
|
||
matches.push({
|
||
fullMatch: match[0],
|
||
prefix: prefix,
|
||
name: name,
|
||
startIndex: match.index,
|
||
endIndex: match.index + match[0].length,
|
||
type: type,
|
||
permanent: prefix.startsWith('!')
|
||
});
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
/**
|
||
* Проверяет, является ли позиция курсора внутри незавершённой ссылки
|
||
* @param text - текст сообщения
|
||
* @param cursorPosition - позиция курсора
|
||
* @returns информация о текущей ссылке или null
|
||
*/
|
||
static getCurrentReference(text: string, cursorPosition: number): ReferenceMatch | null {
|
||
const beforeCursor = text.substring(0, cursorPosition);
|
||
|
||
// Измененная регулярка: теперь она понимает, если после символа # идет кавычка
|
||
// Группа 1: Префикс, Группа 2: Имя в кавычках, Группа 3: Имя без кавычек
|
||
const incompleteRegex = /(![@#$%]|[@#$%])(?:"([^"]*)|([^\s@#$%"!]*?))$/;
|
||
const match = beforeCursor.match(incompleteRegex);
|
||
|
||
if (match) {
|
||
const prefix = match[1];
|
||
const nameInQuotes = match[2]; // текст внутри кавычек
|
||
const nameWithoutQuotes = match[3]; // текст до первого пробела
|
||
const partialName = nameInQuotes !== undefined ? nameInQuotes : nameWithoutQuotes;
|
||
|
||
const startIndex = beforeCursor.length - match[0].length;
|
||
|
||
const typeChar = prefix.replace('!', '');
|
||
let type: 'file' | 'prompt' | 'external' | 'url';
|
||
|
||
if (typeChar === '@') type = 'file';
|
||
else if (typeChar === '#') type = 'prompt';
|
||
else if (typeChar === '$') type = 'external';
|
||
else type = 'url';
|
||
|
||
return {
|
||
fullMatch: match[0],
|
||
prefix: prefix,
|
||
name: partialName,
|
||
startIndex: startIndex,
|
||
endIndex: cursorPosition,
|
||
type: type,
|
||
permanent: prefix.startsWith('!')
|
||
};
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|