Files
dbx/apps/desktop/src/lib/aiTableMentions.ts
T

104 lines
2.9 KiB
TypeScript

export interface AiTableMention {
raw: string;
schema?: string;
table: string;
}
const SIMPLE_IDENTIFIER_RE = /^[\p{L}_][\p{L}\p{N}_$]*$/u;
function isMentionBoundary(char: string | undefined): boolean {
return !char || /\s|[([{,;:]/.test(char);
}
function isUnquotedMentionChar(char: string): boolean {
return /[\p{L}\p{N}_$.-]/u.test(char);
}
function readQuotedSegment(source: string, start: number): { value: string; end: number } | null {
const quote = source[start];
if (quote !== '"' && quote !== "`") return null;
let value = "";
for (let i = start + 1; i < source.length; i++) {
const char = source[i];
if (char === quote) {
if (source[i + 1] === quote) {
value += quote;
i++;
continue;
}
return { value, end: i + 1 };
}
value += char;
}
return null;
}
function readMentionToken(source: string, start: number): { raw: string; parts: string[]; end: number } | null {
let i = start;
const parts: string[] = [];
let raw = "";
while (i < source.length) {
const quoted = readQuotedSegment(source, i);
if (quoted) {
parts.push(quoted.value);
raw += source.slice(i, quoted.end);
i = quoted.end;
} else {
let value = "";
const valueStart = i;
while (i < source.length && isUnquotedMentionChar(source[i]) && source[i] !== ".") {
value += source[i];
i++;
}
if (!value) break;
parts.push(value);
raw += source.slice(valueStart, i);
}
if (source[i] !== ".") break;
raw += ".";
i++;
}
const usableParts = parts.filter(Boolean);
if (!usableParts.length) return null;
return { raw, parts: usableParts, end: i };
}
export function parseAiTableMentions(text: string): AiTableMention[] {
const mentions: AiTableMention[] = [];
const seen = new Set<string>();
for (let i = 0; i < text.length; i++) {
if (text[i] !== "@" || !isMentionBoundary(text[i - 1])) continue;
const token = readMentionToken(text, i + 1);
if (!token) continue;
const table = token.parts[token.parts.length - 1];
const schema = token.parts.length > 1 ? token.parts[token.parts.length - 2] : undefined;
const key = `${schema || ""}.${table}`.toLowerCase();
if (!seen.has(key)) {
seen.add(key);
mentions.push({ raw: `@${token.raw}`, schema, table });
}
i = token.end - 1;
}
return mentions;
}
export function formatAiTableMention(schema: string | undefined, table: string): string {
const parts = [schema, table].filter((part): part is string => !!part);
return `@${parts.map(formatMentionPart).join(".")}`;
}
function formatMentionPart(part: string): string {
if (SIMPLE_IDENTIFIER_RE.test(part)) return part;
return `"${part.replace(/"/g, '""')}"`;
}
export function aiTableMentionKey(schema: string | undefined, table: string): string {
return `${schema || ""}.${table}`.toLowerCase();
}