Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Pure command-palette search helpers (filter + rank).
|
||||
* No Svelte / i18n / $app — safe for node:test.
|
||||
*/
|
||||
|
||||
export type PaletteSearchItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
keywords: string;
|
||||
};
|
||||
|
||||
/** Trim + lowercase; empty when the user has not typed a query yet. */
|
||||
export function normalizePaletteQuery(query: string): string {
|
||||
return query.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
/** True when `token` starts a whole word in `hay` (space-separated). */
|
||||
function wordStartsWith(hay: string, token: string): boolean {
|
||||
if (!token) return false;
|
||||
return new RegExp(`(?:^|\\s)${escapeRegExp(token)}`).test(hay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Score one query token against label + keywords.
|
||||
* Higher = better match. 0 = no match.
|
||||
*/
|
||||
export function scorePaletteToken(token: string, label: string, keywords: string): number {
|
||||
const t = token.trim().toLowerCase();
|
||||
if (!t) return 0;
|
||||
const lab = label.toLowerCase();
|
||||
const keys = keywords.toLowerCase();
|
||||
|
||||
if (lab === t) return 100;
|
||||
if (lab.startsWith(t)) return 80;
|
||||
if (wordStartsWith(lab, t)) return 70;
|
||||
if (lab.includes(t)) return 50;
|
||||
if (wordStartsWith(keys, t)) return 40;
|
||||
if (keys.includes(t)) return 20;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score an item for a full query. Multi-token queries use AND:
|
||||
* every token must score > 0; total is the sum.
|
||||
* Empty query scores 1 (preserve input order when idle).
|
||||
*/
|
||||
export function scorePaletteItem(query: string, item: PaletteSearchItem): number {
|
||||
const q = normalizePaletteQuery(query);
|
||||
if (!q) return 1;
|
||||
|
||||
const tokens = q.split(/\s+/).filter(Boolean);
|
||||
let total = 0;
|
||||
for (const token of tokens) {
|
||||
const part = scorePaletteToken(token, item.label, item.keywords);
|
||||
if (part <= 0) return 0;
|
||||
total += part;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out non-matches and rank by score (desc), then label (asc).
|
||||
* Empty / whitespace query returns items in original order.
|
||||
*/
|
||||
export function filterAndRankPaletteItems<T extends PaletteSearchItem>(
|
||||
query: string,
|
||||
items: readonly T[]
|
||||
): T[] {
|
||||
const q = normalizePaletteQuery(query);
|
||||
if (!q) return [...items];
|
||||
|
||||
return items
|
||||
.map((item) => ({ item, score: scorePaletteItem(q, item) }))
|
||||
.filter((row) => row.score > 0)
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return a.item.label.localeCompare(b.item.label);
|
||||
})
|
||||
.map((row) => row.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-aware shortcut hint for discoverability (⌘K vs Ctrl+K).
|
||||
* Pass `platform` in tests; defaults to `navigator.platform` / `userAgent` in browser.
|
||||
*/
|
||||
export function commandPaletteShortcutLabel(
|
||||
platform?: string | null,
|
||||
userAgent?: string | null
|
||||
): string {
|
||||
const p = (platform ?? "").toLowerCase();
|
||||
const ua = (userAgent ?? "").toLowerCase();
|
||||
const hay = `${p} ${ua}`;
|
||||
if (
|
||||
hay.includes("mac") ||
|
||||
hay.includes("iphone") ||
|
||||
hay.includes("ipad") ||
|
||||
hay.includes("ipod")
|
||||
) {
|
||||
return "⌘K";
|
||||
}
|
||||
return "Ctrl+K";
|
||||
}
|
||||
|
||||
/** Resolve shortcut from the current environment (SSR-safe). */
|
||||
export function commandPaletteShortcutLabelFromEnv(): string {
|
||||
if (typeof navigator === "undefined") return "Ctrl+K";
|
||||
return commandPaletteShortcutLabel(navigator.platform, navigator.userAgent);
|
||||
}
|
||||
Reference in New Issue
Block a user