catalog-workspace.ts (2293B)
1 import { FACET_KEYS, facetValues, matches, type Technique, type CatalogFilters, type FacetKey } from './techniques'; 2 export type SortOrder = 'tool' | 'source' | 'reviewed'; 3 export function sortTechniques(rows: Technique[], order: SortOrder, query = ''): Technique[] { 4 const needle = query.trim().toLowerCase(); 5 return [...rows].sort((a, b) => { 6 const exact = Number(b.toolName.toLowerCase() === needle) - Number(a.toolName.toLowerCase() === needle); 7 if (exact) return exact; 8 if (order === 'reviewed') { const date = (b.reviewedAt || '').localeCompare(a.reviewedAt || ''); if (date) return date; } 9 if (order === 'source') { const source = a.source.localeCompare(b.source); if (source) return source; } 10 return a.toolName.localeCompare(b.toolName, 'en', { numeric: true, sensitivity: 'base' }) || a.id.localeCompare(b.id); 11 }); 12 } 13 export function groupTechniques(rows: Technique[]): Technique[][] { 14 const groups = new Map<string, Technique[]>(); 15 for (const t of rows) { const group = groups.get(t.toolId) || []; group.push(t); groups.set(t.toolId, group); } 16 return [...groups.values()]; 17 } 18 export function contextualCounts(rows: Technique[], filters: CatalogFilters): Record<FacetKey, Record<string, number>> { 19 return Object.fromEntries(FACET_KEYS.map(key => { 20 const counts: Record<string, number> = {}; 21 for (const t of rows) if (matches(t, { ...filters, [key]: [] })) for (const value of facetValues(t, key)) counts[value] = (counts[value] || 0) + 1; 22 return [key, counts]; 23 })) as Record<FacetKey, Record<string, number>>; 24 } 25 export function quoteArgument(value: string, shell: 'posix' | 'powershell'): string { 26 if (/[\r\n\0]/.test(value)) throw new Error('Use a single-line value.'); 27 return shell === 'powershell' ? `'${value.replace(/'/g, "''")}'` : `'${value.replace(/'/g, "'\"'\"'")}'`; 28 } 29 export function configureCommand(template: NonNullable<Technique['template']>, values: Record<string, string>): string { 30 const keys = new Set(template.variables.map(v => v.key)); 31 return template.command.replace(/\{\{([a-z][a-z0-9_]*)\}\}/g, (_, key: string) => { 32 if (!keys.has(key)) throw new Error(`Unknown variable: ${key}`); 33 return quoteArgument(values[key] ?? template.variables.find(v => v.key === key)!.default, template.shell); 34 }); 35 }