igniteui-theming
Version:
A set of Sass variables, mixins, and functions for generating palettes, typography, and elevations used by Ignite UI components.
145 lines (144 loc) • 5.76 kB
JavaScript
//#region src/knowledge/component-search.ts
var FRAMEWORK_PREFIX_PATTERN = /\big[cx]-/g;
var NON_ALPHANUMERIC_PATTERN = /[^a-z0-9]+/g;
var MIN_SEARCH_SCORE = 500;
function stripFrameworkPrefixToken(token) {
if (token.startsWith("igx") && token.length > 3) return token.slice(3);
if (token.startsWith("igc") && token.length > 3) return token.slice(3);
return token;
}
function normalizeSearchTerm(term) {
const lowerTerm = term.toLowerCase().trim();
if (!lowerTerm) return;
const normalizedDelimiters = lowerTerm.replace(FRAMEWORK_PREFIX_PATTERN, "").replace(NON_ALPHANUMERIC_PATTERN, " ").trim();
if (!normalizedDelimiters) return;
const tokens = normalizedDelimiters.split(/\s+/).map(stripFrameworkPrefixToken).filter((token) => token.length > 0);
if (tokens.length === 0) return;
const uniqueTokens = [...new Set(tokens)];
return {
compact: tokens.join(""),
tokenSetKey: uniqueTokens.slice().sort().join("|"),
tokens: uniqueTokens
};
}
function getSelectorSearchSignals(selectors) {
if (!selectors) return [];
const values = [selectors.angular, selectors.webcomponents];
const signals = [];
for (const value of values) {
if (!value) continue;
if (Array.isArray(value)) {
signals.push(...value);
continue;
}
signals.push(value);
}
return signals;
}
function getComponentSearchSignals(componentName, metadataByName) {
const metadata = metadataByName[componentName];
const signals = new Set([componentName]);
if (!metadata) return [...signals];
metadata.aliases?.forEach((alias) => {
signals.add(alias);
});
getSelectorSearchSignals(metadata.selectors).forEach((selector) => {
signals.add(selector);
});
return [...signals];
}
function buildComponentSearchIndex(searchableNames, metadataByName) {
return searchableNames.map((name) => {
return {
name,
signals: getComponentSearchSignals(name, metadataByName).map(normalizeSearchTerm).filter((signal) => !!signal)
};
});
}
function getTokenCoverageScore(query, signal) {
if (query.tokens.length === 0 || signal.tokens.length === 0) return 0;
const signalTokens = new Set(signal.tokens);
let overlapCount = 0;
for (const token of query.tokens) if (signalTokens.has(token)) overlapCount++;
if (overlapCount === 0) return 0;
const queryCoverage = overlapCount / query.tokens.length;
const signalCoverage = overlapCount / signal.tokens.length;
if (query.tokens.length === 1 && query.tokens[0].length < 4) return queryCoverage === 1 && signalCoverage === 1 ? 900 : 0;
if (queryCoverage === 1) return 800 + overlapCount * 10 - Math.max(0, signal.tokens.length - query.tokens.length);
if (query.tokens.length > 1 && queryCoverage >= .5) return 650 + Math.round(queryCoverage * 100 + signalCoverage * 50);
return 0;
}
function getSubstringFallbackScore(query, signal) {
if (query.compact.length < 4 || signal.compact.length === 0) return 0;
if (signal.compact.includes(query.compact)) return 500 + Math.min(query.compact.length, 100);
return 0;
}
function getEditDistanceWithinLimit(source, target, limit) {
if (source === target) return 0;
const sourceLength = source.length;
const targetLength = target.length;
if (Math.abs(sourceLength - targetLength) > limit) return;
let previous = new Array(targetLength + 1);
let current = new Array(targetLength + 1);
for (let j = 0; j <= targetLength; j++) previous[j] = j;
for (let i = 1; i <= sourceLength; i++) {
current[0] = i;
let rowMin = current[0];
for (let j = 1; j <= targetLength; j++) {
const substitutionCost = source[i - 1] === target[j - 1] ? 0 : 1;
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + substitutionCost);
rowMin = Math.min(rowMin, current[j]);
}
if (rowMin > limit) return;
[previous, current] = [current, previous];
}
return previous[targetLength] <= limit ? previous[targetLength] : void 0;
}
function getTypoFallbackScore(query, signal) {
if (query.tokens.length !== 1) return 0;
const [queryToken] = query.tokens;
if (queryToken.length < 5) return 0;
let bestScore = 0;
for (const signalToken of signal.tokens) {
if (signalToken.length < 5) continue;
if (getEditDistanceWithinLimit(queryToken, signalToken, 1) === 1) bestScore = Math.max(bestScore, 540);
}
return bestScore;
}
function scoreSearchEntry(query, entry) {
let bestScore = 0;
for (const signal of entry.signals) {
if (signal.compact === query.compact) {
bestScore = Math.max(bestScore, 1e3);
continue;
}
if (signal.tokenSetKey === query.tokenSetKey) {
bestScore = Math.max(bestScore, 900);
continue;
}
bestScore = Math.max(bestScore, getTokenCoverageScore(query, signal));
bestScore = Math.max(bestScore, getSubstringFallbackScore(query, signal));
bestScore = Math.max(bestScore, getTypoFallbackScore(query, signal));
}
return bestScore;
}
/**
* Build a pre-indexed component searcher from theme names and metadata.
*
* The returned searcher normalises queries, scores them against an
* index of canonical names / aliases / selectors, and returns results
* ranked by confidence (exact > token-set > overlap > substring > typo).
*/
function createComponentSearcher(options) {
const componentSearchIndex = buildComponentSearchIndex(Array.from(new Set([...options.componentNames, ...Object.keys(options.metadata)])).sort((a, b) => a.localeCompare(b)), options.metadata);
return { search(query) {
const normalizedQuery = normalizeSearchTerm(query);
if (!normalizedQuery) return [];
return componentSearchIndex.map((entry) => ({
name: entry.name,
score: scoreSearchEntry(normalizedQuery, entry)
})).filter((match) => match.score >= MIN_SEARCH_SCORE).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).map((match) => match.name);
} };
}
//#endregion
export { createComponentSearcher };