# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: EthanHealy01 <ethan.healy.21@gmail.com> Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
import { ToolId } from "src/types/toolId";
|
|
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
|
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
|
|
|
export interface RankedToolItem {
|
|
item: [ToolId, ToolRegistryEntry];
|
|
matchedText?: string;
|
|
}
|
|
|
|
export function filterToolRegistryByQuery(
|
|
toolRegistry: Record<ToolId, ToolRegistryEntry>,
|
|
query: string
|
|
): RankedToolItem[] {
|
|
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
|
if (!query.trim()) {
|
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
|
}
|
|
|
|
const nq = normalizeForSearch(query);
|
|
const threshold = minScoreForQuery(query);
|
|
|
|
const exactName: Array<{ id: ToolId; tool: ToolRegistryEntry; pos: number }> = [];
|
|
const exactSyn: Array<{ id: ToolId; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
|
const fuzzyName: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
|
const fuzzySyn: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
|
|
|
for (const [id, tool] of entries) {
|
|
const nameNorm = normalizeForSearch(tool.name || '');
|
|
const pos = nameNorm.indexOf(nq);
|
|
if (pos !== -1) {
|
|
exactName.push({ id, tool, pos });
|
|
continue;
|
|
}
|
|
|
|
const syns = Array.isArray(tool.synonyms) ? tool.synonyms : [];
|
|
let matchedExactSyn: { text: string; pos: number } | null = null;
|
|
for (const s of syns) {
|
|
const sn = normalizeForSearch(s);
|
|
const sp = sn.indexOf(nq);
|
|
if (sp !== -1) {
|
|
matchedExactSyn = { text: s, pos: sp };
|
|
break;
|
|
}
|
|
}
|
|
if (matchedExactSyn) {
|
|
exactSyn.push({ id, tool, text: matchedExactSyn.text, pos: matchedExactSyn.pos });
|
|
continue;
|
|
}
|
|
|
|
// Fuzzy name
|
|
const nameScore = scoreMatch(query, tool.name || '');
|
|
if (nameScore >= threshold) {
|
|
fuzzyName.push({ id, tool, score: nameScore, text: tool.name || '' });
|
|
}
|
|
|
|
// Fuzzy synonyms (we'll consider these only if fuzzy name results are weak)
|
|
let bestSynScore = 0;
|
|
let bestSynText = '';
|
|
for (const s of syns) {
|
|
const synScore = scoreMatch(query, s);
|
|
if (synScore > bestSynScore) {
|
|
bestSynScore = synScore;
|
|
bestSynText = s;
|
|
}
|
|
if (bestSynScore >= 95) break;
|
|
}
|
|
if (bestSynScore >= threshold) {
|
|
fuzzySyn.push({ id, tool, score: bestSynScore, text: bestSynText });
|
|
}
|
|
}
|
|
|
|
// Sort within buckets
|
|
exactName.sort((a, b) => a.pos - b.pos || (a.tool.name || '').length - (b.tool.name || '').length);
|
|
exactSyn.sort((a, b) => a.pos - b.pos || a.text.length - b.text.length);
|
|
fuzzyName.sort((a, b) => b.score - a.score);
|
|
fuzzySyn.sort((a, b) => b.score - a.score);
|
|
|
|
// Concatenate buckets with de-duplication by tool id
|
|
const seen = new Set<string>();
|
|
const ordered: RankedToolItem[] = [];
|
|
|
|
const push = (id: ToolId, tool: ToolRegistryEntry, matchedText?: string) => {
|
|
if (seen.has(id)) return;
|
|
seen.add(id);
|
|
ordered.push({ item: [id, tool], matchedText });
|
|
};
|
|
|
|
for (const { id, tool } of exactName) push(id as ToolId, tool, tool.name);
|
|
for (const { id, tool, text } of exactSyn) push(id as ToolId, tool, text);
|
|
for (const { id, tool, text } of fuzzyName) push(id as ToolId, tool, text);
|
|
for (const { id, tool, text } of fuzzySyn) push(id as ToolId, tool, text);
|
|
|
|
if (ordered.length > 0) return ordered;
|
|
|
|
// Fallback: return everything unchanged
|
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
|
}
|
|
|
|
|
|
|