mirror of
https://github.com/nuxt/ui.git
synced 2026-09-14 19:51:10 +08:00
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
export interface SearchableComponent {
|
|
name?: string
|
|
title?: string
|
|
description?: string
|
|
keywords?: string[]
|
|
}
|
|
|
|
export function tokenize(query: string): string[] {
|
|
return normalizeQuery(query).split(/[^a-z0-9]+/).filter(token => token.length >= 3)
|
|
}
|
|
|
|
function normalizeQuery(query: string): string {
|
|
return query.toLowerCase().normalize('NFD').replace(/[\u0300-\u036F]/g, '').trim()
|
|
}
|
|
|
|
/** Tokens only contain `[a-z0-9]` so they are safe to inline in a regex. */
|
|
function matchesWord(text: string, token: string): boolean {
|
|
return new RegExp(`\\b${token}\\b`).test(text)
|
|
}
|
|
|
|
/**
|
|
* Scores a component against a search query, matching name, title, keywords
|
|
* and description at both phrase and token level. Keywords hold alternate
|
|
* names from other ecosystems (e.g. "segmented control" for FieldGroup) and
|
|
* only match on word boundaries to keep short keywords from firing inside
|
|
* unrelated words.
|
|
*/
|
|
export function scoreComponent(component: SearchableComponent, query: string): number {
|
|
const search = normalizeQuery(query)
|
|
if (!search) {
|
|
return 0
|
|
}
|
|
|
|
const name = (component.name ?? '').toLowerCase()
|
|
const nameWords = name.replace(/-/g, ' ')
|
|
const title = (component.title ?? '').toLowerCase()
|
|
const keywords = (component.keywords ?? []).map(keyword => keyword.toLowerCase())
|
|
const description = (component.description ?? '').toLowerCase()
|
|
const searchTokens = tokenize(search)
|
|
|
|
let score = 0
|
|
|
|
if (name === search || nameWords === search || title === search) {
|
|
score += 100
|
|
} else if (name.includes(search) || nameWords.includes(search) || title.includes(search)) {
|
|
score += 60
|
|
}
|
|
|
|
if (keywords.includes(search)) {
|
|
score += 50
|
|
} else if (keywords.some((keyword) => {
|
|
if (keyword.startsWith(search)) {
|
|
return true
|
|
}
|
|
// Only multi-word keywords match by containment, single words score at token level
|
|
const keywordTokens = tokenize(keyword)
|
|
return keywordTokens.length > 1 && keywordTokens.every(token => searchTokens.includes(token))
|
|
})) {
|
|
score += 40
|
|
}
|
|
|
|
if (description.includes(search)) {
|
|
score += 20
|
|
}
|
|
|
|
for (const token of searchTokens) {
|
|
if (matchesWord(nameWords, token) || matchesWord(title, token)) {
|
|
score += 12
|
|
}
|
|
if (keywords.some(keyword => matchesWord(keyword, token))) {
|
|
score += 8
|
|
}
|
|
if (matchesWord(description, token)) {
|
|
score += 3
|
|
}
|
|
}
|
|
|
|
return score
|
|
}
|